source: trunk/gsdl/perllib/plugins/RecPlug.pm@ 11090

Last change on this file since 11090 was 10254, checked in by kjdon, 19 years ago

added 'use strict' to all plugins, and made modifications (mostly adding 'my') to make them compile

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
File size: 21.4 KB
Line 
1###########################################################################
2#
3# RecPlug.pm --
4# A component of the Greenstone digital library software
5# from the New Zealand Digital Library Project at the
6# University of Waikato, New Zealand.
7#
8# Copyright (C) 1999 New Zealand Digital Library Project
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation; either version 2 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program; if not, write to the Free Software
22# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23#
24###########################################################################
25
26# RecPlug is a plugin which recurses through directories processing
27# each file it finds.
28
29# RecPlug has one option: use_metadata_files. When this is set, it will
30# check each directory for an XML file called "metadata.xml" that specifies
31# metadata for the files (and subdirectories) in the directory.
32#
33# Here's an example of a metadata file that uses three FileSet structures
34# (ignore the # characters):
35
36#<?xml version="1.0" encoding="UTF-8" standalone="no"?>
37#<!DOCTYPE DirectoryMetadata SYSTEM "http://greenstone.org/dtd/DirectoryMetadata/1.0/DirectoryMetadata.dtd">
38#<DirectoryMetadata>
39# <FileSet>
40# <FileName>nugget.*</FileName>
41# <Description>
42# <Metadata name="Title">Nugget Point, The Catlins</Metadata>
43# <Metadata name="Place" mode="accumulate">Nugget Point</Metadata>
44# </Description>
45# </FileSet>
46# <FileSet>
47# <FileName>nugget-point-1.jpg</FileName>
48# <Description>
49# <Metadata name="Title">Nugget Point Lighthouse, The Catlins</Metadata>
50# <Metadata name="Subject">Lighthouse</Metadata>
51# </Description>
52# </FileSet>
53# <FileSet>
54# <FileName>kaka-point-dir</FileName>
55# <Description>
56# <Metadata name="Title">Kaka Point, The Catlins</Metadata>
57# </Description>
58# </FileSet>
59#</DirectoryMetadata>
60
61# Metadata elements are read and applied to files in the order they appear
62# in the file.
63#
64# The FileName element describes the subfiles in the directory that the
65# metadata applies to as a perl regular expression (a FileSet group may
66# contain multiple FileName elements). So, <FileName>nugget.*</FileName>
67# indicates that the metadata records in the following Description block
68# apply to every subfile that starts with "nugget". For these files, a
69# Title metadata element is set, overriding any old value that the Title
70# might have had.
71#
72# Occasionally, we want to have multiple metadata values applied to a
73# document; in this case we use the "mode=accumulate" attribute of the
74# particular Metadata element. In the second metadata element of the first
75# FileSet above, the "Place" metadata is accumulating, and may therefore be
76# given several values. If we wanted to override these values and use a
77# single metadata element again, we could set the mode attribute to
78# "override" instead. Remember: every element is assumed to be in override
79# mode unless you specify otherwise, so if you want to accumulate metadata
80# for some field, every occurance must have "mode=accumulate" specified.
81#
82# The second FileSet element above applies to a specific file, called
83# nugget-point-1.jpg. This element overrides the Title metadata set in the
84# first FileSet, and adds a "Subject" metadata field.
85#
86# The third and final FileSet sets metadata for a subdirectory rather than
87# a file. The metadata specified (a Title) will be passed into the
88# subdirectory and applied to every file that occurs in the subdirectory
89# (and to every subsubdirectory and its contents, and so on) unless the
90# metadata is explictly overridden later in the import.
91
92
93
94package RecPlug;
95
96use BasPlug;
97use plugin;
98use util;
99
100use File::Basename;
101use strict;
102no strict 'refs';
103
104BEGIN {
105 @RecPlug::ISA = ('BasPlug');
106 unshift (@INC, "$ENV{'GSDLHOME'}/perllib/cpan");
107}
108
109use XMLParser;
110
111my $arguments =
112 [ { 'name' => "block_exp",
113 'desc' => "{BasPlug.block_exp}",
114 'type' => "regexp",
115 'deft' => &get_default_block_exp(),
116 'reqd' => "no" },
117 { 'name' => "use_metadata_files",
118 'desc' => "{RecPlug.use_metadata_files}",
119 'type' => "flag",
120 'reqd' => "no" },
121 { 'name' => "recheck_directories",
122 'desc' => "{RecPlug.recheck_directories}",
123 'type' => "flag",
124 'reqd' => "no" } ];
125
126my $options = { 'name' => "RecPlug",
127 'desc' => "{RecPlug.desc}",
128 'abstract' => "no",
129 'inherits' => "yes",
130 'args' => $arguments };
131
132
133my ($self);
134
135sub new {
136 my ($class) = shift (@_);
137 my ($pluginlist,$inputargs,$hashArgOptLists) = @_;
138 push(@$pluginlist, $class);
139
140 if(defined $arguments){ push(@{$hashArgOptLists->{"ArgList"}},@{$arguments});}
141 if(defined $options) { push(@{$hashArgOptLists->{"OptList"}},$options)};
142
143 $self = (defined $hashArgOptLists)? new BasPlug($pluginlist,$inputargs,$hashArgOptLists): new BasPlug($pluginlist,$inputargs);
144 if ($self->{'use_metadata_files'}) {
145 # create XML::Parser object for parsing metadata.xml files
146 my $parser = new XML::Parser('Style' => 'Stream',
147 'Handlers' => {'Char' => \&Char,
148 'Doctype' => \&Doctype
149 });
150
151 $self->{'parser'} = $parser;
152 $self->{'in_filename'} = 0;
153 }
154
155 $self->{'subdir_extrametakeys'} = {};
156
157 return bless $self, $class;
158}
159
160sub begin {
161 my $self = shift (@_);
162 my ($pluginfo, $base_dir, $processor, $maxdocs) = @_;
163
164 my $proc_package_name = ref $processor;
165
166 if ($proc_package_name !~ /buildproc$/) {
167
168 # Only lookup timestamp info for import.pl
169
170 my $output_dir = $processor->getoutputdir();
171 my $archives_inf = &util::filename_cat($output_dir,"archives.inf");
172
173 if ( -e $archives_inf ) {
174 $self->{'inf_timestamp'} = -M $archives_inf;
175 }
176 }
177
178 $self->SUPER::begin($pluginfo, $base_dir, $processor, $maxdocs);
179}
180
181
182# return 1 if this class might recurse using $pluginfo
183sub is_recursive {
184 my $self = shift (@_);
185
186 return 1;
187}
188
189sub get_default_block_exp {
190 my $self = shift (@_);
191
192 return 'CVS';
193}
194
195# return number of files processed, undef if can't process
196# Note that $base_dir might be "" and that $file might
197# include directories
198
199# This function passes around metadata hash structures. Metadata hash
200# structures are hashes that map from a (scalar) key (the metadata element
201# name) to either a scalar metadata value or a reference to an array of
202# such values.
203
204sub read {
205 my $self = shift (@_);
206 my ($pluginfo, $base_dir, $file, $in_metadata, $processor, $maxdocs, $total_count, $gli) = @_;
207
208 my $outhandle = $self->{'outhandle'};
209 my $verbosity = $self->{'verbosity'};
210 my $read_metadata_files = $self->{'use_metadata_files'};
211
212 # Calculate the directory name and ensure it is a directory and
213 # that it is not explicitly blocked.
214 my $dirname = $file;
215 $dirname = &util::filename_cat ($base_dir, $file) if $base_dir =~ /\w/;
216 return undef unless (-d $dirname);
217 return 0 if ($self->{'block_exp'} ne "" && $dirname =~ /$self->{'block_exp'}/);
218
219 # check to make sure we're not reading the archives or index directory
220 my $gsdlhome = quotemeta($ENV{'GSDLHOME'});
221 if ($dirname =~ m/^$gsdlhome\/.*?\/import.*?\/(archives|index)$/) {
222 print $outhandle "RecPlug: $dirname appears to be a reference to a Greenstone collection, skipping.\n";
223 return 0;
224 }
225
226 # check to see we haven't got a cyclic path...
227 if ($dirname =~ m%(/.*){,41}%) {
228 print $outhandle "RecPlug: $dirname is 40 directories deep, is this a recursive path? if not increase constant in RecPlug.pm.\n";
229 return 0;
230 }
231
232 # check to see we haven't got a cyclic path...
233 if ($dirname =~ m%.*?import/(.+?)/import/\1.*%) {
234 print $outhandle "RecPlug: $dirname appears to be in a recursive loop...\n";
235 return 0;
236 }
237
238 if (($verbosity > 2) && ((scalar keys %$in_metadata) > 0)) {
239 print $outhandle "RecPlug: metadata passed in: ",
240 join(", ", keys %$in_metadata), "\n";
241 }
242
243 # Recur over directory contents.
244 my (@dir, $subfile);
245 my $count = 0;
246
247 print $outhandle "RecPlug: getting directory $dirname\n" if ($verbosity);
248
249 # find all the files in the directory
250 if (!opendir (DIR, $dirname)) {
251 if ($gli) {
252 print STDERR "<ProcessingError n='$file' r='Could not read directory $dirname'>\n";
253 }
254 print $outhandle "RecPlug: WARNING - couldn't read directory $dirname\n";
255 return -1; # error in processing
256 }
257 @dir = readdir (DIR);
258 closedir (DIR);
259
260 # Re-order the files in the list so any directories ending with .all are moved to the end
261 for (my $i = scalar(@dir) - 1; $i >= 0; $i--) {
262 if (-d &util::filename_cat($dirname, $dir[$i]) && $dir[$i] =~ /\.all$/) {
263 push(@dir, splice(@dir, $i, 1));
264 }
265 }
266
267 # read XML metadata files (if supplied)
268 my $additionalmetadata = 0; # is there extra metadata available?
269 my %extrametadata; # maps from filespec to extra metadata keys
270 my @extrametakeys; # keys of %extrametadata in order read
271
272 my $dirsepre = &util::get_re_dirsep();
273 my $dirsep = &util::get_dirsep();
274 my $local_dirname = $dirname;
275 $local_dirname =~ s/^$base_dir($dirsepre)//;
276 $local_dirname .= $dirsep;
277
278 if (defined $self->{'subdir_extrametakeys'}->{$local_dirname}) {
279 my $extrakeys = $self->{'subdir_extrametakeys'}->{$local_dirname};
280 foreach my $ek (@$extrakeys) {
281 my $extrakeys_re = $ek->{'re'};
282 my $extrakeys_md = $ek->{'md'};
283 push(@extrametakeys,$extrakeys_re);
284 $extrametadata{$extrakeys_re} = $extrakeys_md;
285 }
286 delete($self->{'subdir_extrametakeys'}->{$local_dirname});
287 }
288
289 if ($read_metadata_files) {
290 #read the directory "metadata.xml" file
291 my $metadatafile = &util::filename_cat ($dirname, 'metadata.xml');
292 if (-e $metadatafile) {
293 print $outhandle "RecPlug: found metadata in $metadatafile\n"
294 if ($verbosity);
295 $self->read_metadata_xml_file($metadatafile, \%extrametadata, \@extrametakeys);
296 $additionalmetadata = 1;
297 }
298 }
299
300 # apply metadata pass for each of the files in the directory
301 my $out_metadata;
302 my $num_files = scalar(@dir);
303 for (my $i = 0; $i < scalar(@dir); $i++) {
304 my $subfile = $dir[$i];
305 my $this_file_base_dir = $base_dir;
306 last if ($maxdocs != -1 && $count >= $maxdocs);
307 next if ($subfile =~ m/^\.\.?$/);
308 #next if ($read_metadata_files && $subfile =~ /metadata\.xml$/);
309
310 # Recursively read each $subfile
311 print $outhandle "RecPlug metadata recurring: $subfile\n" if ($verbosity > 2);
312
313 $count += &plugin::metadata_read ($pluginfo, $this_file_base_dir,
314 &util::filename_cat($file, $subfile),
315 $out_metadata, \@extrametakeys, \%extrametadata,
316 $processor, $maxdocs, $gli);
317 $additionalmetadata = 1;
318 }
319
320 # filter out any extrametakeys that mention subdirectories and store
321 # for later use (i.e. when that sub-directory is being processed)
322
323 foreach my $ek (@extrametakeys) {
324 my ($subdir_re,$extrakey_dir) = &File::Basename::fileparse($ek);
325 $extrakey_dir =~ s/\\\./\./g; # remove RE syntax
326
327 my $dirsep_re = &util::get_re_dirsep();
328
329 if ($ek =~ m/$dirsep_re/) { # specifies at least one directory
330 my $md = $extrametadata{$ek};
331
332 my $subdir_extrametakeys = $self->{'subdir_extrametakeys'};
333
334 my $subdir_rec = { 're' => $subdir_re, 'md' => $md };
335 push(@{$subdir_extrametakeys->{$extrakey_dir}},$subdir_rec);
336 }
337 }
338
339 # import each of the files in the directory
340 $count=0;
341 for (my $i = 0; $i <= scalar(@dir); $i++) {
342 # When every file in the directory has been done, pause for a moment (figuratively!)
343 # If the -recheck_directories argument hasn't been provided, stop now (default)
344 # Otherwise, re-read the contents of the directory to check for new files
345 # Any new files are added to the @dir list and are processed as normal
346 # This is necessary when documents to be indexed are specified in bibliographic DBs
347 # These files are copied/downloaded and stored in a new folder at import time
348 if ($i == $num_files) {
349 last unless $self->{'recheck_directories'};
350
351 # Re-read the files in the directory to see if there are any new files
352 last if (!opendir (DIR, $dirname));
353 my @dirnow = readdir (DIR);
354 closedir (DIR);
355
356 # We're only interested if there are more files than there were before
357 last if (scalar(@dirnow) <= scalar(@dir));
358
359 # Any new files are added to the end of @dir to get processed by the loop
360 my $j;
361 foreach my $subfilenow (@dirnow) {
362 for ($j = 0; $j < $num_files; $j++) {
363 last if ($subfilenow eq $dir[$j]);
364 }
365 if ($j == $num_files) {
366 # New file
367 push(@dir, $subfilenow);
368 }
369 }
370 # When the new files have been processed, check again
371 $num_files = scalar(@dir);
372 }
373
374 my $subfile = $dir[$i];
375 my $this_file_base_dir = $base_dir;
376 last if ($maxdocs != -1 && ($count + $total_count) >= $maxdocs);
377 next if ($subfile =~ /^\.\.?$/);
378 next if ($read_metadata_files && $subfile =~ /metadata\.xml$/);
379
380 # Follow Windows shortcuts
381 if ($subfile =~ /(?i)\.lnk$/ && $ENV{'GSDLOS'} =~ /^windows$/i) {
382 require Win32::Shortcut;
383 my $shortcut = new Win32::Shortcut(&util::filename_cat($dirname, $subfile));
384 if ($shortcut) {
385 # The file to be processed is now the target of the shortcut
386 $this_file_base_dir = "";
387 $file = "";
388 $subfile = $shortcut->Path;
389 }
390 }
391
392 # check for a symlink pointing back to a leading directory
393 if (-d "$dirname/$subfile" && -l "$dirname/$subfile") {
394 # readlink gives a "fatal error" on systems that don't implement
395 # symlinks. This assumes the the -l test above would fail on those.
396 my $linkdest=readlink "$dirname/$subfile";
397 if (!defined ($linkdest)) {
398 # system error - file not found?
399 warn "RecPlug: symlink problem - $!";
400 } else {
401 # see if link points to current or a parent directory
402 if ($linkdest =~ m@^[\./\\]+$@ ||
403 index($dirname, $linkdest) != -1) {
404 warn "RecPlug: Ignoring recursive symlink ($dirname/$subfile -> $linkdest)\n";
405 next;
406 ;
407 }
408 }
409 }
410
411 print $outhandle "RecPlug: preparing metadata for $subfile\n" if ($verbosity > 2);
412
413 # Make a copy of $in_metadata to pass to $subfile
414 $out_metadata = {};
415 &combine_metadata_structures($out_metadata, $in_metadata);
416
417 # Next add metadata read in XML files (if it is supplied)
418 if ($additionalmetadata == 1) {
419
420 my ($filespec, $mdref);
421 foreach $filespec (@extrametakeys) {
422 if ($subfile =~ /^$filespec$/) {
423 print $outhandle "File \"$subfile\" matches filespec \"$filespec\"\n"
424 if ($verbosity > 2);
425 $mdref = $extrametadata{$filespec};
426 &combine_metadata_structures($out_metadata, $mdref);
427 }
428 }
429 }
430
431
432 my $file_subfile = &util::filename_cat($file, $subfile);
433 my $filename_subfile
434 = &util::filename_cat($this_file_base_dir,$file_subfile);
435 if (defined $self->{'inf_timestamp'}) {
436 my $inf_timestamp = $self->{'inf_timestamp'};
437
438 if (! -d $filename_subfile) {
439 my $filename_timestamp = -M $filename_subfile;
440 if ($filename_timestamp > $inf_timestamp) {
441 # filename has been around for longer than inf
442##### print $outhandle "**** Skipping $subfile\n";
443 next;
444 }
445 }
446 }
447
448 # Recursively read each $subfile
449 print $outhandle "RecPlug recurring: $subfile\n" if ($verbosity > 2);
450
451 $count += &plugin::read ($pluginfo, $this_file_base_dir,
452 $file_subfile,
453 $out_metadata, $processor, $maxdocs, ($total_count + $count), $gli);
454 }
455
456 return $count;
457}
458
459
460
461# Read a manually-constructed metadata file and store the data
462# it contains in the $metadataref structure.
463#
464# (metadataref is a reference to a hash whose keys are filenames
465# and whose values are metadata hash structures.)
466
467sub read_metadata_xml_file {
468 my $self = shift(@_);
469 my ($filename, $metadataref, $metakeysref) = @_;
470 $self->{'metadataref'} = $metadataref;
471 $self->{'metakeysref'} = $metakeysref;
472
473 eval {
474 $self->{'parser'}->parsefile($filename);
475 };
476
477 if ($@) {
478 die "RecPlug: ERROR $filename is not a well formed metadata.xml file ($@)\n";
479 }
480}
481
482sub Doctype {
483 my ($expat, $name, $sysid, $pubid, $internal) = @_;
484
485 # allow the short-lived and badly named "GreenstoneDirectoryMetadata" files
486 # to be processed as well as the "DirectoryMetadata" files which should now
487 # be created by import.pl
488 die if ($name !~ /^(Greenstone)?DirectoryMetadata$/);
489}
490
491sub StartTag {
492 my ($expat, $element) = @_;
493
494 if ($element eq "FileSet") {
495 $self->{'saved_targets'} = [];
496 $self->{'saved_metadata'} = {};
497 }
498 elsif ($element eq "FileName") {
499 $self->{'in_filename'} = 1;
500 }
501 elsif ($element eq "Metadata") {
502 $self->{'metadata_name'} = $_{'name'};
503 if ((defined $_{'mode'}) && ($_{'mode'} eq "accumulate")) {
504 $self->{'metadata_accumulate'} = 1;
505 } else {
506 $self->{'metadata_accumulate'} = 0;
507 }
508 }
509}
510
511sub EndTag {
512 my ($expat, $element) = @_;
513
514 if ($element eq "FileSet") {
515 push (@{$self->{'metakeysref'}}, @{$self->{'saved_targets'}});
516 foreach my $target (@{$self->{'saved_targets'}}) {
517 my $file_metadata = $self->{'metadataref'}->{$target};
518 my $saved_metadata = $self->{'saved_metadata'};
519 if (!defined $file_metadata) {
520 $self->{'metadataref'}->{$target} = $saved_metadata;
521 }
522 else {
523 $self->combine_metadata_structures($file_metadata,$saved_metadata);
524 }
525 }
526 }
527 elsif ($element eq "FileName") {
528 $self->{'in_filename'} = 0;
529 }
530 elsif ($element eq "Metadata") {
531 $self->{'metadata_name'} = "";
532 }
533
534}
535
536sub store_saved_metadata
537{
538 my $self = shift(@_);
539 my ($mname,$mvalue,$md_accumulate) = @_;
540
541 if (defined $self->{'saved_metadata'}->{$mname}) {
542 if ($md_accumulate) {
543 # accumulate mode - add value to existing value(s)
544 if (ref ($self->{'saved_metadata'}->{$mname}) eq "ARRAY") {
545 push (@{$self->{'saved_metadata'}->{$mname}}, $mvalue);
546 } else {
547 $self->{'saved_metadata'}->{$mname} =
548 [$self->{'saved_metadata'}->{$mname}, $mvalue];
549 }
550 } else {
551 # override mode
552 $self->{'saved_metadata'}->{$mname} = $mvalue;
553 }
554 } else {
555 if ($md_accumulate) {
556 # accumulate mode - add value into (currently empty) array
557 $self->{'saved_metadata'}->{$mname} = [$mvalue];
558 } else {
559 # override mode
560 $self->{'saved_metadata'}->{$mname} = $mvalue;
561 }
562 }
563}
564
565
566sub Text {
567
568 if ($self->{'in_filename'}) {
569 # $_ == FileName content
570 push (@{$self->{'saved_targets'}}, $_);
571 }
572 elsif (defined ($self->{'metadata_name'}) && $self->{'metadata_name'} ne "") {
573 # $_ == Metadata content
574 my $mname = $self->{'metadata_name'};
575 my $mvalue = $_;
576 my $md_accumulate = $self->{'metadata_accumulate'};
577 $self->store_saved_metadata($mname,$mvalue,$md_accumulate);
578 }
579}
580
581# This Char function overrides the one in XML::Parser::Stream to overcome a
582# problem where $expat->{Text} is treated as the return value, slowing
583# things down significantly in some cases.
584sub Char {
585 use bytes; # Necessary to prevent encoding issues with XML::Parser 2.31+
586 $_[0]->{'Text'} .= $_[1];
587 return undef;
588}
589
590# Combine two metadata structures. Given two references to metadata
591# element structures, add every field of the second ($mdref2) to the first
592# ($mdref1).
593#
594# Afterwards $mdref1 will be updated, and $mdref2 will be unchanged.
595#
596# We have to be careful about the way we merge metadata when one metadata
597# structure is in "override" mode and one is in "merge" mode. In fact, we
598# use the mode from the second structure, $mdref2, because it is generally
599# defined later (lower in the directory structure) and is therefore more
600# "local" to the document concerned.
601#
602# Another issue is the use of references to pass metadata around. If we
603# simply copy one metadata structure reference to another, then we're
604# effectively just copyinga pointer, and changes to the new referene
605# will affect the old (copied) one also. This also applies to ARRAY
606# references used as metadata element values (hence the "clonedata"
607# function below).
608
609sub combine_metadata_structures {
610 my ($mdref1, $mdref2) = @_;
611 my ($key, $value1, $value2);
612
613 foreach $key (keys %$mdref2) {
614
615 $value1 = $mdref1->{$key};
616 $value2 = $mdref2->{$key};
617
618 # If there is no existing value for this metadata field in
619 # $mdref1, so we simply copy the value from $mdref2 over.
620 if (!defined $value1) {
621 $mdref1->{$key} = &clonedata($value2);
622 }
623 # Otherwise we have to add the new values to the existing ones.
624 # If the second structure is accumulated, then acculate all the
625 # values into the first structure
626 elsif ((ref $value2) eq "ARRAY") {
627 # If the first metadata element is a scalar we have to
628 # convert it into an array before we add anything more.
629 if ((ref $value1) ne 'ARRAY') {
630 $mdref1->{$key} = [$value1];
631 $value1 = $mdref1->{$key};
632 }
633 # Now add the value(s) from the second array to the first
634 $value2 = &clonedata($value2);
635 push @$value1, @$value2;
636 }
637 # Finally, If the second structure is not an array erference, we
638 # know it is in override mode, so override the first structure.
639 else {
640 $mdref1->{$key} = &clonedata($value2);
641 }
642 }
643}
644
645
646# Make a "cloned" copy of a metadata value.
647# This is trivial for a simple scalar value,
648# but not for an array reference.
649
650sub clonedata {
651 my ($value) = @_;
652 my $result;
653
654 if ((ref $value) eq 'ARRAY') {
655 $result = [];
656 foreach my $item (@$value) {
657 push @$result, $item;
658 }
659 } else {
660 $result = $value;
661 }
662 return $result;
663}
664
665
6661;
Note: See TracBrowser for help on using the repository browser.