1<?php
2/**
3 * DokuWiki Plugin wikistats (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Chris4x4 <4x4.chris@gmail.com>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) die();
11
12if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
13require_once(DOKU_PLUGIN.'syntax.php');
14
15/**
16 * All DokuWiki plugins to extend the parser/rendering mechanism
17 * need to inherit from this class
18 */
19class syntax_plugin_wikistats extends DokuWiki_Syntax_Plugin {
20   /**
21    * Get an associative array with plugin info.
22    *
23    * <p>
24    * The returned array holds the following fields:
25    * <dl>
26    * <dt>author</dt><dd>Author of the plugin</dd>
27    * <dt>email</dt><dd>Email address to contact the author</dd>
28    * <dt>date</dt><dd>Last modified date of the plugin in
29    * <tt>YYYY-MM-DD</tt> format</dd>
30    * <dt>name</dt><dd>Name of the plugin</dd>
31    * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd>
32    * <dt>url</dt><dd>Website with more information on the plugin
33    * (eg. syntax description)</dd>
34    * </dl>
35    * @param none
36    * @return Array Information about this plugin class.
37    * @public
38    * @static
39    */
40    function getInfo(){
41        return confToHash(dirname(__FILE__).'/plugin.info.txt');
42    }
43
44   /**
45    * Get the type of syntax this plugin defines.
46    *
47    * The type of this plugin is "substition".
48    *
49    * @param none
50    * @return String <tt>'substition'</tt>.
51    * @public
52    * @static
53    */
54    public function getType() {
55        return 'substition';
56    }
57
58   /**
59    * Define how this plugin is handled regarding paragraphs.
60    *
61    * <p>
62    * This method is important for correct XHTML nesting. It returns
63    * one of the following values:
64    * </p>
65    * <dl>
66    * <dt>normal</dt><dd>The plugin can be used inside paragraphs.</dd>
67    * <dt>block</dt><dd>Open paragraphs need to be closed before
68    * plugin output.</dd>
69    * <dt>stack</dt><dd>Special case: Plugin wraps other paragraphs.</dd>
70    * </dl>
71    * @param none
72    * @return String <tt>'normal'</tt>.
73    * @public
74    * @static
75    */
76    public function getPType() {
77        return 'normal';
78    }
79
80   /**
81    * Where to sort in?
82    *
83    * Sort the plugin in just behind the formating tokens
84    * Low numbers go before high numbers
85    *
86    * @param none
87    * @return Integer <tt>128</tt>.
88    * @public
89    * @static
90    */
91    public function getSort() {
92        return 128;
93    }
94
95   /**
96    * Connect lookup pattern to lexer.
97    *
98    * @param $aMode String The desired rendermode.
99    * @return none
100    * @public
101    * @see render()
102    */
103    public function connectTo($mode) {
104        $this->Lexer->addSpecialPattern('\{\{wikistats>[^}]*\}\}',$mode,'plugin_wikistats');
105    }
106
107    /**
108     * Handle matches of the wikistats syntax
109     *
110     * @param string $match The match of the syntax
111     * @param int    $state The state of the handler
112     * @param int    $pos The position in the document
113     * @param Doku_Handler    $handler The handler
114     * @return array Data for the renderer
115     */
116    public function handle($match, $state, $pos, Doku_Handler &$handler){
117        $match = substr($match, 12, -2);
118
119        $data = array(
120            'ns' => array(),
121            'type' => array()
122        );
123
124        $match = explode('&', $match);
125        foreach($match as $m) {
126            if (preg_match('/(\w+)\s*=(.+)/', $m, $temp) == 1){
127                $this->handleNamedParameter($temp[1], trim($temp[2]), $data);
128            } else {
129                $this->addNamespace($data, trim($m));
130            }
131        }
132
133        return $data;
134    }
135
136    /**
137     * Handle parameters that are specified using <name>=<value> syntax
138     */
139    function handleNamedParameter($name, $value, &$data) {
140        static $types = array('pages', 'medias', 'stats');
141
142        switch($name) {
143            case 'ns':
144                foreach(preg_split('/\s*,\s*/', $value) as $value) {
145                    $this->addNamespace($data, $value);
146                }
147                break;
148            case 'type':
149                if (preg_match('/(\w+)/', $value, $match) == 1) {
150                    if (in_array($match[1], $types)) {
151                        $data[$name] = $match[1];
152                    }
153                }
154                break;
155        }
156    }
157
158    /**
159     * Clean-up the namespace name and add it (if valid) into the $data array
160     */
161    function addNamespace(&$data, $namespace) {
162        $action = ($namespace{0} == '-') ? 'exclude' : 'include';
163        $namespace = cleanID(preg_replace('/^[+-]/', '', $namespace));
164        if (!empty($namespace)) {
165            $data['ns'][$action][] = $namespace;
166        }
167    }
168
169    /**
170     * Render xhtml output or metadata
171     *
172     * @param string         $mode      Renderer mode (supported modes: xhtml)
173     * @param Doku_Renderer  $renderer  The renderer
174     * @param array          $data      The data from the handler() function
175     * @return bool If rendering was successful.
176     */
177    public function render($mode, Doku_Renderer &$renderer, $data) {
178	    global $conf;
179        global $ID;
180        global $TOC;
181
182        if ($mode != 'xhtml') return false;
183
184        // prevent caching to ensure the included pages are always fresh
185        $renderer->info['cache'] = false;
186
187        $list = array();
188        $content = '';
189
190        switch ($data['type']) {
191            case "pages":
192                search($list, $conf['datadir'], array($this, '_search_count'), array('type' => $data['type'], 'ns' => $data['ns']), '');
193                $content = $list['pages_count'];
194                break;
195            case "medias":
196                search($list, $conf['mediadir'], array($this, '_search_count'), array('type' => $data['type'], 'ns' => $data['ns']));
197                $content = $list['medias_count'];
198                break;
199            case "stats":
200                search($list, $conf['datadir'], array($this, '_search_count'), array('type' => $data['type'], 'ns' => $data['ns']), '');
201                search($list, $conf['mediadir'], array($this, '_search_count'), array('type' => $data['type'], 'ns' => $data['ns']));
202
203                if ($this->getConf('display_toc')) {
204                    $TOC = p_get_metadata($ID,'description tableofcontents');
205                } else {
206                    $TOC = NULL;
207                }
208                $content .= $this->displayResourceStats($list);
209                $content .= $this->displayTagStats($list);
210                $content .= $this->displayNamespaceStats($list['dir_label']['ns']);
211
212                break;
213        }
214        $renderer->doc .= $content;
215
216        return true;
217    }
218
219    function _search_count(&$data, $base, $file, $type, $lvl, $opts){
220        if ($type == 'd') {
221            if ($data['dir_nest'] < $lvl) $data['dir_nest'] = $lvl;
222
223            $data['dir_count']++;
224
225            return true;
226        }
227
228        $file = str_replace("/", ":", $file);
229
230        // filter included namespaces
231        if (isset($opts['ns']['include'])) {
232            if (!$this->isInNamespace($opts['ns']['include'], $file)) return false;
233        }
234
235        // filter excluded namespaces
236        if (isset($opts['ns']['exclude'])) {
237            if ($this->isInNamespace($opts['ns']['exclude'], $file)) return false;
238        }
239
240        switch ($opts['type']) {
241            case "pages":
242                $data['pages_count']++;
243                break;
244            case "medias":
245                $data['medias_count']++;
246                break;
247            case "stats":
248                $path = $file;
249                $file = substr($file, 1);
250                $file = substr($file, 0, strrpos($file, ':'));
251                //if (pathinfo($path, PATHINFO_EXTENSION) != 'txt') $file = substr($file, 0, strrpos($file, ':'));
252
253                switch (pathinfo($path, PATHINFO_EXTENSION)) {
254                    case 'txt':
255                        $data['pages_count']++;
256                        $data['dir_label']['ns'][$file]['pages']++;
257                        break;
258                    case 'swf':
259                        $data['flash_count']++;
260                        $data['dir_label']['ns'][$file]['medias']++;
261                        break;
262                    case 'jpg':
263                    case 'jpeg':
264                    case 'png':
265                    case 'gif':
266                    case 'svg':
267                        $data['images_count']++;
268                        $data['dir_label']['ns'][$file]['medias']++;
269                        break;
270                    case 'mov':
271                    case 'avi':
272                    case 'flv':
273                    case 'mp4':
274                    case 'webm':
275                    case 'ogv':
276                        $data['movies_count']++;
277                        $data['dir_label']['ns'][$file]['medias']++;
278                        break;
279                    case 'wav':
280                    case 'mp3':
281                    case 'ogg':
282                        $data['audio_count']++;
283                        $data['dir_label']['ns'][$file]['medias']++;
284                        break;
285                    case 'xls':
286                    case 'xlsm':
287                    case 'xlsx':
288                    case 'ods':
289                        $data['sheets_count']++;
290                        $data['dir_label']['ns'][$file]['medias']++;
291                        break;
292                    case 'doc':
293                    case 'docx':
294                    case 'odt':
295                        $data['writer_count']++;
296                        $data['dir_label']['ns'][$file]['medias']++;
297                        break;
298                    case 'ppt':
299                    case 'pptx':
300                    case 'odp':
301                        $data['presentation_count']++;
302                        $data['dir_label']['ns'][$file]['medias']++;
303                        break;
304                    case 'djvu':
305                    case 'epub':
306                    case 'mobi':
307                    case 'pdf':
308                        $data['documents_count']++;
309                        $data['dir_label']['ns'][$file]['medias']++;
310                        break;
311                    case 'tar':
312                    case 'arj':
313                    case 'zip':
314                    case 'bzip':
315                    case 'rar':
316                    case 'tgz':
317                    case 'gz':
318                    case '7z':
319                    case 'bz2':
320                        $data['archives_count']++;
321                        $data['dir_label']['ns'][$file]['medias']++;
322                        break;
323                    case 'exe':
324                    case 'com':
325                        $data['binaries_count']++;
326                        $data['dir_label']['ns'][$file]['medias']++;
327                        break;
328                    default:
329                        //var_export($file);
330                        $data['others_count']++;
331                        $data['dir_label']['ns'][$file]['medias']++;
332                        break;
333                }
334                break;
335        }
336
337        return false;
338    }
339
340    /**
341     * Check if page belongs to one of namespaces in the list
342     */
343    function isInNamespace($namespaces, $id) {
344        foreach($namespaces as $ns) {
345            if ((strpos($id, ':' . $ns . ':') === 0)) {
346                return true;
347            }
348        }
349        return false;
350    }
351
352    /**
353     * Display pages and medias stats
354     */
355    private function displayResourceStats($list) {
356        global $TOC;
357
358        $content = '';
359
360        if ($this->getConf('display_ressources_stats')) {
361            if ($this->getConf('display_toc')) {
362                $TOC[] = Array('hid' => 'resources', 'title' => $this->getLang('resources_title'), 'type' => 'ul', 'level' => '2');
363            }
364
365            $class = $this->getConf('display_type');
366            $col = "page";
367
368            $content .= '<!-- Resources Stats -->'.DOKU_LF;
369            $content .= '<h2 class="sectionedit2" id="resources">'.$this->getLang('resources_title').'</h2>'.DOKU_LF;
370            $content .= '<div class="level2">'.DOKU_LF;
371            $content .= '<table class="'.$class.'">'.DOKU_LF;
372            $content .= DOKU_TAB.'<tr>'.DOKU_LF;
373            $content .= DOKU_TAB.DOKU_TAB.'<th class="'.$col.'">'.$this->getLang('resources').'</th>'.DOKU_LF;
374            $content .= DOKU_TAB.DOKU_TAB.'<th class="'.$col.'">#</th>'.DOKU_LF;
375            $content .= DOKU_TAB.'</tr>'.DOKU_LF;
376
377            if(empty($list)) {
378                // Skip output
379                $content .= DOKU_TAB.'<tr>'.DOKU_LF;
380                $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'" colspan="2">'.$this->getLang('empty_output').'</td>'.DOKU_LF;
381                $content .= DOKU_TAB.'</tr>'.DOKU_LF;
382            } else {
383                foreach($list as $resname => $count) {
384                    if ($count <= 0) continue; // don't display resources with zero occurrences
385                    if ($resname === "dir_nest") continue;
386                    if ($resname === "dir_count") continue;
387                    if ($resname === "dir_label") continue;
388                    $content .= DOKU_TAB.'<tr>'.DOKU_LF;
389                    $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'">'.$this->getLang($resname).'</td>'.DOKU_LF;
390                    $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'">'.$count.'</td>'.DOKU_LF;
391                    $content .= DOKU_TAB.'</tr>'.DOKU_LF;
392                }
393            }
394
395            $content .= '</table>'.DOKU_LF;
396            $content .= '</div>'.DOKU_LF.DOKU_LF;
397        }
398        return $content;
399    }
400
401    /**
402     * Display tags related stats
403     */
404    private function displayTagStats($list) {
405        global $TOC;
406
407        $content = '';
408
409        if ($this->getConf('display_tags_stats')) {
410            if (plugin_isdisabled('tag') || (!$tag = plugin_load('helper', 'tag'))) {
411                msg('The Tag Plugin must be installed to display tag related stats.', -1);
412            } else {
413                if ($this->getConf('display_toc')) {
414                    $TOC[] = Array('hid' => 'tags', 'title' => $this->getLang('tags_title'), 'type' => 'ul', 'level' => '2');
415                }
416
417                $occurrences = $tag->tagOccurrences(NULL, NULL, true, NULL);
418                ksort($occurrences);
419
420                $class = $this->getConf('display_type');
421                $col = "page";
422
423                $content .= '<!-- Tags Stats -->'.DOKU_LF;
424                $content .= '<h2 class="sectionedit2" id="tags">'.$this->getLang('tags_title').'</h2>'.DOKU_LF;
425                $content .= '<div class="level2">'.DOKU_LF;
426                $content .= '<table class="'.$class.'">'.DOKU_LF;
427                $content .= DOKU_TAB.'<tr>'.DOKU_LF;
428                $content .= DOKU_TAB.DOKU_TAB.'<th class="'.$col.'">'.$this->getLang('tags').' ('.count($occurrences).')</th>'.DOKU_LF;
429                $content .= DOKU_TAB.DOKU_TAB.'<th class="'.$col.'">#</th>'.DOKU_LF;
430                $content .= DOKU_TAB.'</tr>'.DOKU_LF;
431
432                if (empty($occurrences)) {
433                    // Skip output
434                    $content .= DOKU_TAB.'<tr>'.DOKU_LF;
435                    $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'" colspan="2">'.$this->getLang('empty_output').'</td>'.DOKU_LF;
436                    $content .= DOKU_TAB.'</tr>'.DOKU_LF;
437                } else {
438                    foreach($occurrences as $tagname => $count) {
439                        if ($count <= 0) continue; // don't display tags with zero occurrences
440                        $content .= DOKU_TAB.'<tr>'.DOKU_LF;
441                        $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'">'.$tagname.'</td>'.DOKU_LF;
442                        $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'">'.$count.'</td>'.DOKU_LF;
443                        $content .= DOKU_TAB.'</tr>'.DOKU_LF;
444                    }
445                }
446
447                $content .= '</table>'.DOKU_LF;
448                $content .= '</div>'.DOKU_LF.DOKU_LF;
449            }
450        }
451        return $content;
452    }
453
454    /**
455     * Display namespaces related stats
456     */
457    private function displayNamespaceStats($list) {
458        global $TOC;
459
460        $content = '';
461
462        if ($this->getConf('display_namespaces_stats')) {
463            if ($this->getConf('display_toc')) {
464                $TOC[] = Array('hid' => 'namespaces', 'title' => $this->getLang('namespaces_title'), 'type' => 'ul', 'level' => '2');
465            }
466
467            $class = $this->getConf('display_type');
468            $col = "page";
469
470            $content .= '<!-- Namespaces Stats -->'.DOKU_LF;
471            $content .= '<h2 class="sectionedit2" id="namespaces">'.$this->getLang('namespaces_title').'</h2>'.DOKU_LF;
472            $content .= '<div class="level2">'.DOKU_LF;
473            $content .= '<table class="'.$class.'">'.DOKU_LF;
474            $content .= DOKU_TAB.'<tr>'.DOKU_LF;
475            $content .= DOKU_TAB.DOKU_TAB.'<th class="'.$col.'">'.$this->getLang('namespaces').' ('.count($list).')</th>'.DOKU_LF;
476            $content .= DOKU_TAB.DOKU_TAB.'<th class="'.$col.'"># '.$this->getLang('pages_count').'</th>'.DOKU_LF;
477            $content .= DOKU_TAB.DOKU_TAB.'<th class="'.$col.'"># '.$this->getLang('medias_count').'</th>'.DOKU_LF;
478            $content .= DOKU_TAB.'</tr>'.DOKU_LF;
479
480            if(empty($list)) {
481                // Skip output
482                $content .= DOKU_TAB.'<tr>'.DOKU_LF;
483                $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'" colspan="3">'.$this->getLang('empty_output').'</td>'.DOKU_LF;
484                $content .= DOKU_TAB.'</tr>'.DOKU_LF;
485            } else {
486                ksort($list);
487
488                foreach($list as $namespace => $arr) {
489                    if ($namespace == '') $namespace = '[root]';
490                    $content .= DOKU_TAB.'<tr>'.DOKU_LF;
491                    $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'">'.$namespace.'</td>'.DOKU_LF;
492                    $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'">'.(isset($arr['pages']) ? $arr['pages'] : '0') .'</td>'.DOKU_LF;
493                    $content .= DOKU_TAB.DOKU_TAB.'<td class="'.$class.'">'.(isset($arr['medias']) ? $arr['medias'] : '0') .'</td>'.DOKU_LF;
494                    $content .= DOKU_TAB.'</tr>'.DOKU_LF;
495                }
496            }
497
498            $content .= '</table>'.DOKU_LF;
499            $content .= '</div>'.DOKU_LF.DOKU_LF;
500        }
501        return $content;
502    }
503
504}
505
506// vim:ts=4:sw=4:et:
507