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