avoid notice messages about undefined variables, resolves #179
[phpfspot.git] / phpfspot.class.php
1 <?php
2
3 /***************************************************************************
4  *
5  * phpfspot, presents your F-Spot photo collection in Web browsers.
6  *
7  * Copyright (c) by Andreas Unterkircher
8  *
9  *  This program is free software; you can redistribute it and/or modify
10  *  it under the terms of the GNU General Public License as published by
11  *  the Free Software Foundation; either version 2 of the License, or
12  *  any later version.
13  *
14  *  This program is distributed in the hope that it will be useful,
15  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  *  GNU General Public License for more details.
18  *
19  *  You should have received a copy of the GNU General Public License
20  *  along with this program; if not, write to the Free Software
21  *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
22  *
23  ***************************************************************************/
24
25 require_once "phpfspot_cfg.php";
26 require_once "phpfspot_db.php";
27
28 /**
29  * PHPFSPOT main class
30  *
31  * this class contains the most functions which will to the major
32  * work for phpfspot.
33  *
34  * @package phpfspot
35  */
36 class PHPFSPOT {
37
38    /**
39      * phpfspot configuration
40      * @access public
41      * @see PHPFSPOT_CFG()
42      * @var PHPFSPOT_CFG
43      */
44    var $cfg;
45
46    /**
47      * SQLite database handle to f-spot database
48      * @see PHPFSPOT_DB()
49      * @access public
50      * @var PHPFSPOT_DB
51      */
52    var $db;
53
54    /**
55      * SQLite database handle to phpfspot database
56      * @see PHPFSPOT_DB()
57      * @access public
58      * @var PHPFSPOT_DB
59      */
60    var $cfg_db;
61
62    /**
63     * Smarty template engine
64     * @link http://smarty.php.net smarty.php.net
65     * @see PHPFSPOT_TMPL()
66     * @access public
67     * @var PHPFSPOT_TMPL
68     */
69    var $tmpl;
70
71    /**
72     * full tag - list
73     * @access public
74     * @var array
75     */
76    var $tags;
77
78    /**
79     * list of available, not-selected, tags
80     * @access public
81     * @var array
82     */
83    var $avail_tags;
84
85    /**
86     * true if runtime error occued
87     * @access private
88     * @var boolean
89     */
90    private $runtime_error = false;
91
92    /**
93     * F-Spot database version
94     * @access private
95     * @var integer
96     */
97    private $dbver;
98
99    /**
100     * class constructor ($cfg, $db, $cfg_db, $tmpl, $db_ver)
101     *
102     * this function will be called on class construct
103     * and will check requirements, loads configuration,
104     * open databases and start the user session
105     */
106    public function __construct()
107    {
108       /**
109        * register PHPFSPOT class global
110        *
111        * @global PHPFSPOT $GLOBALS['phpfspot']
112        * @name $phpfspot
113        */
114       $GLOBALS['phpfspot'] =& $this;
115
116       $this->cfg = new PHPFSPOT_CFG;
117
118       /* verify config settings */
119       if($this->check_config_options()) {
120          exit(1);
121       }
122
123       /* set application name and version information */
124       $this->cfg->product = "phpfspot";
125       $this->cfg->version = "1.7";
126       $this->cfg->db_version = 2;
127
128       $this->sort_orders= array(
129          'date_asc' => 'Date &uarr;',
130          'date_desc' => 'Date &darr;',
131          'name_asc' => 'Name &uarr;',
132          'name_desc' => 'Name &darr;',
133          'tags_asc' => 'Tags &uarr;',
134          'tags_desc' => 'Tags &darr;',
135       );
136
137       /* Check necessary requirements */
138       if(!$this->check_requirements()) {
139          exit(1);
140       }
141
142       /******* Opening F-Spot's sqlite database *********/
143
144       /* Check if database file exists and is readable */
145       if(!file_exists($this->cfg->fspot_db) || !is_readable($this->cfg->fspot_db)) {
146          print "Error: ". $this->cfg->fspot_db ." does not exist or is not readable for user ". $this->getuid() .".\n";
147          exit(1);
148       }
149
150       /* Check if database file is writeable */
151       if(!is_writeable($this->cfg->fspot_db)) {
152          print "Error: ". $this->cfg->fspot_db ." is not writeable for user ". $this->getuid() .".\n";
153          print "Please fix permissions so phpfspot can create indices within the F-Spot database to"
154             ." speed up some database operations.\n";
155          exit(1);
156       }
157
158       /* open the database */
159       $this->db  = new PHPFSPOT_DB($this, $this->cfg->fspot_db);
160
161       /* change sqlite temp directory, if requested */
162       if(isset($this->cfg->sqlite_temp_dir)) {
163          $this->db->db_exec("
164             PRAGMA
165                temp_store_directory = '". $this->cfg->sqlite_temp_dir ."'
166          ");
167       }
168
169       /* get F-Spot database version */
170       $this->dbver = $this->getFspotDBVersion();
171
172       if(!is_writeable($this->cfg->base_path ."/templates_c")) {
173          print $this->cfg->base_path ."/templates_c: directory is not writeable for user ". $this->getuid() ."\n";
174          exit(1);
175       }
176
177       if(!is_writeable($this->cfg->thumb_path)) {
178          print $this->cfg->thumb_path .": directory is not writeable for user ". $this->getuid() ."\n";
179          exit(1);
180       }
181
182       /******* Opening phpfspot's sqlite database *********/
183
184       /* Check if directory where the database file is stored is writeable  */
185       if(!is_writeable(dirname($this->cfg->phpfspot_db))) {
186          print "Error: ". dirname($this->cfg->phpfspot_db) .": directory is not writeable for user ". $this->getuid() .".\n";
187          print "Please fix permissions so phpfspot can create its own sqlite database to store some settings.\n";
188          exit(1);
189       }
190
191       /* Check if database file is writeable */
192       if(file_exists($this->cfg->phpfspot_db) && !is_writeable($this->cfg->phpfspot_db)) {
193          print "Error: ". $this->cfg->phpfspot_db ." is not writeable for user ". $this->getuid() .".\n";
194          print "Please fix permissions so phpfspot can create its own sqlite database to store some settings.\n";
195          exit(1);
196       }
197
198       /* open the database */
199       $this->cfg_db = new PHPFSPOT_DB($this, $this->cfg->phpfspot_db);
200
201       /* change sqlite temp directory, if requested */
202       if(isset($this->cfg->sqlite_temp_dir)) {
203          $this->cfg_db->db_exec("
204             PRAGMA
205                temp_store_directory = '". $this->cfg->sqlite_temp_dir ."'
206          ");
207       }
208
209       /* Check if some tables need to be created */
210       $this->check_phpfspot_db();
211
212       /* overload Smarty class with our own template handler */
213       require_once "phpfspot_tmpl.php";
214       $this->tmpl = new PHPFSPOT_TMPL();
215
216       /* pre-set some template variables */
217       $this->tmpl->assign('web_path', $this->cfg->web_path);
218
219       /* Starting with F-Spot 0.4.2, the rating-feature was available */
220       if($this->dbver > 10) {
221          $this->tmpl->assign('has_rating', true);
222          $this->sort_orders = array_merge($this->sort_orders, array(
223             'rate_asc' => 'Rate &uarr;',
224             'rate_desc' => 'Rate &darr;',
225          ));
226       }
227
228       /* check if all necessary indices exist */
229       $this->checkDbIndices();
230
231       /* if session is not yet started, do it now */
232       if(session_id() == "")
233          session_start();
234
235       if(!isset($_SESSION['tag_condition']))
236          $_SESSION['tag_condition'] = 'or';
237
238       /* if sort-order has not been set yet, get the one specified in the config */
239       if(!isset($_SESSION['sort_order']))
240          $_SESSION['sort_order'] = $this->cfg->sort_order;
241
242       if(!isset($_SESSION['searchfor_tag']))
243          $_SESSION['searchfor_tag'] = '';
244
245       // if begin_with is still set but thumbs_per_page is now 0, unset it
246       if(isset($_SESSION['begin_with']) && $this->cfg->thumbs_per_page == 0)
247          unset($_SESSION['begin_with']);
248
249       // if user-friendly-url's are enabled, set also a flag for the template handler
250       if($this->is_user_friendly_url()) {
251          $this->tmpl->assign('user_friendly_url', 'true');
252       }
253
254    } // __construct()
255
256    public function __destruct()
257    {
258
259    } // __destruct()
260
261    /**
262     * show - generate html output
263     *
264     * this function can be called after the constructor has
265     * prepared everyhing. it will load the index.tpl smarty
266     * template. if necessary it will registere pre-selects
267     * (photo index, photo, tag search, date search) into
268     * users session.
269     */
270    public function show()
271    {
272       $this->tmpl->assign('searchfor_tag', $_SESSION['searchfor_tag']);
273       $this->tmpl->assign('page_title', $this->cfg->page_title);
274       $this->tmpl->assign('current_condition', $_SESSION['tag_condition']);
275       $this->tmpl->assign('template_path', 'themes/'. $this->cfg->theme_name);
276
277       /* parse URL */
278       if($this->is_user_friendly_url()) {
279          $content = $this->parse_user_friendly_url($_SERVER['REQUEST_URI']);
280       }
281
282       if(isset($_GET['mode'])) {
283
284          $_SESSION['start_action'] = $_GET['mode'];
285
286          switch($_GET['mode']) {
287             case 'showpi':
288                if(isset($_GET['tags'])) {
289                   $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
290                }
291                if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
292                   $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
293                }
294                if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
295                   $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
296                }
297                break;
298             case 'showp':
299                if(isset($_GET['tags'])) {
300                   $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
301                   $_SESSION['start_action'] = 'showp';
302                }
303                if(isset($_GET['id']) && is_numeric($_GET['id'])) {
304                   if($_SESSION['current_photo'] != $_GET['id'])
305                      unset($_SESSION['current_version']);
306                   $_SESSION['current_photo'] = $_GET['id'];
307                   $_SESSION['start_action'] = 'showp';
308                }
309                if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
310                   $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
311                }
312                if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
313                   $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
314                }
315                break;
316             case 'export':
317                /* fetch export template */
318                print $this->tmpl->fetch("export.tpl");
319                /* no further execution necessary. */
320                return;
321                break;
322             case 'slideshow':
323                /* fetch slideshow template */
324                print $this->tmpl->show("slideshow.tpl");
325                /* no further execution necessary. */
326                return;
327                break;
328             case 'rss':
329                if(isset($_GET['tags'])) {
330                   $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
331                }
332                if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
333                   $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
334                }
335                if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
336                   $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
337                }
338                $this->getRSSFeed();
339                return;
340                break;
341          }
342       }
343
344       /* if date-search variables are registered in the session, set the check
345          for "consider date-range" in the html output
346       */
347       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date']))
348          $this->tmpl->assign('date_search_enabled', true);
349
350       /* if rate-search variables are registered in the session, set the check
351          for "consider rate-range" in the html output
352       */
353       if(isset($_SESSION['rate_from']) && isset($_SESSION['rate_to'])) {
354          $this->tmpl->assign('rate_search_enabled', true);
355       }
356
357       $this->tmpl->register_function("sort_select_list", array(&$this, "smarty_sort_select_list"), false);
358       $this->tmpl->assign('search_from_date', $this->get_date_text_field('from'));
359       $this->tmpl->assign('search_to_date', $this->get_date_text_field('to'));
360
361       $this->tmpl->assign('preset_selected_tags', $this->getSelectedTags());
362       $this->tmpl->assign('preset_available_tags', $this->getAvailableTags());
363       $this->tmpl->assign('rate_search', $this->get_rate_search());
364
365       /* if no site-content has been set yet... */
366       if(!isset($content)) {
367          /* if tags are already selected, we can immediately display photo-index */
368          if((isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags']) &&
369              isset($_SESSION['start_action']) && $_SESSION['start_action'] != 'showp') ||
370             (isset($_SESSION['start_action']) && $_SESSION['start_action'] == 'showpi'))
371             $this->tmpl->assign('initial_content', $this->showPhotoIndex());
372          else {
373             /* if a photo is already selected, we can immediately display single-photo */
374             if(isset($_SESSION['current_photo']) && !empty($_SESSION['current_photo']))
375                $this->tmpl->assign('initial_content', $this->showPhoto($_SESSION['current_photo']));
376             else {
377                /* ok, then let us show the welcome page... */
378                $this->tmpl->assign('initial_content', $this->tmpl->fetch('welcome.tpl'));
379             }
380          }
381       }
382       else
383          $this->tmpl->assign('initial_content', $content);
384
385       $this->tmpl->show("index.tpl");
386
387    } // show()
388
389    /**
390     * get_tags - grab all tags of f-spot's database
391     *
392     * this function will get all available tags from
393     * the f-spot database and store them within two
394     * arrays within this class for later usage. in
395     * fact, if the user requests (hide_tags) it will
396     * opt-out some of them.
397     *
398     * this function is getting called once by show()
399     */
400    private function get_tags()
401    {
402       $this->avail_tags = Array();
403       $count = 0;
404    
405       /* if show_tags has been set in the configuration (only show photos
406          which are tagged by these tags) they following will take care,
407          that only these other tags are displayed where the photo is also
408          tagged with one of show_tags.
409       */
410       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
411          $query_str="
412             SELECT
413                DISTINCT t1.id as id, t1.name as name
414             FROM
415                photo_tags pt1
416             INNER JOIN photo_tags
417                pt2 ON pt1.photo_id=pt2.photo_id
418             INNER JOIN tags t1
419                ON t1.id=pt1.tag_id
420             INNER JOIN tags t2
421                ON t2.id=pt2.tag_id
422             WHERE
423                t2.name IN  ('".implode("','",$this->cfg->show_tags)."')
424             ORDER BY
425                t1.sort_priority ASC";
426
427          $result = $this->db->db_query($query_str);
428       }
429       else
430       {
431          $result = $this->db->db_query("
432             SELECT id as id,name as name
433             FROM tags
434             ORDER BY sort_priority ASC
435          ");
436       }
437       
438       while($row = $this->db->db_fetch_object($result)) {
439
440          $tag_id = $row['id'];
441          $tag_name = $row['name'];
442
443          /* if the user has specified to ignore this tag in phpfspot's
444             configuration, ignore it here so it does not get added to
445             the tag list.
446          */
447          if(in_array($row['name'], $this->cfg->hide_tags))
448             continue;
449
450          /* if you include the following if-clause and the user has specified
451             to only show certain tags which are specified in phpfspot's
452             configuration, ignore all others so they will not be added to the
453             tag list.
454          if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags) &&
455             !in_array($row['name'], $this->cfg->show_tags))
456             continue;
457          */
458
459          $this->tags[$tag_id] = $tag_name; 
460          $this->avail_tags[$count] = $tag_id;
461          $count++;
462
463       }
464
465    } // get_tags()
466
467    /** 
468     * get all photo details from F-Spot database
469     * 
470     * this function queries the F-Spot database for all available
471     * details of the requested photo. It returns them as a object.
472     *
473     * Furthermore it takes care of the photo version to be requested.
474     * If photo version is not yet, it queries information for the
475     * original photo.
476     *
477     * @param integer $idx
478     * @return object|null
479     */
480    public function get_photo_details($idx, $version_idx = NULL)
481    {
482       /* ~ F-Spot version 0.3.x */
483       if($this->dbver < 9) {
484          $query_str = "
485             SELECT
486                p.id as id,
487                p.name as name,
488                p.time as time,
489                p.directory_path as directory_path,
490                p.description as description
491             FROM
492                photos p
493          ";
494       }
495       else {
496          /* till F-Spot version 0.4.1 */
497          if($this->dbver < 11) {
498             $query_str = "
499                SELECT
500                   p.id as id,
501                   p.uri as uri,
502                   p.time as time,
503                   p.description as description
504                FROM
505                   photos p
506             ";
507          }
508          else {
509             /* rating value got introduced */
510             $query_str = "
511                SELECT
512                   p.id as id,
513                   p.uri as uri,
514                   p.time as time,
515                   p.description as description,
516                   p.rating as rating
517                FROM
518                   photos p
519             ";
520          }
521       }
522
523       /* if show_tags is set, only return details of photos which are
524          tagged with a tag that has been specified to be shown.
525       */
526       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
527          $query_str.= "
528             INNER JOIN photo_tags pt
529                ON p.id=pt.photo_id
530             INNER JOIN tags t
531                ON pt.tag_id=t.id
532             WHERE p.id='". $idx ."'
533             AND t.name IN ('".implode("','",$this->cfg->show_tags)."')";
534       }
535       else {
536          $query_str.= "
537             WHERE p.id='". $idx ."'
538          ";
539       }
540
541       if($row = $this->db->db_fetchSingleRow($query_str)) {
542
543          /* before F-Spot db version 9 there was no uri column but
544             seperated fields for directory_path and name (= filename).
545          */
546          if($this->dbver < 9) {
547             $row['uri'] = "file://". $row['directory_path'] ."/". $row['name'];
548          }
549
550          /* if version-idx has not yet been set, get the latest photo version */
551          if(!isset($version_idx) || !$this->is_valid_version($idx, $version_idx))
552             $version_idx = $this->get_latest_version($idx);
553
554          /* if an alternative version has been requested. But we
555             support this only for F-Spot database versions from
556             v9.
557          */
558          if($version_idx > 0 && $this->dbver >= 9) {
559             /* check for alternative versions */
560             if($version = $this->db->db_fetchSingleRow("
561                SELECT
562                   version_id, name, uri
563                FROM
564                   photo_versions
565                WHERE
566                   photo_id LIKE '". $idx ."'
567                AND
568                   version_id LIKE '". $version_idx ."'")) {
569
570                $row['name'] = $version['name'];
571                $row['uri'] = $version['uri'];
572             }
573          }
574
575          return $row;
576
577       }
578
579       return null;
580
581    } // get_photo_details()
582
583    /**
584     * returns aligned photo names 
585     *
586     * this function returns aligned (length) names for a specific photo.
587     * If the length of the name exceeds $limit the name will bei
588     * shrinked (...).
589     *
590     * @param integer $idx
591     * @param integer $limit
592     * @return string|null
593     */
594    public function getPhotoName($idx, $limit = 0)
595    {
596       if($details = $this->get_photo_details($idx)) {
597          if($long_name = $this->parse_uri($details['uri'], 'filename')) {
598             $name = $this->shrink_text($long_name, $limit);
599             return $name;
600          }
601       }
602
603       return null;
604
605    } // getPhotoName()
606
607    /**
608     * get photo rating level
609     *
610     * this function will return the integer-based rating level of a
611     * photo. This can only be done, if the F-Spot database is at a
612     * specific version. If rating value can not be found, zero will
613     * be returned indicating no rating value is available.
614     *
615     * @param integer idx
616     * @return integer
617     */
618    public function get_photo_rating($idx)
619    {
620       if($detail = $this->get_photo_details($idx)) {
621          if(isset($detail['rating']))
622             return $detail['rating'];
623       }
624
625       return 0;
626
627    } // get_photo_rating()
628
629    /**
630     * get rate-search bars
631     *
632     * this function will return the rating-bars for the search field.
633     *
634     * @return string
635     */
636    public function get_rate_search()
637    {
638       $bar = "";
639
640       for($i = 1; $i <= 5; $i++) {
641
642          $bar.= "<img id=\"rate_from_". $i ."\" src=\"";
643
644          if(isset($_SESSION['rate_from']) && $i <= $_SESSION['rate_from'])
645             $bar.= $this->cfg->web_path ."/resources/star.png";
646          else
647             $bar.= $this->cfg->web_path ."/resources/empty_rate.png";
648
649          $bar.= "\"
650             onmouseover=\"show_rate('from', ". $i .");\"
651             onmouseout=\"reset_rate('from');\"
652             onclick=\"set_rate('from', ". $i .")\" />";
653       }
654
655       $bar.= "<br />\n";
656
657        for($i = 1; $i <= 5; $i++) {
658
659          $bar.= "<img id=\"rate_to_". $i ."\" src=\"";
660
661          if(isset($_SESSION['rate_to']) && $i <= $_SESSION['rate_to'])
662             $bar.= $this->cfg->web_path ."/resources/star.png";
663          else
664             $bar.= $this->cfg->web_path ."/resources/empty_rate.png";
665
666          $bar.= "\"
667             onmouseover=\"show_rate('to', ". $i .");\"
668             onmouseout=\"reset_rate('to');\"
669             onclick=\"set_rate('to', ". $i .");\" />";
670       }
671
672       return $bar;
673
674    } // get_rate_search()
675
676    /**
677     * shrink text according provided limit
678     *
679     * If the length of the name exceeds $limit, text will be shortend
680     * and inner content will be replaced with "...".
681     *
682     * @param string $ext
683     * @param integer $limit
684     * @return string
685     */
686    private function shrink_text($text, $limit)
687    {
688       if($limit != 0 && strlen($text) > $limit) {
689          $text = substr($text, 0, $limit-5) ."...". substr($text, -($limit-5));
690       }
691
692       return $text;
693
694    } // shrink_text();
695
696    /**
697     * translate f-spoth photo path
698     * 
699     * as the full-qualified path recorded in the f-spot database
700     * is usally not the same as on the webserver, this function
701     * will replace the path with that one specified in the cfg
702     * @param string $path
703     * @return string
704     */
705    public function translate_path($path)
706    {  
707       return str_replace($this->cfg->path_replace_from, $this->cfg->path_replace_to, $path);
708
709    } // translate_path
710
711    /**
712     * control HTML ouput for a single photo
713     *
714     * this function provides all the necessary information
715     * for the single photo template.
716     * @param integer photo
717     */
718    public function showPhoto($photo)
719    {
720       /* get all photos from the current photo selection */
721       $all_photos = $this->getPhotoSelection();
722       $count = count($all_photos);
723
724       for($i = 0; $i < $count; $i++) {
725          
726          // $get_next will be set, when the photo which has to
727          // be displayed has been found - this means that the
728          // next available is in fact the NEXT image (for the
729          // navigation icons) 
730          if(isset($get_next)) {
731             $next_img = $all_photos[$i];
732             break;
733          }
734
735          /* the next photo is our NEXT photo */
736          if($all_photos[$i] == $photo) {
737             $get_next = 1;
738          }
739          else {
740             $previous_img = $all_photos[$i];
741          }
742
743          if($photo == $all_photos[$i]) {
744                $current = $i;
745          }
746       }
747
748       $details = $this->get_photo_details($photo);
749
750       if(!$details) {
751          print "error";
752          return;
753       }
754
755       $orig_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
756
757       /* if current version is already set, use it */
758       if($this->get_current_version() !== false)
759          $version = $this->get_current_version();
760
761       /* if version not set yet, we assume to display the latest version */
762       if(!isset($version) || !$this->is_valid_version($photo, $version))
763          $version = $this->get_latest_version($photo);
764
765       $thumb_path = $this->get_thumb_path($this->cfg->photo_width, $photo, $version);
766
767       if(!file_exists($orig_path)) {
768          $this->_error("Photo ". $orig_path ." does not exist!<br />\n");
769          return;
770       }
771
772       if(!is_readable($orig_path)) {
773          $this->_error("Photo ". $orig_path ." is not readable for user ". $this->getuid() ."<br />\n");
774          return;
775       }
776
777       /* If the thumbnail doesn't exist yet, try to create it */
778       if(!file_exists($thumb_path)) {
779          $this->gen_thumb($photo, true);
780          $thumb_path = $this->get_thumb_path($this->cfg->photo_width, $photo, $version);
781       }
782
783       /* get mime-type, height and width from the original photo */
784       $info = getimagesize($orig_path);
785
786       /* get EXIF information if JPEG */
787       if(isset($info['mime']) && $info['mime'] == "image/jpeg") {
788          $meta = $this->get_meta_informations($orig_path);
789       }
790
791       /* If EXIF data are available, use them */
792       if(isset($meta['ExifImageWidth'])) {
793          $meta_res = $meta['ExifImageWidth'] ."x". $meta['ExifImageLength'];
794       } else {
795          $meta_res = $info[0] ."x". $info[1]; 
796       }
797
798       $meta_make = isset($meta['Make']) ? $meta['Make'] ." / ". $meta['Model'] : "n/a";
799       $meta_size = isset($meta['FileSize']) ? round($meta['FileSize']/1024, 1) ."kbyte" : "n/a";
800
801       $extern_link = "index.php?mode=showp&id=". $photo;
802       $current_tags = $this->getCurrentTags();
803       if($current_tags != "") {
804          $extern_link.= "&tags=". $current_tags;
805       }
806       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
807          $extern_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
808       }
809
810       $this->tmpl->assign('extern_link', $extern_link);
811
812       if(!file_exists($thumb_path)) {
813          $this->_error("Can't open file ". $thumb_path ."\n");
814          return;
815       }
816
817       $info_thumb = getimagesize($thumb_path);
818
819       $this->tmpl->assign('description', $details['description']);
820       $this->tmpl->assign('image_name', $this->parse_uri($details['uri'], 'filename'));
821       $this->tmpl->assign('image_rating', $this->get_photo_rating($photo));
822
823       $this->tmpl->assign('width', $info_thumb[0]);
824       $this->tmpl->assign('height', $info_thumb[1]);
825       $this->tmpl->assign('ExifMadeOn', strftime("%a %x %X", $details['time']));
826       $this->tmpl->assign('ExifMadeWith', $meta_make);
827       $this->tmpl->assign('ExifOrigResolution', $meta_res);
828       $this->tmpl->assign('ExifFileSize', $meta_size);
829  
830       if($this->is_user_friendly_url()) {
831          $this->tmpl->assign('image_url', '/photo/'. $photo ."/". $this->cfg->photo_width .'/'. $version);
832          $this->tmpl->assign('image_url_full', '/photo/'. $photo);
833       }
834       else {
835          $this->tmpl->assign('image_url', 'phpfspot_img.php?idx='. $photo ."&amp;width=". $this->cfg->photo_width ."&amp;version=". $version);
836          $this->tmpl->assign('image_url_full', 'phpfspot_img.php?idx='. $photo);
837       }
838
839       $this->tmpl->assign('image_filename', $this->parse_uri($details['uri'], 'filename'));
840
841       $this->tmpl->assign('tags', $this->get_photo_tags($photo));
842       $this->tmpl->assign('current_page', $this->getCurrentPage($current, $count));
843       $this->tmpl->assign('current_img', $photo);
844
845       if(isset($previous_img)) {
846          $this->tmpl->assign('previous_url', "javascript:showPhoto(". $previous_img .");");
847          $this->tmpl->assign('prev_img', $previous_img);
848       }
849
850       if(isset($next_img)) {
851          $this->tmpl->assign('next_url', "javascript:showPhoto(". $next_img .");");
852          $this->tmpl->assign('next_img', $next_img);
853       }
854
855       $this->tmpl->assign('mini_width', $this->cfg->mini_width);
856       $this->tmpl->assign('photo_width', $this->cfg->photo_width);
857       $this->tmpl->assign('photo_number', $i);
858       $this->tmpl->assign('photo_count', count($all_photos));
859       $this->tmpl->assign('photo', $photo);
860       $this->tmpl->assign('version', $version);
861
862       /* if the photo as alternative versions, set a flag for the template */
863       if($this->get_photo_versions($photo))
864          $this->tmpl->assign('has_versions', true);
865
866       $this->tmpl->register_function("photo_version_select_list", array(&$this, "smarty_photo_version_select_list"), false);
867
868       return $this->tmpl->fetch("single_photo.tpl");
869
870    } // showPhoto()
871
872    /**
873     * all available tags and tag cloud
874     *
875     * this function outputs all available tags (time ordered)
876     * and in addition output them as tag cloud (tags which have
877     * many photos will appears more then others)
878     */
879    public function getAvailableTags()
880    {
881       /* retrive tags from database */
882       $this->get_tags();
883
884       $output = "";
885
886       $result = $this->db->db_query("
887          SELECT tag_id as id, count(tag_id) as quantity
888          FROM photo_tags
889          INNER JOIN tags t
890             ON t.id = tag_id
891          GROUP BY tag_id
892          ORDER BY t.name ASC
893       ");
894
895       $tags = Array();
896
897       while($row = $this->db->db_fetch_object($result)) {
898          $tags[$row['id']] = $row['quantity'];
899       }
900
901       // change these font sizes if you will
902       $max_size = 125; // max font size in %
903       $min_size = 75; // min font size in %
904
905       // color
906       $max_sat = hexdec('cc');
907       $min_sat = hexdec('44');
908
909       // get the largest and smallest array values
910       $max_qty = max(array_values($tags));
911       $min_qty = min(array_values($tags));
912
913       // find the range of values
914       $spread = $max_qty - $min_qty;
915       if (0 == $spread) { // we don't want to divide by zero
916          $spread = 1;
917       }
918
919       // determine the font-size increment
920       // this is the increase per tag quantity (times used)
921       $step = ($max_size - $min_size)/($spread);
922       $step_sat = ($max_sat - $min_sat)/($spread);
923
924       // loop through our tag array
925       foreach ($tags as $key => $value) {
926
927          /* has the currently processed tag already been added to
928             the selected tag list? if so, ignore it here...
929          */
930          if(isset($_SESSION['selected_tags']) && in_array($key, $_SESSION['selected_tags']))
931             continue;
932
933          // calculate CSS font-size
934          // find the $value in excess of $min_qty
935          // multiply by the font-size increment ($size)
936          // and add the $min_size set above
937          $size = $min_size + (($value - $min_qty) * $step);
938          // uncomment if you want sizes in whole %:
939          $size = ceil($size);
940
941          $color = $min_sat + ($value - $min_qty) * $step_sat;
942
943          $r = '44';
944          $g = dechex($color);
945          $b = '88';
946
947          if(isset($this->tags[$key])) {
948             if($this->is_user_friendly_url()) {
949                $output.= "<a href=\"". $this->cfg->web_path ."/tag/". $key ."\"
950                   onclick=\"Tags('add', ". $key ."); return false;\"
951                   class=\"tag\"
952                   style=\"font-size: ". $size ."%; color: #". $r.$g.$b .";\"
953                   title=\"Tag ". $this->tags[$key] .", ". $tags[$key] ." picture(s)\">". $this->tags[$key] ."</a>, ";
954             }
955             else {
956                $output.= "<a href=\"". $this->cfg->web_path ."/index.php?mode=showpi\"
957                   onclick=\"Tags('add', ". $key ."); return false;\"
958                   class=\"tag\"
959                   style=\"font-size: ". $size ."%; color: #". $r.$g.$b .";\"
960                   title=\"Tag ". $this->tags[$key] .", ". $tags[$key] ." picture(s)\">". $this->tags[$key] ."</a>, ";
961             }
962          }
963       }
964
965       $output = substr($output, 0, strlen($output)-2);
966       return $output;
967
968    } // getAvailableTags()
969
970    /**
971     * output all selected tags
972     *
973     * this function output all tags which have been selected
974     * by the user. the selected tags are stored in the 
975     * session-variable $_SESSION['selected_tags']
976     * @return string
977     */
978    public function getSelectedTags($type = 'link')
979    {
980       /* retrive tags from database */
981       $this->get_tags();
982
983       $output = "";
984
985       foreach($this->avail_tags as $tag)
986       {
987          // return all selected tags
988          if(isset($_SESSION['selected_tags']) && in_array($tag, $_SESSION['selected_tags'])) {
989
990             switch($type) {
991                default:
992                case 'link':
993                   $output.= "<a href=\"javascript:Tags('del', ". $tag .");\" class=\"tag\">". $this->tags[$tag] ."</a>, ";
994                   break;
995                case 'img':
996                   $output.= "
997                   <div class=\"tagresulttag\">
998                    <a href=\"javascript:Tags('del', ". $tag .");\" title=\"". $this->tags[$tag] ."\">
999                     <img src=\"". $this->cfg->web_path ."/phpfspot_img.php?tagidx=". $tag ."\" />
1000                    </a>
1001                   </div>
1002                   ";
1003                   break;
1004             }
1005          }
1006       }
1007
1008       if($output != "") {
1009          $output = substr($output, 0, strlen($output)-2);
1010          return $output;
1011       }
1012       else {
1013          return "no tags selected";
1014       }
1015
1016    } // getSelectedTags()
1017
1018    /**
1019     * add tag to users session variable
1020     *
1021     * this function will add the specified to users current
1022     * tag selection. if a date search has been made before
1023     * it will be now cleared
1024     * @return string
1025     */
1026    public function addTag($tag)
1027    {
1028       if(!isset($_SESSION['selected_tags']))
1029          $_SESSION['selected_tags'] = Array();
1030
1031       if(isset($_SESSION['searchfor_tag']))
1032          unset($_SESSION['searchfor_tag']);
1033
1034       // has the user requested to hide this tag, and still someone,
1035       // somehow tries to add it, don't allow this.
1036       if(!isset($this->cfg->hide_tags) &&
1037          in_array($this->get_tag_name($tag), $this->cfg->hide_tags))
1038          return "ok";
1039
1040       if(!in_array($tag, $_SESSION['selected_tags']))
1041          array_push($_SESSION['selected_tags'], $tag);
1042
1043       return "ok";
1044    
1045    } // addTag()
1046
1047    /**
1048     * remove tag to users session variable
1049     *
1050     * this function removes the specified tag from
1051     * users current tag selection
1052     * @param string $tag
1053     * @return string
1054     */
1055    public function delTag($tag)
1056    {
1057       if(isset($_SESSION['searchfor_tag']))
1058          unset($_SESSION['searchfor_tag']);
1059
1060       if(isset($_SESSION['selected_tags'])) {
1061          $key = array_search($tag, $_SESSION['selected_tags']);
1062          unset($_SESSION['selected_tags'][$key]);
1063          sort($_SESSION['selected_tags']);
1064       }
1065
1066       return "ok";
1067
1068    } // delTag()
1069
1070    /**
1071     * reset tag selection
1072     *
1073     * if there is any tag selection, it will be
1074     * deleted now
1075     */
1076    public function resetTags()
1077    {
1078       if(isset($_SESSION['selected_tags']))
1079          unset($_SESSION['selected_tags']);
1080
1081    } // resetTags()
1082
1083    /**
1084     * returns the value for the autocomplete tag-search
1085     * @return string
1086     */
1087    public function get_xml_tag_list()
1088    {
1089       if(!isset($_GET['search']) || !is_string($_GET['search']))
1090          $_GET['search'] = '';
1091       
1092       $length = 15;
1093       $i = 1;
1094          
1095       /* retrive tags from database */
1096       $this->get_tags();
1097
1098       $matched_tags = Array();
1099
1100       header("Content-Type: text/xml");
1101
1102       $string = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
1103       $string.= "<results>\n";
1104
1105       foreach($this->avail_tags as $tag)
1106       {
1107          if(!empty($_GET['search']) &&
1108             preg_match("/". $_GET['search'] ."/i", $this->tags[$tag]) &&
1109             count($matched_tags) < $length) {
1110
1111             $count = $this->get_num_photos($tag);
1112
1113             if($count == 1) {
1114                $string.= " <rs id=\"". $i ."\" info=\"". $count ." photo\">". $this->tags[$tag] ."</rs>\n";
1115             }
1116             else {
1117                $string.= " <rs id=\"". $i ."\" info=\"". $count ." photos\">". $this->tags[$tag] ."</rs>\n";
1118
1119             }
1120             $i++;
1121          }
1122
1123          /* if we have collected enough items, break out */
1124          if(count($matched_tags) >= $length)
1125             break;
1126       }
1127
1128       $string.= "</results>\n";
1129
1130       return $string;
1131
1132    } // get_xml_tag_list()
1133
1134
1135    /**
1136     * reset single photo
1137     *
1138     * if a specific photo was requested (external link)
1139     * unset the session variable now
1140     */
1141    public function resetPhotoView()
1142    {
1143       if(isset($_SESSION['current_photo']))
1144          unset($_SESSION['current_photo']);
1145
1146       if(isset($_SESSION['current_version']))
1147          unset($_SESSION['current_version']);
1148
1149    } // resetPhotoView();
1150
1151    /**
1152     * reset tag search
1153     *
1154     * if any tag search has taken place, reset it now
1155     */
1156    public function resetTagSearch()
1157    {
1158       if(isset($_SESSION['searchfor_tag']))
1159          unset($_SESSION['searchfor_tag']);
1160
1161    } // resetTagSearch()
1162
1163    /**
1164     * reset name search
1165     *
1166     * if any name search has taken place, reset it now
1167     */
1168    public function resetNameSearch()
1169    {
1170       if(isset($_SESSION['searchfor_name']))
1171          unset($_SESSION['searchfor_name']);
1172
1173    } // resetNameSearch()
1174
1175    /**
1176     * reset date search
1177     *
1178     * if any date search has taken place, reset it now.
1179     */
1180    public function resetDateSearch()
1181    {
1182       if(isset($_SESSION['from_date']))
1183          unset($_SESSION['from_date']);
1184       if(isset($_SESSION['to_date']))
1185          unset($_SESSION['to_date']);
1186
1187    } // resetDateSearch();
1188
1189    /**
1190     * reset rate search
1191     *
1192     * if any rate search has taken place, reset it now.
1193     */
1194    public function resetRateSearch()
1195    {
1196       if(isset($_SESSION['rate_from']))
1197          unset($_SESSION['rate_from']);
1198       if(isset($_SESSION['rate_to']))
1199          unset($_SESSION['rate_to']);
1200
1201    } // resetRateSearch();
1202
1203    /**
1204     * return all photo according selection
1205     *
1206     * this function returns all photos based on
1207     * the tag-selection, tag- or date-search.
1208     * the tag-search also has to take care of AND
1209     * and OR conjunctions
1210     * @return array
1211     */
1212    public function getPhotoSelection()
1213    {
1214       $matched_photos = Array();
1215       $additional_where_cond = "";
1216
1217       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
1218          $from_date = $_SESSION['from_date'];
1219          $to_date = $_SESSION['to_date'];
1220          $additional_where_cond.= "
1221                p.time>='". $from_date ."'
1222             AND
1223                p.time<='". $to_date ."'
1224          ";
1225       } 
1226
1227       if(isset($_SESSION['searchfor_name'])) {
1228
1229          /* check for previous conditions. if so add 'AND' */
1230          if(!empty($additional_where_cond)) {
1231             $additional_where_cond.= " AND ";
1232          }
1233
1234          if($this->dbver < 9) {
1235             $additional_where_cond.= "
1236                   (
1237                         p.name LIKE '%". $_SESSION['searchfor_name'] ."%'
1238                      OR
1239                         p.description LIKE '%". $_SESSION['searchfor_name'] ."%'
1240                   )
1241             ";
1242          }
1243          else {
1244             $additional_where_cond.= "
1245                   (
1246                         basename(p.uri) LIKE '%". $_SESSION['searchfor_name'] ."%'
1247                      OR
1248                         p.description LIKE '%". $_SESSION['searchfor_name'] ."%'
1249                   )
1250             ";
1251          }
1252       }
1253
1254       /* limit result based on rate-search */
1255       if(isset($_SESSION['rate_from']) && isset($_SESSION['rate_to'])) {
1256
1257          if($this->dbver > 10) {
1258
1259             /* check for previous conditions. if so add 'AND' */
1260             if(!empty($additional_where_cond)) {
1261                $additional_where_cond.= " AND ";
1262             }
1263
1264             $additional_where_cond.= "
1265                      p.rating >= ". $_SESSION['rate_from'] ."
1266                   AND
1267                      p.rating <= ". $_SESSION['rate_to'] ."
1268             ";
1269          }
1270       }
1271
1272       if(isset($_SESSION['sort_order'])) {
1273          $order_str = $this->get_sort_order();
1274       }
1275
1276       /* return a search result */
1277       if(isset($_SESSION['searchfor_tag']) && $_SESSION['searchfor_tag'] != '') {
1278          $query_str = "
1279             SELECT DISTINCT
1280                pt1.photo_id as photo_id
1281             FROM
1282                photo_tags pt1
1283             INNER JOIN photo_tags pt2
1284                ON pt1.photo_id=pt2.photo_id
1285             INNER JOIN tags t
1286                ON pt1.tag_id=t.id
1287             INNER JOIN photos p
1288                ON pt1.photo_id=p.id
1289             INNER JOIN tags t2
1290                ON pt2.tag_id=t2.id
1291             WHERE t.name LIKE '%". $_SESSION['searchfor_tag'] ."%' ";
1292
1293          if(!empty($additional_where_cond))
1294             $query_str.= "AND ". $additional_where_cond ." ";
1295
1296          if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
1297             $query_str.= "AND t2.name IN ('".implode("','",$this->cfg->show_tags)."')";
1298          }
1299          
1300          if(isset($order_str))
1301             $query_str.= $order_str;
1302
1303          $result = $this->db->db_query($query_str);
1304          while($row = $this->db->db_fetch_object($result)) {
1305             array_push($matched_photos, $row['photo_id']);
1306          }
1307          return $matched_photos;
1308       }
1309
1310       /* return according the selected tags */
1311       if(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
1312          $selected = "";
1313          foreach($_SESSION['selected_tags'] as $tag)
1314             $selected.= $tag .",";
1315          $selected = substr($selected, 0, strlen($selected)-1);
1316
1317          /* photo has to match at least on of the selected tags */
1318          if($_SESSION['tag_condition'] == 'or') {
1319             $query_str = "
1320                SELECT DISTINCT
1321                   pt1.photo_id as photo_id
1322                FROM
1323                   photo_tags pt1
1324                INNER JOIN photo_tags pt2
1325                   ON pt1.photo_id=pt2.photo_id
1326                INNER JOIN tags t
1327                   ON pt2.tag_id=t.id
1328                INNER JOIN photos p
1329                   ON pt1.photo_id=p.id
1330                WHERE pt1.tag_id IN (". $selected .")
1331             ";
1332             if(!empty($additional_where_cond))
1333                $query_str.= "AND ". $additional_where_cond ." ";
1334
1335             if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
1336                $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags)."')";
1337             }
1338
1339             if(isset($order_str))
1340                $query_str.= $order_str;
1341          }
1342          /* photo has to match all selected tags */
1343          elseif($_SESSION['tag_condition'] == 'and') {
1344
1345             if(count($_SESSION['selected_tags']) >= 32) {
1346                print "A SQLite limit of 32 tables within a JOIN SELECT avoids to<br />\n";
1347                print "evaluate your tag selection. Please remove some tags from your selection.\n";
1348                return Array();
1349             } 
1350
1351             /* Join together a table looking like
1352
1353                pt1.photo_id pt1.tag_id pt2.photo_id pt2.tag_id ...
1354
1355                so the query can quickly return all images matching the
1356                selected tags in an AND condition
1357
1358             */
1359
1360             $query_str = "
1361                SELECT DISTINCT
1362                   pt1.photo_id as photo_id
1363                FROM
1364                   photo_tags pt1
1365             ";
1366
1367             if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
1368                $query_str.= "
1369                   INNER JOIN tags t
1370                      ON pt1.tag_id=t.id
1371                ";
1372             }
1373
1374             for($i = 0; $i < count($_SESSION['selected_tags']); $i++) {
1375                $query_str.= "
1376                   INNER JOIN photo_tags pt". ($i+2) ."
1377                      ON pt1.photo_id=pt". ($i+2) .".photo_id
1378                ";
1379             }
1380             $query_str.= "
1381                INNER JOIN photos p
1382                   ON pt1.photo_id=p.id
1383             ";
1384             $query_str.= "WHERE pt2.tag_id=". $_SESSION['selected_tags'][0]." ";
1385             for($i = 1; $i < count($_SESSION['selected_tags']); $i++) {
1386                $query_str.= "
1387                   AND pt". ($i+2) .".tag_id=". $_SESSION['selected_tags'][$i] ."
1388                "; 
1389             }
1390             if(!empty($additional_where_cond))
1391                $query_str.= "AND ". $additional_where_cond;
1392
1393             if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
1394                $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags). "')";
1395             }
1396
1397             if(isset($order_str))
1398                $query_str.= $order_str;
1399
1400          }
1401
1402          $result = $this->db->db_query($query_str);
1403          while($row = $this->db->db_fetch_object($result)) {
1404             array_push($matched_photos, $row['photo_id']);
1405          }
1406          return $matched_photos;
1407       }
1408
1409       /* return all available photos */
1410       $query_str = "
1411          SELECT DISTINCT
1412             p.id as id
1413          FROM
1414             photos p
1415          LEFT JOIN photo_tags pt
1416             ON p.id=pt.photo_id
1417          LEFT JOIN tags t
1418             ON pt.tag_id=t.id
1419       ";
1420
1421       if(!empty($additional_where_cond))
1422          $query_str.= "WHERE ". $additional_where_cond ." ";
1423
1424       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
1425          if(!empty($additional_where_cond))
1426             $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags). "')";
1427          else
1428             $query_str.= "WHERE t.name IN ('".implode("','",$this->cfg->show_tags). "')";
1429       }
1430  
1431       if(isset($order_str))
1432          $query_str.= $order_str;
1433
1434       $result = $this->db->db_query($query_str);
1435       while($row = $this->db->db_fetch_object($result)) {
1436          array_push($matched_photos, $row['id']);
1437       }
1438       return $matched_photos;
1439
1440    } // getPhotoSelection()
1441
1442     /**
1443     * control HTML ouput for photo index
1444     *
1445     * this function provides all the necessary information
1446     * for the photo index template.
1447     * @return string
1448     */
1449    public function showPhotoIndex()
1450    {
1451       $photos = $this->getPhotoSelection();
1452       $current_tags = $this->getCurrentTags();
1453
1454       $count = count($photos);
1455
1456       /* if all thumbnails should be shown on one page */
1457       if(!isset($this->cfg->thumbs_per_page) || $this->cfg->thumbs_per_page == 0) {
1458          $begin_with = 0;
1459          $end_with = $count;
1460       }
1461       /* thumbnails should be splitted up in several pages */
1462       elseif($this->cfg->thumbs_per_page > 0) {
1463
1464          if(!isset($_SESSION['begin_with']) || $_SESSION['begin_with'] == 0) {
1465             $begin_with = 0;
1466          }
1467          else {
1468             $begin_with = $_SESSION['begin_with'];
1469          }
1470
1471          $end_with = $begin_with + $this->cfg->thumbs_per_page;
1472       }
1473
1474       $thumbs = 0;
1475
1476       for($i = $begin_with; $i < $end_with; $i++) {
1477
1478          if(!isset($photos[$i]))
1479             continue;
1480
1481          /* on first run, initalize all used variables */
1482          if($thumbs == 0) {
1483             $images = Array();
1484             $images[$thumbs]        = Array();
1485             $img_height[$thumbs]    = Array();
1486             $img_width[$thumbs]     = Array();
1487             $img_id[$thumbs]        = Array();
1488             $img_name[$thumbs]      = Array();
1489             $img_fullname[$thumbs]  = Array();
1490             $img_title              = Array();
1491             $img_rating             = Array();
1492          }
1493
1494          $images[$thumbs] = $photos[$i];
1495          $img_id[$thumbs] = $i;
1496          $img_name[$thumbs] = htmlspecialchars($this->getPhotoName($photos[$i], 15));
1497          $img_fullname[$thumbs] = htmlspecialchars($this->getPhotoName($photos[$i], 0));
1498          $img_title[$thumbs] = "Click to view photo ". htmlspecialchars($this->getPhotoName($photos[$i], 0));
1499          $img_rating[$thumbs] = $this->get_photo_rating($photos[$i]);
1500
1501          /* get local path of the thumbnail image to be displayed */
1502          $thumb_path = $this->get_thumb_path($this->cfg->thumb_width, $photos[$i], $this->get_latest_version($photos[$i]));
1503
1504          /* if the image exist and is readable, extract some details */
1505          if(file_exists($thumb_path) && is_readable($thumb_path)) {
1506             if($info = getimagesize($thumb_path) !== false) {
1507                $img_width[$thumbs] = $info[0];
1508                $img_height[$thumbs] = $info[1];
1509             }
1510          }
1511          $thumbs++;
1512       }
1513
1514       if(isset($_SESSION['searchfor_tag']) && $_SESSION['searchfor_tag'] != '')
1515          $this->tmpl->assign('searchfor_tag', $_SESSION['searchfor_tag']);
1516
1517       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
1518          $this->tmpl->assign('from_date', $this->ts2str($_SESSION['from_date']));
1519          $this->tmpl->assign('to_date', $this->ts2str($_SESSION['to_date']));
1520       }
1521
1522       if(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
1523          $this->tmpl->assign('tag_result', 1);
1524       }
1525
1526       /* do we have to display the page selector ? */
1527       if($this->cfg->thumbs_per_page != 0) {
1528
1529          $page_select = "";
1530       
1531          /* calculate the page switchers */
1532          $previous_start = $begin_with - $this->cfg->thumbs_per_page;
1533          $next_start = $begin_with + $this->cfg->thumbs_per_page;
1534
1535          if($begin_with != 0) 
1536             $this->tmpl->assign("previous_url", "javascript:showPhotoIndex(". $previous_start .");"); 
1537          if($end_with < $count)
1538             $this->tmpl->assign("next_url", "javascript:showPhotoIndex(". $next_start .");"); 
1539
1540          $photo_per_page  = $this->cfg->thumbs_per_page;
1541          $last_page = ceil($count / $photo_per_page);
1542
1543          /* get the current selected page */
1544          if($begin_with == 0) {
1545             $current_page = 1;
1546          } else {
1547             $current_page = 0;
1548             for($i = $begin_with; $i >= 0; $i-=$photo_per_page) {
1549                $current_page++;
1550             }
1551          } 
1552
1553          $dotdot_made = 0;
1554
1555          for($i = 1; $i <= $last_page; $i++) {
1556
1557             if($current_page == $i)
1558                $style = "style=\"font-size: 125%; text-decoration: underline;\"";
1559             elseif($current_page-1 == $i || $current_page+1 == $i)
1560                $style = "style=\"font-size: 105%;\"";
1561             elseif(($current_page-5 >= $i) && ($i != 1) ||
1562                ($current_page+5 <= $i) && ($i != $last_page))
1563                $style = "style=\"font-size: 75%;\"";
1564             else
1565                $style = "";
1566
1567             $start_with = ($i*$photo_per_page)-$photo_per_page;
1568
1569             if($this->is_user_friendly_url()) {
1570                $select = "<a href=\"". $this->cfg->web_path ."/tag/205/". $start_with ."\"";
1571             }
1572             else {
1573                $select = "<a href=\"". $this->cfg->web_path ."/index.php?mode=showpi&nbsp;tags=". $current_tags ."&nbsp;begin_with=". $begin_with ."\"";
1574             }
1575             $select.= " onclick=\"showPhotoIndex(". $start_with ."); return false;\"";
1576
1577                if($style != "")
1578                   $select.= $style;
1579             $select.= ">". $i ."</a>&nbsp;";
1580
1581             // until 9 pages we show the selector from 1-9
1582             if($last_page <= 9) {
1583                $page_select.= $select;
1584                continue;
1585             } else {
1586                if($i == 1 /* first page */ || 
1587                   $i == $last_page /* last page */ ||
1588                   $i == $current_page /* current page */ ||
1589                   $i == ceil($last_page * 0.25) /* first quater */ ||
1590                   $i == ceil($last_page * 0.5) /* half */ ||
1591                   $i == ceil($last_page * 0.75) /* third quater */ ||
1592                   (in_array($i, array(1,2,3,4,5,6)) && $current_page <= 4) /* the first 6 */ ||
1593                   (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 */ ||
1594                   $i == $current_page-3 || $i == $current_page-2 || $i == $current_page-1 /* three before */ ||
1595                   $i == $current_page+3 || $i == $current_page+2 || $i == $current_page+1 /* three after */) {
1596
1597                   $page_select.= $select;
1598                   $dotdot_made = 0;
1599                   continue;
1600
1601                }
1602             }
1603
1604             if(!$dotdot_made) {
1605                $page_select.= ".........&nbsp;";
1606                $dotdot_made = 1;
1607             }
1608          }
1609
1610          /* only show the page selector if we have more then one page */
1611          if($last_page > 1)
1612             $this->tmpl->assign('page_selector', $page_select);
1613       }
1614       
1615       $extern_link = "index.php?mode=showpi";
1616       $rss_link = "index.php?mode=rss";
1617       if($current_tags != "") {
1618          $extern_link.= "&tags=". $current_tags;
1619          $rss_link.= "&tags=". $current_tags;
1620       }
1621       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
1622          $extern_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
1623          $rss_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
1624       }
1625
1626       $export_link = "index.php?mode=export";
1627       $slideshow_link = "index.php?mode=slideshow";
1628
1629       $this->tmpl->assign('extern_link', $extern_link);
1630       $this->tmpl->assign('slideshow_link', $slideshow_link);
1631       $this->tmpl->assign('export_link', $export_link);
1632       $this->tmpl->assign('rss_link', $rss_link);
1633       $this->tmpl->assign('count', $count);
1634       $this->tmpl->assign('width', $this->cfg->thumb_width);
1635       $this->tmpl->assign('preview_width', $this->cfg->photo_width);
1636       $this->tmpl->assign('thumb_container_width', $this->cfg->thumb_width);
1637       $this->tmpl->assign('thumb_container_height', $this->cfg->thumb_height+20);
1638       $this->tmpl->assign('selected_tags', $this->getSelectedTags('img'));
1639       // +1 for for smarty's selection iteration
1640       $this->tmpl->assign('thumbs', $thumbs+1);
1641
1642       if($thumbs > 0) {
1643          $this->tmpl->assign('images', $images);
1644          $this->tmpl->assign('img_width', $img_width);
1645          $this->tmpl->assign('img_height', $img_height);
1646          $this->tmpl->assign('img_id', $img_id);
1647          $this->tmpl->assign('img_name', $img_name);
1648          $this->tmpl->assign('img_fullname', $img_fullname);
1649          $this->tmpl->assign('img_title', $img_title);
1650          $this->tmpl->assign('img_rating', $img_rating);
1651       }
1652
1653       $result = $this->tmpl->fetch("photo_index.tpl");
1654
1655       /* if we are returning to photo index from an photo-view,
1656          scroll the window to the last shown photo-thumbnail.
1657          after this, unset the last_photo session variable.
1658       */
1659       if(isset($_SESSION['last_photo'])) {
1660          $result.= "<script language=\"JavaScript\">moveToThumb(". $_SESSION['last_photo'] .");</script>\n";
1661          unset($_SESSION['last_photo']);
1662       }
1663
1664       return $result;
1665
1666    } // showPhotoIndex()
1667
1668    /**
1669     * show credit template
1670     */
1671    public function showCredits()
1672    {
1673       $this->tmpl->assign('version', $this->cfg->version);
1674       $this->tmpl->assign('product', $this->cfg->product);
1675       $this->tmpl->assign('db_version', $this->dbver);
1676       $this->tmpl->show("credits.tpl");
1677
1678    } // showCredits()
1679
1680    /**
1681     * create thumbnails for the requested width
1682     *
1683     * this function creates image thumbnails of $orig_image
1684     * stored as $thumb_image. It will check if the image is
1685     * in a supported format, if necessary rotate the image
1686     * (based on EXIF orientation meta headers) and re-sizing.
1687     * @param string $orig_image
1688     * @param string $thumb_image
1689     * @param integer $width
1690     * @return boolean
1691     */
1692    public function create_thumbnail($orig_image, $thumb_image, $width)
1693    {  
1694       if(!file_exists($orig_image)) {
1695          return false;
1696       }
1697
1698       $mime = $this->get_mime_info($orig_image);
1699
1700       /* check if original photo is a support image type */
1701       if(!$this->checkifImageSupported($mime))
1702          return false;
1703
1704       switch($mime) {
1705
1706          case 'image/jpeg':
1707
1708             $meta = $this->get_meta_informations($orig_image);
1709
1710             $rotate = 0;
1711             $flip_hori = false;
1712             $flip_vert = false;
1713
1714             if(isset($meta['Orientation'])) {
1715                switch($meta['Orientation']) {
1716                   case 1: /* top, left */
1717                      /* nothing to do */ break;
1718                   case 2: /* top, right */
1719                      $rotate = 0; $flip_hori = true; break;
1720                   case 3: /* bottom, left */
1721                      $rotate = 180; break;
1722                   case 4: /* bottom, right */
1723                      $flip_vert = true; break;
1724                   case 5: /* left side, top */
1725                      $rotate = 90; $flip_vert = true; break;
1726                   case 6: /* right side, top */
1727                      $rotate = 90; break;
1728                   case 7: /* left side, bottom */
1729                      $rotate = 270; $flip_vert = true; break;
1730                   case 8: /* right side, bottom */
1731                      $rotate = 270; break;
1732                }
1733             }
1734
1735             $src_img = @imagecreatefromjpeg($orig_image);
1736             $handler = "gd";
1737             break;
1738
1739          case 'image/png':
1740
1741             $src_img = @imagecreatefrompng($orig_image);
1742             $handler = "gd";
1743             break;
1744
1745          case 'image/x-portable-pixmap':
1746
1747             $src_img = new Imagick($orig_image);
1748             $handler = "imagick";
1749             break;
1750
1751       }
1752
1753       if(!isset($src_img) || empty($src_img)) {
1754          print "Can't load image from ". $orig_image ."\n";
1755          return false;
1756       }
1757
1758       switch($handler) {
1759
1760          case 'gd':
1761
1762             /* grabs the height and width */
1763             $cur_width = imagesx($src_img);
1764             $cur_height = imagesy($src_img);
1765
1766             // If requested width is more then the actual image width,
1767             // do not generate a thumbnail, instead safe the original
1768             // as thumbnail but with lower quality. But if the image
1769             // is to heigh too, then we still have to resize it.
1770             if($width >= $cur_width && $cur_height < $this->cfg->thumb_height) {
1771                $result = imagejpeg($src_img, $thumb_image, 75);
1772                imagedestroy($src_img);
1773                return true;
1774             }
1775             break;
1776
1777          case 'imagick':
1778
1779             $cur_width = $src_img->getImageWidth();
1780             $cur_height = $src_img->getImageHeight();
1781
1782             // If requested width is more then the actual image width,
1783             // do not generate a thumbnail, instead safe the original
1784             // as thumbnail but with lower quality. But if the image
1785             // is to heigh too, then we still have to resize it.
1786             if($width >= $cur_width && $cur_height < $this->cfg->thumb_height) {
1787                $src_img->setCompressionQuality(75);
1788                $src_img->setImageFormat('jpeg');
1789                $src_img->writeImage($thumb_image);
1790                $src_img->clear();
1791                $src_img->destroy();
1792                return true;
1793             }
1794             break;
1795
1796       }
1797
1798       // If the image will be rotate because EXIF orientation said so
1799       // 'virtually rotate' the image for further calculations
1800       if($rotate == 90 || $rotate == 270) {
1801          $tmp = $cur_width;
1802          $cur_width = $cur_height;
1803          $cur_height = $tmp;
1804       }
1805
1806       /* calculates aspect ratio */
1807       $aspect_ratio = $cur_height / $cur_width;
1808
1809       /* sets new size */
1810       if($aspect_ratio < 1) {
1811          $new_w = $width;
1812          $new_h = abs($new_w * $aspect_ratio);
1813       } else {
1814          /* 'virtually' rotate the image and calculate it's ratio */
1815          $tmp_w = $cur_height;
1816          $tmp_h = $cur_width;
1817          /* now get the ratio from the 'rotated' image */
1818          $tmp_ratio = $tmp_h/$tmp_w;
1819          /* now calculate the new dimensions */
1820          $tmp_w = $width;
1821          $tmp_h = abs($tmp_w * $tmp_ratio);
1822
1823          // now that we know, how high they photo should be, if it
1824          // gets rotated, use this high to scale the image
1825          $new_h = $tmp_h;
1826          $new_w = abs($new_h / $aspect_ratio);
1827
1828          // If the image will be rotate because EXIF orientation said so
1829          // now 'virtually rotate' back the image for the image manipulation
1830          if($rotate == 90 || $rotate == 270) {
1831             $tmp = $new_w;
1832             $new_w = $new_h;
1833             $new_h = $tmp;
1834          }
1835       }
1836
1837       switch($handler) {
1838
1839          case 'gd':
1840
1841             /* creates new image of that size */
1842             $dst_img = imagecreatetruecolor($new_w, $new_h);
1843
1844             imagefill($dst_img, 0, 0, ImageColorAllocate($dst_img, 255, 255, 255));
1845
1846             /* copies resized portion of original image into new image */
1847             imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $new_w, $new_h, imagesx($src_img), imagesy($src_img));
1848
1849             /* needs the image to be flipped horizontal? */
1850             if($flip_hori) {
1851                $this->_debug("(FLIP)");
1852                $dst_img = $this->flipImage($dst_img, 'hori');
1853             }
1854             /* needs the image to be flipped vertical? */
1855             if($flip_vert) {
1856                $this->_debug("(FLIP)");
1857                $dst_img = $this->flipImage($dst_img, 'vert');
1858             }
1859
1860             if($rotate) {
1861                $this->_debug("(ROTATE)");
1862                $dst_img = $this->rotateImage($dst_img, $rotate);
1863             }
1864
1865             /* write down new generated file */
1866             $result = imagejpeg($dst_img, $thumb_image, 75);
1867
1868             /* free your mind */
1869             imagedestroy($dst_img);
1870             imagedestroy($src_img);
1871
1872             if($result === false) {
1873                print "Can't write thumbnail ". $thumb_image ."\n";
1874                return false;
1875             }
1876
1877             return true;
1878
1879             break;
1880
1881          case 'imagick':
1882
1883             $src_img->resizeImage($new_w, $new_h, Imagick::FILTER_LANCZOS, 1);
1884
1885             /* needs the image to be flipped horizontal? */
1886             if($flip_hori) {
1887                $this->_debug("(FLIP)");
1888                $src_img->rotateImage(new ImagickPixel(), 90);
1889                $src_img->flipImage();
1890                $src_img->rotateImage(new ImagickPixel(), -90);
1891             }
1892             /* needs the image to be flipped vertical? */
1893             if($flip_vert) {
1894                $this->_debug("(FLIP)");
1895                $src_img->flipImage();
1896             }
1897
1898             if($rotate) {
1899                $this->_debug("(ROTATE)");
1900                $src_img->rotateImage(new ImagickPixel(), $rotate);
1901             }
1902
1903             $src_img->setCompressionQuality(75);
1904             $src_img->setImageFormat('jpeg');
1905
1906             if(!$src_img->writeImage($thumb_image)) {
1907                print "Can't write thumbnail ". $thumb_image ."\n";
1908                return false;
1909             }
1910
1911             $src_img->clear();
1912             $src_img->destroy();
1913             return true;
1914
1915             break;
1916
1917       }
1918
1919    } // create_thumbnail()
1920
1921    /**
1922     * return all exif meta data from the file
1923     * @param string $file
1924     * @return array
1925     */
1926    public function get_meta_informations($file)
1927    {
1928       return exif_read_data($file);
1929
1930    } // get_meta_informations()
1931
1932    /**
1933     * create phpfspot own sqlite database
1934     *
1935     * this function creates phpfspots own sqlite database
1936     * if it does not exist yet. this own is used to store
1937     * some necessary informations (md5 sum's, ...).
1938     */
1939    public function check_phpfspot_db()
1940    {
1941       // if the config table doesn't exist yet, create it
1942       if(!$this->cfg_db->db_check_table_exists("images")) {
1943          $this->cfg_db->db_exec("
1944             CREATE TABLE images (
1945                img_idx int,
1946                img_version_idx int,
1947                img_md5 varchar(32),
1948                UNIQUE(img_idx, img_version_idx)
1949             )
1950          ");
1951       }
1952
1953       if(!$this->cfg_db->db_check_table_exists("meta")) {
1954          $this->cfg_db->db_exec("
1955             CREATE TABLE meta (
1956                meta_key varchar(255),
1957                meta_value varchar(255)
1958             )
1959          ");
1960
1961          /* db_version was added with phpfspot 1.7, before changes
1962             on the phpfspot database where not necessary.
1963          */
1964
1965          $this->cfg_db->db_exec("
1966             INSERT INTO meta (
1967                meta_key, meta_value
1968             ) VALUES (
1969                'phpfspot Database Version',
1970                '". $this->cfg->db_version ."'
1971             )
1972          ");
1973       }
1974
1975       /* if version <= 2 and column img_version_idx does not exist yet */
1976       if($this->get_db_version() <= 2 &&
1977          !$this->cfg_db->db_check_column_exists("images", "img_version_idx")) {
1978
1979          if(!$this->cfg_db->db_start_transaction())
1980             die("Can not start database transaction");
1981
1982          $result = $this->cfg_db->db_exec("
1983             CREATE TEMPORARY TABLE images_temp (
1984                img_idx int,
1985                img_version_idx int,
1986                img_md5 varchar(32),
1987                UNIQUE(img_idx, img_version_idx)
1988             )
1989          ");
1990
1991          if(!$result) {
1992             $this->cfg_db->db_rollback_transaction();
1993             die("Upgrade failed - transaction rollback");
1994          }
1995
1996          $result = $this->cfg_db->db_exec("
1997            INSERT INTO images_temp
1998                SELECT
1999                   img_idx,
2000                   0,
2001                   img_md5
2002                FROM images
2003          ");
2004
2005          if(!$result) {
2006             $this->cfg_db->db_rollback_transaction();
2007             die("Upgrade failed - transaction rollback");
2008          }
2009
2010          $result = $this->cfg_db->db_exec("
2011             DROP TABLE images
2012          ");
2013
2014          if(!$result) {
2015             $this->cfg_db->db_rollback_transaction();
2016             die("Upgrade failed - transaction rollback");
2017          }
2018
2019          $result = $this->cfg_db->db_exec("
2020             CREATE TABLE images (
2021                img_idx int,
2022                img_version_idx int,
2023                img_md5 varchar(32),
2024                UNIQUE(img_idx, img_version_idx)
2025             )
2026          ");
2027
2028          if(!$result) {
2029             $this->cfg_db->db_rollback_transaction();
2030             die("Upgrade failed - transaction rollback");
2031          }
2032
2033          $result = $this->cfg_db->db_exec("
2034             INSERT INTO images
2035                SELECT *
2036                FROM images_temp
2037          ");
2038
2039          if(!$result) {
2040             $this->cfg_db->db_rollback_transaction();
2041             die("Upgrade failed - transaction rollback");
2042          }
2043
2044          $result = $this->cfg_db->db_exec("
2045             DROP TABLE images_temp
2046          ");
2047
2048          if(!$result) {
2049             $this->cfg_db->db_rollback_transaction();
2050             die("Upgrade failed - transaction rollback");
2051          }
2052
2053          if(!$this->cfg_db->db_commit_transaction())
2054             die("Can not commit database transaction");
2055
2056       }
2057
2058    } // check_phpfspot_db
2059
2060    /**
2061     * generates thumbnails
2062     *
2063     * This function generates JPEG thumbnails from
2064     * provided F-Spot photo indize and its alternative
2065     * versions.
2066     *
2067     * 1. Check if all thumbnail generations (width) are already in place and
2068     *    readable
2069     * 2. Check if the md5sum of the original file has changed
2070     * 3. Generate the thumbnails if needed
2071     * @param integer $idx
2072     * @param integer $force
2073     * @param boolean $overwrite
2074     */
2075    public function gen_thumb($idx = 0, $force = 0, $overwrite = false)
2076    {
2077       $error = 0;
2078       $versions = Array(0);
2079
2080       $resolutions = Array(
2081          $this->cfg->thumb_width,
2082          $this->cfg->photo_width,
2083          $this->cfg->mini_width,
2084          30,
2085       );
2086
2087       if($alt_versions = $this->get_photo_versions($idx))
2088          $versions = array_merge($versions, $alt_versions);
2089
2090       foreach($versions as $version) {
2091
2092          /* get details from F-Spot's database */
2093          $details = $this->get_photo_details($idx, $version);
2094
2095          /* calculate file MD5 sum */
2096          $full_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
2097
2098          if(!file_exists($full_path)) {
2099             $this->_error("File ". $full_path ." does not exist");
2100             return;
2101          }
2102
2103          if(!is_readable($full_path)) {
2104             $this->_error("File ". $full_path ." is not readable for ". $this->getuid() ."\n");
2105             return;
2106          }
2107
2108          $this->_debug("Image [". $idx ."] ". $this->shrink_text($this->parse_uri($details['uri'], 'filename'), 20) ." Thumbnails:");
2109
2110          /* If Nikon NEF format, we need to treat it another way */
2111          if(isset($this->cfg->dcraw_bin) &&
2112             file_exists($this->cfg->dcraw_bin) &&
2113             is_executable($this->cfg->dcraw_bin) &&
2114             preg_match('/\.nef$/i', $details['uri'])) {
2115
2116             $ppm_path = preg_replace('/\.nef$/i', '.ppm', $full_path);
2117
2118             /* if PPM file does not exist, let dcraw convert it from NEF */
2119             if(!file_exists($ppm_path)) {
2120                system($this->cfg->dcraw_bin ." -a ". $full_path);
2121             }
2122
2123             /* for now we handle the PPM instead of the NEF */
2124             $full_path = $ppm_path;
2125
2126          }
2127
2128          $file_md5 = md5_file($full_path);
2129          $changes = false;
2130
2131          foreach($resolutions as $resolution) {
2132
2133             $generate_it = false;
2134
2135             $thumb_sub_path = substr($file_md5, 0, 2);
2136             $thumb_path = $this->cfg->thumb_path ."/". $thumb_sub_path ."/". $resolution ."_". $file_md5;
2137
2138             /* if thumbnail-subdirectory does not exist yet, create it */
2139             if(!file_exists(dirname($thumb_path))) {
2140                mkdir(dirname($thumb_path), 0755);
2141             }
2142
2143             /* if the thumbnail file doesn't exist, create it */
2144             if(!file_exists($thumb_path) || $force) {
2145                $generate_it = true;
2146             }
2147             elseif($file_md5 != $this->getMD5($idx, $version)) {
2148                $generate_it = true;
2149             }
2150
2151             if($generate_it || $overwrite) {
2152
2153                $this->_debug(" ". $resolution ."px");
2154                if(!$this->create_thumbnail($full_path, $thumb_path, $resolution))
2155                   $error = 1;
2156
2157                $changes = true;
2158             }
2159          }
2160
2161          if(!$changes) {
2162             $this->_debug(" already exist");
2163          }
2164
2165          /* set the new/changed MD5 sum for the current photo */
2166          if(!$error) {
2167             $this->setMD5($idx, $file_md5, $version);
2168          }
2169
2170          $this->_debug("\n");
2171
2172       }
2173
2174    } // gen_thumb()
2175
2176    /**
2177     * returns stored md5 sum for a specific photo
2178     *
2179     * this function queries the phpfspot database for a stored MD5
2180     * checksum of the specified photo. It also takes care of the
2181     * requested photo version - original or alternative photo.
2182     *
2183     * @param integer $idx
2184     * @return string|null
2185     */
2186    public function getMD5($idx, $version_idx = 0)
2187    {
2188       $result = $this->cfg_db->db_query("
2189          SELECT
2190             img_md5
2191          FROM
2192             images
2193          WHERE
2194             img_idx='". $idx ."'
2195          AND
2196             img_version_idx='". $version_idx ."'
2197       ");
2198
2199       if(!$result)
2200          return NULL;
2201
2202       if($img = $this->cfg_db->db_fetch_object($result))
2203          return $img['img_md5'];
2204
2205       return NULL;
2206       
2207    } // getMD5()
2208
2209    /**
2210     * set MD5 sum for the specific photo
2211     * @param integer $idx
2212     * @param string $md5
2213     */
2214    private function setMD5($idx, $md5, $version_idx = 0)
2215    {
2216       $result = $this->cfg_db->db_exec("
2217          INSERT OR REPLACE INTO images (
2218             img_idx, img_version_idx, img_md5
2219          ) VALUES (
2220             '". $idx ."',
2221             '". $version_idx ."',
2222             '". $md5 ."'
2223          )
2224       ");
2225
2226    } // setMD5()
2227
2228    /**
2229     * store current tag condition
2230     *
2231     * this function stores the current tag condition
2232     * (AND or OR) in the users session variables
2233     * @param string $mode
2234     * @return string
2235     */
2236    public function setTagCondition($mode)
2237    {
2238       $_SESSION['tag_condition'] = $mode;
2239
2240       return "ok";
2241
2242    } // setTagCondition()
2243
2244    /** 
2245     * invoke tag & date search 
2246     *
2247     * this function will return all matching tags and store
2248     * them in the session variable selected_tags. furthermore
2249     * it also handles the date search.
2250     * getPhotoSelection() will then only return the matching
2251     * photos.
2252     * @return string
2253     */
2254    public function startSearch()
2255    {
2256       /* date search */
2257       if(isset($_POST['date_from']) && $this->isValidDate($_POST['date_from'])) {
2258          $date_from = $_POST['date_from'];
2259       }
2260       if(isset($_POST['date_to']) && $this->isValidDate($_POST['date_to'])) {
2261          $date_to = $_POST['date_to'];
2262       }
2263
2264       /* tag-name search */
2265       if(isset($_POST['for_tag']) && is_string($_POST['for_tag'])) {
2266          $searchfor_tag = $_POST['for_tag'];
2267          $_SESSION['searchfor_tag'] = $_POST['for_tag'];
2268       }
2269       else {
2270          unset($_SESSION['searchfor_tag']);
2271       }
2272
2273       /* file-name search */
2274       if(isset($_POST['for_name']) && is_string($_POST['for_name'])) {
2275          $_SESSION['searchfor_name'] = $_POST['for_name'];
2276       }
2277       else {
2278          unset($_SESSION['searchfor_name']);
2279       }
2280
2281       /* rate-search */
2282       if(isset($_POST['rate_from']) && is_numeric($_POST['rate_from'])) {
2283
2284          $_SESSION['rate_from'] = $_POST['rate_from'];
2285
2286          if(isset($_POST['rate_to']) && is_numeric($_POST['rate_to'])) {
2287             $_SESSION['rate_to'] = $_POST['rate_to'];
2288          }
2289       }
2290       else {
2291          /* delete any previously set value */
2292          unset($_SESSION['rate_to'], $_SESSION['rate_from']);
2293       }
2294
2295       $this->get_tags();
2296
2297       if(isset($date_from) && !empty($date_from))
2298          $_SESSION['from_date'] = strtotime($date_from ." 00:00:00");
2299       else
2300          unset($_SESSION['from_date']);
2301
2302       if(isset($date_to) && !empty($date_to))
2303          $_SESSION['to_date'] = strtotime($date_to ." 23:59:59");
2304       else
2305          unset($_SESSION['to_date']);
2306
2307       if(isset($searchfor_tag) && !empty($searchfor_tag)) {
2308          /* new search, reset the current selected tags */
2309          $_SESSION['selected_tags'] = Array();
2310          foreach($this->avail_tags as $tag) {
2311             if(preg_match('/'. $searchfor_tag .'/i', $this->tags[$tag]))
2312                array_push($_SESSION['selected_tags'], $tag);
2313          }
2314       }
2315
2316       return "ok";
2317
2318    } // startSearch()
2319
2320    /**
2321     * updates sort order in session variable
2322     *
2323     * this function is invoked by RPC and will sort the requested
2324     * sort order in the session variable.
2325     * @param string $sort_order
2326     * @return string
2327     */
2328    public function updateSortOrder($order)
2329    {
2330       if(isset($this->sort_orders[$order])) {
2331          $_SESSION['sort_order'] = $order;
2332          return "ok";
2333       }
2334
2335       return "unkown error";
2336
2337    } // updateSortOrder()
2338
2339    /**
2340     * update photo version in session variable
2341     *
2342     * this function is invoked by RPC and will set the requested
2343     * photo version in the session variable.
2344     * @param string $photo_version
2345     * @return string
2346     */
2347    public function update_photo_version($photo_idx, $photo_version)
2348    {
2349       if($this->is_valid_version($photo_idx, $photo_version)) {
2350          $_SESSION['current_version'] = $photo_version;
2351          return "ok";
2352       }
2353
2354       return "incorrect photo version provided";
2355
2356    } // update_photo_version()
2357
2358    /**
2359     * rotate image
2360     *
2361     * this function rotates the image according the
2362     * specified angel.
2363     * @param string $img
2364     * @param integer $degress
2365     * @return image
2366     */
2367    private function rotateImage($img, $degrees)
2368    {
2369       if(function_exists("imagerotate")) {
2370          $img = imagerotate($img, $degrees, 0);
2371       } else {
2372          function imagerotate($src_img, $angle)
2373          {
2374             $src_x = imagesx($src_img);
2375             $src_y = imagesy($src_img);
2376             if ($angle == 180) {
2377                $dest_x = $src_x;
2378                $dest_y = $src_y;
2379             }
2380             elseif ($src_x <= $src_y) {
2381                $dest_x = $src_y;
2382                $dest_y = $src_x;
2383             }
2384             elseif ($src_x >= $src_y) {
2385                $dest_x = $src_y;
2386                $dest_y = $src_x;
2387             }
2388                
2389             $rotate=imagecreatetruecolor($dest_x,$dest_y);
2390             imagealphablending($rotate, false);
2391                
2392             switch ($angle) {
2393             
2394                case 90:
2395                   for ($y = 0; $y < ($src_y); $y++) {
2396                      for ($x = 0; $x < ($src_x); $x++) {
2397                         $color = imagecolorat($src_img, $x, $y);
2398                         imagesetpixel($rotate, $dest_x - $y - 1, $x, $color);
2399                      }
2400                   }
2401                   break;
2402
2403                case 270:
2404                   for ($y = 0; $y < ($src_y); $y++) {
2405                      for ($x = 0; $x < ($src_x); $x++) {
2406                         $color = imagecolorat($src_img, $x, $y);
2407                         imagesetpixel($rotate, $y, $dest_y - $x - 1, $color);
2408                      }
2409                   }
2410                   break;
2411
2412                case 180:
2413                   for ($y = 0; $y < ($src_y); $y++) {
2414                      for ($x = 0; $x < ($src_x); $x++) {
2415                         $color = imagecolorat($src_img, $x, $y);
2416                         imagesetpixel($rotate, $dest_x - $x - 1, $dest_y - $y - 1, $color);
2417                      }
2418                   }
2419                   break;
2420
2421                default:
2422                   $rotate = $src_img;
2423                   break;
2424             };
2425
2426             return $rotate;
2427
2428          }
2429
2430          $img = imagerotate($img, $degrees);
2431
2432       }
2433
2434       return $img;
2435
2436    } // rotateImage()
2437
2438    /**
2439     * returns flipped image
2440     *
2441     * this function will return an either horizontal or
2442     * vertical flipped truecolor image.
2443     * @param string $image
2444     * @param string $mode 
2445     * @return image
2446     */
2447    private function flipImage($image, $mode)
2448    {
2449       $w = imagesx($image);
2450       $h = imagesy($image);
2451       $flipped = imagecreatetruecolor($w, $h);
2452
2453       switch($mode) {
2454          case 'vert':
2455             for ($y = 0; $y < $h; $y++) {
2456                imagecopy($flipped, $image, 0, $y, 0, $h - $y - 1, $w, 1);
2457             }
2458             break;
2459          case 'hori':
2460             for ($x = 0; $x < $w; $x++) {
2461                imagecopy($flipped, $image, $x, 0, $w - $x - 1, 0, 1, $h);
2462             }
2463             break;
2464       }
2465
2466       return $flipped;
2467
2468    } // flipImage()
2469
2470    /**
2471     * return all assigned tags for the specified photo
2472     * @param integer $idx
2473     * @return array
2474     */
2475    private function get_photo_tags($idx)
2476    {
2477       $result = $this->db->db_query("
2478          SELECT t.id as id, t.name as name
2479          FROM tags t
2480          INNER JOIN photo_tags pt
2481             ON t.id=pt.tag_id
2482          WHERE pt.photo_id='". $idx ."'
2483       ");
2484
2485       $tags = Array();
2486
2487       while($row = $this->db->db_fetch_object($result)) {
2488          if(isset($this->cfg->hide_tags) && in_array($row['name'], $this->cfg->hide_tags))
2489             continue;
2490          $tags[$row['id']] = $row['name'];
2491       }
2492
2493       return $tags;
2494
2495    } // get_photo_tags()
2496
2497    /**
2498     * create on-the-fly images with text within
2499     * @param string $txt
2500     * @param string $color
2501     * @param integer $space
2502     * @param integer $font
2503     * @param integer $w
2504     */
2505    public function showTextImage($txt, $color=000000, $space=4, $font=4, $w=300)
2506    {
2507       if (strlen($color) != 6) 
2508          $color = 000000;
2509
2510       $int = hexdec($color);
2511       $h = imagefontheight($font);
2512       $fw = imagefontwidth($font);
2513       $txt = explode("\n", wordwrap($txt, ($w / $fw), "\n"));
2514       $lines = count($txt);
2515       $im = imagecreate($w, (($h * $lines) + ($lines * $space)));
2516       $bg = imagecolorallocate($im, 255, 255, 255);
2517       $color = imagecolorallocate($im, 0xFF & ($int >> 0x10), 0xFF & ($int >> 0x8), 0xFF & $int);
2518       $y = 0;
2519
2520       foreach ($txt as $text) {
2521          $x = (($w - ($fw * strlen($text))) / 2);
2522          imagestring($im, $font, $x, $y, $text, $color);
2523          $y += ($h + $space);
2524       }
2525
2526       Header("Content-type: image/png");
2527       ImagePng($im);
2528
2529    } // showTextImage()
2530
2531    /**
2532     * check if all requirements are met
2533     * @return boolean
2534     */
2535    private function check_requirements()
2536    {
2537       if(!function_exists("imagecreatefromjpeg")) {
2538          print "PHP GD library extension is missing<br />\n";
2539          $missing = true;
2540       }
2541
2542       if($this->cfg->db_access == "native" && !function_exists("sqlite3_open")) {
2543          print "PHP SQLite3 library extension is missing<br />\n";
2544          $missing = true;
2545       }
2546
2547       /* Check for HTML_AJAX PEAR package, lent from Horde project */
2548       ini_set('track_errors', 1);
2549       @include_once 'HTML/AJAX/Server.php';
2550       if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
2551          print "PEAR HTML_AJAX package is missing<br />\n";
2552          $missing = true;
2553       }
2554       @include_once 'Calendar/Calendar.php';
2555       if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
2556          print "PEAR Calendar package is missing<br />\n";
2557          $missing = true;
2558       }
2559       @include_once 'Console/Getopt.php';
2560       if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
2561          print "PEAR Console_Getopt package is missing<br />\n";
2562          $missing = true;
2563       }
2564       @include_once $this->cfg->smarty_path .'/libs/Smarty.class.php';
2565       if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
2566          print "Smarty template engine can not be found in ". $this->cfg->smarty_path ."/libs/Smarty.class.php<br />\n";
2567          $missing = true;
2568       }
2569       ini_restore('track_errors');
2570
2571       if(isset($missing))
2572          return false;
2573
2574       return true;
2575
2576    } // check_requirements()
2577
2578    private function _debug($text)
2579    {
2580       if(isset($this->fromcmd)) {
2581          print $text;
2582       }
2583
2584    } // _debug()
2585
2586    /**
2587     * check if specified MIME type is supported
2588     * @param string $mime
2589     * @return boolean
2590     */
2591    public function checkifImageSupported($mime)
2592    {
2593       $supported_types =  Array(
2594          "image/jpeg",
2595          "image/png",
2596          "image/x-portable-pixmap",
2597          "image/tiff"
2598       );
2599
2600       if(in_array($mime, $supported_types))
2601          return true;
2602
2603       return false;
2604
2605    } // checkifImageSupported()
2606
2607    /**
2608     * output error text
2609     * @param string $text
2610     */
2611    public function _error($text)
2612    {
2613       switch($this->cfg->logging) {
2614          default:
2615          case 'display':
2616             if(isset($this->fromcmd))
2617                print $text ."\n";
2618             else {
2619                print "<img src=\"resources/green_info.png\" alt=\"warning\" />\n";
2620                print $text ."<br />\n";
2621             }
2622             break;
2623          case 'errorlog':  
2624             error_log($text);
2625             break;
2626          case 'logfile':
2627             error_log($text, 3, $this->cfg->log_file);
2628             break;
2629       }
2630
2631       $this->runtime_error = true;
2632
2633    } // _error()
2634
2635    /**
2636     * get calendar input-text fields
2637     *
2638     * this function returns a text-field used for the data selection.
2639     * Either it will be filled with the current date or, if available,
2640     * filled with the date user entered previously.
2641     *
2642     * @param string $mode
2643     * @return string
2644     */
2645    private function get_date_text_field($mode)
2646    {
2647       $date = isset($_SESSION[$mode .'_date']) ? date("Y", $_SESSION[$mode .'_date']) : date("Y");
2648       $date.= "-";
2649       $date.= isset($_SESSION[$mode .'_date']) ? date("m", $_SESSION[$mode .'_date']) : date("m");
2650       $date.= "-";
2651       $date.= isset($_SESSION[$mode .'_date']) ? date("d", $_SESSION[$mode .'_date']) : date("d");
2652
2653       $output = "<input type=\"text\" size=\"15\" id=\"date_". $mode ."\" value=\"". $date ."\"";
2654       if(!isset($_SESSION[$mode .'_date']))
2655          $output.= " disabled=\"disabled\"";
2656       $output.= " />\n";
2657
2658       return $output;
2659
2660    } // get_date_text_field()
2661
2662    /**
2663     * output calendar matrix
2664     * @param integer $year
2665     * @param integer $month
2666     * @param integer $day
2667     */
2668    public function get_calendar_matrix($year = 0, $month = 0, $day = 0)
2669    {
2670       if (!isset($year)) $year = date('Y');
2671       if (!isset($month)) $month = date('m');
2672       if (!isset($day)) $day = date('d');
2673       $rows = 1;
2674       $cols = 1;
2675       $matrix = Array();
2676
2677       require_once CALENDAR_ROOT.'Month/Weekdays.php';
2678       require_once CALENDAR_ROOT.'Day.php';
2679
2680       // Build the month
2681       $month = new Calendar_Month_Weekdays($year,$month);
2682
2683       // Create links
2684       $prevStamp = $month->prevMonth(true);
2685       $prev = "javascript:setMonth(". date('Y',$prevStamp) .", ". date('n',$prevStamp) .", ". date('j',$prevStamp) .");";
2686       $nextStamp = $month->nextMonth(true);
2687       $next = "javascript:setMonth(". date('Y',$nextStamp) .", ". date('n',$nextStamp) .", ". date('j',$nextStamp) .");";
2688
2689       $selectedDays = array (
2690          new Calendar_Day($year,$month,$day),
2691          new Calendar_Day($year,12,25),
2692       );
2693
2694       // Build the days in the month
2695       $month->build($selectedDays);
2696
2697       $this->tmpl->assign('current_month', date('F Y',$month->getTimeStamp()));
2698       $this->tmpl->assign('prev_month', $prev);
2699       $this->tmpl->assign('next_month', $next);
2700
2701       while ( $day = $month->fetch() ) {
2702    
2703          if(!isset($matrix[$rows]))
2704             $matrix[$rows] = Array();
2705
2706          $string = "";
2707
2708          $dayStamp = $day->thisDay(true);
2709          $link = "javascript:setCalendarDate(". date('Y',$dayStamp) .", ". date('n',$dayStamp).", ". date('j',$dayStamp) .");";
2710
2711          // isFirst() to find start of week
2712          if ( $day->isFirst() )
2713             $string.= "<tr>\n";
2714
2715          if ( $day->isSelected() ) {
2716             $string.= "<td class=\"selected\">".$day->thisDay()."</td>\n";
2717          } else if ( $day->isEmpty() ) {
2718             $string.= "<td>&nbsp;</td>\n";
2719          } else {
2720             $string.= "<td><a class=\"calendar\" href=\"".$link."\">".$day->thisDay()."</a></td>\n";
2721          }
2722
2723          // isLast() to find end of week
2724          if ( $day->isLast() )
2725             $string.= "</tr>\n";
2726
2727          $matrix[$rows][$cols] = $string;
2728
2729          $cols++;
2730
2731          if($cols > 7) {
2732             $cols = 1;
2733             $rows++;
2734          }
2735       }
2736
2737       $this->tmpl->assign('matrix', $matrix);
2738       $this->tmpl->assign('rows', $rows);
2739       $this->tmpl->show("calendar.tpl");
2740
2741    } // get_calendar_matrix()
2742
2743    /**
2744     * output export page
2745     * @param string $mode
2746     */
2747    public function getExport($mode)
2748    {
2749       $pictures = $this->getPhotoSelection();
2750       $current_tags = $this->getCurrentTags();  
2751
2752       foreach($pictures as $picture) {
2753
2754          $orig_url = $this->get_phpfspot_url() ."/index.php?mode=showp&id=". $picture;
2755          if($current_tags != "") {
2756             $orig_url.= "&tags=". $current_tags;
2757          } 
2758          if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
2759             $orig_url.= "&from_date=". $_SESSION['from_date'] ."&to_date=". $_SESSION['to_date'];
2760          }
2761
2762          if($this->is_user_friendly_url()) {
2763             $thumb_url = $this->get_phpfspot_url() ."/photo/". $picture ."/". $this->cfg->thumb_width;
2764          }
2765          else {
2766             $thumb_url = $this->get_phpfspot_url() ."/phpfspot_img.php?idx=". $picture ."&width=". $this->cfg->thumb_width;
2767          }
2768
2769          switch($mode) {
2770
2771             case 'HTML':
2772                // <a href="%pictureurl%"><img src="%thumbnailurl%" ></a>
2773                print htmlspecialchars("<a href=\"". $orig_url ."\"><img src=\"". $thumb_url ."\" /></a>") ."<br />\n";
2774                break;
2775                
2776             case 'MoinMoin':
2777                // "[%pictureurl% %thumbnailurl%]"
2778                print htmlspecialchars("[".$orig_url." ".$thumb_url."&fake=1.jpg]") ."<br />\n";
2779                break;
2780
2781             case 'MoinMoinList':
2782                // " * [%pictureurl% %thumbnailurl%]"
2783                print "&nbsp;" . htmlspecialchars("* [".$orig_url." ".$thumb_url."&fake=1.jpg]") ."<br />\n";
2784                break;
2785          }
2786
2787       }
2788
2789    } // getExport()
2790
2791    /**
2792     * output RSS feed
2793     */
2794    public function getRSSFeed()
2795    {
2796       Header("Content-type: text/xml; charset=utf-8");
2797       print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
2798 ?>
2799 <rss version="2.0"
2800    xmlns:media="http://search.yahoo.com/mrss/"
2801    xmlns:dc="http://purl.org/dc/elements/1.1/"
2802  >
2803  <channel>
2804   <title>phpfspot</title>
2805   <description>phpfspot RSS feed</description>
2806   <link><?php print htmlspecialchars($this->get_phpfspot_url()); ?></link>
2807   <pubDate><?php print strftime("%a, %d %b %Y %T %z"); ?></pubDate>
2808   <generator>phpfspot</generator>
2809 <?php
2810
2811       $pictures = $this->getPhotoSelection();
2812       $current_tags = $this->getCurrentTags();  
2813
2814       foreach($pictures as $picture) {
2815
2816          $orig_url = $this->get_phpfspot_url() ."/index.php?mode=showp&id=". $picture;
2817          if($current_tags != "") {
2818             $orig_url.= "&tags=". $current_tags;
2819          } 
2820          if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
2821             $orig_url.= "&from_date=". $_SESSION['from_date'] ."&to_date=". $_SESSION['to_date'];
2822          }
2823
2824          $details = $this->get_photo_details($picture);
2825
2826          if($this->is_user_friendly_url()) {
2827             $thumb_url = $this->get_phpfspot_url() ."/photo/". $picture ."/". $this->cfg->thumb_width;
2828          }
2829          else {
2830             $thumb_url = $this->get_phpfspot_url() ."/phpfspot_img.php?idx=". $picture ."&width=". $this->cfg->thumb_width;
2831          }
2832
2833          $thumb_html = htmlspecialchars("
2834 <a href=\"". $orig_url ."\"><img src=\"". $thumb_url ."\" /></a>
2835 <br>
2836 ". $details['description']);
2837
2838          $orig_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
2839
2840          /* get EXIF information if JPEG */
2841          if(isset($details['mime']) && $details['mime'] == "image/jpeg") {
2842             $meta = $this->get_meta_informations($orig_path);
2843          }
2844
2845 ?>
2846   <item>
2847    <title><?php print htmlspecialchars($this->parse_uri($details['uri'], 'filename')); ?></title>
2848    <link><?php print htmlspecialchars($orig_url); ?></link>
2849    <guid><?php print htmlspecialchars($orig_url); ?></guid>
2850    <dc:date.Taken><?php print strftime("%Y-%m-%dT%H:%M:%S+00:00", $details['time']); ?></dc:date.Taken>
2851    <description>
2852     <?php print $thumb_html; ?> 
2853    </description>
2854    <pubDate><?php print strftime("%a, %d %b %Y %T %z", $details['time']); ?></pubDate>
2855   </item>
2856 <?php
2857
2858       }
2859 ?>
2860  </channel>
2861 </rss>
2862 <?php
2863
2864
2865    } // getExport()
2866
2867  
2868    /**
2869     * get all selected tags
2870     *
2871     * This function will return all selected tags as one string, seperated
2872     * by a comma.
2873     * @return array
2874     */
2875    private function getCurrentTags()
2876    {
2877       $current_tags = "";
2878       if(isset($_SESSION['selected_tags']) && $_SESSION['selected_tags'] != "") {
2879          foreach($_SESSION['selected_tags'] as $tag)
2880             $current_tags.= $tag .",";
2881          $current_tags = substr($current_tags, 0, strlen($current_tags)-1);
2882       }
2883       return $current_tags;
2884
2885    } // getCurrentTags()
2886
2887    /**
2888     * return the current photo
2889     */
2890    public function get_current_photo()
2891    {
2892       if(isset($_SESSION['current_photo'])) {
2893          return $_SESSION['current_photo'];
2894       }
2895
2896       return NULL;
2897
2898    } // get_current_photo()
2899
2900    /**
2901     * current selected photo version
2902     *
2903     * this function returns the current selected photo version
2904     * from the session variables.
2905     *
2906     * @return int
2907     */
2908    public function get_current_version()
2909    {
2910       /* if current version is set, return it, if the photo really has that version */
2911       if(isset($_SESSION['current_version']) && is_numeric($_SESSION['current_version']))
2912          return $_SESSION['current_version'];
2913
2914       return false;
2915
2916    } // get_current_version()
2917
2918    /**
2919     * returns latest available photo version
2920     *
2921     * this function returns the latested available version
2922     * for the requested photo.
2923     *
2924     * @return int
2925     */
2926    public function get_latest_version($photo_idx)
2927    {
2928       /* try to get the lasted version for the current photo */
2929       if($versions = $this->get_photo_versions($photo_idx))
2930          return $versions[count($versions)-1];
2931
2932       /* if no alternative version were found, return original version */
2933       return 0;
2934
2935    } // get_current_version()
2936
2937    /**
2938     * tells the client browser what to do
2939     *
2940     * this function is getting called via AJAX by the
2941     * client browsers. it will tell them what they have
2942     * to do next. This is necessary for directly jumping
2943     * into photo index or single photo view when the are
2944     * requested with specific URLs
2945     * @return string
2946     */
2947    public function whatToDo()
2948    {
2949       if(isset($_SESSION['current_photo']) && $_SESSION['start_action'] == 'showp') {
2950       }
2951       elseif(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
2952          return "showpi_tags";
2953       }
2954       elseif(isset($_SESSION['start_action']) && $_SESSION['start_action'] == 'showpi') {
2955          return "showpi";
2956       }
2957
2958    } // whatToDo()
2959
2960    /**
2961     * return the current process-user
2962     * @return string
2963     */
2964    private function getuid()
2965    {
2966       if($uid = posix_getuid()) {
2967          if($user = posix_getpwuid($uid)) {
2968             return $user['name'];
2969          }
2970       }
2971    
2972       return 'n/a';
2973    
2974    } // getuid()
2975
2976    /**
2977     * photo version select list
2978     *
2979     * this function returns a HTML select list (drop down)
2980     * to select a alternative photo version of the original photo.
2981     *
2982     * @param array $params
2983     * @param smarty $smarty
2984     * @return string
2985     */
2986    public function smarty_photo_version_select_list($params, &$smarty)
2987    {
2988       if(!isset($params['photo']) || !isset($params['current']))
2989          return NULL;
2990
2991       $output = "<option value=\"0\">Original</option>";
2992       $versions = $this->get_photo_versions($params['photo']);
2993
2994       foreach($versions as $version) {
2995
2996          $output.= "<option value=\"". $version ."\"";
2997          if($version == $params['current']) {
2998             $output.= " selected=\"selected\"";
2999          }
3000          $output.= ">". $this->get_photo_version_name($params['photo'], $version) ."</option>";
3001       }
3002
3003       return $output;
3004
3005    } // smarty_photo_version_select_list()
3006
3007    /**
3008     * returns a select-dropdown box to select photo index sort parameters
3009     * @param array $params
3010     * @param smarty $smarty
3011     * @return string
3012     */
3013    public function smarty_sort_select_list($params, &$smarty)
3014    {
3015       $output = "";
3016
3017       foreach($this->sort_orders as $key => $value) {
3018          $output.= "<option value=\"". $key ."\"";
3019          if($key == $_SESSION['sort_order']) {
3020             $output.= " selected=\"selected\"";
3021          }
3022          $output.= ">". $value ."</option>";
3023       }
3024
3025       return $output;
3026
3027    } // smarty_sort_select_list()
3028
3029    /**
3030     * returns the currently selected sort order
3031     * @return string
3032     */ 
3033    private function get_sort_order()
3034    {
3035       switch($_SESSION['sort_order']) {
3036          case 'date_asc':
3037             return " ORDER BY p.time ASC";
3038             break;
3039          case 'date_desc':
3040             return " ORDER BY p.time DESC";
3041             break;
3042          case 'name_asc':
3043             if($this->dbver < 9) {
3044                return " ORDER BY p.name ASC";
3045             }
3046             else {
3047                return " ORDER BY basename(p.uri) ASC";
3048             }
3049             break;
3050          case 'name_desc':
3051             if($this->dbver < 9) {
3052                return " ORDER BY p.name DESC";
3053             }
3054             else {
3055                return " ORDER BY basename(p.uri) DESC";
3056             }
3057             break;
3058          case 'tags_asc':
3059             return " ORDER BY t.name ASC ,p.time ASC";
3060             break;
3061          case 'tags_desc':
3062             return " ORDER BY t.name DESC ,p.time ASC";
3063             break;
3064          case 'rate_asc':
3065             return " ORDER BY p.rating ASC, t.name ASC";
3066             break;
3067          case 'rate_desc':
3068             return " ORDER BY p.rating DESC, t.name ASC";
3069             break;
3070       }
3071
3072    } // get_sort_order()
3073
3074    /**
3075     * return the next to be shown slide show image
3076     *
3077     * this function returns the URL of the next image
3078     * in the slideshow sequence.
3079     * @return string
3080     */
3081    public function getNextSlideShowImage()
3082    {
3083       $all_photos = $this->getPhotoSelection();
3084
3085       if(!isset($_SESSION['slideshow_img']) || $_SESSION['slideshow_img'] == count($all_photos)-1) 
3086          $_SESSION['slideshow_img'] = 0;
3087       else
3088          $_SESSION['slideshow_img']++;
3089
3090       if($this->is_user_friendly_url()) {
3091          return $this->get_phpfspot_url() ."/photo/". $all_photos[$_SESSION['slideshow_img']] ."/". $this->cfg->photo_width;
3092       }
3093
3094       return $this->get_phpfspot_url() ."/phpfspot_img.php?idx=". $all_photos[$_SESSION['slideshow_img']] ."&width=". $this->cfg->photo_width;
3095
3096    } // getNextSlideShowImage()
3097
3098    /**
3099     * return the previous to be shown slide show image
3100     *
3101     * this function returns the URL of the previous image
3102     * in the slideshow sequence.
3103     * @return string
3104     */
3105    public function getPrevSlideShowImage()
3106    {
3107       $all_photos = $this->getPhotoSelection();
3108
3109       if(!isset($_SESSION['slideshow_img']) || $_SESSION['slideshow_img'] == 0)
3110          $_SESSION['slideshow_img'] = 0;
3111       else
3112          $_SESSION['slideshow_img']--;
3113
3114       if($this->is_user_friendly_url()) {
3115          return $this->get_phpfspot_url() ."/photo/". $all_photos[$_SESSION['slideshow_img']] ."/". $this->cfg->photo_width;
3116       }
3117
3118       return $this->get_phpfspot_url() ."/phpfspot_img.php?idx=". $all_photos[$_SESSION['slideshow_img']] ."&width=". $this->cfg->photo_width;
3119
3120    } // getPrevSlideShowImage()
3121
3122    public function resetSlideShow()
3123    {
3124       if(isset($_SESSION['slideshow_img']))
3125          unset($_SESSION['slideshow_img']);
3126
3127    } // resetSlideShow()
3128    
3129    /**
3130     * get random photo
3131     *
3132     * this function will get all photos from the fspot
3133     * database and randomly return ONE entry
3134     *
3135     * saddly there is yet no sqlite3 function which returns
3136     * the bulk result in array, so we have to fill up our
3137     * own here.
3138     * @return array
3139     */
3140    public function get_random_photo()
3141    {
3142       $all = Array();
3143
3144       $query_str = "
3145          SELECT p.id as id
3146          FROM photos p
3147       ";
3148
3149       /* if show_tags is set, only return details for photos which
3150          are specified to be shown
3151       */
3152       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
3153          $query_str.= "
3154             INNER JOIN photo_tags pt
3155                ON p.id=pt.photo_id
3156             INNER JOIN tags t
3157                ON pt.tag_id=t.id
3158             WHERE
3159                t.name IN ('".implode("','",$this->cfg->show_tags)."')";
3160       }
3161
3162       $result = $this->db->db_query($query_str);
3163
3164       while($row = $this->db->db_fetch_object($result)) {
3165          array_push($all, $row['id']);
3166       }
3167
3168       return $all[array_rand($all)];
3169
3170    } // get_random_photo()
3171
3172    /**
3173     * get random photo tag photo
3174     *
3175     * this function will get all photos tagged with the requested
3176     * tag from the fspot database and randomly return ONE entry
3177     *
3178     * saddly there is yet no sqlite3 function which returns
3179     * the bulk result in array, so we have to fill up our
3180     * own here.
3181     * @return array
3182     */
3183    public function get_random_tag_photo($tagidx)
3184    {
3185       $all = Array();
3186
3187       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
3188          $query_str.= "
3189             SELECT
3190                DISTINCT pt1.photo_id as id
3191             FROM
3192                photo_tags pt1
3193             INNER JOIN photo_tags
3194                pt2 ON pt1.photo_id=pt2.photo_id
3195             INNER JOIN tags t1
3196                ON t1.id=pt1.tag_id
3197             INNER JOIN tags t2
3198                ON t2.id=pt2.tag_id
3199             WHERE
3200                pt1.tag_id LIKE '". $tagidx ."'
3201             AND
3202                t2.name IN  ('".implode("','",$this->cfg->show_tags)."')
3203             ORDER BY
3204                t1.sort_priority ASC";
3205       }
3206       else {
3207          $query_str = "
3208             SELECT
3209                p.id as id
3210             FROM
3211                photos p
3212             INNER JOIN photo_tags pt
3213                ON p.id=pt.photo_id
3214             WHERE
3215                pt.tag_id LIKE '". $tagidx ."'";
3216       }
3217
3218       $result = $this->db->db_query($query_str);
3219
3220       while($row = $this->db->db_fetch_object($result)) {
3221          array_push($all, $row['id']);
3222       }
3223
3224       return $all[array_rand($all)];
3225
3226    } // get_random_tag_photo()
3227
3228    /**
3229     * validates provided date
3230     *
3231     * this function validates if the provided date
3232     * contains a valid date and will return true 
3233     * if it is.
3234     * @param string $date_str
3235     * @return boolean
3236     */
3237    public function isValidDate($date_str)
3238    {
3239       $timestamp = strtotime($date_str);
3240    
3241       if(is_numeric($timestamp))
3242          return true;
3243       
3244       return false;
3245
3246    } // isValidDate()
3247
3248    /**
3249     * timestamp to string conversion
3250     * @param integer $timestamp
3251     * @return string
3252     */
3253    private function ts2str($timestamp)
3254    {
3255       if(!empty($timestamp) && is_numeric($timestamp))
3256          return strftime("%Y-%m-%d", $timestamp);
3257
3258    } // ts2str()
3259
3260    /**
3261     * extract tag-names from $_GET['tags']
3262     * @param string $tags_str
3263     * @return string
3264     */
3265    private function extractTags($tags_str)
3266    {
3267       $not_validated = split(',', $tags_str);
3268       $validated = array();
3269
3270       foreach($not_validated as $tag) {
3271          if(is_numeric($tag))
3272             array_push($validated, $tag);
3273       }
3274    
3275       return $validated;
3276    
3277    } // extractTags()
3278
3279    /**
3280     * returns the full path to a thumbnail
3281     * @param integer $width
3282     * @param integer $photo
3283     * @return string
3284     */
3285    public function get_thumb_path($width, $photo_idx, $version_idx)
3286    {
3287       $md5 = $this->getMD5($photo_idx, $version_idx);
3288       $sub_path = substr($md5, 0, 2);
3289       return $this->cfg->thumb_path
3290          . "/"
3291          . $sub_path
3292          . "/"
3293          . $width
3294          . "_"
3295          . $md5;
3296
3297    } // get_thumb_path()
3298
3299    /**
3300     * returns server's virtual host name
3301     * @return string
3302     */
3303    private function get_server_name()
3304    {
3305       return $_SERVER['SERVER_NAME'];
3306    } // get_server_name()
3307
3308    /**
3309     * returns type of webprotocol which is currently used
3310     * @return string
3311     */
3312    private function get_web_protocol()
3313    {
3314       if(!isset($_SERVER['HTTPS']))
3315          return "http";
3316       else
3317          return "https";
3318    } // get_web_protocol()
3319
3320    /**
3321     * return url to this phpfspot installation
3322     * @return string
3323     */
3324    private function get_phpfspot_url()
3325    {
3326       return $this->get_web_protocol() ."://". $this->get_server_name() . $this->cfg->web_path;
3327
3328    } // get_phpfspot_url()
3329
3330    /**
3331     * returns the number of photos which are tagged with $tag_id
3332     * @param integer $tag_id
3333     * @return integer
3334     */
3335    public function get_num_photos($tag_id)
3336    {
3337       if($result = $this->db->db_fetchSingleRow("
3338          SELECT count(*) as number
3339          FROM photo_tags
3340          WHERE
3341             tag_id LIKE '". $tag_id ."'")) {
3342
3343          return $result['number'];
3344
3345       }
3346
3347       return 0;
3348       
3349    } // get_num_photos()
3350    
3351    /**
3352     * check file exists and is readable
3353     *
3354     * returns true, if everything is ok, otherwise false
3355     * if $silent is not set, this function will output and
3356     * error message
3357     * @param string $file
3358     * @param boolean $silent
3359     * @return boolean
3360     */
3361    private function check_readable($file, $silent = null)
3362    {
3363       if(!file_exists($file)) {
3364          if(!isset($silent))
3365             print "File \"". $file ."\" does not exist.\n";
3366          return false;
3367       }
3368
3369       if(!is_readable($file)) {
3370          if(!isset($silent))
3371             print "File \"". $file ."\" is not reachable for user ". $this->getuid() ."\n";
3372          return false;
3373       }
3374
3375       return true;
3376
3377    } // check_readable()
3378
3379    /**
3380     * check if all needed indices are present
3381     *
3382     * this function checks, if some needed indices are already
3383     * present, or if not, create them on the fly. they are
3384     * necessary to speed up some queries like that one look for
3385     * all tags, when show_tags is specified in the configuration.
3386     */
3387    private function checkDbIndices()
3388    {
3389       $result = $this->db->db_exec("
3390          CREATE INDEX IF NOT EXISTS
3391             phototag
3392          ON
3393             photo_tags
3394                (photo_id, tag_id)
3395       ");
3396
3397    } // checkDbIndices()
3398
3399    /**
3400     * retrive F-Spot database version
3401     *
3402     * this function will return the F-Spot database version number
3403     * It is stored within the sqlite3 database in the table meta
3404     * @return string|null
3405     */
3406    public function getFspotDBVersion()
3407    {
3408       if($result = $this->db->db_fetchSingleRow("
3409          SELECT data as version
3410          FROM meta
3411          WHERE
3412             name LIKE 'F-Spot Database Version'
3413       "))
3414          return $result['version'];
3415
3416       return null;
3417
3418    } // getFspotDBVersion()
3419
3420    /**
3421     * parse the provided URI and will returned the requested chunk
3422     * @param string $uri
3423     * @param string $mode
3424     * @return string
3425     */
3426    public function parse_uri($uri, $mode)
3427    {
3428       if(($components = parse_url($uri)) !== false) {
3429
3430          switch($mode) {
3431             case 'filename':
3432                return basename($components['path']);
3433                break;
3434             case 'dirname':
3435                return dirname($components['path']);
3436                break;
3437             case 'fullpath':
3438                return $components['path'];
3439                break;
3440          }
3441       }
3442
3443       return $uri;
3444
3445    } // parse_uri()
3446
3447    /**
3448     * validate config options
3449     *
3450     * this function checks if all necessary configuration options are
3451     * specified and set.