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