1<?php
2
3use dokuwiki\Cache\Cache;
4
5class helper_plugin_tagfilter_syntax extends DokuWiki_Plugin
6{
7    /**
8     *
9     * @param array $opt
10     * with amongst others:
11     *      array 'tagfilterFlags' with:
12     *          string[] 'excludeNs',
13     *          string[] 'withTags' (optional),
14     *          string[] 'excludeTags (optional)
15     *      array 'tagFilters' with three arrays with same keys::
16     *          key=>string 'label'
17     *          key=>string 'tagExpression'
18     *          key=>array 'selectedTags'
19     *      - string 'ns'
20     * @return array
21     *  with
22     *      - array $tagFilters with four arrays with same key for each tagExpression:
23     *          key=>string 'label'
24     *          key=>string 'tagExpression'
25     *          key=>array 'selectedTags'
26     *          key=>array 'pagesPerMatchedTag' with
27     *              tag=>array of pageids of pages having the tag
28     *      - array $pageids ids of related pages
29     *
30     */
31    public function getTagPageRelations($opt)
32    {
33        /* @var helper_plugin_tagfilter $Htagfilter */
34        $Htagfilter = $this->loadHelper('tagfilter');
35
36        $flags = $opt['tagfilterFlags'];
37
38        $tagFilters = $opt['tagFilters'];
39        foreach ($tagFilters['tagExpression'] as $key => $tagExpression) { //build tag->pages relation
40            $tagFilters['pagesPerMatchedTags'][$key] = $Htagfilter->getPagesByMatchedTags($tagExpression, $opt['ns']);
41        }
42
43        //extract all pageids
44        $allPageids = [];
45        foreach ($tagFilters['pagesPerMatchedTags'] as $pagesPerMatchedTag) {
46            if (!is_array($pagesPerMatchedTag)) {
47                continue;
48            }
49            foreach ($pagesPerMatchedTag as $tag => $pageidsPerTag) {
50                if (!empty($flags['withTags']) && !in_array($tag, $flags['withTags'])) {
51                    continue;
52                }
53                if (!empty($flags['excludeTags']) && in_array($tag, $flags['excludeTags'])) {
54                    continue;
55                }
56                $allPageids = array_merge($allPageids, $pageidsPerTag);
57            }
58        }
59
60        $allPageids = array_filter($allPageids, function ($val) use ($opt) {
61            //Template nicht anzeigen
62            if (strpos($val, '_template') !== false) {
63                return false;
64            }
65
66            foreach ($opt['tagfilterFlags']['excludeNs'] as $excludeNs) {
67                if (strpos($val, $excludeNs) === 0) {
68                    return false;
69                }
70            }
71            return true;
72        });
73
74        $allPageids = array_unique($allPageids); //TODO cache this
75
76        //cache $pageids and $tagFilters for all users
77        return [
78            $tagFilters,
79            $allPageids
80        ];
81    }
82
83    /**
84     * Prepare array with data for each page suitable for displaying with the pagelist plugin
85     *
86     * @param array $pageids pages to list
87     * @param array $flags with
88     *      - array 'tagcolumn' (optional)
89     *          - string tagexpr
90     *      - array 'tagimagecolumn'
91     *          - string tagexpr
92     *          - string namespace of images
93     *      - bool 'rsort' whether reverse sort
94     * @return array[] with
95     *      - array with
96     *          - string 'title'
97     *          - string 'id' page id
98     *          - string 'tmp_id'
99     *          - for each tagcolumn: string '<tagexpr as column key>' html of cell
100     *          - for each tagimagecolumn: string '<tagexpr as column key>' html of cell
101     */
102    public function prepareList($pageids, $flags)
103    {
104        global $ID;
105        global $INFO;
106
107        /* @var helper_plugin_tagfilter $Htagfilter */
108        $Htagfilter = $this->loadHelper('tagfilter');
109
110        if (!isset($flags['tagcolumn'])) {
111            $flags['tagcolumn'] = [];
112        }
113
114
115        $pages = [];
116        $_uniqueid = 0;
117        foreach ($pageids as $page) {
118
119            $depends = ['files' => [
120                $INFO['filepath'],
121                wikiFN($page)
122            ]];
123            $cache_key = implode('_', ['plugin_tagfilter', $ID, $page, $flags['sortbypageid']]);
124            $cache = new Cache($cache_key, '.tpcache');
125            if (!$cache->useCache($depends)) {
126                $title = p_get_metadata($page, 'title', METADATA_DONT_RENDER);
127
128                $cache_page = [
129                    'title' => $title ?: $page,
130                    'id' => $page,
131                    'tmp_id' => $flags['sortbypageid']
132                        ? $page
133                        : ($title ?: (noNS($page) ?: $page)),
134                ];
135
136                foreach ($flags['tagcolumn'] as $tagcolumn) {
137                    $cache_page[hsc($tagcolumn)] = $Htagfilter->td($page, hsc($tagcolumn));
138                }
139                foreach ($flags['tagimagecolumn'] as $tagimagecolumn) {
140                    $cache_page[hsc($tagimagecolumn[0]) . ' '] = $Htagfilter->getTagImageColumn($page, $tagimagecolumn[0], $tagimagecolumn[1]);
141                }
142                $cache->storeCache(serialize($cache_page));
143            } else {
144                $cache_page = unserialize($cache->retrieveCache());
145            }
146
147            //create unique key
148            $tmp_id = $cache_page['tmp_id'];
149            if (isset($pages[$tmp_id])) {
150                $tmp_id .= '_' . $_uniqueid++;
151            }
152
153            $pages[$tmp_id] = $cache_page;
154        }
155
156
157        if ($flags['rsort']) {
158            krsort($pages, SORT_NATURAL | SORT_FLAG_CASE);
159        } else {
160            ksort($pages, SORT_NATURAL | SORT_FLAG_CASE);
161        }
162        return $pages;
163    }
164
165
166    /**
167     * Generated list of the give page data
168     *
169     * @param array $pages for format @see prepareList()
170     * @param array $flags tagfilter flags with at least:
171     *      - array 'tagcolumn' (optional)
172     *          - string tagexpr
173     *      - array 'tagimagecolumn'
174     *          - string tagexpr
175     *          - string namespace of images
176     * @param array $pagelistflags all flags set by user
177     * @return false|string
178     */
179    public function renderList($pages, $flags, $pagelistflags)
180    {
181        if (!isset($flags['tagcolumn'])) {
182            $flags['tagcolumn'] = [];
183        }
184
185
186        // let Pagelist Plugin do the work for us
187        /* @var helper_plugin_pagelist $Hpagelist */
188        if (plugin_isdisabled('pagelist')
189            || (!$Hpagelist = plugin_load('helper', 'pagelist'))) {
190            msg($this->getLang('missing_pagelistplugin'), -1);
191            return false;
192        }
193
194        foreach ($flags['tagcolumn'] as $tagcolumn) {
195            $Hpagelist->addColumn('tagfilter', hsc($tagcolumn));
196        }
197        foreach ($flags['tagimagecolumn'] as $tagimagecolumn) {
198            $Hpagelist->addColumn('tagfilter', hsc($tagimagecolumn[0] . ' '));
199        }
200
201        unset($flags['tagcolumn']);  //TODO unset is not needed because pagelistflags are separate array?
202        $Hpagelist->setFlags($pagelistflags);
203        $Hpagelist->startList();
204
205        foreach ($pages as $page) {
206            $Hpagelist->addPage($page);
207        }
208
209        return $Hpagelist->finishList();
210    }
211
212
213    /**
214     * parseFlags checks for tagfilter flags and returns them as true/false
215     *
216     * @param array $flags array with (all optional):
217     *      multi, chosen, tagimage, pagesearch, cacheage, nocache, rsort, nolabels, noneonclear, tagimagecolumn,
218     *      tagcolumn, excludeNs, withTags, excludeTags, images, count, tagintersect, sortbypageid, include
219     * @return array tagfilter flags with:
220     *      multi, chosen, tagimage, pagesearch, pagesearchlabel, cache, rsort, labels, noneonclear, tagimagecolumn,
221     *      tagcolumn (optional), excludeNs, withTags, excludeTags, images, count, tagintersect, sortbypageid, include
222     */
223    public function parseFlags($flags)
224    {
225        $conf = [
226            'multi' => false,
227            'chosen' => false,
228            'tagimage' => false,
229            'pagesearch' => false,
230            'pagesearchlabel' => 'Seiten',
231            'cache' => false,
232            'rsort' => false,
233            'labels' => true,
234            'noneonclear' => false,
235            'tagimagecolumn' => [],
236            'excludeNs' => [],
237            'withTags' => [],
238            'excludeTags' => [],
239            'images' => false,
240            'count' => false,
241            'tagintersect' => false,
242            'sortbypageid' => false,
243            'include' => [],
244        ];
245        if (!is_array($flags)) {
246            return $conf;
247        }
248
249        foreach ($flags as $flag) {
250            list($flag, $value) = array_pad(explode('=', $flag, 2), 2, '');
251            $flag = trim($flag);
252            $value = trim($value);
253            switch ($flag) {
254                case 'multi':
255                    $conf['multi'] = true;
256                    break;
257                case 'chosen':
258                    $conf['chosen'] = true;
259                    break;
260                case 'tagimage':
261                    $conf['tagimage'] = true;
262                    break;
263                case 'pagesearch':
264                    $conf['pagesearch'] = true;
265                    if ($value != '') {
266                        $conf['pagesearchlabel'] = hsc($value);
267                    }
268                    break;
269                case 'cacheage':
270                    $conf['cache'] = intval($value);
271                    break;
272                case 'nocache':
273                    $conf['cache'] = null;
274                    break;
275                case 'tagcolumn':
276                    $conf['tagcolumn'][] = $value;
277                    break;
278                case 'tagimagecolumn':
279                    $conf['tagimagecolumn'][] = explode('=', $value, 2);
280                    break;
281                case 'rsort':
282                    $conf['rsort'] = true;
283                    break;
284                case 'nolabels':
285                    $conf['labels'] = false;
286                    break;
287                case 'noneonclear':
288                    $conf['noneonclear'] = true;
289                    break;
290                case 'excludeNs':
291                    $conf['excludeNs'] = explode(',', $value, 2); //TODO really maximum of two namespaces?
292                    break;
293                case 'withTags':
294                    $conf['withTags'] = explode(',', $value, 2); //TODO really maximum of two tags?
295                    break;
296                case 'excludeTags':
297                    $conf['excludeTags'] = explode(',', $value, 2); //TODO really maximum of two tags?
298                    break;
299                case 'images':
300                    $conf['images'] = true;
301                    break;
302                case 'count':
303                    $conf['count'] = true;
304                    break;
305                case 'tagintersect':
306                    $conf['tagintersect'] = true;
307                    break;
308                case 'sortbypageid':
309                    $conf['sortbypageid'] = true;
310                    break;
311                case 'include':
312                    $conf['include'] = explode(';', $value);
313                    break;
314            }
315        }
316
317        return $conf;
318    }
319
320
321    /**
322     * This function just lists documents (for RSS namespace export)
323     *
324     * @param array $data Reference to the result data structure
325     * @param string $base Base usually $conf['datadir']
326     * @param string $file current file or directory relative to $base
327     * @param string $type Type either 'd' for directory or 'f' for file
328     * @param int $lvl Current recursion depth
329     * @param array $opts option array as given to search() with:
330     *      string[] 'excludeNs'
331     * @return bool if this directory should be traversed (true) or not (false)
332     *              return value is ignored for files
333     * @author  Andreas Gohr <andi@splitbrain.org>
334     */
335    public function search_all_pages(&$data, $base, $file, $type, $lvl, $opts)
336    {
337        global $conf;
338
339        //we do nothing with directories
340        if ($type == 'd') {
341            return true;
342        }
343
344        //only search txt files
345        if (substr($file, -4) == '.txt') {
346            foreach ($opts['excludeNs'] as $excludeNs) {
347                if (strpos($file, str_replace(':', '/', $excludeNs)) === 0) {
348                    return true;
349                }
350            }
351
352            //check ACL
353            $data[] = $conf['datadir'] . '/' . $file;
354        }
355        return false;
356    }
357}
358