source: extensions/AMetaData/amd_aip.class.inc.php @ 4999

Revision 4999, 42.0 KB checked in by grum, 10 years ago (diff)

update plugin for the "Magic" metadata management

  • Property svn:executable set to *
Line 
1<?php
2/*
3 * -----------------------------------------------------------------------------
4 * Plugin Name: Advanced MetaData
5 * -----------------------------------------------------------------------------
6 * Author     : Grum
7 *   email    : grum@piwigo.org
8 *   website  : http://photos.grum.fr
9 *   PWG user : http://forum.piwigo.org/profile.php?id=3706
10 *
11 *   << May the Little SpaceFrog be with you ! >>
12 *
13 * -----------------------------------------------------------------------------
14 *
15 * See main.inc.php for release information
16 *
17 * AIP classe => manage integration in administration interface
18 *
19 * -----------------------------------------------------------------------------
20 */
21
22if (!defined('PHPWG_ROOT_PATH')) { die('Hacking attempt!'); }
23
24include_once('amd_root.class.inc.php');
25include_once(PHPWG_ROOT_PATH.'admin/include/tabsheet.class.php');
26include_once(PHPWG_PLUGINS_PATH.'grum_plugins_classes-2/ajax.class.inc.php');
27include_once(PHPWG_PLUGINS_PATH.'grum_plugins_classes-2/genericjs.class.inc.php');
28
29
30
31class AMD_AIP extends AMD_root
32{
33  protected $tabsheet;
34  protected $ajax;
35  protected $jpegMD;
36
37  /**
38   *
39   * constructor needs the prefix of piwigo's tables and the location of plugin
40   *
41   * @param String $prefixeTable
42   * @param String $filelocation
43   */
44  function __construct($prefixeTable, $filelocation)
45  {
46    parent::__construct($prefixeTable, $filelocation);
47
48    $this->load_config();
49    $this->init_events();
50
51    $this->tabsheet = new tabsheet();
52    $this->tabsheet->add('metadata',
53                          l10n('g003_metadata'),
54                          $this->page_link.'&amp;fAMD_tabsheet=metadata');
55    $this->tabsheet->add('database',
56                          l10n('g003_database'),
57                          $this->page_link.'&amp;fAMD_tabsheet=database');
58    $this->ajax = new Ajax();
59    $this->jpegMD=new JpegMetaData();
60  }
61
62  function __destruct()
63  {
64    unset($this->tabsheet);
65    unset($this->ajax);
66    unset($this->jpegMD);
67  }
68
69
70  /*
71   * ---------------------------------------------------------------------------
72   * Public classe functions
73   * ---------------------------------------------------------------------------
74   */
75
76
77  /**
78   * manage the plugin integration into piwigo's admin interface
79   */
80  public function manage()
81  {
82    global $template, $page;
83
84    $template->set_filename('plugin_admin_content', dirname(__FILE__)."/admin/amd_admin.tpl");
85
86    $this->initRequest();
87
88    $this->returnAjaxContent();
89
90    $this->tabsheet->select($_REQUEST['fAMD_tabsheet']);
91    $this->tabsheet->assign();
92    $selected_tab=$this->tabsheet->get_selected();
93    $template->assign($this->tabsheet->get_titlename(), "[".$selected_tab['caption']."]");
94
95    $template_plugin["AMD_VERSION"] = "<i>".$this->plugin_name."</i> ".l10n('g003_version').AMD_VERSION;
96    $template_plugin["AMD_PAGE"] = $_REQUEST['fAMD_tabsheet'];
97    $template_plugin["PATH"] = AMD_PATH;
98
99    $template->assign('plugin', $template_plugin);
100
101    if($_REQUEST['fAMD_tabsheet']=='database')
102    {
103      $this->displayDatabase();
104    }
105    elseif($_REQUEST['fAMD_tabsheet']=='metadata')
106    {
107      $this->displayMetaData($_REQUEST['fAMD_page']);
108    }
109
110    $template->assign_var_from_handle('ADMIN_CONTENT', 'plugin_admin_content');
111  }
112
113  /**
114   * initialize events call for the plugin
115   */
116  public function init_events()
117  {
118    add_event_handler('loc_end_page_header', array(&$this->css, 'apply_CSS'));
119  }
120
121  /**
122   * ---------------------------------------------------------------------------
123   * Private & protected functions
124   * ---------------------------------------------------------------------------
125   */
126
127  /**
128   * manage the ajax requests
129   * this function function determine if there is an ajax call, manage the
130   * request and returns the content of the request
131   *
132   * no params are given, the function works with the "$_REQUEST" var
133   *
134   * @return String
135   */
136  protected function returnAjaxContent()
137  {
138    global $ajax, $template;
139
140    if(isset($_REQUEST['ajaxfct']))
141    {
142      //$this->debug("AJAXFCT:".$_REQUEST['ajaxfct']);
143      $result="<p class='errors'>".l10n('g002_error_invalid_ajax_call')."</p>";
144      switch($_REQUEST['ajaxfct'])
145      {
146        case 'makeStatsGetList':
147          $result=$this->ajax_amd_makeStatsGetList($_REQUEST['selectMode'], $_REQUEST['numOfItems']);
148          break;
149        case 'makeStatsDoAnalyze':
150          $result=$this->ajax_amd_makeStatsDoAnalyze($_REQUEST['imagesList']);
151          break;
152        case 'makeStatsConsolidation':
153          $result=$this->ajax_amd_makeStatsConsolidation();
154          break;
155        case 'makeStatsGetStatus':
156          $result=$this->ajax_amd_makeStatsGetStatus();
157          break;
158        case 'showStatsGetListTags':
159          $result=$this->ajax_amd_showStatsGetListTags($_REQUEST['orderType'], $_REQUEST['filterType'], $_REQUEST['excludeUnusedTag'], $_REQUEST['selectedTagOnly']);
160          break;
161        case 'showStatsGetListImages':
162          $result=$this->ajax_amd_showStatsGetListImages($_REQUEST['tagId'], $_REQUEST['orderType']);
163          break;
164        case 'updateTagSelect':
165          $result=$this->ajax_amd_updateTagSelect($_REQUEST['numId'], $_REQUEST['tagSelected']);
166          break;
167        case 'groupGetList':
168          $result=$this->ajax_amd_groupGetList();
169          break;
170        case 'groupDelete':
171          $result=$this->ajax_amd_groupDelete($_REQUEST['id']);
172          break;
173        case 'groupGetNames':
174          $result=$this->ajax_amd_groupGetNames($_REQUEST['id']);
175          break;
176        case 'groupSetNames':
177          $result=$this->ajax_amd_groupSetNames($_REQUEST['id'], $_REQUEST['listNames']);
178          break;
179        case 'groupSetOrder':
180          $result=$this->ajax_amd_groupSetOrder($_REQUEST['listGroup']);
181          break;
182        case 'groupGetTagList':
183          $result=$this->ajax_amd_groupGetTagList($_REQUEST['id']);
184          break;
185        case 'groupSetTagList':
186          $result=$this->ajax_amd_groupSetTagList($_REQUEST['id'], $_REQUEST['listTag']);
187          break;
188        case 'groupGetOrderedTagList':
189          $result=$this->ajax_amd_groupGetOrderedTagList($_REQUEST['id']);
190          break;
191        case 'groupSetOrderedTagList':
192          $result=$this->ajax_amd_groupSetOrderedTagList($_REQUEST['id'], $_REQUEST['listTag']);
193          break;
194      }
195      $this->ajax->return_result($result);
196    }
197  }
198
199
200  /**
201   * if empty, initialize the $_REQUEST var
202   *
203   * if not empty, check validity for the request values
204   *
205   */
206  private function initRequest()
207  {
208    //initialise $REQUEST values if not defined
209    if($this->getNumOfPictures()==0)
210    {
211      $defautTabsheet="database";
212    }
213    else
214    {
215      $defautTabsheet="metadata";
216    }
217
218    if(!isset($_REQUEST['fAMD_tabsheet']))
219    {
220      $_REQUEST['fAMD_tabsheet']=$defautTabsheet;
221    }
222
223    if($_REQUEST['fAMD_tabsheet']!="metadata" and
224       $_REQUEST['fAMD_tabsheet']!="database")
225    {
226      $_REQUEST['fAMD_tabsheet']=$defautTabsheet;
227    }
228
229    if($_REQUEST['fAMD_tabsheet']=="metadata" and !isset($_REQUEST['fAMD_page']))
230    {
231      $_REQUEST['fAMD_page']="select";
232    }
233
234    if($_REQUEST['fAMD_tabsheet']=="metadata" and
235       !($_REQUEST['fAMD_page']=="select" or
236         $_REQUEST['fAMD_page']=="display"))
237    {
238      $_REQUEST['fAMD_page']="select";
239    }
240
241    /*
242     * check ajax
243     */
244    if(isset($_REQUEST['ajaxfct']))
245    {
246      /*
247       * check makeStatsGetList values
248       */
249      if($_REQUEST['ajaxfct']=="makeStatsGetList" and !isset($_REQUEST['selectMode']))
250      {
251        $_REQUEST['selectMode']="notAnalyzed";
252      }
253
254      if($_REQUEST['ajaxfct']=="makeStatsGetList" and
255         !($_REQUEST['selectMode']=="notAnalyzed" or
256           $_REQUEST['selectMode']=="all"))
257      {
258        $_REQUEST['selectMode']="notAnalyzed";
259      }
260
261      if($_REQUEST['ajaxfct']=="makeStatsGetList" and !isset($_REQUEST['numOfItems']))
262      {
263        $_REQUEST['numOfItems']=25;
264      }
265
266      /*
267       * check makeStatsDoAnalyze values
268       */
269      if($_REQUEST['ajaxfct']=="makeStatsDoAnalyze" and !isset($_REQUEST['imagesList']))
270      {
271        $_REQUEST['imagesList']="";
272      }
273
274      /*
275       * check makeStatsConsolidate values
276       */
277      if($_REQUEST['ajaxfct']=="makeStatsConsolidate" and !isset($_REQUEST['step']))
278      {
279        $_REQUEST['step']="*";
280      }
281
282      /*
283       * check showStatsGetListTags values
284       */
285      if($_REQUEST['ajaxfct']=="showStatsGetListTags" and !isset($_REQUEST['orderType']))
286      {
287        $_REQUEST['orderType']="tag";
288      }
289
290      if($_REQUEST['ajaxfct']=="showStatsGetListTags" and
291         !($_REQUEST['orderType']=="tag" or
292           $_REQUEST['orderType']=="num"))
293      {
294        $_REQUEST['orderType']="tag";
295      }
296
297      if($_REQUEST['ajaxfct']=="showStatsGetListTags" and !isset($_REQUEST['filterType']))
298      {
299        $_REQUEST['filterType']="";
300      }
301
302      if($_REQUEST['ajaxfct']=="showStatsGetListTags" and
303         !($_REQUEST['filterType']=="" or
304           $_REQUEST['filterType']=="magic" or
305           $_REQUEST['filterType']=="exif" or
306           $_REQUEST['filterType']=="xmp" or
307           $_REQUEST['filterType']=="iptc"))
308      {
309        $_REQUEST['filterType']="";
310      }
311
312      if($_REQUEST['ajaxfct']=="showStatsGetListTags" and !isset($_REQUEST['excludeUnusedTag']))
313      {
314        $_REQUEST['excludeUnusedTag']="n";
315      }
316
317      if($_REQUEST['ajaxfct']=="showStatsGetListTags" and
318         !($_REQUEST['excludeUnusedTag']=="y" or
319           $_REQUEST['excludeUnusedTag']=="n" ))
320      {
321        $_REQUEST['excludeUnusedTag']="n";
322      }
323
324      if($_REQUEST['ajaxfct']=="showStatsGetListTags" and !isset($_REQUEST['selectedTagOnly']))
325      {
326        $_REQUEST['selectedTagOnly']="n";
327      }
328
329      if($_REQUEST['ajaxfct']=="showStatsGetListTags" and
330         !($_REQUEST['selectedTagOnly']=="y" or
331           $_REQUEST['selectedTagOnly']=="n" ))
332      {
333        $_REQUEST['selectedTagOnly']="n";
334      }
335
336
337      /*
338       * check showStatsGetListImagess values
339       */
340      if($_REQUEST['ajaxfct']=="showStatsGetListImages" and !isset($_REQUEST['orderType']))
341      {
342        $_REQUEST['orderType']="num";
343      }
344
345      if($_REQUEST['ajaxfct']=="showStatsGetListImages" and
346         !($_REQUEST['orderType']=="value" or
347           $_REQUEST['orderType']=="num"))
348      {
349        $_REQUEST['orderType']="num";
350      }
351
352      if($_REQUEST['ajaxfct']=="showStatsGetListImages" and !isset($_REQUEST['tagId']))
353      {
354        $_REQUEST['tagId']="*";
355      }
356
357
358      /*
359       * check showStatsGetListImagess values
360       */
361      if($_REQUEST['ajaxfct']=="updateTagSelect" and !isset($_REQUEST['numId']))
362      {
363        $_REQUEST['numId']="";
364      }
365
366      if($_REQUEST['ajaxfct']=="updateTagSelect" and !isset($_REQUEST['tagSelected']))
367      {
368        $_REQUEST['tagSelected']="";
369      }
370
371
372
373
374      /*
375       * check groupDelete values
376       */
377      if($_REQUEST['ajaxfct']=="groupDelete" and !isset($_REQUEST['id']))
378      {
379        $_REQUEST['id']="";
380      }
381
382
383
384      /*
385       * check groupSetOrder values
386       */
387      if($_REQUEST['ajaxfct']=="groupSetOrder" and !isset($_REQUEST['listGroup']))
388      {
389        $_REQUEST['listGroup']="";
390      }
391
392      /*
393       * check groupGetNames values
394       */
395      if($_REQUEST['ajaxfct']=="groupGetNames" and !isset($_REQUEST['id']))
396      {
397        $_REQUEST['id']="";
398      }
399
400      /*
401       * check groupSetNames values
402       */
403      if($_REQUEST['ajaxfct']=="groupSetNames" and !isset($_REQUEST['listNames']))
404      {
405        $_REQUEST['listNames']="";
406      }
407
408      if($_REQUEST['ajaxfct']=="groupSetNames" and !isset($_REQUEST['id']))
409      {
410        $_REQUEST['id']="";
411      }
412
413
414      /*
415       * check groupGetTagList values
416       */
417      if($_REQUEST['ajaxfct']=="groupGetTagList" and !isset($_REQUEST['id']))
418      {
419        $_REQUEST['id']="";
420      }
421
422      /*
423       * check groupSetTagList values
424       */
425      if($_REQUEST['ajaxfct']=="groupSetTagList" and !isset($_REQUEST['id']))
426      {
427        $_REQUEST['id']="";
428      }
429
430      if($_REQUEST['ajaxfct']=="groupSetTagList" and !isset($_REQUEST['listTag']))
431      {
432        $_REQUEST['listTag']="";
433      }
434
435
436      /*
437       * check groupGetOrderedTagList values
438       */
439      if($_REQUEST['ajaxfct']=="groupGetOrderedTagList" and !isset($_REQUEST['id']))
440      {
441        $_REQUEST['id']="";
442      }
443
444      /*
445       * check groupSetOrderedTagList values
446       */
447      if($_REQUEST['ajaxfct']=="groupSetOrderedTagList" and !isset($_REQUEST['id']))
448      {
449        $_REQUEST['id']="";
450      }
451
452      if($_REQUEST['ajaxfct']=="groupSetOrderedTagList" and !isset($_REQUEST['listTag']))
453      {
454        $_REQUEST['listTag']="";
455      }
456
457    }
458  } //init_request
459
460
461  /**
462   * manage adviser profile
463   *
464   * @return Boolean : true if user is adviser, otherwise false (and push a
465   *                   message in the error list)
466   */
467  protected function adviser_abort()
468  {
469    if(is_adviser())
470    {
471      $this->display_result(l10n("g003_adviser_not_allowed"), false);
472      return(true);
473    }
474    return(false);
475  }
476
477
478  /**
479   * display and manage the database page
480   *
481   * the function automatically update the AMD tables :
482   *  - add new pictures in the AMD image table (assuming image is not analyzed
483   *    yet)
484   *  - remove deleted pictures in the AMD image & image_tags table
485   *
486   * @return String : the content of the page
487   */
488  private function displayDatabase()
489  {
490    global $template, $page;
491
492    /*
493     * insert new image (from piwigo images table) in the AMD images table, with
494     * statut 'not analyzed'
495     */
496    $sql="INSERT INTO ".$this->tables['images']."
497            SELECT id, 'n', 0
498              FROM ".IMAGES_TABLE."
499              WHERE id NOT IN (SELECT imageId FROM ".$this->tables['images'].")";
500    pwg_query($sql);
501
502
503    /*
504     * delete image who are in the AMD images table and not in the piwigo image
505     * table
506     */
507    $sql="DELETE FROM ".$this->tables['images']."
508            WHERE imageId NOT IN (SELECT id FROM ".IMAGES_TABLE.")";
509    pwg_query($sql);
510
511
512    /*
513     * delete metdata for images that are not in the AMD image table
514     */
515    $sql="DELETE FROM ".$this->tables['images_tags']."
516            WHERE imageId NOT IN (SELECT imageId FROM ".$this->tables['images'].")";
517    pwg_query($sql);
518
519
520    $template->set_filename('body_page', dirname(__FILE__).'/admin/amd_database.tpl');
521
522    $datas=array(
523      'urlRequest' => $this->page_link,
524      'NumberOfItemsPerRequest' => $this->my_config['amd_NumberOfItemsPerRequest'],
525    );
526
527    $template->assign("datas", $datas);
528
529    $template->assign_var_from_handle('AMD_BODY_PAGE', 'body_page');
530  } // displayDatabase
531
532
533
534  /**
535   * display and manage the metadata page
536   * the page have two tabsheet :
537   *  - select tag management, to manage tags to be selected on the galerie
538   *  - display tag management, to choose how the tags are displayed
539   *
540   * @param String $tab : the selected tab on the stat page
541   */
542  protected function displayMetaData($tab)
543  {
544    global $template, $user;
545    $template->set_filename('body_page', dirname(__FILE__).'/admin/amd_metadata.tpl');
546
547    $statTabsheet = new tabsheet('statTabsheet', $this->tabsheet->get_titlename());
548    $statTabsheet->select($tab);
549    $statTabsheet->add('select',
550                          l10n('g003_select'),
551                          $this->page_link.'&amp;fAMD_tabsheet=metadata&amp;fAMD_page=select');
552    $statTabsheet->add('display',
553                          l10n('g003_display'),
554                          $this->page_link.'&amp;fAMD_tabsheet=metadata&amp;fAMD_page=display');
555    $statTabsheet->assign();
556
557
558
559    if($tab=="select")
560    {
561      $template->assign('sheetContent', $this->displayMetaDataSelect());
562    }
563    else
564    {
565      $template->assign('sheetContent', $this->displayMetaDataDisplay());
566    }
567
568    $template->assign_var_from_handle('AMD_BODY_PAGE', 'body_page');
569  }
570
571  /**
572   * display and manage the metadata page allowing to make tags selection
573   *
574   * @return String : the content of the page
575   */
576  protected function displayMetaDataSelect()
577  {
578    global $template;
579
580    $template->set_filename('sheet_page',
581                  dirname($this->filelocation).'/admin/amd_metadata_select.tpl');
582
583    $datas=array(
584      'urlRequest' => $this->page_link,
585      'config_GetListTags_OrderType' => $this->my_config['amd_GetListTags_OrderType'],
586      'config_GetListTags_FilterType' => $this->my_config['amd_GetListTags_FilterType'],
587      'config_GetListTags_ExcludeUnusedTag' => $this->my_config['amd_GetListTags_ExcludeUnusedTag'],
588      'config_GetListTags_SelectedTagOnly' => $this->my_config['amd_GetListTags_SelectedTagOnly'],
589      'config_GetListImages_OrderType' => $this->my_config['amd_GetListImages_OrderType']
590    );
591
592
593    $template->assign('datas', $datas);
594
595    return($template->parse('sheet_page', true));
596  }
597
598
599  /**
600   * display and manage the metadata page allowing to choose tags order
601   *
602   * @return String : the content of the page
603   */
604  protected function displayMetaDataDisplay()
605  {
606    global $user, $template;
607
608    //$local_tpl = new Template(AMD_PATH."admin/", "");
609    $template->set_filename('sheet_page',
610                  dirname($this->filelocation).'/admin/amd_metadata_display.tpl');
611
612
613    $datas=array(
614      'urlRequest' => $this->page_link,
615      'selectedTags' => Array(),
616      'groups' => Array(),
617      'tagByGroup' => Array(),
618    );
619
620    $sql="SELECT st.tagId, st.order, st.groupId, ut.numId
621          FROM ".$this->tables['selected_tags']." st
622            LEFT JOIN ".$this->tables['used_tags']." ut
623              ON ut.tagId = st.tagId
624          ORDER BY st.groupId ASC, st.order ASC, st.tagId ASC";
625    $result=pwg_query($sql);
626    if($result)
627    {
628      while($row=mysql_fetch_assoc($result))
629      {
630        if($row['groupId']==-1)
631        {
632          $datas['selectedTags'][]=Array(
633            'numId' => $row['numId'],
634            'tagId' => $row['tagId']
635          );
636        }
637        else
638        {
639          $datas['tagByGroup'][]=Array(
640            'numId' => $row['numId'],
641            'tagId' => $row['tagId'],
642            'group' => $row['groupId'],
643            'order' => $row['order']
644          );
645        }
646      }
647    }
648
649    $sql="SELECT g.groupId, gn.name
650          FROM ".$this->tables['groups']." g
651            LEFT JOIN ".$this->tables['groups_names']." gn
652              ON g.groupId = gn.groupId
653          WHERE gn.lang = '".$user['language']."'
654          ORDER BY g.order;";
655    $result=pwg_query($sql);
656    if($result)
657    {
658      while($row=mysql_fetch_assoc($result))
659      {
660        $datas['groups'][]=Array(
661          'id' => $row['groupId'],
662          'name' => $row['name']
663        );
664      }
665    }
666
667    $template->assign('datas', $datas);
668    return($template->parse('sheet_page', true));
669  }
670
671
672
673  /**
674   * this function analyze tags from a picture, and insert the result into the
675   * database
676   *
677   * NOTE : only implemented tags are analyzed and stored
678   *
679   * @param String $fileName : filename of picture to analyze
680   * @param Integer $imageId : id of image in piwigo's database
681   */
682  protected function analyzeImageFile($fileName, $imageId)
683  {
684    /*
685     * the JpegMetaData object is instancied in the constructor
686     */
687    $this->jpegMD->load($fileName, Array(
688      'filter' => JpegMetaData::TAGFILTER_IMPLEMENTED,
689      'optimizeIptcDateTime' => true)
690    );
691
692    $sqlInsert="";
693    $massInsert=array();
694    $nbTags=0;
695    foreach($this->jpegMD->getTags() as $key => $val)
696    {
697      $value=$val->getLabel();
698
699      if($val->isTranslatable())
700        $translatable="y";
701      else
702        $translatable="n";
703
704      if($value instanceof DateTime)
705      {
706        $value=$value->format("Y-m-d H:i:s");
707      }
708      elseif(is_array($value))
709      {
710        /*
711         * array values are stored in a serialized string
712         */
713        $value=serialize($value);
714      }
715
716      $sql="SELECT numId FROM ".$this->tables['used_tags']." WHERE tagId = '$key'";
717      $result=pwg_query($sql);
718      if($result)
719      {
720        $numId=-1;
721        while($row=mysql_fetch_assoc($result))
722        {
723          $numId=$row['numId'];
724        }
725
726        if($numId>0)
727        {
728          $nbTags++;
729          if($sqlInsert!="") $sqlInsert.=", ";
730          $sqlInsert.="($imageId, '$numId', '".addslashes($value)."')";
731          $massInsert[]=array(
732            'imageId' => $imageId,
733            'numId' => $numId,
734            'value' => addslashes($value)
735          );
736        }
737      }
738    }
739
740    /*if($sqlInsert!="")
741    {
742      $sqlInsert="INSERT INTO ".$this->tables['images_tags']." VALUES ".$sqlInsert;
743      pwg_query($sqlInsert);
744    }*/
745    mass_inserts($this->tables['images_tags'], array('imageId', 'numId', 'value'), $massInsert);
746
747    $sql="UPDATE ".$this->tables['images']."
748            SET analyzed = 'y', nbTags=".$nbTags."
749            WHERE imageId=$imageId;";
750    pwg_query($sql);
751
752
753    return("$imageId=$nbTags;");
754  }
755
756
757  /**
758   * returns the number of pictures analyzed
759   *
760   * @return Integer
761   */
762  protected function getNumOfPictures()
763  {
764    $numOfPictures=0;
765    $sql="SELECT COUNT(imageId) FROM ".$this->tables['images']."
766            WHERE analyzed='y';";
767    $result=pwg_query($sql);
768    if($result)
769    {
770      while($row=mysql_fetch_row($result))
771      {
772        $numOfPictures=$row[0];
773      }
774    }
775    return($numOfPictures);
776  }
777
778
779  /*
780   * ---------------------------------------------------------------------------
781   * ajax functions
782   * ---------------------------------------------------------------------------
783   */
784
785  /**
786   * return a list of picture Id
787   *
788   * picture id are separated with a space " "
789   * picture id are grouped in blocks of 'amd_NumberOfItemsPerRequest' items and
790   * are separated with a semi-colon ";"
791   *
792   * client side just have to split blocks, and transmit it to the server
793   *
794   * There is two mode to determine the pictures being analyzed :
795   *  - "all"         : analyze all the images
796   *  - "notAnalyzed" : analyze only the images not yet analyzed
797   *
798   * @param String $mode
799   * @param Integer $nbOfItems : number of items per request
800   * @return String : list of image id to be analyzed, separated with a space
801   *                      "23 78 4523 5670"
802   */
803  private function ajax_amd_makeStatsGetList($mode, $nbOfItems)
804  {
805    $returned="";
806    $this->my_config['amd_NumberOfItemsPerRequest']=$nbOfItems;
807    $this->save_config();
808
809    $sql="SELECT imageId FROM ".$this->tables['images'];
810    if($mode=="notAnalyzed")
811    {
812      $sql.=" WHERE analyzed='n'";
813    }
814    else
815    {
816      pwg_query("UPDATE ".$this->tables['images']." SET analyzed='n', nbTags=0");
817      pwg_query("UPDATE ".$this->tables['used_tags']." SET numOfImg=0");
818      pwg_query("DELETE FROM ".$this->tables['images_tags']);
819    }
820
821    $result=pwg_query($sql);
822    if($result)
823    {
824      $i=0;
825      while($row=mysql_fetch_row($result))
826      {
827        $returned.=$row[0];
828        $i++;
829        if($i>=$nbOfItems)
830        {
831          $returned.=";";
832          $i=0;
833        }
834        else
835        {
836          $returned.=" ";
837        }
838      }
839    }
840    return(trim($returned).";");
841  }
842
843
844  /**
845   * extract metadata from images
846   *
847   * @param String $imageList : list of image id to be analyzed, separated with
848   *                            a space
849   *                                "23 78 4523 5670"
850   * @return String : list of the analyzed pictures, with number of tags found
851   *                  for each picture
852   *                    "23=0;78=66;4523=33;5670=91;"
853   */
854  private function ajax_amd_makeStatsDoAnalyze($imagesList)
855  {
856    $list=explode(" ", trim($imagesList));
857
858    $returned="";
859
860    if(count($list)>0)
861    {
862      // $path = path of piwigo's on the server filesystem
863      $path=dirname(dirname(dirname(__FILE__)));
864
865      $sql="SELECT id, path FROM ".IMAGES_TABLE." WHERE id IN (".implode(", ", $list).")";
866      $result=pwg_query($sql);
867      if($result)
868      {
869        while($row=mysql_fetch_assoc($result))
870        {
871          /*
872           * in some case (in a combination of some pictures), when there is too
873           * much pictures to analyze in the same request, a fatal error occurs
874           * with the message : "Allowed memory size of XXXXX bytes exhausted"
875           *
876           *
877           * tracking memory leak is not easy... :-(
878           *
879           */
880          //echo "analyzing:".$row['id']."\n";
881          //$mem1=memory_get_usage();
882          //echo "memory before analyze:".$mem1."\n";
883          $returned.=$this->analyzeImageFile($path."/".$row['path'], $row['id']);
884          //echo $returned."\n";
885          //$mem2=memory_get_usage();
886          //echo "memory after analyze:".$mem2." (".($mem2-$mem1).")\n";
887        }
888      }
889    }
890    return($returned);
891  }
892
893  /**
894   * do some consolidation on database to optimize other requests
895   *
896   */
897  private function ajax_amd_makeStatsConsolidation()
898  {
899    $sql="UPDATE ".$this->tables['used_tags']." ut,
900            (SELECT COUNT(imageId) AS nb, numId
901              FROM ".$this->tables['images_tags']."
902              GROUP BY numId) nb
903          SET ut.numOfImg = nb.nb
904          WHERE ut.numId = nb.numId;";
905    pwg_query($sql);
906  }
907
908  /**
909   * returns a list of formated string, separated with a semi-colon :
910   *  - number of current analyzed pictures + number of current analyzed tags
911   *    for the analyzed pictures
912   *  - number of pictures not analyzed
913   *  - number of pictures without tag
914   *
915   * @return String
916   */
917  private function ajax_amd_makeStatsGetStatus()
918  {
919    $numOfMetaData=0;
920    $numOfPictures=0;
921    $numOfPicturesNotAnalyzed=0;
922
923    $sql="SELECT COUNT(imageId), SUM(nbTags) FROM ".$this->tables['images']."
924            WHERE analyzed='y';";
925    $result=pwg_query($sql);
926    if($result)
927    {
928      while($row=mysql_fetch_row($result))
929      {
930        $numOfPictures=$row[0];
931        $numOfMetaData=$row[1];
932      }
933    }
934
935
936    $sql="SELECT COUNT(imageId) FROM ".$this->tables['images']."
937            WHERE analyzed='n';";
938    $result=pwg_query($sql);
939    if($result)
940    {
941      while($row=mysql_fetch_row($result))
942      {
943        $numOfPicturesNotAnalyzed=$row[0];
944      }
945    }
946
947    $sql="SELECT COUNT(imageId) FROM ".$this->tables['images']."
948            WHERE nbTags=0;";
949    $result=pwg_query($sql);
950    if($result)
951    {
952      while($row=mysql_fetch_row($result))
953      {
954        $numOfPicturesWithoutTags=$row[0];
955      }
956    }
957
958    return(sprintf(l10n("g003_numberOfAnalyzedPictures"), $numOfPictures, $numOfMetaData).";".
959              sprintf(l10n("g003_numberOfNotAnalyzedPictures"), $numOfPicturesNotAnalyzed).";".
960              sprintf(l10n("g003_numberOfPicturesWithoutTags"), $numOfPicturesWithoutTags));
961  }
962
963
964  /**
965   * return a formatted <table> (using the template "amd_stat_show_iListTags")
966   * of used tag with, for each tag, the number and the percentage of pictures
967   * where the tag was found
968   *
969   * @param String $orderType : order for the list (by tag 'tag' or by number of
970   *                            pictures 'num')
971   * @param String $filterType : filter for the list ('exif', 'xmp', 'iptc' or '')
972   * @return String
973   */
974  private function ajax_amd_showStatsGetListTags($orderType, $filterType, $excludeUnusedTag, $selectedTagOnly)
975  {
976    $this->my_config['amd_GetListTags_OrderType'] = $orderType;
977    $this->my_config['amd_GetListTags_FilterType'] = $filterType;
978    $this->my_config['amd_GetListTags_ExcludeUnusedTag'] = $excludeUnusedTag;
979    $this->my_config['amd_GetListTags_SelectedTagOnly'] = $selectedTagOnly;
980    $this->save_config();
981
982    $local_tpl = new Template(AMD_PATH."admin/", "");
983    $local_tpl->set_filename('body_page',
984                  dirname($this->filelocation).'/admin/amd_metadata_select_iListTags.tpl');
985
986    $numOfPictures=$this->getNumOfPictures();
987
988    $datas=array();
989    $sql="SELECT ut.numId, ut.tagId, ut.translatable, ut.name, ut.numOfImg, if(st.tagId IS NULL, 'n', 'y') as checked
990            FROM ".$this->tables['used_tags']." ut
991              LEFT JOIN ".$this->tables['selected_tags']." st
992                ON st.tagId = ut.tagId ";
993    $where="";
994
995    if($filterType!='')
996    {
997      $where.=" WHERE ut.tagId LIKE '".$filterType.".%' ";
998    }
999
1000    if($excludeUnusedTag=='y')
1001    {
1002      ($where=="")?$where=" WHERE ":$where.=" AND ";
1003      $where.=" ut.numOfImg > 0 ";
1004    }
1005
1006    if($selectedTagOnly=='y')
1007    {
1008      ($where=="")?$where=" WHERE ":$where.=" AND ";
1009      $where.=" st.tagId IS NOT NULL ";
1010    }
1011
1012    $sql.=$where;
1013
1014    switch($orderType)
1015    {
1016      case 'tag':
1017        $sql.=" ORDER BY tagId ASC";
1018        break;
1019      case 'num':
1020        $sql.=" ORDER BY numOfImg DESC, tagId ASC";
1021        break;
1022    }
1023
1024    $result=pwg_query($sql);
1025    if($result)
1026    {
1027      while($row=mysql_fetch_assoc($result))
1028      {
1029        $datas[]=array(
1030          "numId" => $row['numId'],
1031          "tagId" => $row['tagId'],
1032          "label" => L10n::get($row['name']),
1033          "nb"    => $row['numOfImg'],
1034          "pct"   => ($numOfPictures!=0)?sprintf("%.2f", 100*$row['numOfImg']/$numOfPictures):"-",
1035          "tagChecked" => ($row['checked']=='y')?"checked":""
1036        );
1037      }
1038    }
1039
1040    $local_tpl->assign('datas', $datas);
1041
1042    return($local_tpl->parse('body_page', true));
1043  }
1044
1045
1046  /*
1047   *
1048   *
1049   */
1050  private function ajax_amd_showStatsGetListImages($tagId, $orderType)
1051  {
1052    $this->my_config['amd_GetListImages_OrderType'] = $orderType;
1053    $this->save_config();
1054
1055    $local_tpl = new Template(AMD_PATH."admin/", "");
1056    $local_tpl->set_filename('body_page',
1057                  dirname($this->filelocation).'/admin/amd_metadata_select_iListImages.tpl');
1058
1059
1060
1061    $datas=array();
1062    $sql="SELECT ut.translatable, ut.numOfImg, COUNT(it.imageId) AS Nb, it.value
1063            FROM ".$this->tables['used_tags']." ut
1064              LEFT JOIN ".$this->tables['images_tags']." it
1065              ON ut.numId = it.numId
1066            WHERE ut.tagId = '".$tagId."'
1067              AND it.value IS NOT NULL
1068            GROUP BY it.value
1069            ORDER BY ";
1070    switch($orderType)
1071    {
1072      case 'value':
1073        $sql.="it.value ASC";
1074        break;
1075      case 'num':
1076        $sql.="Nb DESC";
1077        break;
1078    }
1079
1080    $result=pwg_query($sql);
1081    if($result)
1082    {
1083      while($row=mysql_fetch_assoc($result))
1084      {
1085        $datas[]=array(
1086          "value" => ($row['translatable']=='y')?L10n::get($row['value']):$row['value'],
1087          "nb"    => $row['Nb'],
1088          "pct"   => ($row['numOfImg']!=0)?sprintf("%.2f", 100*$row['Nb']/$row['numOfImg']):"-"
1089        );
1090      }
1091    }
1092
1093    if(count($datas)>0)
1094    {
1095      $local_tpl->assign('datas', $datas);
1096      return($local_tpl->parse('body_page', true));
1097    }
1098    else
1099    {
1100      return("<div style='width:100%;text-align:center;padding-top:20px;'>".l10n('g003_selected_tag_isnot_linked_with_any_picture')."</div>");
1101    }
1102  }
1103
1104
1105  /*
1106   *
1107   *
1108   */
1109  private function ajax_amd_updateTagSelect($numId, $selected)
1110  {
1111    if($selected=='y')
1112    {
1113      $sql="SELECT ut.tagId FROM ".$this->tables['selected_tags']." st
1114              LEFT JOIN ".$this->tables['used_tags']." ut
1115                ON ut.tagID = st.tagId
1116              WHERE ut.numId = $numId;";
1117      $result=pwg_query($sql);
1118      if($result)
1119      {
1120        if(mysql_num_rows($result)==0)
1121        {
1122          $sql="INSERT INTO ".$this->tables['selected_tags']."
1123                  SELECT ut.tagId, 0, -1
1124                  FROM ".$this->tables['used_tags']." ut
1125                    LEFT JOIN ".$this->tables['selected_tags']." st
1126                      ON ut.tagID = st.tagId
1127                  WHERE ut.numId = $numId;";
1128          pwg_query($sql);
1129        }
1130      }
1131    }
1132    elseif($selected=='n')
1133    {
1134      $sql="DELETE FROM ".$this->tables['selected_tags']." st
1135              USING phpwebgallery_amd_used_tags ut
1136                LEFT JOIN phpwebgallery_amd_selected_tags st
1137                  ON ut.tagID = st.tagId
1138              WHERE ut.numId = $numId;";
1139      pwg_query($sql);
1140    }
1141
1142  }
1143
1144
1145  /**
1146   * this function return the list of tags :
1147   *  - associated with the group
1148   *  - not associated with a group
1149   * the list is used to make tags selection
1150   *
1151   * @param String $id      : Id of the current group
1152   * @return String : an HTML formatted list with checkbox
1153   */
1154  private function ajax_amd_groupGetTagList($id)
1155  {
1156    if($id!="")
1157    {
1158      $sql="SELECT st.tagId, st.groupId, ut.name, ut.numId
1159            FROM ".$this->tables['selected_tags']." st
1160              LEFT JOIN ".$this->tables['used_tags']." ut
1161                ON st.tagId = ut.tagId
1162            ORDER BY tagId";
1163      $result=pwg_query($sql);
1164      if($result)
1165      {
1166        $datas=Array();
1167        while($row=mysql_fetch_assoc($result))
1168        {
1169          if($row['groupId']==$id)
1170          {
1171            $state="checked";
1172          }
1173          elseif($row['groupId']==-1)
1174          {
1175            $state="";
1176          }
1177          else
1178          {
1179            $state="n/a";
1180          }
1181
1182          if($state!="n/a")
1183            $datas[]=Array(
1184              'tagId' => $row['tagId'],
1185              'name'  => L10n::get($row['name']),
1186              'state' => $state,
1187              'numId' => $row['numId']
1188            );
1189        }
1190
1191        if(count($datas)>0)
1192        {
1193          $local_tpl = new Template(AMD_PATH."admin/", "");
1194          $local_tpl->set_filename('body_page',
1195                        dirname($this->filelocation).'/admin/amd_metadata_display_groupListTagSelect.tpl');
1196          $local_tpl->assign('datas', $datas);
1197          return($local_tpl->parse('body_page', true));
1198        }
1199        else
1200        {
1201          return(l10n("g003_no_tag_can_be_selected"));
1202        }
1203      }
1204    }
1205    else
1206    {
1207      return(l10n("g003_invalid_group_id"));
1208    }
1209  }
1210
1211
1212  /**
1213   * this function associate tags to a group
1214   *
1215   * @param String $id      : Id of group
1216   * @param String $listTag : list of selected tags, items are separated by a
1217   *                          semi-colon ";" char
1218   */
1219  private function ajax_amd_groupSetTagList($id, $listTag)
1220  {
1221    if($id!="")
1222    {
1223      $sql="UPDATE ".$this->tables['selected_tags']."
1224            SET groupId = -1
1225            WHERE groupId = $id;";
1226      pwg_query($sql);
1227
1228      if($listTag!="")
1229      {
1230        $sql="UPDATE ".$this->tables['selected_tags']." st, ".$this->tables['used_tags']." ut
1231              SET st.groupId = $id
1232              WHERE st.tagId = ut.tagId
1233                AND ut.numId IN ($listTag);";
1234        pwg_query($sql);
1235      }
1236    }
1237    else
1238    {
1239      return("KO");
1240    }
1241  }
1242
1243
1244  /**
1245   * this function returns an ordered list of tags associated with a group
1246   *
1247   * @param String $id        : the group Id
1248   * @return String : an HTML formatted list
1249   */
1250  private function ajax_amd_groupGetOrderedTagList($id)
1251  {
1252    global $template;
1253    if($id!="")
1254    {
1255      $numOfPictures=$this->getNumOfPictures();
1256
1257      $sql="SELECT st.tagId, ut.name, ut.numId, ut.numOfImg
1258            FROM ".$this->tables['selected_tags']." st
1259              LEFT JOIN ".$this->tables['used_tags']." ut
1260                ON st.tagId = ut.tagId
1261            WHERE st.groupId = $id
1262            ORDER BY st.order ASC, st.tagId ASC";
1263      $result=pwg_query($sql);
1264      if($result)
1265      {
1266        $datas=Array();
1267        while($row=mysql_fetch_assoc($result))
1268        {
1269          $datas[]=Array(
1270            'tagId' => $row['tagId'],
1271            'name'  => L10n::get($row['name']),
1272            'numId' => $row['numId'],
1273            'nbItems' => $row['numOfImg'],
1274            'pct'   => sprintf("%.2f", 100*$row['numOfImg']/$numOfPictures)
1275          );
1276        }
1277
1278        if(count($datas)>0)
1279        {
1280          $template->set_filename('list_page',
1281                        dirname($this->filelocation).'/admin/amd_metadata_display_groupListTagOrder.tpl');
1282          $template->assign('datas', $datas);
1283          $template->assign('group', $id);
1284          return($template->parse('list_page', true));
1285        }
1286        else
1287        {
1288          return(l10n("g003_no_tag_can_be_selected"));
1289        }
1290      }
1291    }
1292    else
1293    {
1294      return(l10n("g003_invalid_group_id"));
1295    }
1296  }
1297
1298
1299  /**
1300   * this function update the tags order inside a group
1301   *
1302   * @param String $id        : the group Id
1303   * @param String $listGroup : the ordered list of tags, items are separated
1304   *                            by a semi-colon ";" char
1305   */
1306  private function ajax_amd_groupSetOrderedTagList($id, $listTag)
1307  {
1308    $tags=explode(';', $listTag);
1309    if($id!="" and count($tags)>0)
1310    {
1311      /*
1312       * by default, all items are set with order equals -1 (if list is not
1313       * complete, forgotten items are sorted in head)
1314       */
1315      pwg_query("UPDATE ".$this->tables['selected_tags']." st
1316                  SET st.order = -1
1317                  WHERE st.groupId = $id;");
1318
1319      foreach($tags as $key=>$val)
1320      {
1321        $sql="UPDATE ".$this->tables['selected_tags']." st, ".$this->tables['used_tags']." ut
1322              SET st.order = $key
1323              WHERE st.groupId = $id
1324                AND st.tagId = ut.tagId
1325                AND ut.numId = $val;";
1326        $result=pwg_query($sql);
1327      }
1328    }
1329  }
1330
1331
1332
1333  /**
1334   * this function update the groups order
1335   *
1336   * @param String $listGroup : the ordered list of groups, items are separated
1337   *                            by a semi-colon ";" char
1338   */
1339  private function ajax_amd_groupSetOrder($listGroup)
1340  {
1341    $groups=explode(";",$listGroup);
1342    if(count($groups)>0)
1343    {
1344      /*
1345       * by default, all items are set with order equals -1 (if list is not
1346       * complete, forgotten items are sorted in head)
1347       */
1348      pwg_query("UPDATE ".$this->tables['groups']." g SET g.order = -1;");
1349
1350      foreach($groups as $key=>$val)
1351      {
1352        $sql="UPDATE ".$this->tables['groups']." g
1353              SET g.order = $key
1354              WHERE g.groupId = $val;";
1355        $result=pwg_query($sql);
1356      }
1357    }
1358  }
1359
1360  /**
1361   * this function is used to create a new group ($groupId = "") or update the
1362   * group name (names are given in all langs in a list)
1363   *
1364   * @param String $groupId : the groupId to update, or "" to create a new groupId
1365   * @param String $listNames : name of the group, in all language given as a
1366   *                            list ; each lang is separated by a carraige
1367   *                            return "\n" char, each items is defined as
1368   *                            lang=value
1369   *                              en_UK=the name group
1370   *                              fr_FR=le nom du groupe
1371   */
1372  private function ajax_amd_groupSetNames($groupId, $listNames)
1373  {
1374    $names=explode("\n", $listNames);
1375    if($groupId=="" and count($names)>0)
1376    {
1377      $sql="INSERT INTO ".$this->tables['groups']." VALUES('', 9999)";
1378      $result=pwg_query($sql);
1379      $groupId=mysql_insert_id();
1380    }
1381
1382    if(is_numeric($groupId) and count($names)>0)
1383    {
1384      $sql="DELETE FROM ".$this->tables['groups_names']."
1385            WHERE groupId = $groupId;";
1386      pwg_query($sql);
1387
1388
1389      $sql="";
1390      foreach($names as $val)
1391      {
1392        $tmp=explode("=", $val);
1393        if($sql!="") $sql.=", ";
1394        $sql.=" ($groupId, '".$tmp[0]."', '".$tmp[1]."')";
1395      }
1396      $sql="INSERT INTO ".$this->tables['groups_names']." VALUES ".$sql;
1397      pwg_query($sql);
1398    }
1399  }
1400
1401  /**
1402   * this function returns an html form, allowing to manage the group
1403   *
1404   * @param String $groupId : the groupId to manage, or "" to return a creation
1405   *                          form
1406   * @return String : the form
1407   */
1408  private function ajax_amd_groupGetNames($groupId)
1409  {
1410    global $user;
1411
1412    $local_tpl = new Template(AMD_PATH."admin/", "");
1413    $local_tpl->set_filename('body_page',
1414                  dirname($this->filelocation).'/admin/amd_metadata_display_groupEdit.tpl');
1415
1416    $datasLang=array(
1417      'language_list' => Array(),
1418      'lang_selected' => $user['language'],
1419      'fromlang' => substr($user['language'],0,2),
1420      'default' => ''
1421    );
1422
1423    $langs=get_languages();
1424    foreach($langs as $key => $val)
1425    {
1426      $datasLang['language_list'][$key] = Array(
1427        'langName' => str_replace("\n", "", $val),
1428        'name' => ""
1429      );
1430    }
1431
1432    if($groupId!="")
1433    {
1434      $sql="SELECT lang, name FROM ".$this->tables['groups_names']."
1435            WHERE groupId = $groupId;";
1436      $result=pwg_query($sql);
1437      if($result)
1438      {
1439        while($row=mysql_fetch_assoc($result))
1440        {
1441          if(array_key_exists($row['lang'], $datasLang['language_list']))
1442          {
1443            $datasLang['language_list'][$row['lang']]['name']=htmlentities($row['name'], ENT_QUOTES, 'UTF-8');
1444            if($user['language']==$row['lang'])
1445            {
1446              $datasLang['default']=$datasLang['language_list'][$row['lang']]['name'];
1447            }
1448          }
1449        }
1450      }
1451    }
1452
1453    $local_tpl->assign('datasLang', $datasLang);
1454
1455    return($local_tpl->parse('body_page', true));
1456  }
1457
1458
1459  /**
1460   * this function returns an html form, allowing to manage the group
1461   *
1462   * @param String $groupId : the groupId to manage, or "" to return a creation
1463   *                          form
1464   * @return String : the form
1465   */
1466  private function ajax_amd_groupGetList()
1467  {
1468    global $user, $template;
1469
1470    //$local_tpl = new Template(AMD_PATH."admin/", "");
1471    $template->set_filename('group_list',
1472                  dirname($this->filelocation).'/admin/amd_metadata_display_groupList.tpl');
1473
1474
1475    $datas=array(
1476      'groups' => Array(),
1477    );
1478
1479    $sql="SELECT g.groupId, gn.name
1480          FROM ".$this->tables['groups']." g
1481            LEFT JOIN ".$this->tables['groups_names']." gn
1482              ON g.groupId = gn.groupId
1483          WHERE gn.lang = '".$user['language']."'
1484          ORDER BY g.order;";
1485    $result=pwg_query($sql);
1486    if($result)
1487    {
1488      while($row=mysql_fetch_assoc($result))
1489      {
1490        $datas['groups'][]=Array(
1491          'id' => $row['groupId'],
1492          'name' => htmlentities($row['name'], ENT_QUOTES, "UTF-8")
1493        );
1494      }
1495    }
1496
1497    $template->assign('datas', $datas);
1498    return($template->parse('group_list', true));
1499  }
1500
1501
1502  /**
1503   * delete the group
1504   * associated tag returns in the available tag list
1505   *
1506   * @param String $groupId : the groupId to delete
1507   */
1508  private function ajax_amd_groupDelete($groupId)
1509  {
1510    if($groupId!="")
1511    {
1512      $sql="DELETE FROM ".$this->tables['groups']."
1513            WHERE groupId = $groupId;";
1514      pwg_query($sql);
1515
1516      $sql="DELETE FROM ".$this->tables['groups_names']."
1517            WHERE groupId = $groupId;";
1518      pwg_query($sql);
1519
1520      $sql="UPDATE ".$this->tables['selected_tags']."
1521            SET groupId = -1
1522            WHERE groupId = $groupId;";
1523      pwg_query($sql);
1524    }
1525  }
1526
1527} // AMD_AIP class
1528
1529
1530?>
Note: See TracBrowser for help on using the repository browser.