1<?php
2
3use dokuwiki\Cache\Cache;
4
5/**
6 * DokuWiki Plugin tagfilter (Syntax Component)
7 *
8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author  lisps
10 */
11/*
12 * All DokuWiki plugins to extend the parser/rendering mechanism
13 * need to inherit from this class
14 */
15
16class syntax_plugin_tagfilter_filter extends DokuWiki_Syntax_Plugin
17{
18
19    /** @var int[] counts forms per page for creating an unique form id */
20    protected $formCounter = [];
21
22    protected function incrementFormCounter()
23    {
24        global $ID;
25        if (array_key_exists($ID, $this->formCounter)) {
26            return $this->formCounter[$ID]++;
27        } else {
28            $this->formCounter[$ID] = 1;
29            return 0;
30        }
31    }
32
33    protected function getFormCounter()
34    {
35        global $ID;
36        if (array_key_exists($ID, $this->formCounter)) {
37            return $this->formCounter[$ID];
38        } else {
39            return 0;
40        }
41    }
42
43    /*
44     * What kind of syntax are we?
45     */
46    public function getType()
47    {
48        return 'substition';
49    }
50
51    /*
52     * Where to sort in?
53     */
54    function getSort()
55    {
56        return 155;
57    }
58
59    /*
60     * Paragraph Type
61     */
62    public function getPType()
63    {
64        return 'block';
65    }
66
67    /*
68     * Connect pattern to lexer
69     */
70    public function connectTo($mode)
71    {
72        $this->Lexer->addSpecialPattern("\{\{tagfilter>.*?\}\}", $mode, 'plugin_tagfilter_filter');
73    }
74
75    /*
76     * Handle the matches
77     */
78    public function handle($match, $state, $pos, Doku_Handler $handler)
79    {
80        $match = trim(substr($match, 12, -2));
81
82        return $this->getOpts($match);
83    }
84
85    /**
86     * Parses syntax written by user
87     *
88     * @param string $match The text matched in the pattern
89     * @return array with:<br>
90     *      int 'id' unique number for current form,
91     *      string 'ns' list only pages from this namespace,
92     *      array 'pagelistFlags' all flags set by user in syntax, will be supplied directly to pagelist plugin,
93     *      array 'tagfilterFlags' only tags for the tagfilter plugin @see helper_plugin_tagfilter_syntax::parseFlags()
94     */
95    protected function getOpts($match)
96    {
97        global $ID;
98
99        /** @var helper_plugin_tagfilter_syntax $HtagfilterSyntax */
100        $HtagfilterSyntax = $this->loadHelper('tagfilter_syntax');
101        $opts['id'] = $this->incrementFormCounter();
102
103        list($match, $flags) = array_pad(explode('&', $match, 2), 2, '');
104        $flags = explode('&', $flags);
105
106
107        list($ns, $tag) = array_pad(explode('?', $match), 2, '');
108        if ($tag === '') {
109            $tag = $ns;
110            $ns = '';
111        }
112
113        if (($ns == '*') || ($ns == ':')) {
114            $ns = '';
115        } elseif ($ns == '.') {
116            $ns = getNS($ID);
117        } else {
118            $ns = cleanID($ns);
119        }
120
121        $opts['ns'] = $ns;
122
123        //only flags for tagfilter
124        $opts['tagfilterFlags'] = $HtagfilterSyntax->parseFlags($flags);
125
126        //all flags set by user for pagelist plugin
127        $opts['pagelistFlags'] = array_map('trim', $flags);
128
129        //read and parse tag
130        $tagFilters = [];
131        $selectExpressions = array_map('trim', explode('|', $tag));
132        foreach ($selectExpressions as $key => $parts) {
133            $parts = explode("=", $parts);//split in Label,RegExp,Default value
134
135            $tagFilters['label'][$key] = trim($parts[0]);
136            $tagFilters['tagExpression'][$key] = trim($parts[1] ?? '');
137            $tagFilters['selectedTags'][$key] = isset($parts[2]) ? explode(' ', $parts[2]) : [];
138        }
139
140        $opts['tagFilters'] = $tagFilters;
141
142        return $opts;
143    }
144
145    /**
146     * Create output
147     *
148     * @param string $format output format being rendered
149     * @param Doku_Renderer $renderer the current renderer object
150     * @param array $opt data created by handler()
151     * @return boolean rendered correctly?
152     */
153    public function render($format, Doku_Renderer $renderer, $opt)
154    {
155        global $INFO, $ID, $conf, $INPUT;
156
157        /* @var  helper_plugin_tagfilter_syntax $HtagfilterSyntax */
158        $HtagfilterSyntax = $this->loadHelper('tagfilter_syntax');
159        $flags = $opt['tagfilterFlags'];
160
161        if ($format === 'metadata') return false;
162        if ($format === 'xhtml') {
163            $renderer->nocache();
164
165            $renderer->cdata("\n");
166
167            $depends = [
168                'files' => [
169                    $INFO['filepath'],
170                    DOKU_CONF . 'acl.auth.php',
171                ]
172            ];
173            $depends['files'] = array_merge($depends['files'], getConfigFiles('main'));
174
175            if ($flags['cache']) {
176                $depends['age'] = $flags['cache'];
177            } else if ($flags['cache'] === false) {
178                //build cache dependencies TODO check if this bruteforce method (adds just all pages of namespace as dependency) is proportional
179                $dir = utf8_encodeFN(str_replace(':', '/', $opt['ns']));
180                $data = [];
181                $opts = [
182                    'ns' => $opt['ns'],
183                    'excludeNs' => $flags['excludeNs']
184                ];
185                search($data, $conf['datadir'], [$HtagfilterSyntax, 'search_all_pages'], $opts, $dir); //all pages inside namespace
186                $depends['files'] = array_merge($depends['files'], $data);
187            } else {
188                $depends['purge'] = true;
189            }
190
191            //cache to store tagfilter options, matched pages and prepared data
192            $filterDataCacheKey = 'plugin_tagfilter_' . $ID . '_' . $opt['id'];
193            $filterDataCache = new Cache($filterDataCacheKey, '.tcache');
194            if (!$filterDataCache->useCache($depends)) {
195                $cachedata = $HtagfilterSyntax->getTagPageRelations($opt);
196                $cachedata[] = $HtagfilterSyntax->prepareList($cachedata[1], $flags);
197                $filterDataCache->storeCache(serialize($cachedata));
198            } else {
199                $cachedata = unserialize($filterDataCache->retrieveCache());
200            }
201
202            list($tagFilters, $allPageids, $preparedPages) = $cachedata;
203
204            // cache to store html per user
205            $htmlPerUserCacheKey = 'plugin_tagfilter_' . $ID . '_' . $opt['id'] . '_' . $INPUT->server->str('REMOTE_USER')
206                . $INPUT->server->str('HTTP_HOST') . $INPUT->server->str('SERVER_PORT');
207            $htmlPerUserCache = new Cache($htmlPerUserCacheKey, '.tucache');
208
209            //purge cache if pages does not exist anymore
210            foreach ($allPageids as $key => $pageid) {
211                if (!page_exists($pageid)) {
212                    unset($allPageids[$key]);
213                    $filterDataCache->removeCache();
214                    $htmlPerUserCache->removeCache();
215                }
216            }
217
218            if (empty($flags['include'])) {
219                if (!$htmlPerUserCache->useCache(['files' => [$filterDataCache->cache]])) {
220                    $html = $this->htmlOutput($tagFilters, $allPageids, $preparedPages, $opt);
221                    $htmlPerUserCache->storeCache($html);
222                } else {
223                    $html = $htmlPerUserCache->retrieveCache();
224                }
225
226                $renderer->doc .= $html;
227            } else {
228                // Use include plugin. Does not use the htmlPerUserCache. TODO?
229
230                // attention: htmlPrepareOutput modifies $tagFilters, $allPageids, $preparedPages.
231                $this->htmlPrepareOutput($tagFilters, $allPageids, $preparedPages, $opt);
232                $renderer->doc .= $this->htmlFormOutput($tagFilters, $allPageids, $opt);
233                $renderer->doc .= "<div id='tagfilter_ergebnis_" . $opt['id'] . "' class='tagfilter'>";
234
235                $includeHelper = $this->loadHelper('include');
236                $includeFlags = $includeHelper->get_flags($flags['include']);
237
238                foreach($preparedPages as $page) {
239                    $renderer->nest($includeHelper->_get_instructions($page['id'], '', 'page', 0, $includeFlags));
240                }
241
242                $renderer->doc .= "</div>";
243            }
244        }
245        return true;
246    }
247
248    /**
249     * Returns html of the tagfilter form
250     *
251     * @param array $tagFilters
252     * @param array $allPageids
253     * @param array $preparedPages
254     * @param array $opt option array from the handler
255     * @return string
256     */
257    private function htmlOutput($tagFilters, $allPageids, $preparedPages, array $opt)
258    {
259        // attention: htmlPrepareOutput modifies $tagFilters, $allPageids, $preparedPages.
260        $this->htmlPrepareOutput($tagFilters, $allPageids, $preparedPages, $opt);
261
262        $output = $this->htmlFormOutput($tagFilters, $allPageids, $opt)
263            . $this->htmlPagelistOutput($preparedPages, $opt);
264
265        return $output;
266    }
267
268    private function htmlPrepareOutput(&$tagFilters, &$allPageids, &$preparedPages, array $opt)
269    {
270        /* @var helper_plugin_tagfilter $Htagfilter */
271        $Htagfilter = $this->loadHelper('tagfilter');
272
273        //check for read access
274        foreach ($allPageids as $key => $pageid) {
275            if (!$Htagfilter->canRead($pageid)) {
276                unset($allPageids[$key]);
277            }
278        }
279
280        //check tags for visibility
281        foreach ($tagFilters['pagesPerMatchedTags'] as &$pagesPerMatchedTag) {
282            if (!is_array($pagesPerMatchedTag)) {
283                $pagesPerMatchedTag = [];
284            }
285            foreach ($pagesPerMatchedTag as $tag => $pageidsPerTag) {
286                if (count(array_intersect($pageidsPerTag, $allPageids)) == 0) {
287                    unset($pagesPerMatchedTag[$tag]);
288                }
289            }
290        }
291        unset($pagesPerMatchedTag);
292
293        foreach ($preparedPages as $key => $page) {
294            if (!in_array($page['id'], $allPageids)) {
295                unset($preparedPages[$key]);
296            }
297        }
298    }
299
300    private function htmlFormOutput($tagFilters, $allPageids, array $opt) {
301        /* @var helper_plugin_tagfilter $Htagfilter */
302        $Htagfilter = $this->loadHelper('tagfilter');
303
304        $flags = $opt['tagfilterFlags'];
305        $output = '';
306
307        $form = new Doku_Form([
308            'id' => 'tagdd_' . $opt['id'],
309            'data-idx' => $opt['id'],
310            'data-plugin' => 'tagfilter',
311            'data-tags' => json_encode($tagFilters['pagesPerMatchedTags']),
312        ]);
313        $output .= "\n";
314        //Fieldset manuell hinzufügen da ein style Parameter übergeben werden soll
315        $form->addElement([
316            '_elem' => 'openfieldset',
317            '_legend' => 'Tagfilter',
318            'style' => 'text-align:left;width:99%',
319            'id' => '__tagfilter_' . $opt['id'],
320            'class' => ($flags['labels'] !== false) ? '' : 'hidelabel',
321
322        ]);
323        $form->_infieldset = true; //Fieldset starten
324
325        if ($flags['pagesearch']) {
326            $label = $flags['pagesearchlabel'];
327
328            $pagetitles = [];
329            foreach ($allPageids as $pageid) {
330                $pagetitles[$pageid] = $Htagfilter->getPageTitle($pageid);
331            }
332            asort($pagetitles, SORT_NATURAL | SORT_FLAG_CASE);
333
334            $selectedTags = [];
335            $id = '__tagfilter_page_' . $opt['id'];
336
337            $attrs = [//generelle Optionen für DropDownListe onchange->submit von id namespace und den flags für pagelist
338                'onChange' => 'tagfilter_submit(' . $opt['id'] . ',' . json_encode($opt['ns']) . ',' . json_encode([$opt['pagelistFlags'], $flags]) . ')',
339                'class' => 'tagdd_select tagfilter tagdd_select_' . $opt['id'] . ($flags['chosen'] ? ' chosen' : ''),
340                'data-placeholder' => hsc($label . ' ' . $this->getLang('choose')),
341                'data-label' => hsc(utf8_strtolower(trim($label))),
342            ];
343            if ($flags['multi']) { //unterscheidung ob Multiple oder Single
344                $attrs['multiple'] = 'multiple';
345                $attrs['size'] = $this->getConf("DropDownList_size");
346            } else {
347                $attrs['size'] = 1;
348                $pagetitles = array_reverse($pagetitles, true);
349                $pagetitles[''] = '';
350                $pagetitles = array_reverse($pagetitles, true);
351            }
352            $form->addElement(form_makeListboxField($label, $pagetitles, $selectedTags, $label, $id, 'tagfilter', $attrs));
353        }
354        $output .= '<script type="text/javascript">/*<![CDATA[*/ var tagfilter_container = {}; /*!]]>*/</script>' . "\n";
355        //$output .= '<script type="text/javascript">/*<![CDATA[*/ '.'tagfilter_container.tagfilter_'.$opt['id'].' = '.json_encode($tagFilters['tags2']).'; /*!]]>*/</script>'."\n";
356        foreach ($tagFilters['pagesPerMatchedTags'] as $key => $pagesPerMatchedTag) {
357            $id = false;
358            $label = $tagFilters['label'][$key];
359            $selectedTags = $tagFilters['selectedTags'][$key];
360
361            //get tag labels
362            $tags = [];
363
364            foreach (array_keys($pagesPerMatchedTag) as $tagid) {
365                $tags[$tagid] = $Htagfilter->getTagLabel($tagid);
366            }
367
368            foreach ($selectedTags as &$item) {
369                $item = utf8_strtolower(trim($item));
370            }
371            unset($item);
372
373
374            $attrs = [//generelle Optionen für DropDownListe onchange->submit von id namespace und den flags für pagelist
375                'onChange' => 'tagfilter_submit(' . $opt['id'] . ',' . json_encode($opt['ns']) . ',' . json_encode([$opt['pagelistFlags'], $flags]) . ')',
376                'class' => 'tagdd_select tagfilter tagdd_select_' . $opt['id'] . ($flags['chosen'] ? ' chosen' : ''),
377                'data-placeholder' => hsc($label . ' ' . $this->getLang('choose')),
378                'data-label' => hsc(str_replace(' ', '_', utf8_strtolower(trim($label)))),
379
380            ];
381            if ($flags['multi']) { //unterscheidung ob Multiple oder Single
382                $attrs['multiple'] = 'multiple';
383                $attrs['size'] = $this->getConf("DropDownList_size");
384            } else {
385                $attrs['size'] = 1;
386                $tags = array_reverse($tags, true);
387                $tags[''] = '';
388                $tags = array_reverse($tags, true);
389            }
390
391            if ($flags['chosen']) {
392                $links = [];
393                foreach ($tags as $k => $t) {
394                    $links[$k] = [
395                        'link' => $Htagfilter->getImageLinkByTag($k),
396                    ];
397                }
398                $jsVar = 'tagfilter_jsVar_' . rand();
399                $output .= '<script type="text/javascript">/*<![CDATA[*/ tagfilter_container.' . $jsVar . ' ='
400                    . json_encode($links) .
401                    '; /*!]]>*/</script>' . "\n";
402
403                $id = '__tagfilter_' . $opt["id"] . '_' . rand();
404
405                if ($flags['tagimage']) {
406                    $attrs['data-tagimage'] = $jsVar;
407                }
408
409            }
410            $form->addElement(form_makeListboxField($label, $tags, $selectedTags, $label, $id, 'tagfilter', $attrs));
411        }
412
413        $form->addElement(form_makeButton('button', '', $this->getLang('Delete filter'), ['onclick' => 'tagfilter_cleanform(' . $opt['id'] . ',true)']));
414        if ($flags['count']) {
415            $form->addElement('<div class="tagfilter_count">' . $this->getLang('found_count') . ': ' . '<span class="tagfilter_count_number"></span></div>');
416        }
417        $form->endFieldset();
418        $output .= $form->getForm();//Form Ausgeben
419
420        return $output;
421    }
422
423    private function htmlPagelistOutput($preparedPages, array $opt) {
424        /* @var  helper_plugin_tagfilter_syntax $HtagfilterSyntax */
425        $HtagfilterSyntax = $this->loadHelper('tagfilter_syntax');
426
427        $output = '';
428
429        $output .= "<div id='tagfilter_ergebnis_" . $opt['id'] . "' class='tagfilter'>";
430        //dbg($opt['pagelistFlags']);
431        $output .= $HtagfilterSyntax->renderList($preparedPages, $opt['tagfilterFlags'], $opt['pagelistFlags']);
432        $output .= "</div>";
433
434        return $output;
435    }
436}
437