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