default theme, menu and logo cell merged
[phpfspot.git] / phpfspot.class.php
1 <?php
2
3 /***************************************************************************
4  *
5  * Copyright (c) by Andreas Unterkircher, unki@netshadow.at
6  * All rights reserved
7  *
8  *  This program is free software; you can redistribute it and/or modify
9  *  it under the terms of the GNU General Public License as published by
10  *  the Free Software Foundation; either version 2 of the License, or
11  *  any later version.
12  *
13  *  This program is distributed in the hope that it will be useful,
14  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  *  GNU General Public License for more details.
17  *
18  *  You should have received a copy of the GNU General Public License
19  *  along with this program; if not, write to the Free Software
20  *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21  *
22  ***************************************************************************/
23
24 require_once "phpfspot_cfg.php";
25 require_once "phpfspot_db.php";
26
27 class PHPFSPOT {
28
29    var $cfg;
30    var $db;
31    var $cfg_db;
32    var $tmpl;
33    var $tags;
34    var $avail_tags;
35    private $dbver;
36
37    /**
38     * class constructor
39     *
40     * this function will be called on class construct
41     * and will check requirements, loads configuration,
42     * open databases and start the user session
43     */
44    public function __construct()
45    {
46       $this->cfg = new PHPFSPOT_CFG;
47
48       /* set application name and version information */
49       $this->cfg->product = "phpfspot";
50       $this->cfg->version = "1.2";
51
52       /* Check necessary requirements */
53       if(!$this->checkRequirements()) {
54          exit(1);
55       }
56
57       $this->db  = new PHPFSPOT_DB($this, $this->cfg->fspot_db);
58       if(!is_writeable($this->cfg->fspot_db)) {
59          print $this->cfg->fspot_db ." is not writeable for user ". $this->getuid() ."\n";
60          exit(1);
61       }
62
63       $this->dbver = $this->getFspotDBVersion();
64
65       if(!is_writeable(dirname($this->cfg->phpfspot_db))) {
66          print dirname($this->cfg->phpfspot_db) .": directory is not writeable for user ". $this->getuid() ."\n";
67          exit(1);
68       }
69
70       if(!is_writeable($this->cfg->base_path ."/templates_c")) {
71          print $this->cfg->base_path ."/templates_c: directory is not writeable for user ". $this->getuid() ."\n";
72          exit(1);
73       }
74
75       $this->cfg_db = new PHPFSPOT_DB($this, $this->cfg->phpfspot_db);
76       if(!is_writeable($this->cfg->phpfspot_db)) {
77          print $this->cfg->phpfspot_db ." is not writeable for user ". $this->getuid() ."\n";
78          exit(1);
79       }
80
81       $this->check_config_table();
82
83       /* include Smarty template engine */
84       if(!$this->check_readable($this->cfg->smarty_path .'/libs/Smarty.class.php')) {
85          exit(1);
86       }
87       require $this->cfg->smarty_path .'/libs/Smarty.class.php';
88       /* overload Smarty class if our own template handler */
89       require_once "phpfspot_tmpl.php";
90       $this->tmpl = new PHPFSPOT_TMPL($this);
91
92       /* check if all necessary indices exist */
93       $this->checkDbIndices();
94
95       /* if session is not yet started, do it now */
96       if(session_id() == "")
97          session_start();
98
99       if(!isset($_SESSION['tag_condition']))
100          $_SESSION['tag_condition'] = 'or';
101
102       if(!isset($_SESSION['sort_order']))
103          $_SESSION['sort_order'] = 'date_asc';
104
105       if(!isset($_SESSION['searchfor']))
106          $_SESSION['searchfor'] = '';
107
108       // if begin_with is still set but rows_per_page is now 0, unset it
109       if(isset($_SESSION['begin_with']) && $this->cfg->rows_per_page == 0)
110          unset($_SESSION['begin_with']);
111
112    } // __construct()
113
114    public function __destruct()
115    {
116
117    } // __destruct()
118
119    /**
120     * show - generate html output
121     *
122     * this function can be called after the constructor has
123     * prepared everyhing. it will load the index.tpl smarty
124     * template. if necessary it will registere pre-selects
125     * (photo index, photo, tag search, date search) into
126     * users session.
127     */
128    public function show()
129    {
130       $this->tmpl->assign('searchfor', $_SESSION['searchfor']);
131       $this->tmpl->assign('page_title', $this->cfg->page_title);
132       $this->tmpl->assign('current_condition', $_SESSION['tag_condition']);
133       $this->tmpl->assign('template_path', 'themes/'. $this->cfg->theme_name);
134
135       if(isset($_GET['mode'])) {
136
137          $_SESSION['start_action'] = $_GET['mode'];
138
139          switch($_GET['mode']) {
140             case 'showpi':
141                if(isset($_GET['tags'])) {
142                   $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
143                }
144                if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
145                   $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
146                }
147                if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
148                   $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
149                }
150                break;
151             case 'showp':
152                if(isset($_GET['tags'])) {
153                   $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
154                   $_SESSION['start_action'] = 'showp';
155                }
156                if(isset($_GET['id']) && is_numeric($_GET['id'])) {
157                   $_SESSION['current_photo'] = $_GET['id'];
158                   $_SESSION['start_action'] = 'showp';
159                }
160                if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
161                   $_SESSION['from_date'] = strtotime($_GET['from_date']);
162                }
163                if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
164                   $_SESSION['to_date'] = strtotime($_GET['to_date']);
165                }
166                break;
167             case 'export':
168                $this->tmpl->show("export.tpl");
169                return;
170                break;
171             case 'slideshow':
172                $this->tmpl->show("slideshow.tpl");
173                return;
174                break;
175             case 'rss':
176                if(isset($_GET['tags'])) {
177                   $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
178                }
179                if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
180                   $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
181                }
182                if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
183                   $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
184                }
185                $this->getRSSFeed();
186                return;
187                break;
188          }
189       }
190
191       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date']))
192          $this->tmpl->assign('date_search_enabled', true);
193
194       $this->tmpl->assign('from_date', $this->get_calendar('from'));
195       $this->tmpl->assign('to_date', $this->get_calendar('to'));
196       $this->tmpl->assign('sort_field', $this->get_sort_field());
197       $this->tmpl->assign('content_page', 'welcome.tpl');
198       $this->tmpl->show("index.tpl");
199
200    } // show()
201
202    /**
203     * get_tags - grab all tags of f-spot's database
204     *
205     * this function will get all available tags from
206     * the f-spot database and store them within two
207     * arrays within this class for later usage. in
208     * fact, if the user requests (hide_tags) it will
209     * opt-out some of them.
210     *
211     * this function is getting called once by show()
212     */
213    private function get_tags()
214    {
215       $this->avail_tags = Array();
216       $count = 0;
217    
218       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
219          $query_str="
220             SELECT
221                DISTINCT t1.id as id, t1.name as name
222             FROM  
223                photo_tags pt1
224             INNER JOIN photo_tags
225                pt2 ON pt1.photo_id=pt2.photo_id
226             INNER JOIN tags t1
227                ON t1.id=pt1.tag_id
228             INNER JOIN tags t2
229                ON t2.id=pt2.tag_id
230             WHERE
231                t2.name IN  ('".implode("','",$this->cfg->show_tags)."')
232             ORDER BY
233                t1.sort_priority ASC";
234
235          $result = $this->db->db_query($query_str);
236       }
237       else
238       {
239          $result = $this->db->db_query("
240             SELECT id,name
241             FROM tags
242             ORDER BY sort_priority ASC
243          ");
244       }
245       
246       while($row = $this->db->db_fetch_object($result)) {
247
248          $tag_id = $row['id'];
249          $tag_name = $row['name'];
250
251          /* if the user has specified to ignore this tag in phpfspot's
252             configuration, ignore it here so it does not get added to
253             the tag list.
254          */
255          if(in_array($row['name'], $this->cfg->hide_tags))
256             continue;
257
258          /* if you include the following if-clause and the user has specified
259             to only show certain tags which are specified in phpfspot's
260             configuration, ignore all others so they will not be added to the
261             tag list.
262          if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags) &&
263             !in_array($row['name'], $this->cfg->show_tags))
264             continue;
265          */
266
267          $this->tags[$tag_id] = $tag_name; 
268          $this->avail_tags[$count] = $tag_id;
269          $count++;
270
271       }
272
273    } // get_tags()
274
275    /** 
276     * extract all photo details
277     * 
278     * retrieve all available details from f-spot's
279     * database and return them as object
280     */
281    public function get_photo_details($idx)
282    {
283       if($this->dbver < 9) {
284          $query_str = "
285             SELECT p.id, p.name, p.time, p.directory_path, p.description
286             FROM photos p
287          ";
288       }
289       else {
290          $query_str = "
291             SELECT p.id, p.uri, p.time, p.description
292             FROM photos p
293          ";
294       }
295
296       /* if show_tags is set, only return details for photos which
297          are specified to be shown
298       */
299       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
300          $query_str.= "
301             INNER JOIN photo_tags pt
302                ON p.id=pt.photo_id
303             INNER JOIN tags t
304                ON pt.tag_id=t.id
305             WHERE p.id='". $idx ."'
306             AND t.name IN ('".implode("','",$this->cfg->show_tags)."')";
307       }
308       else {
309          $query_str.= "
310             WHERE p.id='". $idx ."'
311          ";
312       }
313
314       if($result = $this->db->db_query($query_str)) {
315
316          $row = $this->db->db_fetch_object($result);
317
318          if($this->dbver < 9) {
319             $row['uri'] = "file://". $row['directory_path'] ."/". $row['name'];
320          }
321
322          return $row;
323
324       }
325    
326       return null;
327
328    } // get_photo_details
329
330    /**
331     * returns aligned photo names 
332     *
333     * this function returns aligned (length) names for
334     * an specific photo. If the length of the name exceeds
335     * $limit the name will be shrinked (...)
336     */
337    public function getPhotoName($idx, $limit = 0)
338    {
339       if($details = $this->get_photo_details($idx)) {
340          if($long_name = $this->parse_uri($details['uri'], 'filename')) {
341             $name = $this->shrink_text($long_name, $limit);
342             return $name;
343          }
344       }
345
346       return null;
347
348    } // getPhotoName()
349
350    /**
351     * shrink text according provided limit
352     *
353     * If the length of the name exceeds $limit the
354     * text will be shortend and some content in between
355     * will be replaced with "..." 
356     */
357    private function shrink_text($text, $limit)
358    {
359       if($limit != 0 && strlen($text) > $limit) {
360          $text = substr($text, 0, $limit-5) ."...". substr($text, -($limit-5));
361       }
362
363       return $text;
364
365    } // shrink_text();
366
367    /**
368     * translate f-spoth photo path
369     * 
370     * as the full-qualified path recorded in the f-spot database
371     * is usally not the same as on the webserver, this function
372     * will replace the path with that one specified in the cfg
373     */
374    public function translate_path($path, $width = 0)
375    {  
376       return str_replace($this->cfg->path_replace_from, $this->cfg->path_replace_to, $path);
377
378    } // translate_path
379
380    /**
381     * control HTML ouput for a single photo
382     *
383     * this function provides all the necessary information
384     * for the single photo template.
385     */
386    public function showPhoto($photo)
387    {
388       /* get all photos from the current photo selection */
389       $all_photos = $this->getPhotoSelection();
390       $count = count($all_photos);
391
392       for($i = 0; $i < $count; $i++) {
393          
394          // $get_next will be set, when the photo which has to
395          // be displayed has been found - this means that the
396          // next available is in fact the NEXT image (for the
397          // navigation icons) 
398          if(isset($get_next)) {
399             $next_img = $all_photos[$i];
400             break;
401          }
402
403          /* the next photo is our NEXT photo */
404          if($all_photos[$i] == $photo) {
405             $get_next = 1;
406          }
407          else {
408             $previous_img = $all_photos[$i];
409          }
410
411          if($photo == $all_photos[$i]) {
412                $current = $i;
413          }
414       }
415
416       $details = $this->get_photo_details($photo);
417
418       if(!$details) {
419          print "error";
420          return;
421       }
422
423       $orig_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
424       $thumb_path = $this->get_thumb_path($this->cfg->photo_width, $photo);
425
426       if(!file_exists($orig_path)) {
427          $this->_error("Photo ". $orig_path ." does not exist!<br />\n");
428          return;
429       }
430
431       if(!is_readable($orig_path)) {
432          $this->_error("Photo ". $orig_path ." is not readable for user ". $this->getuid() ."<br />\n");
433          return;
434       }
435
436       /* If the thumbnail doesn't exist yet, try to create it */
437       if(!file_exists($thumb_path)) {
438          $this->gen_thumb($photo, true);
439          $thumb_path = $this->get_thumb_path($this->cfg->photo_width, $photo);
440       }
441
442       /* get f-spot database meta information */
443       $meta = $this->get_meta_informations($orig_path);
444
445       /* If EXIF data are available, use them */
446       if(isset($meta['ExifImageWidth'])) {
447          $meta_res = $meta['ExifImageWidth'] ."x". $meta['ExifImageLength'];
448       } else {
449          $info = getimagesize($orig_path);
450          $meta_res = $info[0] ."x". $info[1]; 
451       }
452
453       $meta_date = isset($meta['FileDateTime']) ? strftime("%a %x %X", $meta['FileDateTime']) : "n/a";
454       $meta_make = isset($meta['Make']) ? $meta['Make'] ." / ". $meta['Model'] : "n/a";
455       $meta_size = isset($meta['FileSize']) ? round($meta['FileSize']/1024, 1) ."kbyte" : "n/a";
456
457       $extern_link = "index.php?mode=showp&id=". $photo;
458       $current_tags = $this->getCurrentTags();
459       if($current_tags != "") {
460          $extern_link.= "&tags=". $current_tags;
461       }
462       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
463          $extern_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
464       }
465
466       $this->tmpl->assign('extern_link', $extern_link);
467
468       if(file_exists($thumb_path)) {
469
470          $info = getimagesize($thumb_path);
471
472          $this->tmpl->assign('description', $details['description']);
473          $this->tmpl->assign('image_name', $this->parse_uri($details['uri'], 'filename'));
474
475          $this->tmpl->assign('width', $info[0]);
476          $this->tmpl->assign('height', $info[1]);
477          $this->tmpl->assign('ExifMadeOn', $meta_date);
478          $this->tmpl->assign('ExifMadeWith', $meta_make);
479          $this->tmpl->assign('ExifOrigResolution', $meta_res);
480          $this->tmpl->assign('ExifFileSize', $meta_size);
481     
482          $this->tmpl->assign('image_url', 'phpfspot_img.php?idx='. $photo ."&amp;width=". $this->cfg->photo_width);
483          $this->tmpl->assign('image_url_full', 'phpfspot_img.php?idx='. $photo);
484
485          $this->tmpl->assign('tags', $this->get_photo_tags($photo));
486          $this->tmpl->assign('current', $current);
487       }
488       else {
489          $this->_error("Can't open file ". $thumb_path ."\n");
490          return;
491       }
492
493       if($previous_img) {
494          $this->tmpl->assign('previous_url', "javascript:showImage(". $previous_img .");");
495          $this->tmpl->assign('prev_img', $previous_img);
496       }
497
498       if($next_img) {
499          $this->tmpl->assign('next_url', "javascript:showImage(". $next_img .");");
500          $this->tmpl->assign('next_img', $next_img);
501       }
502       $this->tmpl->assign('mini_width', $this->cfg->mini_width);
503       $this->tmpl->assign('photo_number', $i);
504       $this->tmpl->assign('photo_count', count($all_photos));
505
506       $this->tmpl->show("single_photo.tpl");
507
508    } // showPhoto()
509
510    /**
511     * all available tags and tag cloud
512     *
513     * this function outputs all available tags (time ordered)
514     * and in addition output them as tag cloud (tags which have
515     * many photos will appears more then others)
516     */
517    public function getAvailableTags()
518    {
519       $this->get_tags();
520
521       $output = "";
522
523       $result = $this->db->db_query("
524          SELECT tag_id as id, count(tag_id) as quantity
525          FROM photo_tags
526          INNER JOIN tags t
527             ON t.id = tag_id
528          GROUP BY tag_id
529          ORDER BY t.name ASC
530       ");
531
532       $tags = Array();
533
534       while($row = $this->db->db_fetch_object($result)) {
535          $tags[$row['id']] = $row['quantity'];
536       }
537
538       // change these font sizes if you will
539       $max_size = 125; // max font size in %
540       $min_size = 75; // min font size in %
541
542       // get the largest and smallest array values
543       $max_qty = max(array_values($tags));
544       $min_qty = min(array_values($tags));
545
546       // find the range of values
547       $spread = $max_qty - $min_qty;
548       if (0 == $spread) { // we don't want to divide by zero
549          $spread = 1;
550       }
551
552       // determine the font-size increment
553       // this is the increase per tag quantity (times used)
554       $step = ($max_size - $min_size)/($spread);
555
556       // loop through our tag array
557       foreach ($tags as $key => $value) {
558
559          if(isset($_SESSION['selected_tags']) && in_array($key, $_SESSION['selected_tags']))
560             continue;
561
562           // calculate CSS font-size
563           // find the $value in excess of $min_qty
564           // multiply by the font-size increment ($size)
565           // and add the $min_size set above
566          $size = $min_size + (($value - $min_qty) * $step);
567           // uncomment if you want sizes in whole %:
568          $size = ceil($size);
569
570          if(isset($this->tags[$key])) {
571             $output.= "<a href=\"javascript:Tags('add', ". $key .");\" class=\"tag\" style=\"font-size: ". $size ."%;\">". $this->tags[$key] ."</a>, ";
572          }
573
574       }
575
576       $output = substr($output, 0, strlen($output)-2);
577       print $output;
578
579    } // getAvailableTags()
580
581    /**
582     * output all selected tags
583     *
584     * this function output all tags which have been selected
585     * by the user. the selected tags are stored in the 
586     * session-variable $_SESSION['selected_tags']
587     */
588    public function getSelectedTags()
589    {
590       $this->get_tags();
591
592       $output = "";
593       foreach($this->avail_tags as $tag)
594       {
595          // return all selected tags
596          if(isset($_SESSION['selected_tags']) && in_array($tag, $_SESSION['selected_tags'])) {
597             $output.= "<a href=\"javascript:Tags('del', ". $tag .");\" class=\"tag\">". $this->tags[$tag] ."</a>, ";
598          }
599       }
600
601       $output = substr($output, 0, strlen($output)-2);
602       print $output;
603
604    } // getSelectedTags()
605
606    /**
607     * add tag to users session variable
608     *
609     * this function will add the specified to users current
610     * tag selection. if a date search has been made before
611     * it will be now cleared
612     */
613    public function addTag($tag)
614    {
615       if(!isset($_SESSION['selected_tags']))
616          $_SESSION['selected_tags'] = Array();
617
618       if(isset($_SESSION['searchfor']))
619          unset($_SESSION['searchfor']);
620
621       if(!in_array($tag, $_SESSION['selected_tags']))
622          array_push($_SESSION['selected_tags'], $tag);
623    
624    } // addTag()
625
626    /**
627     * remove tag to users session variable
628     *
629     * this function removes the specified tag from
630     * users current tag selection
631     */
632    public function delTag($tag)
633    {
634       if(isset($_SESSION['searchfor']))
635          unset($_SESSION['searchfor']);
636
637       if(isset($_SESSION['selected_tags'])) {
638          $key = array_search($tag, $_SESSION['selected_tags']);
639          unset($_SESSION['selected_tags'][$key]);
640          sort($_SESSION['selected_tags']);
641       }
642
643    } // delTag()
644
645    /**
646     * reset tag selection
647     *
648     * if there is any tag selection, it will be
649     * deleted now
650     */
651    public function resetTags()
652    {
653       if(isset($_SESSION['selected_tags']))
654          unset($_SESSION['selected_tags']);
655
656    } // resetTags()
657
658    /**
659     * reset single photo
660     *
661     * if a specific photo was requested (external link)
662     * unset the session variable now
663     */
664    public function resetPhotoView()
665    {
666       if(isset($_SESSION['current_photo']))
667          unset($_SESSION['current_photo']);
668
669    } // resetPhotoView();
670
671    /**
672     * reset tag search
673     *
674     * if any tag search has taken place, reset
675     * it now
676     */
677    public function resetTagSearch()
678    {
679       if(isset($_SESSION['searchfor']))
680          unset($_SESSION['searchfor']);
681
682    } // resetTagSearch()
683
684     /**
685     * reset date search
686     *
687     * if any date search has taken place, reset
688     * it now
689     */
690    public function resetDateSearch()
691    {
692       if(isset($_SESSION['from_date']))
693          unset($_SESSION['from_date']);
694       if(isset($_SESSION['to_date']))
695          unset($_SESSION['to_date']);
696
697    } // resetDateSearch();
698
699    /**
700     * return all photo according selection
701     *
702     * this function returns all photos based on
703     * the tag-selection, tag- or date-search.
704     * the tag-search also has to take care of AND
705     * and OR conjunctions
706     */
707    public function getPhotoSelection()
708    {  
709       $matched_photos = Array();
710
711       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
712          $from_date = $_SESSION['from_date'];
713          $to_date = $_SESSION['to_date'];
714          $additional_where_cond = "
715                p.time>='". $from_date ."'
716             AND
717                p.time<='". $to_date ."'
718          ";
719       } 
720
721       if(isset($_SESSION['sort_order'])) {
722          $order_str = $this->get_sort_order();
723       }
724
725       /* return a search result */
726       if(isset($_SESSION['searchfor']) && $_SESSION['searchfor'] != '') {
727          $query_str = "
728             SELECT DISTINCT pt1.photo_id
729                FROM photo_tags pt1
730             INNER JOIN photo_tags pt2
731                ON pt1.photo_id=pt2.photo_id
732             INNER JOIN tags t1
733                ON pt1.tag_id=t1.id
734             INNER JOIN photos p
735                ON pt1.photo_id=p.id
736             INNER JOIN tags t2
737                ON pt2.tag_id=t2.id
738             WHERE t1.name LIKE '%". $_SESSION['searchfor'] ."%' ";
739
740          if(isset($additional_where_cond))
741             $query_str.= "AND ". $additional_where_cond ." ";
742
743          if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
744             $query_str.= "AND t2.name IN ('".implode("','",$this->cfg->show_tags)."')";
745          }
746          
747          if(isset($order_str))
748             $query_str.= $order_str;
749
750          $result = $this->db->db_query($query_str);
751          while($row = $this->db->db_fetch_object($result)) {
752             array_push($matched_photos, $row['photo_id']);
753          }
754          return $matched_photos;
755       }
756
757       /* return according the selected tags */
758       if(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
759          $selected = "";
760          foreach($_SESSION['selected_tags'] as $tag)
761             $selected.= $tag .",";
762          $selected = substr($selected, 0, strlen($selected)-1);
763
764          /* photo has to match at least on of the selected tags */
765          if($_SESSION['tag_condition'] == 'or') {
766             $query_str = "
767                SELECT DISTINCT pt1.photo_id
768                   FROM photo_tags pt1
769                INNER JOIN photo_tags pt2
770                   ON pt1.photo_id=pt2.photo_id
771                INNER JOIN tags t
772                   ON pt2.tag_id=t.id
773                INNER JOIN photos p
774                   ON pt1.photo_id=p.id
775                WHERE pt1.tag_id IN (". $selected .")
776             ";
777             if(isset($additional_where_cond)) 
778                $query_str.= "AND ". $additional_where_cond ." ";
779
780             if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
781                $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags)."')";
782             }
783
784             if(isset($order_str))
785                $query_str.= $order_str;
786          }
787          /* photo has to match all selected tags */
788          elseif($_SESSION['tag_condition'] == 'and') {
789
790             if(count($_SESSION['selected_tags']) >= 32) {
791                print "A SQLite limit of 32 tables within a JOIN SELECT avoids to<br />\n";
792                print "evaluate your tag selection. Please remove some tags from your selection.\n";
793                return Array();
794             } 
795
796             /* Join together a table looking like
797
798                pt1.photo_id pt1.tag_id pt2.photo_id pt2.tag_id ...
799
800                so the query can quickly return all images matching the
801                selected tags in an AND condition
802
803             */
804
805             $query_str = "
806                SELECT DISTINCT pt1.photo_id
807                   FROM photo_tags pt1
808             ";
809
810             if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
811                $query_str.= "
812                   INNER JOIN tags t
813                      ON pt1.tag_id=t.id
814                ";
815             }
816
817             for($i = 0; $i < count($_SESSION['selected_tags']); $i++) {
818                $query_str.= "
819                   INNER JOIN photo_tags pt". ($i+2) ."
820                      ON pt1.photo_id=pt". ($i+2) .".photo_id
821                ";
822             }
823             $query_str.= "
824                INNER JOIN photos p
825                   ON pt1.photo_id=p.id
826             ";
827             $query_str.= "WHERE pt2.tag_id=". $_SESSION['selected_tags'][0]." ";
828             for($i = 1; $i < count($_SESSION['selected_tags']); $i++) {
829                $query_str.= "
830                   AND pt". ($i+2) .".tag_id=". $_SESSION['selected_tags'][$i] ."
831                "; 
832             }
833             if(isset($additional_where_cond)) 
834                $query_str.= "AND ". $additional_where_cond;
835
836             if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
837                $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags). "')";
838             }
839
840             if(isset($order_str))
841                $query_str.= $order_str;
842
843          }
844
845          $result = $this->db->db_query($query_str);
846          while($row = $this->db->db_fetch_object($result)) {
847             array_push($matched_photos, $row['photo_id']);
848          }
849          return $matched_photos;
850       }
851
852       /* return all available photos */
853       $query_str = "
854          SELECT DISTINCT photo_id
855             FROM photo_tags pt
856          INNER JOIN photos p
857             ON p.id=pt.photo_id
858          INNER JOIN tags t
859             ON pt.tag_id=t.id
860       ";
861       if(isset($additional_where_cond)) 
862          $query_str.= "WHERE ". $additional_where_cond ." ";
863
864       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
865          $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags). "')";
866       }
867  
868       if(isset($order_str))
869          $query_str.= $order_str;
870
871       $result = $this->db->db_query($query_str);
872       while($row = $this->db->db_fetch_object($result)) {
873          array_push($matched_photos, $row['photo_id']);
874       }
875       return $matched_photos;
876
877    } // getPhotoSelection()
878
879     /**
880     * control HTML ouput for photo index
881     *
882     * this function provides all the necessary information
883     * for the photo index template.
884     */
885    public function showPhotoIndex()
886    {
887       $photos = $this->getPhotoSelection();
888
889       $count = count($photos);
890
891       if(isset($_SESSION['begin_with']) && $_SESSION['begin_with'] != "")
892          $anchor = $_SESSION['begin_with'];
893
894       if(!isset($this->cfg->rows_per_page) || $this->cfg->rows_per_page == 0) {
895
896          $begin_with = 0;
897          $end_with = $count;
898
899       }
900       elseif($this->cfg->rows_per_page > 0) {
901
902          if(!isset($_SESSION['begin_with']) || $_SESSION['begin_with'] == 0) {
903             $begin_with = 0;
904          }
905          else {
906
907             $begin_with = $_SESSION['begin_with'];
908
909             // verify $begin_with - perhaps the thumbs-per-rows or
910             // rows-per-page variables have changed or the jump back
911             // from a photo wasn't exact - so calculate the real new
912             // starting point
913             $multiplicator = $this->cfg->rows_per_page * $this->cfg->thumbs_per_row;
914             for($i = 0; $i <= $count; $i+=$multiplicator) {
915                if($begin_with >= $i && $begin_with < $i+$multiplicator) {
916                   $begin_with = $i;
917                   break;
918                }
919             }
920          }
921
922          $end_with = $begin_with + ($this->cfg->rows_per_page * $this->cfg->thumbs_per_row);
923       }
924
925    
926       $rows = 0;
927       $cols = 0;
928       $images[$rows] = Array();
929       $img_height[$rows] = Array();
930       $img_width[$rows] = Array();
931       $img_id[$rows] = Array();
932       $img_name[$rows] = Array();
933       $img_title = Array();
934
935       for($i = $begin_with; $i < $end_with; $i++) {
936
937          if(isset($photos[$i])) {
938
939             $images[$rows][$cols] = $photos[$i];
940             $img_id[$rows][$cols] = $i;
941             $img_name[$rows][$cols] = htmlspecialchars($this->getPhotoName($photos[$i], 15));
942             $img_title[$rows][$cols] = "Click to view photo ". htmlspecialchars($this->getPhotoName($photos[$i], 0));
943
944             $thumb_path = $this->get_thumb_path($this->cfg->thumb_width, $photos[$i]);
945
946             if(file_exists($thumb_path)) {
947                $info = getimagesize($thumb_path); 
948                $img_width[$rows][$cols] = $info[0];
949                $img_height[$rows][$cols] = $info[1];
950             }
951
952             if($cols == $this->cfg->thumbs_per_row-1) {
953                $cols = 0;
954                $rows++;
955                $images[$rows] = Array();
956                $img_width[$rows] = Array();
957                $img_height[$rows] = Array();
958             }
959             else {
960                $cols++;
961             }
962          } 
963       }
964
965       // +1 for for smarty's selection iteration
966       $rows++;
967
968       if(isset($_SESSION['searchfor']) && $_SESSION['searchfor'] != '')
969          $this->tmpl->assign('searchfor', $_SESSION['searchfor']);
970
971       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
972          $this->tmpl->assign('from_date', $this->ts2str($_SESSION['from_date']));
973          $this->tmpl->assign('to_date', $this->ts2str($_SESSION['to_date']));
974       }
975
976       if(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
977          $this->tmpl->assign('tag_result', 1);
978       }
979
980       /* do we have to display the page selector ? */
981       if($this->cfg->rows_per_page != 0) {
982
983          $page_select = "";
984       
985          /* calculate the page switchers */
986          $previous_start = $begin_with - ($this->cfg->rows_per_page * $this->cfg->thumbs_per_row);
987          $next_start = $begin_with + ($this->cfg->rows_per_page * $this->cfg->thumbs_per_row);
988
989          if($begin_with != 0) 
990             $this->tmpl->assign("previous_url", "javascript:showPhotoIndex(". $previous_start .");"); 
991          if($end_with < $count)
992             $this->tmpl->assign("next_url", "javascript:showPhotoIndex(". $next_start .");"); 
993
994          $photo_per_page  = $this->cfg->rows_per_page * $this->cfg->thumbs_per_row;
995          $last_page = ceil($count / $photo_per_page);
996
997          /* get the current selected page */
998          if($begin_with == 0) {
999             $current_page = 1;
1000          } else {
1001             $current_page = 0;
1002             for($i = $begin_with; $i >= 0; $i-=$photo_per_page) {
1003                $current_page++;
1004             }
1005          } 
1006
1007          $dotdot_made = 0;
1008
1009          for($i = 1; $i <= $last_page; $i++) {
1010
1011             if($current_page == $i)
1012                $style = "style=\"font-size: 125%; text-decoration: underline;\"";
1013             elseif($current_page-1 == $i || $current_page+1 == $i)
1014                $style = "style=\"font-size: 105%;\"";
1015             elseif(($current_page-5 >= $i) && ($i != 1) ||
1016                ($current_page+5 <= $i) && ($i != $last_page))
1017                $style = "style=\"font-size: 75%;\"";
1018             else
1019                $style = "";
1020
1021             $select = "<a href=\"javascript:showPhotoIndex(". (($i*$photo_per_page)-$photo_per_page) .");\"";
1022                if($style != "")
1023                   $select.= $style;
1024             $select.= ">". $i ."</a>&nbsp;";
1025
1026             // until 9 pages we show the selector from 1-9
1027             if($last_page <= 9) {
1028                $page_select.= $select;
1029                continue;
1030             } else {
1031                if($i == 1 /* first page */ || 
1032                   $i == $last_page /* last page */ ||
1033                   $i == $current_page /* current page */ ||
1034                   $i == ceil($last_page * 0.25) /* first quater */ ||
1035                   $i == ceil($last_page * 0.5) /* half */ ||
1036                   $i == ceil($last_page * 0.75) /* third quater */ ||
1037                   (in_array($i, array(1,2,3,4,5,6)) && $current_page <= 4) /* the first 6 */ ||
1038                   (in_array($i, array($last_page, $last_page-1, $last_page-2, $last_page-3, $last_page-4, $last_page-5)) && $current_page >= $last_page-4) /* the last 6 */ ||
1039                   $i == $current_page-3 || $i == $current_page-2 || $i == $current_page-1 /* three before */ ||
1040                   $i == $current_page+3 || $i == $current_page+2 || $i == $current_page+1 /* three after */) {
1041
1042                   $page_select.= $select;
1043                   $dotdot_made = 0;
1044                   continue;
1045
1046                }
1047             }
1048
1049             if(!$dotdot_made) {
1050                $page_select.= ".........&nbsp;";
1051                $dotdot_made = 1;
1052             }
1053          }
1054
1055          /* only show the page selector if we have more then one page */
1056          if($last_page > 1)
1057             $this->tmpl->assign('page_selector', $page_select);
1058       }
1059
1060       
1061       $current_tags = $this->getCurrentTags();
1062       $extern_link = "index.php?mode=showpi";
1063       $rss_link = "index.php?mode=rss";
1064       if($current_tags != "") {
1065          $extern_link.= "&tags=". $current_tags;
1066          $rss_link.= "&tags=". $current_tags;
1067       }
1068       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
1069          $extern_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
1070          $rss_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
1071       }
1072
1073       $export_link = "index.php?mode=export";
1074       $slideshow_link = "index.php?mode=slideshow";
1075
1076       $this->tmpl->assign('extern_link', $extern_link);
1077       $this->tmpl->assign('slideshow_link', $slideshow_link);
1078       $this->tmpl->assign('export_link', $export_link);
1079       $this->tmpl->assign('rss_link', $rss_link);
1080       $this->tmpl->assign('count', $count);
1081       $this->tmpl->assign('width', $this->cfg->thumb_width);
1082       $this->tmpl->assign('images', $images);
1083       $this->tmpl->assign('img_width', $img_width);
1084       $this->tmpl->assign('img_height', $img_height);
1085       $this->tmpl->assign('img_id', $img_id);
1086       $this->tmpl->assign('img_name', $img_name);
1087       $this->tmpl->assign('img_title', $img_title);
1088       $this->tmpl->assign('rows', $rows);
1089       $this->tmpl->assign('columns', $this->cfg->thumbs_per_row);
1090
1091       $this->tmpl->show("photo_index.tpl");
1092
1093       if(isset($anchor))
1094          print "<script language=\"JavaScript\">self.location.hash = '#image". $anchor ."';</script>\n";
1095
1096    } // showPhotoIndex()
1097
1098    /**
1099     * show credit template
1100     */
1101    public function showCredits()
1102    {
1103       $this->tmpl->assign('version', $this->cfg->version);
1104       $this->tmpl->assign('product', $this->cfg->product);
1105       $this->tmpl->assign('db_version', $this->dbver);
1106       $this->tmpl->show("credits.tpl");
1107
1108    } // showCredits()
1109
1110    /**
1111     * create_thumbnails for the requested width
1112     *
1113     * this function creates image thumbnails of $orig_image
1114     * stored as $thumb_image. It will check if the image is
1115     * in a supported format, if necessary rotate the image
1116     * (based on EXIF orientation meta headers) and re-sizing.
1117     */
1118    public function create_thumbnail($orig_image, $thumb_image, $width)
1119    {  
1120       if(!file_exists($orig_image)) {
1121          return false;
1122       }
1123
1124       $details = getimagesize($orig_image);
1125       
1126       /* check if original photo is a support image type */
1127       if(!$this->checkifImageSupported($details['mime']))
1128          return false;
1129
1130       $meta = $this->get_meta_informations($orig_image);
1131
1132       $rotate = 0;
1133       $flip = false;
1134
1135       switch($meta['Orientation']) {
1136
1137          case 1: /* top, left */
1138             $rotate = 0; $flip = false; break;
1139          case 2: /* top, right */
1140             $rotate = 0; $flip = true; break;
1141          case 3: /* bottom, left */
1142             $rotate = 180; $flip = false; break;
1143          case 4: /* bottom, right */
1144             $rotate = 180; $flip = true; break;
1145          case 5: /* left side, top */
1146             $rotate = 90; $flip = true; break;
1147          case 6: /* right side, top */
1148             $rotate = 90; $flip = false; break;
1149          case 7: /* left side, bottom */
1150             $rotate = 270; $flip = true; break;
1151          case 8: /* right side, bottom */
1152             $rotate = 270; $flip = false; break;
1153       }
1154
1155       $src_img = @imagecreatefromjpeg($orig_image);
1156
1157       if(!$src_img) {
1158          print "Can't load image from ". $orig_image ."\n";
1159          return false;
1160       }
1161
1162       /* grabs the height and width */
1163       $cur_width = imagesx($src_img);
1164       $cur_height = imagesy($src_img);
1165
1166       // If requested width is more then the actual image width,
1167       // do not generate a thumbnail, instead safe the original
1168       // as thumbnail but with lower quality
1169
1170       if($width >= $cur_width) {
1171          $result = imagejpeg($src_img, $thumb_image, 75);
1172          imagedestroy($src_img);
1173          return true;
1174       }
1175
1176       // If the image will be rotate because EXIF orientation said so
1177       // 'virtually rotate' the image for further calculations
1178       if($rotate == 90 || $rotate == 270) {
1179          $tmp = $cur_width;
1180          $cur_width = $cur_height;
1181          $cur_height = $tmp;
1182       }
1183
1184       /* calculates aspect ratio */
1185       $aspect_ratio = $cur_height / $cur_width;
1186
1187       /* sets new size */
1188       if($aspect_ratio < 1) {
1189          $new_w = $width;
1190          $new_h = abs($new_w * $aspect_ratio);
1191       } else {
1192          /* 'virtually' rotate the image and calculate it's ratio */
1193          $tmp_w = $cur_height;
1194          $tmp_h = $cur_width;
1195          /* now get the ratio from the 'rotated' image */
1196          $tmp_ratio = $tmp_h/$tmp_w;
1197          /* now calculate the new dimensions */
1198          $tmp_w = $width;
1199          $tmp_h = abs($tmp_w * $tmp_ratio);
1200
1201          // now that we know, how high they photo should be, if it
1202          // gets rotated, use this high to scale the image
1203          $new_h = $tmp_h;
1204          $new_w = abs($new_h / $aspect_ratio);
1205
1206          // If the image will be rotate because EXIF orientation said so
1207          // now 'virtually rotate' back the image for the image manipulation
1208          if($rotate == 90 || $rotate == 270) {
1209             $tmp = $new_w;
1210             $new_w = $new_h;
1211             $new_h = $tmp;
1212          }
1213       }
1214
1215       /* creates new image of that size */
1216       $dst_img = imagecreatetruecolor($new_w, $new_h);
1217
1218       imagefill($dst_img, 0, 0, ImageColorAllocate($dst_img, 255, 255, 255));
1219
1220       /* copies resized portion of original image into new image */
1221       imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $new_w, $new_h, imagesx($src_img), imagesy($src_img));
1222
1223       /* needs the image to be flipped horizontal? */
1224       if($flip) {
1225          print "(FLIP)";
1226          $image = $dst_img;
1227          for($x = 0; $x < $new_w; $x++) {
1228             imagecopy($dst_img, $image, $x, 0, $w - $x - 1, 0, 1, $h);
1229          }
1230       }
1231
1232       if($rotate) {
1233          $this->_debug("(ROTATE)");
1234          $dst_img = $this->rotateImage($dst_img, $rotate);
1235       }
1236
1237       /* write down new generated file */
1238       $result = imagejpeg($dst_img, $thumb_image, 75);
1239
1240       /* free your mind */
1241       imagedestroy($dst_img);
1242       imagedestroy($src_img);
1243
1244       if($result === false) {
1245          print "Can't write thumbnail ". $thumb_image ."\n";
1246          return false;
1247       }
1248
1249       return true;
1250
1251    } // create_thumbnail()
1252
1253    /**
1254     * return all exif meta data from the file
1255     */
1256    public function get_meta_informations($file)
1257    {
1258       return exif_read_data($file);
1259
1260    } // get_meta_informations()
1261
1262    /**
1263     * create phpfspot own sqlite database
1264     *
1265     * this function creates phpfspots own sqlite database
1266     * if it does not exist yet. this own is used to store
1267     * some necessary informations (md5 sum's, ...).
1268     */
1269    public function check_config_table()
1270    {
1271       // if the config table doesn't exist yet, create it
1272       if(!$this->cfg_db->db_check_table_exists("images")) {
1273          $this->cfg_db->db_exec("
1274             CREATE TABLE images (
1275                img_idx int primary key,
1276                img_md5 varchar(32)
1277             )
1278             ");
1279       }
1280
1281    } // check_config_table
1282
1283    /**
1284     * Generates a thumbnail from photo idx
1285     *
1286     * This function will generate JPEG thumbnails from provided F-Spot photo
1287     * indizes.
1288     *
1289     * 1. Check if all thumbnail generations (width) are already in place and
1290     *    readable
1291     * 2. Check if the md5sum of the original file has changed
1292     * 3. Generate the thumbnails if needed
1293     */
1294    public function gen_thumb($idx = 0, $force = 0)
1295    {
1296       $error = 0;
1297
1298       $resolutions = Array(
1299          $this->cfg->thumb_width,
1300          $this->cfg->photo_width,
1301          $this->cfg->mini_width,
1302       );
1303
1304       /* get details from F-Spot's database */
1305       $details = $this->get_photo_details($idx);
1306
1307       /* calculate file MD5 sum */
1308       $full_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
1309
1310       if(!file_exists($full_path)) {
1311          $this->_error("File ". $full_path ." does not exist\n");
1312          return;
1313       }
1314
1315       if(!is_readable($full_path)) {
1316          $this->_error("File ". $full_path ." is not readable for ". $this->getuid() ."\n");
1317          return;
1318       }
1319
1320       $file_md5 = md5_file($full_path);
1321
1322       $this->_debug("Image [". $idx ."] ". $this->shrink_text($this->parse_uri($details['uri'], 'filename'), 20) ." Thumbnails:");
1323
1324       $changes = false;
1325
1326       foreach($resolutions as $resolution) {
1327
1328          $thumb_sub_path = substr($file_md5, 0, 2);
1329          $thumb_path = $this->cfg->thumb_path ."/". $thumb_sub_path ."/". $resolution ."_". $file_md5;
1330
1331          if(!file_exists(dirname($thumb_path))) {
1332             mkdir(dirname($thumb_path), 0755);
1333          }
1334
1335          /* if the thumbnail file doesn't exist, create it */
1336          if(!file_exists($thumb_path)) {
1337
1338             $this->_debug(" ". $resolution ."px");
1339             if(!$this->create_thumbnail($full_path, $thumb_path, $resolution))
1340                $error = 1;
1341
1342             $changes = true;
1343          }
1344          /* if the file hasn't changed there is no need to regen the thumb */
1345          elseif($file_md5 != $this->getMD5($idx) || $force) {
1346
1347             $this->_debug(" ". $resolution ."px");
1348             if(!$this->create_thumbnail($full_path, $thumb_path, $resolution))
1349                $error = 1;
1350
1351             $changes = true;
1352          }
1353       }
1354
1355       if(!$changes) {
1356          $this->_debug(" already exist");
1357       }
1358
1359       /* set the new/changed MD5 sum for the current photo */
1360       if(!$error) {
1361          $this->setMD5($idx, $file_md5);
1362       }
1363
1364       $this->_debug("\n");
1365
1366    } // gen_thumb()
1367
1368    /**
1369     * returns stored md5 sum for a specific photo
1370     *
1371     * this function queries the phpfspot database for a
1372     * stored MD5 checksum of the specified photo
1373     */
1374    public function getMD5($idx)
1375    {
1376       $result = $this->cfg_db->db_query("
1377          SELECT img_md5 
1378          FROM images
1379          WHERE img_idx='". $idx ."'
1380       ");
1381
1382       if(!$result)
1383          return 0;
1384
1385       $img = $this->cfg_db->db_fetch_object($result);
1386       return $img['img_md5'];
1387       
1388    } // getMD5()
1389
1390    /**
1391     * set MD5 sum for the specific photo
1392     */
1393    private function setMD5($idx, $md5)
1394    {
1395       $result = $this->cfg_db->db_exec("
1396          REPLACE INTO images (img_idx, img_md5)
1397          VALUES ('". $idx ."', '". $md5 ."')
1398       ");
1399
1400    } // setMD5()
1401
1402    /**
1403     * store current tag condition
1404     *
1405     * this function stores the current tag condition
1406     * (AND or OR) in the users session variables
1407     */
1408    public function setTagCondition($mode)
1409    {
1410       $_SESSION['tag_condition'] = $mode;
1411
1412    } // setTagCondition()
1413
1414    /** 
1415     * invoke tag & date search 
1416     *
1417     * this function will return all matching tags and store
1418     * them in the session variable selected_tags. furthermore
1419     * it also handles the date search.
1420     * getPhotoSelection() will then only return the matching
1421     * photos.
1422     */
1423    public function startSearch($searchfor, $sort_order, $from = 0, $to = 0)
1424    {
1425       $this->get_tags();
1426
1427       $_SESSION['searchfor'] = $searchfor;
1428       $_SESSION['sort_order'] = $sort_order;
1429       if($from != 0)
1430          $_SESSION['from_date'] = strtotime($from);
1431       else
1432          unset($_SESSION['from_date']);
1433       if($to != 0)
1434          $_SESSION['to_date'] = strtotime($to);
1435       else
1436          unset($_SESSION['to_date']);
1437
1438       if($searchfor != "") {
1439          /* new search, reset the current selected tags */
1440          $_SESSION['selected_tags'] = Array();
1441          foreach($this->avail_tags as $tag) {
1442             if(preg_match('/'. $searchfor .'/i', $this->tags[$tag]))
1443                array_push($_SESSION['selected_tags'], $tag);
1444          }
1445       }
1446
1447    } // startSearch()
1448
1449    /**
1450     * rotate image
1451     *
1452     * this function rotates the image according the
1453     * specified angel.
1454     */
1455    private function rotateImage($img, $degrees)
1456    {
1457       if(function_exists("imagerotate")) {
1458          $img = imagerotate($img, $degrees, 0);
1459       } else {
1460          function imagerotate($src_img, $angle)
1461          {
1462             $src_x = imagesx($src_img);
1463             $src_y = imagesy($src_img);
1464             if ($angle == 180) {
1465                $dest_x = $src_x;
1466                $dest_y = $src_y;
1467             }
1468             elseif ($src_x <= $src_y) {
1469                $dest_x = $src_y;
1470                $dest_y = $src_x;
1471             }
1472             elseif ($src_x >= $src_y) {
1473                $dest_x = $src_y;
1474                $dest_y = $src_x;
1475             }
1476                
1477             $rotate=imagecreatetruecolor($dest_x,$dest_y);
1478             imagealphablending($rotate, false);
1479                
1480             switch ($angle) {
1481             
1482                case 90:
1483                   for ($y = 0; $y < ($src_y); $y++) {
1484                      for ($x = 0; $x < ($src_x); $x++) {
1485                         $color = imagecolorat($src_img, $x, $y);
1486                         imagesetpixel($rotate, $dest_x - $y - 1, $x, $color);
1487                      }
1488                   }
1489                   break;
1490
1491                case 270:
1492                   for ($y = 0; $y < ($src_y); $y++) {
1493                      for ($x = 0; $x < ($src_x); $x++) {
1494                         $color = imagecolorat($src_img, $x, $y);
1495                         imagesetpixel($rotate, $y, $dest_y - $x - 1, $color);
1496                      }
1497                   }
1498                   break;
1499
1500                case 180:
1501                   for ($y = 0; $y < ($src_y); $y++) {
1502                      for ($x = 0; $x < ($src_x); $x++) {
1503                         $color = imagecolorat($src_img, $x, $y);
1504                         imagesetpixel($rotate, $dest_x - $x - 1, $dest_y - $y - 1, $color);
1505                      }
1506                   }
1507                   break;
1508
1509                default:
1510                   $rotate = $src_img;
1511                   break;
1512             };
1513
1514             return $rotate;
1515
1516          }
1517
1518          $img = imagerotate($img, $degrees);
1519
1520       }
1521
1522       return $img;
1523
1524    } // rotateImage()
1525
1526    /**
1527     * return all assigned tags for the specified photo
1528     */
1529    private function get_photo_tags($idx)
1530    {
1531       $result = $this->db->db_query("
1532          SELECT t.id, t.name
1533          FROM tags t
1534          INNER JOIN photo_tags pt
1535             ON t.id=pt.tag_id
1536          WHERE pt.photo_id='". $idx ."'
1537       ");
1538
1539       $tags = Array();
1540
1541       while($row = $this->db->db_fetch_object($result))
1542          $tags[$row['id']] = $row['name'];
1543
1544       return $tags;
1545
1546    } // get_photo_tags()
1547
1548    /**
1549     * create on-the-fly images with text within
1550     */
1551    public function showTextImage($txt, $color=000000, $space=4, $font=4, $w=300)
1552    {
1553       if (strlen($color) != 6) 
1554          $color = 000000;
1555
1556       $int = hexdec($color);
1557       $h = imagefontheight($font);
1558       $fw = imagefontwidth($font);
1559       $txt = explode("\n", wordwrap($txt, ($w / $fw), "\n"));
1560       $lines = count($txt);
1561       $im = imagecreate($w, (($h * $lines) + ($lines * $space)));
1562       $bg = imagecolorallocate($im, 255, 255, 255);
1563       $color = imagecolorallocate($im, 0xFF & ($int >> 0x10), 0xFF & ($int >> 0x8), 0xFF & $int);
1564       $y = 0;
1565
1566       foreach ($txt as $text) {
1567          $x = (($w - ($fw * strlen($text))) / 2);
1568          imagestring($im, $font, $x, $y, $text, $color);
1569          $y += ($h + $space);
1570       }
1571
1572       Header("Content-type: image/png");
1573       ImagePng($im);
1574
1575    } // showTextImage()
1576
1577    /**
1578     * check if all requirements are met
1579     */
1580    private function checkRequirements()
1581    {
1582       if(!function_exists("imagecreatefromjpeg")) {
1583          print "PHP GD library extension is missing<br />\n";
1584          $missing = true;
1585       }
1586
1587       if($this->cfg->db_access == "native" && !function_exists("sqlite3_open")) {
1588          print "PHP SQLite3 library extension is missing<br />\n";
1589          $missing = true;
1590       }
1591
1592       /* Check for HTML_AJAX PEAR package, lent from Horde project */
1593       ini_set('track_errors', 1);
1594       @include_once 'HTML/AJAX/Server.php';
1595       if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
1596          print "PEAR HTML_AJAX package is missing<br />\n";
1597          $missing = true;
1598       }
1599       @include_once 'Calendar/Calendar.php';
1600       if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
1601          print "PEAR Calendar package is missing<br />\n";
1602          $missing = true;
1603       }
1604       ini_restore('track_errors');
1605
1606       if(isset($missing))
1607          return false;
1608
1609       return true;
1610
1611    } // checkRequirements()
1612
1613    private function _debug($text)
1614    {
1615       if($this->fromcmd) {
1616          print $text;
1617       }
1618
1619    } // _debug()
1620
1621    /**
1622     * check if specified MIME type is supported
1623     */
1624    public function checkifImageSupported($mime)
1625    {
1626       if(in_array($mime, Array("image/jpeg")))
1627          return true;
1628
1629       return false;
1630
1631    } // checkifImageSupported()
1632
1633    public function _error($text)
1634    {
1635       switch($this->cfg->logging) {
1636          case 'display':
1637             print "<img src=\"resources/green_info.png\" alt=\"warning\" />\n";
1638             print $text;
1639             break;
1640          case 'errorlog':  
1641             error_log($text);
1642             break;
1643          case 'logfile':
1644             error_log($text, 3, $his->cfg->log_file);
1645             break;
1646       }
1647
1648    } // _error()
1649
1650    /**
1651     * output calendard input fields
1652     */
1653    private function get_calendar($mode)
1654    {
1655       $year = isset($_SESSION[$mode .'_date']) ? date("Y", $_SESSION[$mode .'_date']) : date("Y");
1656       $month = isset($_SESSION[$mode .'_date']) ? date("m", $_SESSION[$mode .'_date']) : date("m");
1657       $day = isset($_SESSION[$mode .'_date']) ? date("d", $_SESSION[$mode .'_date']) : date("d");
1658
1659       $output = "<input type=\"text\" size=\"3\" id=\"". $mode ."year\" value=\"". $year ."\"";
1660       if(!isset($_SESSION[$mode .'_date']))
1661          $output.= " disabled=\"disabled\"";
1662       $output.= " />\n";
1663       $output.= "<input type=\"text\" size=\"1\" id=\"". $mode ."month\" value=\"". $month ."\"";
1664       if(!isset($_SESSION[$mode .'_date']))
1665          $output.= " disabled=\"disabled\"";
1666       $output.= " />\n";
1667       $output.= "<input type=\"text\" size=\"1\" id=\"". $mode ."day\" value=\"". $day ."\"";
1668       if(!isset($_SESSION[$mode .'_date']))
1669          $output.= " disabled=\"disabled\"";
1670       $output.= " />\n";
1671
1672       return $output;
1673
1674    } // get_calendar()
1675
1676    /**
1677     * output calendar matrix
1678     */
1679    public function get_calendar_matrix($year = 0, $month = 0, $day = 0)
1680    {
1681       if (!isset($year)) $year = date('Y');
1682       if (!isset($month)) $month = date('m');
1683       if (!isset($day)) $day = date('d');
1684       $rows = 1;
1685       $cols = 1;
1686       $matrix = Array();
1687
1688       require_once CALENDAR_ROOT.'Month/Weekdays.php';
1689       require_once CALENDAR_ROOT.'Day.php';
1690
1691       // Build the month
1692       $month = new Calendar_Month_Weekdays($year,$month);
1693
1694       // Create links
1695       $prevStamp = $month->prevMonth(true);
1696       $prev = "javascript:setMonth(". date('Y',$prevStamp) .", ". date('n',$prevStamp) .", ". date('j',$prevStamp) .");";
1697       $nextStamp = $month->nextMonth(true);
1698       $next = "javascript:setMonth(". date('Y',$nextStamp) .", ". date('n',$nextStamp) .", ". date('j',$nextStamp) .");";
1699
1700       $selectedDays = array (
1701          new Calendar_Day($year,$month,$day),
1702          new Calendar_Day($year,12,25),
1703       );
1704
1705       // Build the days in the month
1706       $month->build($selectedDays);
1707
1708       $this->tmpl->assign('current_month', date('F Y',$month->getTimeStamp()));
1709       $this->tmpl->assign('prev_month', $prev);
1710       $this->tmpl->assign('next_month', $next);
1711
1712       while ( $day = $month->fetch() ) {
1713    
1714          if(!isset($matrix[$rows]))
1715             $matrix[$rows] = Array();
1716
1717          $string = "";
1718
1719          $dayStamp = $day->thisDay(true);
1720          $link = "javascript:setCalendarDate(". date('Y',$dayStamp) .", ". date('n',$dayStamp).", ". date('j',$dayStamp) .");";
1721
1722          // isFirst() to find start of week
1723          if ( $day->isFirst() )
1724             $string.= "<tr>\n";
1725
1726          if ( $day->isSelected() ) {
1727             $string.= "<td class=\"selected\">".$day->thisDay()."</td>\n";
1728          } else if ( $day->isEmpty() ) {
1729             $string.= "<td>&nbsp;</td>\n";
1730          } else {
1731             $string.= "<td><a class=\"calendar\" href=\"".$link."\">".$day->thisDay()."</a></td>\n";
1732          }
1733
1734          // isLast() to find end of week
1735          if ( $day->isLast() )
1736             $string.= "</tr>\n";
1737
1738          $matrix[$rows][$cols] = $string;
1739
1740          $cols++;
1741
1742          if($cols > 7) {
1743             $cols = 1;
1744             $rows++;
1745          }
1746       }
1747
1748       $this->tmpl->assign('matrix', $matrix);
1749       $this->tmpl->assign('rows', $rows);
1750       $this->tmpl->show("calendar.tpl");
1751
1752    } // get_calendar_matrix()
1753
1754    /**
1755     * output export page
1756     */
1757    public function getExport($mode)
1758    {
1759       $pictures = $this->getPhotoSelection();
1760       $current_tags = $this->getCurrentTags();  
1761
1762       foreach($pictures as $picture) {
1763
1764          $orig_url = $this->get_phpfspot_url() ."index.php?mode=showp&id=". $picture;
1765          if($current_tags != "") {
1766             $orig_url.= "&tags=". $current_tags;
1767          } 
1768          if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
1769             $orig_url.= "&from_date=". $_SESSION['from_date'] ."&to_date=". $_SESSION['to_date'];
1770          }
1771
1772          $thumb_url = $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $picture ."&width=". $this->cfg->thumb_width;
1773
1774          switch($mode) {
1775
1776             case 'HTML':
1777                // <a href="%pictureurl%"><img src="%thumbnailurl%" ></a>
1778                print htmlspecialchars("<a href=\"". $orig_url ."\"><img src=\"". $thumb_url ."\" /></a>") ."<br />\n";
1779                break;
1780                
1781             case 'MoinMoin':
1782                // "[%pictureurl% %thumbnailurl%]"
1783                print htmlspecialchars("[".$orig_url." ".$thumb_url."&fake=1.jpg]") ."<br />\n";
1784                break;
1785
1786             case 'MoinMoinList':
1787                // " * [%pictureurl% %thumbnailurl%]"
1788                print "&nbsp;" . htmlspecialchars("* [".$orig_url." ".$thumb_url."&fake=1.jpg]") ."<br />\n";
1789                break;
1790          }
1791
1792       }
1793
1794    } // getExport()
1795
1796    /**
1797     * output RSS feed
1798     */
1799    public function getRSSFeed()
1800    {
1801       Header("Content-type: text/xml; charset=utf-8");
1802       print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1803 ?>
1804 <rss version="2.0"
1805    xmlns:media="http://search.yahoo.com/mrss/"
1806    xmlns:dc="http://purl.org/dc/elements/1.1/"
1807  >
1808  <channel>
1809   <title>phpfspot</title>
1810   <description>phpfspot RSS feed</description>
1811   <link><?php print htmlspecialchars($this->get_phpfspot_url()); ?></link>
1812   <pubDate><?php print strftime("%a, %d %b %Y %T %z"); ?></pubDate>
1813   <generator>phpfspot</generator>
1814 <?php
1815
1816       $pictures = $this->getPhotoSelection();
1817       $current_tags = $this->getCurrentTags();  
1818
1819       foreach($pictures as $picture) {
1820
1821          $orig_url = $this->get_phpfspot_url() ."index.php?mode=showp&id=". $picture;
1822          if($current_tags != "") {
1823             $orig_url.= "&tags=". $current_tags;
1824          } 
1825          if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
1826             $orig_url.= "&from_date=". $_SESSION['from_date'] ."&to_date=". $_SESSION['to_date'];
1827          }
1828
1829          $details = $this->get_photo_details($picture);
1830
1831          $thumb_url = $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $picture ."&width=". $this->cfg->thumb_width;
1832          $thumb_html = htmlspecialchars("
1833 <a href=\"". $orig_url ."\"><img src=\"". $thumb_url ."\" /></a>
1834 <br>
1835 ". $details['description']);
1836
1837          $orig_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
1838          $meta = $this->get_meta_informations($orig_path);
1839          $meta_date = isset($meta['FileDateTime']) ? $meta['FileDateTime'] : filemtime($orig_path);
1840
1841 ?>
1842   <item>
1843    <title><?php print htmlspecialchars($this->parse_uri($details['uri'], 'filename')); ?></title>
1844    <link><?php print htmlspecialchars($orig_url); ?></link>
1845    <guid><?php print htmlspecialchars($orig_url); ?></guid>
1846    <dc:date.Taken><?php print strftime("%Y-%m-%dT%H:%M:%S+00:00", $meta_date); ?></dc:date.Taken>
1847    <description>
1848     <?php print $thumb_html; ?> 
1849    </description>
1850    <pubDate><?php print strftime("%a, %d %b %Y %T %z", $meta_date); ?></pubDate>
1851   </item>
1852 <?php
1853
1854       }
1855 ?>
1856  </channel>
1857 </rss>
1858 <?php
1859
1860
1861    } // getExport()
1862
1863  
1864    /**
1865     * return all selected tags as one string
1866     */
1867    private function getCurrentTags()
1868    {
1869       $current_tags = "";
1870       if(isset($_SESSION['selected_tags']) && $_SESSION['selected_tags'] != "") {
1871          foreach($_SESSION['selected_tags'] as $tag)
1872             $current_tags.= $tag .",";
1873          $current_tags = substr($current_tags, 0, strlen($current_tags)-1);
1874       }
1875       return $current_tags;
1876
1877    } // getCurrentTags()
1878
1879    /**
1880     * return the current photo
1881     */
1882    public function getCurrentPhoto()
1883    {
1884       if(isset($_SESSION['current_photo'])) {
1885          print $_SESSION['current_photo'];
1886       }
1887    } // getCurrentPhoto()
1888
1889    /**
1890     * tells the client browser what to do
1891     *
1892     * this function is getting called via AJAX by the
1893     * client browsers. it will tell them what they have
1894     * to do next. This is necessary for directly jumping
1895     * into photo index or single photo view when the are
1896     * requested with specific URLs
1897     */
1898    public function whatToDo()
1899    {
1900       if(isset($_SESSION['current_photo']) && $_SESSION['start_action'] == 'showp') {
1901          return "show_photo";
1902       }
1903       elseif(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
1904          return "showpi_tags";
1905       }
1906       elseif(isset($_SESSION['start_action']) && $_SESSION['start_action'] == 'showpi') {
1907          return "showpi";
1908       }
1909
1910       return "nothing special";
1911
1912    } // whatToDo()
1913
1914    /**
1915     * return the current process-user
1916     */
1917    private function getuid()
1918    {
1919       if($uid = posix_getuid()) {
1920          if($user = posix_getpwuid($uid)) {
1921             return $user['name'];
1922          }
1923       }
1924    
1925       return 'n/a';
1926    
1927    } // getuid()
1928
1929    /**
1930     * returns a select-dropdown box to select photo index sort parameters
1931     */
1932    private function get_sort_field()
1933    {
1934       $output = "<select name=\"sort_order\">";
1935       foreach(array('date_asc', 'date_desc', 'name_asc', 'name_desc') as $sort_order) {
1936          $output.= "<option value=\"". $sort_order ."\"";
1937          if($sort_order == $_SESSION['sort_order']) {
1938             $output.= " selected=\"selected\"";
1939          }
1940          $output.= ">". $sort_order ."</option>";
1941       }
1942       $output.= "</select>";
1943       return $output;
1944
1945    } // get_sort_field()
1946
1947    /**
1948     * returns the currently selected sort order
1949     */ 
1950    private function get_sort_order()
1951    {
1952       switch($_SESSION['sort_order']) {
1953          case 'date_asc':
1954             return " ORDER BY p.time ASC";
1955             break;
1956          case 'date_desc':
1957             return " ORDER BY p.time DESC";
1958             break;
1959          case 'name_asc':
1960             if($this->dbver < 9) {
1961                return " ORDER BY p.name ASC";
1962             }
1963             else {
1964                return " ORDER BY basename(p.uri) ASC";
1965             }
1966             break;
1967          case 'name_desc':
1968             if($this->dbver < 9) {
1969                return " ORDER BY p.name DESC";
1970             }
1971             else {
1972                return " ORDER BY basename(p.uri) DESC";
1973             }
1974             break;
1975       }
1976
1977    } // get_sort_order()
1978
1979    /***
1980      * return the next to be shown slide show image
1981      *
1982      * this function returns the URL of the next image
1983      * in the slideshow sequence.
1984      */
1985    public function getNextSlideShowImage()
1986    {
1987       $all_photos = $this->getPhotoSelection();
1988
1989       if(!isset($_SESSION['slideshow_img']) || $_SESSION['slideshow_img'] == count($all_photos)-1) 
1990          $_SESSION['slideshow_img'] = 0;
1991       else
1992          $_SESSION['slideshow_img']++;
1993
1994       return $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $all_photos[$_SESSION['slideshow_img']] ."&width=". $this->cfg->photo_width;
1995
1996    } // getNextSlideShowImage()
1997
1998    /***
1999      * return the previous to be shown slide show image
2000      *
2001      * this function returns the URL of the previous image
2002      * in the slideshow sequence.
2003      */
2004    public function getPrevSlideShowImage()
2005    {
2006       $all_photos = $this->getPhotoSelection();
2007
2008       if(!isset($_SESSION['slideshow_img']) || $_SESSION['slideshow_img'] == 0)
2009          $_SESSION['slideshow_img'] = 0;
2010       else
2011          $_SESSION['slideshow_img']--;
2012
2013       return $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $all_photos[$_SESSION['slideshow_img']] ."&width=". $this->cfg->photo_width;
2014
2015    } // getPrevSlideShowImage()
2016
2017    public function resetSlideShow()
2018    {
2019       if(isset($_SESSION['slideshow_img']))
2020          unset($_SESSION['slideshow_img']);
2021    } // resetSlideShow()
2022    
2023    /***
2024      * get random photo
2025      *
2026      * this function will get all photos from the fspot
2027      * database and randomly return ONE entry
2028      *
2029      * saddly there is yet no sqlite3 function which returns
2030      * the bulk result in array, so we have to fill up our
2031      * own here.
2032      */ 
2033    public function get_random_photo()
2034    {
2035       $all = Array();
2036
2037       $result = $this->db->db_query("
2038          SELECT id
2039          FROM photos
2040       ");
2041       
2042       while($row = $this->db->db_fetch_object($result)) {
2043          array_push($all, $row['id']);
2044       }
2045
2046       return $all[array_rand($all)];
2047
2048    } // get_random_photo()
2049
2050    /**
2051     * validates provided date
2052     *
2053     * this function validates if the provided date
2054     * contains a valid date and will return true 
2055     * if it is.
2056     */
2057    public function isValidDate($date_str)
2058    {
2059       $timestamp = strtotime($date_str);
2060    
2061       if(is_numeric($timestamp))
2062          return true;
2063       
2064       return false;
2065
2066    } // isValidDate()
2067
2068    /**
2069     * timestamp to string conversion
2070     */
2071    private function ts2str($timestamp)
2072    {
2073       return strftime("%Y-%m-%d", $timestamp);
2074    } // ts2str()
2075
2076    private function extractTags($tags_str)
2077    {
2078       $not_validated = split(',', $_GET['tags']);
2079       $validated = array();
2080
2081       foreach($not_validated as $tag) {
2082          if(is_numeric($tag))
2083             array_push($validated, $tag);
2084       }
2085    
2086       return $validated;
2087    
2088    } // extractTags()
2089
2090    /**
2091     * returns the full path to a thumbnail
2092     */
2093    public function get_thumb_path($width, $photo)
2094    {
2095       $md5 = $this->getMD5($photo);
2096       $sub_path = substr($md5, 0, 2);
2097       return $this->cfg->thumb_path
2098          . "/"
2099          . $sub_path
2100          . "/"
2101          . $width
2102          . "_"
2103          . $md5;
2104
2105    } // get_thumb_path()
2106
2107    /**
2108     * returns server's virtual host name
2109     */
2110    private function get_server_name()
2111    {
2112       return $_SERVER['SERVER_NAME'];
2113    } // get_server_name()
2114
2115    /**
2116     * returns type of webprotocol which is
2117     * currently used
2118     */
2119    private function get_web_protocol()
2120    {
2121       if(!isset($_SERVER['HTTPS']))
2122          return "http";
2123       else
2124          return "https";
2125    } // get_web_protocol()
2126
2127    /**
2128     * return url to this phpfspot installation
2129     */
2130    private function get_phpfspot_url()
2131    {
2132       return $this->get_web_protocol() ."://". $this->get_server_name() . $this->cfg->web_path;
2133    } // get_phpfspot_url()
2134    
2135    /**
2136     * check file exists and is readable
2137     *
2138     * returns true, if everything is ok, otherwise false
2139     * if $silent is not set, this function will output and
2140     * error message
2141     */
2142    private function check_readable($file, $silent = null)
2143    {
2144       if(!file_exists($file)) {
2145          if(!isset($silent))
2146             print "File \"". $file ."\" does not exist.\n";
2147          return false;
2148       }
2149
2150       if(!is_readable($file)) {
2151          if(!isset($silent))
2152             print "File \"". $file ."\" is not reachable for user ". $this->getuid() ."\n";
2153          return false;
2154       }
2155
2156       return true;
2157
2158    } // check_readable()
2159
2160    /**
2161     * check if all needed indices are present
2162     *
2163     * this function checks, if some needed indices are already
2164     * present, or if not, create them on the fly. they are
2165     * necessary to speed up some queries like that one look for
2166     * all tags, when show_tags is specified in the configuration.
2167     */
2168    private function checkDbIndices()
2169    {
2170       $result = $this->db->db_exec("
2171          CREATE INDEX IF NOT EXISTS
2172             phototag
2173          ON
2174             photo_tags
2175                (photo_id, tag_id)
2176       ");
2177
2178    } // checkDbIndices()
2179
2180    /**
2181     * retrive F-Spot database version
2182     *
2183     * this function will return the F-Spot database version number
2184     * It is stored within the sqlite3 database in the table meta
2185     */
2186    public function getFspotDBVersion()
2187    {
2188       if($result = $this->db->db_fetchSingleRow("
2189          SELECT data as version
2190          FROM meta
2191          WHERE
2192             name LIKE 'F-Spot Database Version'
2193       "))
2194          return $result['version'];
2195
2196       return null;
2197
2198    } // getFspotDBVersion()
2199
2200    /**
2201     * parse the provided URI and will returned the
2202     * requested chunk
2203     */
2204    public function parse_uri($uri, $mode)
2205    {
2206       if(($components = parse_url($uri)) !== false) {
2207
2208          switch($mode) {
2209             case 'filename':
2210                return basename($components['path']);
2211                break;
2212             case 'dirname':
2213                return dirname($components['path']);
2214                break;
2215             case 'fullpath':
2216                return $components['path'];
2217                break;
2218          }
2219       }
2220
2221       return $uri;
2222
2223    } // parse_uri()
2224
2225 } // class PHPFSPOT
2226
2227 ?>