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