source: extensions/EStat/estat_root.class.inc.php @ 17758

Last change on this file since 17758 was 17758, checked in by grum, 8 years ago

version 0.1.0b

. Fix install bugs
. Manage anonymous directories
. Manage CSV export options settings
. Fix IPadress<=>country association bug & improve join performances
. Fix bug on IP filter
. Improve performances for history consult

  • Property svn:executable set to *
File size: 20.8 KB
Line 
1<?php
2/* -----------------------------------------------------------------------------
3  Plugin     : EStat
4  Author     : Grum
5    email    : grum@piwigo.org
6    website  : http://www.grum.fr
7
8    << May the Little SpaceFrog be with you ! >>
9  ------------------------------------------------------------------------------
10  See main.inc.php for release information
11
12  EStat_root : common classe for admin and public classes
13
14  --------------------------------------------------------------------------- */
15  include_once(PHPWG_PLUGINS_PATH.'GrumPluginClasses/classes/CommonPlugin.class.inc.php');
16  include_once(ESTAT_LIB.'statDB.class.inc.php');
17  include_once(ESTAT_LIB.'statDBGlobal.class.inc.php');
18  include_once(ESTAT_LIB.'statDBMonth.class.inc.php');
19  include_once(ESTAT_LIB.'statDBCountry.class.inc.php');
20
21  class EStat_root extends CommonPlugin
22  {
23    const DATA_DIRECTORY='plugins/EStat/data/';
24    const EXPORT_DIRECTORY='plugins/EStat/export/';
25    const FILE_MONTH='ES-M';
26    const FILE_GLOBAL='ES-G';
27    const FILE_COUNTRY='ES-IPCountry';
28
29    public $countryCodes=array(
30              'AD','AE','AF','AG','AI','AL','AM','AO','AQ','AR','AS','AT','AU','AW','AX','AZ',
31              'BA','BB','BD','BE','BF','BG','BH','BI','BJ','BL','BM','BN','BO','BQ','BR','BS','BT','BV','BW','BY','BZ',
32              'CA','CC','CD','CF','CG','CH','CI','CK','CL','CM','CN','CO','CR','CU','CV','CW','CX','CY','CZ',
33              'DE','DJ','DK','DM','DO','DZ',
34              'EC','EE','EG','EH','ER','ES','ET',
35              'FI','FJ','FK','FM','FO','FR',
36              'GA','GB','GD','GE','GF','GG','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GW','GY',
37              'HK','HM','HN','HR','HT','HU',
38              'ID','IE','IL','IM','IN','IO','IQ','IR','IS','IT',
39              'JE','JM','JO','JP',
40              'KE','KG','KH','KI','KM','KN','KP','KR','KW','KY','KZ',
41              'LA','LB','LC','LI','LK','LR','LS','LT','LU','LV','LY',
42              'MA','MC','MD','ME','MF','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MX','MY','MZ',
43              'NA','NC','NE','NF','NG','NI','NL','NO','NP','NR','NU','NZ',
44              'OM',
45              'PA','PE','PF','PG','PH','PK','PL','PM','PN','PR','PS','PT','PW','PY',
46              'QA',
47              'RE','RO','RS','RU','RW',
48              'SA','SB','SC','SD','SE','SG','SH','SI','SJ','SK','SL','SM','SN','SO','SR','SS','ST','SV','SX','SY','SZ',
49              'TC','TD','TF','TG','TH','TJ','TK','TL','TM','TN','TO','TR','TT','TV','TW','TZ',
50              'UA','UG','UM','US','UY','UZ',
51              'VA','VC','VE','VG','VI','VN','VU',
52              'WF','WS',
53              'XA',
54              'YE','YT',
55              'ZA','ZM','ZW'
56            );
57
58    protected $fileStatDir='';
59    protected $fileExportDir='';
60
61    public function __construct($prefixeTable, $filelocation)
62    {
63      $this->setPluginName('EStat');
64      $this->setPluginNameFiles("estat");
65      parent::__construct($prefixeTable, $filelocation);
66      $this->loadConfig();
67      $this->section_name=$this->getPluginNameFiles();
68      $this->setFileStatDir();
69    }
70
71    /**
72     * initialize default values
73     */
74    public function initConfig()
75    {
76      global $user;
77
78      $lang='';
79      if(isset($user['language'])) $lang=$user['language'];
80
81      switch($lang)
82      {
83        case 'fr_FR':
84          $csvExport=array(
85            'export.csv.separator' => ';',
86            'export.csv.decimalDot' => ','
87          );
88          break;
89        default:
90          $csvExport=array(
91            'export.csv.separator' => ',',
92            'export.csv.decimalDot' => '.'
93          );
94          break;
95      }
96
97      $this->config=array(
98        'installed' => '00.00.00',
99        'plugin.newInstall' => 'y',
100        'plugin.active' => 'n',
101        'plugin.historyImported' => 'n',
102        'plugin.ipCountryBuild' => 'n',
103        'plugin.pathKey' => '',          // key to be used to know path where are stored the SQLite files
104
105        'build.delay' => '+24hours',     // delay between 2 consolidation
106        'build.start' => '2:30:00',      // consolidation can be done from
107        'build.end' => '6:30:00',        // build.start hour to build.end hour
108        'compress.public' => 'n',        // compress file in public session
109        'compress.method' => 'none',     // none, gz
110
111        'global.itemPerPage' => 250,
112
113        'export.csv.separator' => $csvExport['export.csv.separator'],
114        'export.csv.decimalDot' => $csvExport['export.csv.decimalDot'],
115        'export.csv.useQuotes' => 'y',
116        'export.csv.lineFeed' => 'unix',
117        'export.ods.fileTitle' => '',
118        'export.ods.keywords' => '',
119
120        'logs.reverseDNS' => 'n',
121        'logs.ipCountry' => 'y'
122      );
123    }
124
125    /**
126     * return admin link
127     * @param String $mode: if equal 'ajax' return url for ajax call
128     * @return String: url
129     */
130    public function getAdminLink($mode='')
131    {
132      switch($mode)
133      {
134        case 'ajax':
135          return('plugins/'.basename(dirname($this->getFileLocation())).'/estat_ajax.php');
136          break;
137        case 'ajaxExport':
138          return('plugins/'.basename(dirname($this->getFileLocation())).'/estat_ajax_export.php');
139          break;
140        default:
141          return(parent::getAdminLink());
142          break;
143      }
144   }
145
146
147
148    /**
149    * checks files that need to be consolidated
150    * pack them if needed
151    *
152    * @param Boolean $force: force consolidation if true, otherwise let system to determinate if needed
153    * @param Boolean $pack: pack files if needed
154    * @return Boolean: true if at least one consolidation was applied, otherwise false
155    */
156    protected function checkBuildStatPeriod($force=false, $pack=true)
157    {
158      $build=false;
159      $currentTime=date('H:i:s');
160      $currentDateTime=strtotime($this->config['build.delay']);
161      $currentYearMonth=date('Ym');
162
163
164      $dbStatG=new StatDBGlobal($this->fileStatDir, self::FILE_GLOBAL);
165      $dbStatG->open(ASDF_OPEN_WRITE);
166
167      // check if current period exist in global file
168      $dbStatG->checkFilePeriod(substr($currentYearMonth, 0,4), substr($currentYearMonth,4,2));
169
170      // get list of files to build
171      $fileList=$dbStatG->getFilesList(ASDF_EXIST_PACKED|ASDF_EXIST_UNPACKED, ASDF_BUILD_MISSING);
172
173      foreach($fileList as $file)
174      {
175        $file['month']=sprintf('%02d', $file['month']);
176
177        if($force or
178          ($currentDateTime>=$file['lastBuilt'] and
179            $currentTime>=$this->config['build.start'] and
180            $currentTime<=$this->config['build.end']))
181        {
182          if($dbStatG->buildStatPeriod($this->fileStatDir, self::FILE_MONTH, $file['year'], $file['month']))
183            $build=true;
184
185          if($pack and  //pack only if asked
186             $file['packed']==0 and //packed size=0 -> not packed
187             $file['year'].$file['month']!=$currentYearMonth) //don't pack current file!
188          {
189            $dbStatM=new StatDBMonth($this->fileStatDir, self::FILE_MONTH, $file['year'], $file['month']);
190            if($dbStatM->pack())
191            {
192              $packedFileName=$dbStatM->getFileName(ASDF_EXIST_PACKED, true);
193              if($packedFileName!='')
194                $dbStatG->updatePackedSize($file['year'], $file['month'], filesize($packedFileName));
195              $dbStatM->delete(ASDF_DELETE_UNPACKED);
196            }
197          }
198        }
199      }
200
201      $dbStatG->close();
202
203      return($build);
204    }
205
206
207    /**
208     * Returns the needed informations to process the data migration
209     *
210     * Returned information is an array:
211     * array(
212     *  'nbLogs'  => number of logs
213     *  'idMax'   => maximum id in HISTORY table
214     *  'idMin'   => minimum id in HISTORY table
215     *  'dateMax' => maximum date in HISTORY table
216     *  'dateMin' => minimum date in HISTORY table
217     *  'periods' => array of periods to process
218     *                 key=period (yyyy-mm)
219     *                 value=number of events for the period
220     *
221     * @return Array
222     */
223    protected function getPiwigoHistoryInfo()
224    {
225      $data=array(
226        'nbLogs' => 0,
227        'idMax' => 0,
228        'idMin' => 0,
229        'dateMax' => '',
230        'dateMin' => '',
231        'periods' => array()
232        );
233
234      $sql="SELECT COUNT(id), MIN(id), MAX(id), MIN(`date`), MAX(`date`)
235            FROM ".HISTORY_TABLE;
236      $result=pwg_query($sql);
237      if($result)
238      {
239        if($tmp=pwg_db_fetch_row($result))
240        {
241          $data['nbLogs']=$tmp[0];
242          $data['idMin']=$tmp[1];
243          $data['idMax']=$tmp[2];
244          $data['dateMin']=$tmp[3];
245          $data['dateMax']=$tmp[4];
246
247          $sql="SELECT COUNT(id) AS nbEvents, YEAR(`date`) AS periodYear, MONTH(`date`) AS periodMonth
248                FROM ".HISTORY_TABLE."
249                GROUP BY periodYear, periodMonth
250                ORDER BY periodYear, periodMonth;";
251
252          $result=pwg_query($sql);
253          if($result)
254          {
255            while($row=pwg_db_fetch_assoc($result))
256            {
257              $data['periods'][sprintf('%04d-%02d', $row['periodYear'], $row['periodMonth'])]=$row['nbEvents'];
258            }
259          }
260        }
261      }
262      return($data);
263    }
264
265
266
267
268
269    /**
270     * set the value for $fileStatDir & $fileExportDir
271     * $rootPath is the root path where are stored the SQLite data files
272     *  example: (...)/local/plugins/EStat/Data/
273     *
274     * This path is known is public and can be found by everyone. To forbid the
275     * download of files stored in this directory, a random key is added in the
276     * path
277     *  example: (...)/local/plugins/EStat/Data-ABCDEF0123456789/
278     *
279     * The random key is stored in config file:
280     *  . if key is not set, try to find the directory
281     *  . if key is set but directory doesn't exist, try to find the directory
282     *  . if there's no key and no directory found, the key is created and saved
283     *
284     * the $fileStatDir provide the complete path with the key
285     * the $fileExportDir path gets the same key than $fileStatDir
286     *
287     * @return String: $fileStatDir value
288     */
289    protected function setFileStatDir()
290    {
291      $rootPath=GPCCore::getPiwigoSystemPath().'/'.PWG_LOCAL_DIR.self::DATA_DIRECTORY;
292
293      $path=substr($rootPath,0,-1).'-'.$this->config['plugin.pathKey'];
294      if(file_exists($path) and $this->config['plugin.pathKey']!='')
295      {
296        $this->fileStatDir=$path;
297        $this->fileExportDir=GPCCore::getPiwigoSystemPath().'/'.PWG_LOCAL_DIR.substr(self::EXPORT_DIRECTORY,0,-1).'-'.$this->config['plugin.pathKey'].'/';
298        return($this->fileStatDir);
299      }
300
301      // key is empty or directory doesn't exist, try to find the diretory
302      $path=$this->findDirectory(substr($rootPath,0,-1).'-');
303      if(file_exists($path))
304      {
305        $this->fileStatDir=$path;
306        $path=explode('-', basename($path)); // /xxx/xxx/xxx/data-abcdef012 => array('data', 'abcdef012')
307        $this->config['plugin.pathKey']=$path[1];
308        $this->saveConfig();
309        $this->fileExportDir=GPCCore::getPiwigoSystemPath().'/'.PWG_LOCAL_DIR.substr(self::EXPORT_DIRECTORY,0,-1).'-'.$this->config['plugin.pathKey'].'/';
310        return($this->fileStatDir);
311      }
312
313      // no directory found, create the key
314      $this->config['plugin.pathKey']=strtoupper(md5(date('Ymd-His').rand(0,65536)));
315      $this->saveConfig();
316      $this->fileStatDir=substr($rootPath,0,-1).'-'.$this->config['plugin.pathKey'];
317      $this->fileExportDir=GPCCore::getPiwigoSystemPath().'/'.PWG_LOCAL_DIR.substr(self::EXPORT_DIRECTORY,0,-1).'-'.$this->config['plugin.pathKey'].'/';
318      return($this->fileStatDir);
319    }
320
321    /**
322     * try to find a directory in the $path
323     *  exemple:
324     *     $path = /local/plugins/EStat/Data-
325     *     => try to find all directory macthing with /local/plugins/EStat/Data-*
326     *
327     * @param String $path:
328     * @return String: found path or empty string if nothing is found
329     */
330    private function findDirectory($path)
331    {
332      $dirname=dirname($path);
333      if(!file_exists($dirname)) return('');
334      $baseName=baseName($path);
335      $dirContent=scandir($dirname);
336      foreach($dirContent as $file)
337      {
338        if($file!='.' and $file!='..' and is_dir($dirname.'/'.$file) and strpos($file, $baseName)!==false)
339        {
340          return($dirname.'/'.$file);
341        }
342      }
343      return('');
344    }
345
346
347
348
349    /*
350     * -------------------------------------------------------------------------
351     * -- functions used by the ajax classes
352     * -------------------------------------------------------------------------
353     */
354
355
356
357    /**
358     * generic function to consolidate a list of Id in a list with distinct Id
359     *
360     * the &$table parameter is an array with predefined keys     *
361     *
362     * @param Array &$table : the table were to put result
363     * @param String $key : the key (of the table) where result have to be put
364     * @param String $sql : SQL request to produce the id list
365     */
366    protected function prepareIdList(&$table, $key, $sql)
367    {
368      $result=pwg_query($sql);
369      if($result)
370      {
371        while($row=pwg_db_fetch_assoc($result))
372        {
373          $table[$key][$row['id']]=$row;
374        }
375      }
376    }
377
378    /**
379     * generic function to prepare a list of unique IP adress with the following
380     * informations:
381     *  - reverse DNS (if asked)
382     *  - country
383     *  returned keys are IP adress, associated value is an array
384     *    ip => array('rdns' => reverseDNS, 'country' => country_code)
385     *
386     * @param Array &$table : the table were to put result
387     * @param Array $items : array of IP adress
388     * @param Boolean $force : if true, force to produce the reversed DNS address
389     */
390    protected function getIpInfos(&$table, $items, $force=false)
391    {
392      $dbCountry=new StatDBCountry($this->fileStatDir, self::FILE_COUNTRY);
393      $dbCountry->open(ASDF_OPEN_READ);
394
395      foreach($items as $IP)
396      {
397        $table['IPadress'][$IP]=array(
398          'rdns' => '',
399          'country' => $dbCountry->getIpCountry($IP, false)
400        );
401
402        if($force or $this->config['logs.reverseDNS']=='y')
403          $table['IPadress'][$IP]=gethostbyaddr($IP);
404      }
405      $dbCountry->close();
406    }
407
408
409
410    /**
411     * return the label associated to the id, or a default value if nothing is
412     * found
413     *
414     * @param Array $table: array of label
415     * @param string $key: type of id
416     * @param string $id: id for which the label have to be retrieved
417     * @param string $default: default value to return if no label found
418     * @return string: label
419     */
420    protected function getId($table, $key, $id, $default='', $field='name')
421    {
422      if(is_array($id))
423      {
424        $tmp=array();
425        foreach($id as $val)
426        {
427          $tmp[]=$this->getId($table, $key, $val, $default, $field);
428        }
429        return(implode(',', $tmp));
430      }
431      elseif(isset($table[$key]) and isset($table[$key][$id]) and isset($table[$key][$id][$field]))
432      {
433        return($table[$key][$id][$field]);
434      }
435      else return($default);
436    }
437
438
439    /**
440     * build a filter of category id; for each given id, the childrens are selected
441     * too
442     *
443     * @param Array $catId: category id list
444     * @return Array
445     */
446    protected function buildCatIdFilter($catId)
447    {
448      $returned=array();
449
450      if(!is_array($catId)) return($returned);
451
452      $where=array();
453      foreach($catId as $id)
454      {
455        $where[]="FIND_IN_SET('$id', uppercats)";
456      }
457      $sql="SELECT DISTINCT id
458            FROM ".CATEGORIES_TABLE."
459            WHERE ".implode(' OR ', $where);
460      $tmpResult=pwg_query($sql);
461      if($tmpResult)
462      {
463        $tmp=array();
464        while($row=pwg_db_fetch_row($tmpResult))
465        {
466          $tmp[]=$row[0];
467        }
468        $returned=array('operator' => 'in', 'value' => $tmp);
469      }
470      return($returned);
471    }
472
473  } //class
474
475
476
477
478/*
479 * ------------------------------------------------------------------------------------------------
480 * -- classes used for ajax                                                                      --
481 * ------------------------------------------------------------------------------------------------
482 */
483
484
485  /**
486   * this class is used to build list of unique Id
487   */
488  class EStat_IdList
489  {
490    protected $listItems=array();
491
492    public function __construct($items)
493    {
494      if(is_array($items)) $this->init($items);
495    }
496
497    public function __destruct()
498    {
499    }
500
501    /**
502     * initialize the names of identifiers pool
503     * for example, init(array('album', 'image')) to declare a pool for 'album' id
504     * and another pool for 'image' id
505     *
506     * @param Array $items: array of names
507     */
508    public function init($items)
509    {
510      $this->listItems=array();
511      foreach($items as $item)
512      {
513        $this->listItems[$item]=array();
514      }
515    }
516
517    /**
518     * add items in the pool
519     *
520     * $items is an array of (key => value)
521     * for example:
522     *  addItems(
523     *    'album' => 1,
524     *    'album' => 2,
525     *    'album' => 1,
526     *    'image' => 236
527     *  );
528     *
529     * will be (internaly) stored as
530     *   listItems(
531     *     'album' => array(1,2),
532     *     'image' => array(236)
533     *   );
534     *
535     * @param Array $items
536     */
537    public function addItems($items)
538    {
539      if(is_array($items))
540      {
541        foreach($items as $key => $item)
542        {
543          $this->add($key, $item);
544        }
545      }
546    }
547
548    /**
549     * return the associated list of item for an identifier
550     * for example:
551     *   getItems('album')
552     * will return:
553     *   array(1,2)
554     *
555     * @param Array $item: identifier
556     * @return Array
557     */
558    public function getItems($item)
559    {
560      if(isset($this->listItems[$item]))
561      {
562        return(array_keys($this->listItems[$item]));
563      }
564      else
565      {
566        return(array());
567      }
568    }
569
570    /**
571     * add an item for an identifier
572     *
573     * @param string $key: identifier
574     * @param string $item: value to add
575     */
576    private function add($key, $item)
577    {
578      if(is_array($item))
579      {
580        foreach($item as $val)
581        {
582          $this->add($key, $val);
583        }
584        return(true);
585      }
586
587      if($item!='' and isset($this->listItems[$key]) and !isset($this->listItems[$key][$item]))
588      {
589        $this->listItems[$key][$item]='';
590      }
591    }
592  } // class EStat_IdList
593
594
595
596
597
598
599
600
601  class ReverseIP
602  {
603    protected $ipList=array();
604    protected $ipFile='';
605
606    /**
607     * add an IP adress in the list, by default there's no browser type set
608     * for the IP adress
609     * @param String $IP: IP adress
610     * @param Integer $type: if not set, value is defined automatically
611     */
612    public function addIP($IP, $type=null)
613    {
614      if(!isset($this->ipList[$IP]))
615        $this->ipList[$IP]=UA_BROWSER_TYPE_UNKNOWN; //$this->fromReverseDNS(gethostbyaddr($IP));
616    }
617
618    /**
619     * return IP browser type
620     * @param String $IP: IP adress
621     * @return Integer: -1 if IP adress not exists
622     */
623    public function getIP($IP)
624    {
625      if(!isset($this->ipList[$IP])) return(-1);
626      return($this->ipList[$IP]);
627    }
628
629    /**
630     * set IP type
631     *
632     * @param String $IP: IP adress
633     * @param Integer $type: type of browser
634     * @return Integer: type set, -1 if an error occurs
635     */
636    public function setIP($IP, $type)
637    {
638      if(!isset($this->ipList[$IP])) return(-1);
639      if($this->ipList[$IP]==-1) $this->ipList[$IP]=$type;
640      return($this->ipList[$IP]);
641    }
642
643    /**
644     * delete the temporary ip file
645     */
646    public function deleteIPFile()
647    {
648      if(file_exists($this->ipFile))
649        unlink($this->ipFile);
650    }
651
652    /**
653     * load an IP file in memory
654     *
655     * @param String $file: file to load
656     * @return Boolean: true or false
657     */
658    public function loadIPFile($file)
659    {
660      $this->ipFile=$file;
661      if(!file_exists($file)) return(false);
662      $this->ipList=array();
663
664      $fHandle=fopen($this->ipFile, 'r');
665      if($fHandle)
666      {
667        $tmp=fread($fHandle, filesize($this->ipFile));
668        fclose($fHandle);
669        $this->ipList=unserialize($tmp);
670        return(true);
671      }
672      return(false);
673    }
674
675    /**
676     * save IP list on a file
677     *
678     * @param String $file: file to load
679     * @return Boolean: true or false
680     */
681    public function saveIPFile()
682    {
683      if($this->ipFile=='') return(false);
684      $fHandle=fopen($this->ipFile, 'w');
685      if($fHandle)
686      {
687        fwrite($fHandle, serialize($this->ipList));
688        fclose($fHandle);
689        return(true);
690      }
691      return(false);
692    }
693
694    /**
695     * try to know from a reversed DNS adress if visitor is a crawler or not
696     *
697     * @param String $revDNS: a reversed DNS IP address
698     * @return Integer: UA_BROWSER_TYPE_UNKNOWN or UA_BROWSER_TYPE_CRAWLER
699     */
700    public function fromReverseDNS($revDNS)
701    {
702      $returned=UA_BROWSER_TYPE_UNKNOWN;
703
704      if(preg_match('/\s*([a-z]*(?:bot|spyder|crawl|crawler|spider)[a-z]*)/i', $revDNS))
705        $returned=UA_BROWSER_TYPE_CRAWLER;
706
707      return($returned);
708    }
709  } // class ReverseIP
710
711
712
713?>
Note: See TracBrowser for help on using the repository browser.