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
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        }
228        return true;
229    }
230
231    /**
232     * Returns html of the tagfilter form
233     *
234     * @param array $tagFilters
235     * @param array $allPageids
236     * @param array $preparedPages
237     * @param array $opt option array from the handler
238     * @return string
239     */
240    private function htmlOutput($tagFilters, $allPageids, $preparedPages, array $opt)
241    {
242        /* @var helper_plugin_tagfilter $Htagfilter */
243        $Htagfilter = $this->loadHelper('tagfilter');
244        /* @var  helper_plugin_tagfilter_syntax $HtagfilterSyntax */
245        $HtagfilterSyntax = $this->loadHelper('tagfilter_syntax');
246        $flags = $opt['tagfilterFlags'];
247
248        $output = '';
249
250        //check for read access
251        foreach ($allPageids as $key => $pageid) {
252            if (!$Htagfilter->canRead($pageid)) {
253                unset($allPageids[$key]);
254            }
255        }
256
257        //check tags for visibility
258        foreach ($tagFilters['pagesPerMatchedTags'] as &$pagesPerMatchedTag) {
259            if (!is_array($pagesPerMatchedTag)) {
260                $pagesPerMatchedTag = [];
261            }
262            foreach ($pagesPerMatchedTag as $tag => $pageidsPerTag) {
263                if (count(array_intersect($pageidsPerTag, $allPageids)) == 0) {
264                    unset($pagesPerMatchedTag[$tag]);
265                }
266            }
267        }
268        unset($pagesPerMatchedTag);
269
270        foreach ($preparedPages as $key => $page) {
271            if (!in_array($page['id'], $allPageids)) {
272                unset($preparedPages[$key]);
273            }
274        }
275
276        $form = new Doku_Form([
277            'id' => 'tagdd_' . $opt['id'],
278            'data-idx' => $opt['id'],
279            'data-plugin' => 'tagfilter',
280            'data-tags' => json_encode($tagFilters['pagesPerMatchedTags']),
281        ]);
282        $output .= "\n";
283        //Fieldset manuell hinzufügen da ein style Parameter übergeben werden soll
284        $form->addElement([
285            '_elem' => 'openfieldset',
286            '_legend' => 'Tagfilter',
287            'style' => 'text-align:left;width:99%',
288            'id' => '__tagfilter_' . $opt['id'],
289            'class' => ($flags['labels'] !== false) ? '' : 'hidelabel',
290
291        ]);
292        $form->_infieldset = true; //Fieldset starten
293
294        if ($flags['pagesearch']) {
295            $label = $flags['pagesearchlabel'];
296
297            $pagetitles = [];
298            foreach ($allPageids as $pageid) {
299                $pagetitles[$pageid] = $Htagfilter->getPageTitle($pageid);
300            }
301            asort($pagetitles, SORT_NATURAL | SORT_FLAG_CASE);
302
303            $selectedTags = [];
304            $id = '__tagfilter_page_' . $opt['id'];
305
306            $attrs = [//generelle Optionen für DropDownListe onchange->submit von id namespace und den flags für pagelist
307                'onChange' => 'tagfilter_submit(' . $opt['id'] . ',' . json_encode($opt['ns']) . ',' . json_encode([$opt['pagelistFlags'], $flags]) . ')',
308                'class' => 'tagdd_select tagfilter tagdd_select_' . $opt['id'] . ($flags['chosen'] ? ' chosen' : ''),
309                'data-placeholder' => hsc($label . ' ' . $this->getLang('choose')),
310                'data-label' => hsc(utf8_strtolower(trim($label))),
311            ];
312            if ($flags['multi']) { //unterscheidung ob Multiple oder Single
313                $attrs['multiple'] = 'multiple';
314                $attrs['size'] = $this->getConf("DropDownList_size");
315            } else {
316                $attrs['size'] = 1;
317                $pagetitles = array_reverse($pagetitles, true);
318                $pagetitles[''] = '';
319                $pagetitles = array_reverse($pagetitles, true);
320            }
321            $form->addElement(form_makeListboxField($label, $pagetitles, $selectedTags, $label, $id, 'tagfilter', $attrs));
322        }
323        $output .= '<script type="text/javascript">/*<![CDATA[*/ var tagfilter_container = {}; /*!]]>*/</script>' . "\n";
324        //$output .= '<script type="text/javascript">/*<![CDATA[*/ '.'tagfilter_container.tagfilter_'.$opt['id'].' = '.json_encode($tagFilters['tags2']).'; /*!]]>*/</script>'."\n";
325        foreach ($tagFilters['pagesPerMatchedTags'] as $key => $pagesPerMatchedTag) {
326            $id = false;
327            $label = $tagFilters['label'][$key];
328            $selectedTags = $tagFilters['selectedTags'][$key];
329
330            //get tag labels
331            $tags = [];
332
333            foreach (array_keys($pagesPerMatchedTag) as $tagid) {
334                $tags[$tagid] = $Htagfilter->getTagLabel($tagid);
335            }
336
337            foreach ($selectedTags as &$item) {
338                $item = utf8_strtolower(trim($item));
339            }
340            unset($item);
341
342
343            $attrs = [//generelle Optionen für DropDownListe onchange->submit von id namespace und den flags für pagelist
344                'onChange' => 'tagfilter_submit(' . $opt['id'] . ',' . json_encode($opt['ns']) . ',' . json_encode([$opt['pagelistFlags'], $flags]) . ')',
345                'class' => 'tagdd_select tagfilter tagdd_select_' . $opt['id'] . ($flags['chosen'] ? ' chosen' : ''),
346                'data-placeholder' => hsc($label . ' ' . $this->getLang('choose')),
347                'data-label' => hsc(str_replace(' ', '_', utf8_strtolower(trim($label)))),
348
349            ];
350            if ($flags['multi']) { //unterscheidung ob Multiple oder Single
351                $attrs['multiple'] = 'multiple';
352                $attrs['size'] = $this->getConf("DropDownList_size");
353            } else {
354                $attrs['size'] = 1;
355                $tags = array_reverse($tags, true);
356                $tags[''] = '';
357                $tags = array_reverse($tags, true);
358            }
359
360            if ($flags['chosen']) {
361                $links = [];
362                foreach ($tags as $k => $t) {
363                    $links[$k] = [
364                        'link' => $Htagfilter->getImageLinkByTag($k),
365                    ];
366                }
367                $jsVar = 'tagfilter_jsVar_' . rand();
368                $output .= '<script type="text/javascript">/*<![CDATA[*/ tagfilter_container.' . $jsVar . ' ='
369                    . json_encode($links) .
370                    '; /*!]]>*/</script>' . "\n";
371
372                $id = '__tagfilter_' . $opt["id"] . '_' . rand();
373
374                if ($flags['tagimage']) {
375                    $attrs['data-tagimage'] = $jsVar;
376                }
377
378            }
379            $form->addElement(form_makeListboxField($label, $tags, $selectedTags, $label, $id, 'tagfilter', $attrs));
380        }
381
382        $form->addElement(form_makeButton('button', '', $this->getLang('Delete filter'), ['onclick' => 'tagfilter_cleanform(' . $opt['id'] . ',true)']));
383        if ($flags['count']) {
384            $form->addElement('<div class="tagfilter_count">' . $this->getLang('found_count') . ': ' . '<span class="tagfilter_count_number"></span></div>');
385        }
386        $form->endFieldset();
387        $output .= $form->getForm();//Form Ausgeben
388
389        $output .= "<div id='tagfilter_ergebnis_" . $opt['id'] . "' class='tagfilter'>";
390        //dbg($opt['pagelistFlags']);
391        $output .= $HtagfilterSyntax->renderList($preparedPages, $flags, $opt['pagelistFlags']);
392        $output .= "</div>";
393
394        return $output;
395    }
396
397}
398