source: trunk/plugins/ExtendedFileManager/Classes/ExtendedFileManager.php @ 580

Last change on this file since 580 was 579, checked in by ray, 13 years ago

ExtendedFileManager? updates:

*to complete cofigurability through js directories can now be set for images and files individually (optional & backwards compatible)

  • relocated button from the end of toolbar next to createlink
  • insert image/link with double click
  • preview img didn't have src when no image selected, which showed a "image not found icon" in IE
  • slight modifications and updates in Readme.txt (kicked out afru's website. The domain isn't even registered anymore)
File size: 21.2 KB
Line 
1<?php
2/**
3 * ExtendedFileManager, list images, directories, and thumbnails.
4 * Authors: Wei Zhuo, Afru, Krzysztof Kotowicz
5 * Version: Updated on 08-01-2005 by Afru
6 * Version: Updated on 04-07-2006 by Krzysztof Kotowicz
7 * Package: ExtendedFileManager (EFM 1.1.2)
8 * http://www.afrusoft.com/htmlarea
9 */
10
11/**
12 * We use classes from ImageManager to avoid code duplication
13 */
14require_once '../ImageManager/Classes/Files.php';
15
16/**
17 * ExtendedFileManager Class.
18 * @author $Author: Wei Zhuo $
19 * @author $Author: Krzysztof Kotowicz $
20 * @version $Id: ExtendedFileManager.php 27 2004-04-01 08:31:57Z Wei Zhuo $
21 */
22class ExtendedFileManager
23{
24        /**
25         * Configuration array.
26         */
27        var $config;
28
29        /**
30         * Array of directory information.
31         */
32        var $dirs;
33       
34    /**
35     * Manager mode - image | link
36     */
37        var $mode;
38
39        /**
40         * Constructor. Create a new Image Manager instance.
41         * @param array $config configuration array, see config.inc.php
42         */
43        function ExtendedFileManager($config, $mode = null)
44        {
45                $this->config = $config;
46               
47                $this->mode = empty($mode) ? (empty($config['insert_mode']) ? 'image' : $config['insert_mode']): $mode;
48        }
49
50        /**
51         * Get the base directory.
52         * @return string base dir, see config.inc.php
53         */
54        function getImagesDir()
55        {
56                if ($this->mode == 'link' && isset($this->config['files_dir']))
57                        Return $this->config['files_dir'];
58                else Return $this->config['images_dir'];
59        }
60
61        /**
62         * Get the base URL.
63         * @return string base url, see config.inc.php
64         */
65        function getImagesURL()
66        {
67                if ($this->mode == 'link' && isset($this->config['files_url']))
68                                Return $this->config['files_url'];
69                else Return $this->config['images_url'];
70        }
71
72        function isValidBase()
73        {
74                return is_dir($this->getImagesDir());
75        }
76
77        /**
78         * Get the tmp file prefix.
79     * @return string tmp file prefix.
80         */
81        function getTmpPrefix()
82        {
83                Return $this->config['tmp_prefix'];
84        }
85
86        /**
87         * Get the sub directories in the base dir.
88         * Each array element contain
89         * the relative path (relative to the base dir) as key and the
90         * full path as value.
91         * @return array of sub directries
92         * <code>array('path name' => 'full directory path', ...)</code>
93         */
94        function getDirs()
95        {
96                if(is_null($this->dirs))
97                {
98                        $dirs = $this->_dirs($this->getImagesDir(),'/');
99                        ksort($dirs);
100                        $this->dirs = $dirs;
101                }
102                return $this->dirs;
103        }
104
105        /**
106         * Recursively travese the directories to get a list
107         * of accessable directories.
108         * @param string $base the full path to the current directory
109         * @param string $path the relative path name
110         * @return array of accessiable sub-directories
111         * <code>array('path name' => 'full directory path', ...)</code>
112         */
113        function _dirs($base, $path)
114        {
115                $base = Files::fixPath($base);
116                $dirs = array();
117
118                if($this->isValidBase() == false)
119                        return $dirs;
120
121                $d = @dir($base);
122               
123                while (false !== ($entry = $d->read()))
124                {
125                        //If it is a directory, and it doesn't start with
126                        // a dot, and if is it not the thumbnail directory
127                        if(is_dir($base.$entry)
128                                && substr($entry,0,1) != '.'
129                                && $this->isThumbDir($entry) == false)
130                        {
131                                $relative = Files::fixPath($path.$entry);
132                                $fullpath = Files::fixPath($base.$entry);
133                                $dirs[$relative] = $fullpath;
134                                $dirs = array_merge($dirs, $this->_dirs($fullpath, $relative));
135                        }
136                }
137                $d->close();
138
139                Return $dirs;
140        }
141
142        /**
143         * Get all the files and directories of a relative path.
144         * @param string $path relative path to be base path.
145         * @return array of file and path information.
146         * <code>array(0=>array('relative'=>'fullpath',...), 1=>array('filename'=>fileinfo array(),...)</code>
147         * fileinfo array: <code>array('url'=>'full url',
148         *                       'relative'=>'relative to base',
149         *                        'fullpath'=>'full file path',
150         *                        'image'=>imageInfo array() false if not image,
151         *                        'stat' => filestat)</code>
152         */
153        function getFiles($path)
154        {
155                $files = array();
156                $dirs = array();
157
158        $valid_extensions = $this->mode == 'image' ? $this->config['allowed_image_extensions'] : $this->config['allowed_link_extensions'];
159
160                if($this->isValidBase() == false)
161                        return array($files,$dirs);
162
163                $path = Files::fixPath($path);
164                $base = Files::fixPath($this->getImagesDir());
165                $fullpath = Files::makePath($base,$path);
166
167
168                $d = @dir($fullpath);
169               
170                while (false !== ($entry = $d->read()))
171                {
172                        //not a dot file or directory
173                        if(substr($entry,0,1) != '.')
174                        {
175                                if(is_dir($fullpath.$entry)
176                                        && $this->isThumbDir($entry) == false)
177                                {
178                                        $relative = Files::fixPath($path.$entry);
179                                        $full = Files::fixPath($fullpath.$entry);
180                                        $count = $this->countFiles($full);
181                                        $dirs[$relative] = array('fullpath'=>$full,'entry'=>$entry,'count'=>$count, 'stat'=>stat($fullpath.$entry));
182                                }
183
184                                else if(is_file($fullpath.$entry) && $this->isThumb($entry)==false && $this->isTmpFile($entry) == false)
185                                {
186                                        $afruext = strtolower(substr(strrchr($entry, "."), 1));
187
188                    if(in_array($afruext,$valid_extensions))
189                                        {
190
191                                                $file['url'] = Files::makePath($this->config['base_url'],$path).$entry;
192                                                $file['relative'] = $path.$entry;
193                                                $file['fullpath'] = $fullpath.$entry;
194                                                $img = $this->getImageInfo($fullpath.$entry);
195                                                if(!is_array($img)) $img[0]=$img[1]=0;
196                                                $file['image'] = $img;
197                                                $file['stat'] = stat($fullpath.$entry);
198                                                $file['ext'] = $afruext;
199                                                $files[$entry] = $file;
200                                        }
201
202                                }
203                        }
204                }
205                $d->close();
206                ksort($dirs);
207                ksort($files);
208               
209                Return array($dirs, $files);
210        }       
211
212        /**
213         * Count the number of files and directories in a given folder
214         * minus the thumbnail folders and thumbnails.
215         */
216        function countFiles($path)
217        {
218                $total = 0;
219
220                if(is_dir($path))
221                {
222                        $d = @dir($path);
223
224                        while (false !== ($entry = $d->read()))
225                        {
226                                //echo $entry."<br>";
227                                if(substr($entry,0,1) != '.'
228                                        && $this->isThumbDir($entry) == false
229                                        && $this->isTmpFile($entry) == false
230                                        && $this->isThumb($entry) == false)
231                                {
232                                        $total++;
233                                }
234                        }
235                        $d->close();
236                }
237                return $total;
238        }
239
240        /**
241         * Get image size information.
242         * @param string $file the image file
243         * @return array of getImageSize information,
244         *  false if the file is not an image.
245         */
246        function getImageInfo($file)
247        {
248                Return @getImageSize($file);
249        }
250
251        /**
252         * Check if the file contains the thumbnail prefix.
253         * @param string $file filename to be checked
254         * @return true if the file contains the thumbnail prefix, false otherwise.
255         */
256        function isThumb($file)
257        {
258                $len = strlen($this->config['thumbnail_prefix']);
259                if(substr($file,0,$len)==$this->config['thumbnail_prefix'])
260                        Return true;
261                else
262                        Return false;
263        }
264
265        /**
266         * Check if the given directory is a thumbnail directory.
267         * @param string $entry directory name
268         * @return true if it is a thumbnail directory, false otherwise
269         */
270        function isThumbDir($entry)
271        {
272                if($this->config['thumbnail_dir'] == false
273                        || strlen(trim($this->config['thumbnail_dir'])) == 0)
274                        Return false;           
275                else
276                        Return ($entry == $this->config['thumbnail_dir']);
277        }
278
279        /**
280         * Check if the given file is a tmp file.
281         * @param string $file file name
282         * @return boolean true if it is a tmp file, false otherwise
283         */
284        function isTmpFile($file)
285        {
286                $len = strlen($this->config['tmp_prefix']);
287                if(substr($file,0,$len)==$this->config['tmp_prefix'])
288                        Return true;
289                else
290                        Return false;           
291        }
292
293        /**
294         * For a given image file, get the respective thumbnail filename
295         * no file existence check is done.
296         * @param string $fullpathfile the full path to the image file
297         * @return string of the thumbnail file
298         */
299        function getThumbName($fullpathfile)
300        {
301                $path_parts = pathinfo($fullpathfile);
302               
303                $thumbnail = $this->config['thumbnail_prefix'].$path_parts['basename'];
304
305                if($this->config['safe_mode'] == true
306                        || strlen(trim($this->config['thumbnail_dir'])) == 0)
307                {
308                        Return Files::makeFile($path_parts['dirname'],$thumbnail);
309                }
310                else
311                {
312                        if(strlen(trim($this->config['thumbnail_dir'])) > 0)
313                        {
314                                $path = Files::makePath($path_parts['dirname'],$this->config['thumbnail_dir']);
315                                if(!is_dir($path))
316                                        Files::createFolder($path);
317                                Return Files::makeFile($path,$thumbnail);
318                        }
319                        else //should this ever happen?
320                        {
321                                //error_log('ExtendedFileManager: Error in creating thumbnail name');
322                        }
323                }
324        }
325       
326        /**
327         * Similar to getThumbName, but returns the URL, base on the
328         * given base_url in config.inc.php
329         * @param string $relative the relative image file name,
330         * relative to the base_dir path
331         * @return string the url of the thumbnail
332         */
333        function getThumbURL($relative)
334        {
335                $path_parts = pathinfo($relative);
336                $thumbnail = $this->config['thumbnail_prefix'].$path_parts['basename'];
337                if($path_parts['dirname']=='\\') $path_parts['dirname']='/';
338
339                if($this->config['safe_mode'] == true
340                        || strlen(trim($this->config['thumbnail_dir'])) == 0)
341                {
342                        Return Files::makeFile($this->getImagesURL(),$thumbnail);
343                }
344                else
345                {
346                        if(strlen(trim($this->config['thumbnail_dir'])) > 0)
347                        {
348                                $path = Files::makePath($path_parts['dirname'],$this->config['thumbnail_dir']);
349                                $url_path = Files::makePath($this->getImagesURL(), $path);
350                                Return Files::makeFile($url_path,$thumbnail);
351                        }
352                        else //should this ever happen?
353                        {
354                                //error_log('ExtendedFileManager: Error in creating thumbnail url');
355                        }
356
357                }
358        }
359
360   /**
361    * For a given image file, get the respective resized filename
362    * no file existence check is done.
363    * @param string $fullpathfile the full path to the image file
364    * @param integer $width the intended width
365    * @param integer $height the intended height
366    * @param boolean $mkDir whether to attempt to make the resized_dir if it doesn't exist
367    * @return string of the resized filename
368    */
369        function getResizedName($fullpathfile, $width, $height, $mkDir = TRUE)
370        {
371                $path_parts = pathinfo($fullpathfile);
372
373                $thumbnail = $this->config['resized_prefix']."_{$width}x{$height}_{$path_parts['basename']}";
374
375                if( strlen(trim($this->config['resized_dir'])) == 0 || $this->config['safe_mode'] == true )
376                {
377                        Return Files::makeFile($path_parts['dirname'],$thumbnail);
378                }
379                else
380                {
381      $path = Files::makePath($path_parts['dirname'],$this->config['resized_dir']);
382      if($mkDir && !is_dir($path))
383        Files::createFolder($path);
384      Return Files::makeFile($path,$thumbnail);
385                }
386        }
387
388        /**
389         * Check if the given path is part of the subdirectories
390         * under the base_dir.
391         * @param string $path the relative path to be checked
392         * @return boolean true if the path exists, false otherwise
393         */
394        function validRelativePath($path)
395        {
396                $dirs = $this->getDirs();
397                if($path == '/')
398                        Return true;
399                //check the path given in the url against the
400                //list of paths in the system.
401                for($i = 0; $i < count($dirs); $i++)
402                {
403                        $key = key($dirs);
404                        //we found the path
405                        if($key == $path)
406                                Return true;
407               
408                        next($dirs);
409                }               
410                Return false;
411        }
412
413        /**
414         * Process uploaded files, assumes the file is in
415         * $_FILES['upload'] and $_POST['dir'] is set.
416         * The dir must be relative to the base_dir and exists.
417         * @return null
418         */
419        function processUploads()
420        {
421                if($this->isValidBase() == false)
422                        return;
423
424                $relative = null;
425
426                if(isset($_POST['dir']))
427                        $relative = rawurldecode($_POST['dir']);
428                else
429                        return;
430
431                //check for the file, and must have valid relative path
432                if(isset($_FILES['upload']) && $this->validRelativePath($relative))
433                {
434                        Return $this->_processFiles($relative, $_FILES['upload']);
435                }
436        }
437
438        /**
439         * Process upload files. The file must be an
440         * uploaded file. Any duplicate
441         * file will be renamed. See Files::copyFile for details
442         * on renaming.
443         * @param string $relative the relative path where the file
444         * should be copied to.
445         * @param array $file the uploaded file from $_FILES
446         * @return boolean true if the file was processed successfully,
447         * false otherwise
448         */
449        function _processFiles($relative, $file)
450        {
451               
452                if($file['error']!=0)
453                {
454                        Return false;
455                }
456
457                if(!is_file($file['tmp_name']))
458                {
459                        Return false;
460                }
461
462                if(!is_uploaded_file($file['tmp_name']))
463                {
464                        Files::delFile($file['tmp_name']);
465                        Return false;
466                }
467
468        $valid_extensions = $this->mode == 'image' ? $this->config['allowed_image_extensions'] : $this->config['allowed_link_extensions'];
469        $max_size = $this->mode == 'image' ? $this->config['max_filesize_kb_image'] : $this->config['max_filesize_kb_link'];
470                $afruext = strtolower(substr(strrchr($file['name'], "."), 1));
471
472                if(!in_array($afruext, $valid_extensions))
473                {
474                        Files::delFile($file['tmp_name']);
475                        Return "Cannot upload .".$afruext." Files. Permission denied.";
476                }
477
478                if($file['size']>($max_size*1024))
479                {
480                        Files::delFile($file['tmp_name']);
481                        Return "Unble to upload file. Maximum file size [".$max_size."Kb] exceeded.";
482                }
483
484                if(!empty($this->config['max_foldersize_mb']) &&  (Files::dirSize($this->getImagesDir()))+$file['size']> ($this->config['max_foldersize_mb']*1048576))
485                {
486                        Files::delFile($file['tmp_name']);
487                        Return ("Cannot upload. Maximum folder size reached. Delete unwanted files and try again.");
488                }
489
490                //now copy the file
491                $path = Files::makePath($this->getImagesDir(),$relative);
492                $result = Files::copyFile($file['tmp_name'], $path, $file['name']);
493
494                //no copy error
495                if(!is_int($result))
496                {
497                        Files::delFile($file['tmp_name']);
498                        Return $file['name']." successfully uploaded.";
499                }
500
501                //delete tmp files.
502                Files::delFile($file['tmp_name']);
503                Return false;
504
505        }
506
507
508        function getDiskInfo()
509        {
510        if (empty($this->config['max_foldersize_mb']))
511            return '';
512           
513                $tmpFreeSize=($this->config['max_foldersize_mb']*1048576)-Files::dirSize($this->getImagesDir());
514
515                if(!is_numeric($tmpFreeSize) || $tmpFreeSize<0) $tmpFreeSize=0;
516       
517                Return 'Total Size : '.$this->config['max_foldersize_mb'].' Mb , Free Space: '.Files::formatSize($tmpFreeSize);
518        }
519
520
521
522        /**
523         * Get the URL of the relative file.
524         * basically appends the relative file to the
525         * base_url given in config.inc.php
526         * @param string $relative a file the relative to the base_dir
527         * @return string the URL of the relative file.
528         */
529        function getFileURL($relative)
530        {
531                Return Files::makeFile($this->getImagesURL(),$relative);
532        }
533
534        /**
535         * Get the fullpath to a relative file.
536         * @param string $relative the relative file.
537         * @return string the full path, .ie. the base_dir + relative.
538         */
539        function getFullPath($relative)
540        {
541                Return Files::makeFile($this->getImagesDir(),$relative);;
542        }
543
544        /**
545         * Get the default thumbnail.
546         * @return string default thumbnail, empty string if
547         * the thumbnail doesn't exist.
548         */
549        function getDefaultThumb()
550        {
551                if(is_file($this->config['default_thumbnail']))
552                        Return $this->config['default_thumbnail'];
553                else
554                        Return '';
555        }
556
557
558        /**
559         * Checks image size. If the image size is less than default size
560         * returns the original size else returns default size to display thumbnail
561        */
562        function checkImageSize($relative)
563        {
564                $fullpath = Files::makeFile($this->getImagesDir(),$relative);
565
566                $afruext = strtolower(substr(strrchr($relative, "."), 1));
567               
568                if(!in_array($afruext,$this->config['thumbnail_extensions']))
569                {
570                        $imgInfo=array(0,0);
571                        Return $imgInfo;
572                }
573                else
574                {
575                        $imgInfo = @getImageSize($fullpath);
576                        //not an image
577                        if(!is_array($imgInfo))
578                        {
579                                $imgInfo=array(0,0);
580                                Return $imgInfo;
581                        }
582                        else
583                        {
584                                if($imgInfo[0] > $this->config['thumbnail_width'])
585                                $imgInfo[0] = $this->config['thumbnail_width'];
586
587                                if($imgInfo[1] > $this->config['thumbnail_height'])
588                                $imgInfo[1] = $this->config['thumbnail_height'];
589
590                                Return $imgInfo;
591                        }
592                }
593
594        }
595
596
597        /**
598         * Get the thumbnail url to be displayed.
599         * If the thumbnail exists, and it is up-to-date
600         * the thumbnail url will be returns. If the
601         * file is not an image, a default image will be returned.
602         * If it is an image file, and no thumbnail exists or
603         * the thumbnail is out-of-date (i.e. the thumbnail
604         * modified time is less than the original file)
605         * then a thumbs.php?img=filename.jpg is returned.
606         * The thumbs.php url will generate a new thumbnail
607         * on the fly. If the image is less than the dimensions
608         * of the thumbnails, the image will be display instead.
609         * @param string $relative the relative image file.
610         * @return string the url of the thumbnail, be it
611         * actually thumbnail or a script to generate the
612         * thumbnail on the fly.
613         */
614        function getThumbnail($relative)
615        {
616                global $IMConfig;
617               
618                $fullpath = Files::makeFile($this->getImagesDir(),$relative);
619
620                //not a file???
621                if(!is_file($fullpath))
622                        Return $this->getDefaultThumb();
623
624                $afruext = strtolower(substr(strrchr($relative, "."), 1));
625               
626                if(!in_array($afruext,$this->config['thumbnail_extensions']))
627                {
628                        if(is_file('icons/'.$afruext.'.gif'))
629                                Return('icons/'.$afruext.'.gif');
630                        else
631                                Return $this->getDefaultThumb();
632                }
633
634                $imgInfo = @getImageSize($fullpath);
635
636                //not an image
637                if(!is_array($imgInfo))
638                        Return $this->getDefaultThumb();
639
640
641                //Returning original image as thumbnail without Image Library by Afru
642                if(!$this->config['img_library']) Return $this->getFileURL($relative);
643
644
645                //the original image is smaller than thumbnails,
646                //so just return the url to the original image.
647                if ($imgInfo[0] <= $this->config['thumbnail_width']
648                 && $imgInfo[1] <= $this->config['thumbnail_height'])
649                        Return $this->getFileURL($relative);
650
651                $thumbnail = $this->getThumbName($fullpath);
652               
653                //check for thumbnails, if exists and
654                // it is up-to-date, return the thumbnail url
655                if(is_file($thumbnail))
656                {
657                        if(filemtime($thumbnail) >= filemtime($fullpath))
658                                Return $this->getThumbURL($relative);
659                }
660
661                //well, no thumbnail was found, so ask the thumbs.php
662                //to generate the thumbnail on the fly.
663                Return $IMConfig['backend_url'] . '__function=thumbs&img='.rawurlencode($relative)."&mode=$this->mode";
664        }
665
666        /**
667         * Delete and specified files.
668         * @return boolean true if delete, false otherwise
669         */
670        function deleteFiles()
671        {
672                if(isset($_GET['delf']))
673                        return $this->_delFile(rawurldecode($_GET['delf']));
674        return false;
675        }
676
677        /**
678         * Delete and specified directories.
679         * @return boolean true if delete, false otherwise
680         */
681        function deleteDirs()
682        {
683                 if(isset($_GET['deld']))
684                        return $this->_delDir(rawurldecode($_GET['deld']));             
685                 else
686                         Return false;
687        }
688
689        /**
690         * Delete the relative file, and any thumbnails.
691         * @param string $relative the relative file.
692         * @return boolean true if deleted, false otherwise.
693         */
694        function _delFile($relative)
695        {
696                $fullpath = Files::makeFile($this->getImagesDir(),$relative);
697       
698                $afruext = strtolower(substr(strrchr($relative, "."), 1));
699
700        $valid_extensions = $this->mode == 'image' ? $this->config['allowed_image_extensions'] : $this->config['allowed_link_extensions'];
701
702                if(!in_array($afruext,$valid_extensions))
703                {
704                        return false;
705                }
706
707                //check that the file is an image
708                if(is_array($this->getImageInfo($fullpath)))
709                {
710                        $thumbnail = $this->getThumbName($fullpath);
711                        Files::delFile($thumbnail);
712                }
713
714                Return Files::delFile($fullpath);
715        }
716
717        /**
718         * Delete directories recursively.
719         * @param string $relative the relative path to be deleted.
720         * @return boolean true if deleted, false otherwise.
721         */
722        function _delDir($relative)
723        {
724                $fullpath = Files::makePath($this->getImagesDir(),$relative);
725                if($this->countFiles($fullpath) <= 0)
726                        return Files::delFolder($fullpath,true); //delete recursively.
727                else
728                        Return false;
729        }
730
731        /**
732         * Create new directories.
733         * If in safe_mode, nothing happens.
734         * @return boolean true if created, false otherwise.
735         */
736        function processNewDir()
737        {
738                if($this->config['safe_mode'] == true)
739                        Return false;
740
741                if(isset($_GET['newDir']) && isset($_GET['dir']))
742                {
743                        $newDir = rawurldecode($_GET['newDir']);
744                        $dir = rawurldecode($_GET['dir']);
745                        $path = Files::makePath($this->getImagesDir(),$dir);
746                        $fullpath = Files::makePath($path, Files::escape($newDir));
747                        if(is_dir($fullpath))
748                                Return false;
749
750                        Return Files::createFolder($fullpath);
751                }
752        }
753
754        /**
755         * Renames files if certain GET variables are set
756         * @return bool
757         */
758        function processRenames()
759        {
760                if(!empty($_GET['rename']) && !empty($_GET['renameTo']))
761                {
762                        // new file name (without path and extension)
763                        $newName = Files::escape(rawurldecode($_GET['renameTo']));
764                        $newName = str_replace('.', '', $newName);
765
766                        // path to file (from base images directory)
767                        $oldName = rawurldecode($_GET['rename']);
768
769                        // strip parent dir ("..") to avoid escaping from base directiory
770                        $oldName = preg_replace('#\.\.#', '', $oldName);
771
772                        // path to old file
773                        $oldPath = Files::makeFile($this->getImagesDir(), $oldName);
774
775                        $ret = Files::renameFile($oldPath, $newName);
776                        if ($ret === true) {
777                                // delete old thumbnail
778                                Files::delFile($this->getThumbname($oldPath));
779                        }
780                        return $ret;
781                }
782               
783                return null;
784        }
785
786}
787
788?>
Note: See TracBrowser for help on using the repository browser.