xref: /plugin/tagging/helper.php (revision df43a7be9125b55733c41a4f1a13770bf4033169)
1f61105deSAdrian Lang<?php
2ec4796e4SAnna Dabrowska
3*df43a7beSAndreas Gohruse dokuwiki\Extension\Plugin;
4*df43a7beSAndreas Gohruse dokuwiki\Utf8\PhpString;
5ec4796e4SAnna Dabrowskause dokuwiki\Form\Form;
6*df43a7beSAndreas Gohruse dokuwiki\Search\Indexer;
7ec4796e4SAnna Dabrowska
8aa627deeSAndreas Gohr/**
9aa627deeSAndreas Gohr * Tagging Plugin (hlper component)
10aa627deeSAndreas Gohr *
11aa627deeSAndreas Gohr * @license GPL 2
12aa627deeSAndreas Gohr */
13*df43a7beSAndreas Gohrclass helper_plugin_tagging extends Plugin
14*df43a7beSAndreas Gohr{
15289f50bdSAndreas Gohr    /**
16b12334e1SAndreas Gohr     * Gives access to the database
17b12334e1SAndreas Gohr     *
18b12334e1SAndreas Gohr     * Initializes the SQLite helper and register the CLEANTAG function
19b12334e1SAndreas Gohr     *
20b12334e1SAndreas Gohr     * @return helper_plugin_sqlite|bool false if initialization fails
21289f50bdSAndreas Gohr     */
22*df43a7beSAndreas Gohr    public function getDB()
23*df43a7beSAndreas Gohr    {
24302a38efSAndreas Gohr        static $db = null;
25aa627deeSAndreas Gohr        if ($db !== null) {
26f61105deSAdrian Lang            return $db;
27f61105deSAdrian Lang        }
28f61105deSAdrian Lang
29302a38efSAndreas Gohr        /** @var helper_plugin_sqlite $db */
30f61105deSAdrian Lang        $db = plugin_load('helper', 'sqlite');
31aa627deeSAndreas Gohr        if ($db === null) {
32f61105deSAdrian Lang            msg('The tagging plugin needs the sqlite plugin', -1);
33ca455b8eSMichael Große
34f61105deSAdrian Lang            return false;
35f61105deSAdrian Lang        }
36aa627deeSAndreas Gohr        $db->init('tagging', __DIR__ . '/db/');
37*df43a7beSAndreas Gohr        $db->create_function('CLEANTAG', [$this, 'cleanTag'], 1);
38*df43a7beSAndreas Gohr        $db->create_function(
39*df43a7beSAndreas Gohr            'GROUP_SORT',
407e05e623SSzymon Olewniczak            function ($group, $newDelimiter) {
4140b94b1aSAnna Dabrowska                $ex = array_filter(explode(',', $group));
427e05e623SSzymon Olewniczak                sort($ex);
43ca455b8eSMichael Große
447e05e623SSzymon Olewniczak                return implode($newDelimiter, $ex);
45*df43a7beSAndreas Gohr            },
46*df43a7beSAndreas Gohr            2
47*df43a7beSAndreas Gohr        );
4840b94b1aSAnna Dabrowska        $db->create_function('GET_NS', 'getNS', 1);
49ca455b8eSMichael Große
50f61105deSAdrian Lang        return $db;
51f61105deSAdrian Lang    }
52f61105deSAdrian Lang
53302a38efSAndreas Gohr    /**
542ace74f4SAndreas Gohr     * Return the user to use for accessing tags
552ace74f4SAndreas Gohr     *
562ace74f4SAndreas Gohr     * Handles the singleuser mode by returning 'auto' as user. Returnes false when no user is logged in.
572ace74f4SAndreas Gohr     *
582ace74f4SAndreas Gohr     * @return bool|string
592ace74f4SAndreas Gohr     */
60*df43a7beSAndreas Gohr    public function getUser()
61*df43a7beSAndreas Gohr    {
620cfde7e9SMichael Große        if (!isset($_SERVER['REMOTE_USER'])) {
630cfde7e9SMichael Große            return false;
640cfde7e9SMichael Große        }
650cfde7e9SMichael Große        if ($this->getConf('singleusermode')) {
660cfde7e9SMichael Große            return 'auto';
670cfde7e9SMichael Große        }
68ca455b8eSMichael Große
692ace74f4SAndreas Gohr        return $_SERVER['REMOTE_USER'];
702ace74f4SAndreas Gohr    }
712ace74f4SAndreas Gohr
722ace74f4SAndreas Gohr    /**
73e4443e5cSAnna Dabrowska     * If plugin elasticsearch is installed, inform it that we have just made changes
74e4443e5cSAnna Dabrowska     * to some data relevant to a page. The page should be re-indexed.
75e4443e5cSAnna Dabrowska     *
76e4443e5cSAnna Dabrowska     * @param string $id
77e4443e5cSAnna Dabrowska     */
78e4443e5cSAnna Dabrowska    public function updateElasticState($id)
79e4443e5cSAnna Dabrowska    {
80e4443e5cSAnna Dabrowska        /** @var \helper_plugin_elasticsearch_plugins $elasticHelper */
81e4443e5cSAnna Dabrowska        $elasticHelper = plugin_load('helper', 'elasticsearch_plugins');
82e4443e5cSAnna Dabrowska        if ($elasticHelper) {
83e4443e5cSAnna Dabrowska            $elasticHelper->updateRefreshState($id);
84e4443e5cSAnna Dabrowska        }
85e4443e5cSAnna Dabrowska    }
86e4443e5cSAnna Dabrowska
87e4443e5cSAnna Dabrowska    /**
88302a38efSAndreas Gohr     * Canonicalizes the tag to its lower case nospace form
89302a38efSAndreas Gohr     *
90302a38efSAndreas Gohr     * @param $tag
910cfde7e9SMichael Große     *
92302a38efSAndreas Gohr     * @return string
93302a38efSAndreas Gohr     */
94*df43a7beSAndreas Gohr    public function cleanTag($tag)
95*df43a7beSAndreas Gohr    {
96*df43a7beSAndreas Gohr        $tag = str_replace([' ', '-', '_', '#'], '', $tag);
97*df43a7beSAndreas Gohr        return PhpString::strtolower($tag);
98302a38efSAndreas Gohr    }
99302a38efSAndreas Gohr
10056d82720SAndreas Gohr    /**
10131396860SSzymon Olewniczak     * Canonicalizes the namespace, remove the first colon and add glob
10231396860SSzymon Olewniczak     *
10331396860SSzymon Olewniczak     * @param $namespace
10431396860SSzymon Olewniczak     *
10531396860SSzymon Olewniczak     * @return string
10631396860SSzymon Olewniczak     */
107*df43a7beSAndreas Gohr    public function globNamespace($namespace)
108*df43a7beSAndreas Gohr    {
109de379874SAnna Dabrowska        return cleanId($namespace) . '*';
11031396860SSzymon Olewniczak    }
11131396860SSzymon Olewniczak
11231396860SSzymon Olewniczak    /**
11356d82720SAndreas Gohr     * Create or Update tags of a page
11456d82720SAndreas Gohr     *
11556d82720SAndreas Gohr     * Uses the translation plugin to store the language of a page (if available)
11656d82720SAndreas Gohr     *
11756d82720SAndreas Gohr     * @param string $id The page ID
11856d82720SAndreas Gohr     * @param string $user
11956d82720SAndreas Gohr     * @param array  $tags
1200cfde7e9SMichael Große     *
12156d82720SAndreas Gohr     * @return bool|SQLiteResult
12256d82720SAndreas Gohr     */
123*df43a7beSAndreas Gohr    public function replaceTags($id, $user, $tags)
124*df43a7beSAndreas Gohr    {
12556d82720SAndreas Gohr        global $conf;
12656d82720SAndreas Gohr        /** @var helper_plugin_translation $trans */
12756d82720SAndreas Gohr        $trans = plugin_load('helper', 'translation');
12856d82720SAndreas Gohr        if ($trans) {
12956d82720SAndreas Gohr            $lang = $trans->realLC($trans->getLangPart($id));
13056d82720SAndreas Gohr        } else {
13156d82720SAndreas Gohr            $lang = $conf['lang'];
13256d82720SAndreas Gohr        }
13356d82720SAndreas Gohr
134f61105deSAdrian Lang        $db = $this->getDB();
135f61105deSAdrian Lang        $db->query('BEGIN TRANSACTION');
136*df43a7beSAndreas Gohr
137*df43a7beSAndreas Gohr        $queries = [['DELETE FROM taggings WHERE pid = ? AND tagger = ?', $id, $user]];
138f61105deSAdrian Lang        foreach ($tags as $tag) {
139*df43a7beSAndreas Gohr            $queries[] = ['INSERT INTO taggings (pid, tagger, tag, lang) VALUES(?, ?, ?, ?)', $id, $user, $tag, $lang];
140f61105deSAdrian Lang        }
141f61105deSAdrian Lang
142f61105deSAdrian Lang        foreach ($queries as $query) {
143*df43a7beSAndreas Gohr            if (!call_user_func_array([$db, 'query'], $query)) {
144f61105deSAdrian Lang                $db->query('ROLLBACK TRANSACTION');
145ca455b8eSMichael Große
146f61105deSAdrian Lang                return false;
147f61105deSAdrian Lang            }
148f61105deSAdrian Lang        }
149ca455b8eSMichael Große
150f61105deSAdrian Lang        return $db->query('COMMIT TRANSACTION');
151f61105deSAdrian Lang    }
152f61105deSAdrian Lang
1530a518a11SAndreas Gohr    /**
154b12334e1SAndreas Gohr     * Get a list of Tags or Pages matching search criteria
1550a518a11SAndreas Gohr     *
156b12334e1SAndreas Gohr     * @param array  $filter What to search for array('field' => 'searchterm')
157b12334e1SAndreas Gohr     * @param string $type   What field to return 'tag'|'pid'
158077ff864SAndreas Gohr     * @param int    $limit  Limit to this many results, 0 for all
1590cfde7e9SMichael Große     *
1600a518a11SAndreas Gohr     * @return array associative array in form of value => count
1610a518a11SAndreas Gohr     */
162*df43a7beSAndreas Gohr    public function findItems($filter, $type, $limit = 0)
163*df43a7beSAndreas Gohr    {
164*df43a7beSAndreas Gohr        $queryBuilder = new helper_plugin_tagging_querybuilder();
1651b4b4fa9SAnna Dabrowska
1664a7da0a5SAnna Dabrowska        $queryBuilder->setField($type);
1674a7da0a5SAnna Dabrowska        $queryBuilder->setLimit($limit);
168739c5360SAnna Dabrowska        $queryBuilder->setTags($this->extractFromQuery($filter));
1694a7da0a5SAnna Dabrowska        if (isset($filter['ns'])) $queryBuilder->includeNS($filter['ns']);
1704a7da0a5SAnna Dabrowska        if (isset($filter['notns'])) $queryBuilder->excludeNS($filter['notns']);
1714a7da0a5SAnna Dabrowska        if (isset($filter['tagger'])) $queryBuilder->setTagger($filter['tagger']);
1724a7da0a5SAnna Dabrowska        if (isset($filter['pid'])) $queryBuilder->setPid($filter['pid']);
173b12334e1SAndreas Gohr
1744a7da0a5SAnna Dabrowska        return $this->queryDb($queryBuilder->getQuery());
175f61105deSAdrian Lang    }
176f61105deSAdrian Lang
177b12334e1SAndreas Gohr    /**
178302a38efSAndreas Gohr     * Constructs the URL to search for a tag
179302a38efSAndreas Gohr     *
1805540f91dSAndreas Gohr     * @param string $tag
1815540f91dSAndreas Gohr     * @param string $ns
1820cfde7e9SMichael Große     *
183302a38efSAndreas Gohr     * @return string
184302a38efSAndreas Gohr     */
185*df43a7beSAndreas Gohr    public function getTagSearchURL($tag, $ns = '')
186*df43a7beSAndreas Gohr    {
187a99fe09cSAnna Dabrowska        $ret = '?do=search&sf=1&q=' . rawurlencode('#' . $this->cleanTag($tag));
1880cfde7e9SMichael Große        if ($ns) {
1890cfde7e9SMichael Große            $ret .= rawurlencode(' @' . $ns);
1900cfde7e9SMichael Große        }
1915540f91dSAndreas Gohr
1925540f91dSAndreas Gohr        return $ret;
193f61105deSAdrian Lang    }
194f61105deSAdrian Lang
1955540f91dSAndreas Gohr    /**
1965540f91dSAndreas Gohr     * Calculates the size levels for the given list of clouds
1975540f91dSAndreas Gohr     *
1985540f91dSAndreas Gohr     * Automatically determines sensible tresholds
1995540f91dSAndreas Gohr     *
2005540f91dSAndreas Gohr     * @param array $tags list of tags => count
2015540f91dSAndreas Gohr     * @param int   $levels
2020cfde7e9SMichael Große     *
203*df43a7beSAndreas Gohr     * @return array
2045540f91dSAndreas Gohr     */
205*df43a7beSAndreas Gohr    public function cloudData($tags, $levels = 10)
206*df43a7beSAndreas Gohr    {
207f61105deSAdrian Lang        $min = min($tags);
208f61105deSAdrian Lang        $max = max($tags);
209f61105deSAdrian Lang
210f61105deSAdrian Lang        // calculate tresholds
211*df43a7beSAndreas Gohr        $tresholds = [];
212f61105deSAdrian Lang        for ($i = 0; $i <= $levels; $i++) {
213*df43a7beSAndreas Gohr            $tresholds[$i] = ($max - $min + 1) ** ($i / $levels) + $min - 1;
214f61105deSAdrian Lang        }
215f61105deSAdrian Lang
216f61105deSAdrian Lang        // assign weights
217f61105deSAdrian Lang        foreach ($tags as $tag => $cnt) {
218f61105deSAdrian Lang            foreach ($tresholds as $tresh => $val) {
219f61105deSAdrian Lang                if ($cnt <= $val) {
220f61105deSAdrian Lang                    $tags[$tag] = $tresh;
221f61105deSAdrian Lang                    break;
222f61105deSAdrian Lang                }
223f61105deSAdrian Lang                $tags[$tag] = $levels;
224f61105deSAdrian Lang            }
225f61105deSAdrian Lang        }
226ca455b8eSMichael Große
227f61105deSAdrian Lang        return $tags;
228f61105deSAdrian Lang    }
229f61105deSAdrian Lang
2305540f91dSAndreas Gohr    /**
2315540f91dSAndreas Gohr     * Display a tag cloud
2325540f91dSAndreas Gohr     *
2335540f91dSAndreas Gohr     * @param array    $tags   list of tags => count
2345540f91dSAndreas Gohr     * @param string   $type   'tag'
2355540f91dSAndreas Gohr     * @param Callable $func   The function to print the link (gets tag and ns)
2365540f91dSAndreas Gohr     * @param bool     $wrap   wrap cloud in UL tags?
2375540f91dSAndreas Gohr     * @param bool     $return returnn HTML instead of printing?
2385540f91dSAndreas Gohr     * @param string   $ns     Add this namespace to search links
2390cfde7e9SMichael Große     *
2405540f91dSAndreas Gohr     * @return string
2415540f91dSAndreas Gohr     */
242*df43a7beSAndreas Gohr    public function html_cloud($tags, $type, $func, $wrap = true, $return = false, $ns = '')
243*df43a7beSAndreas Gohr    {
244a66f6715SAndreas Gohr        global $INFO;
245a66f6715SAndreas Gohr
246a66f6715SAndreas Gohr        $hidden_str = $this->getConf('hiddenprefix');
247a66f6715SAndreas Gohr        $hidden_len = strlen($hidden_str);
248a66f6715SAndreas Gohr
249f61105deSAdrian Lang        $ret = '';
2500cfde7e9SMichael Große        if ($wrap) {
2510cfde7e9SMichael Große            $ret .= '<ul class="tagging_cloud clearfix">';
2520cfde7e9SMichael Große        }
253f61105deSAdrian Lang        if (count($tags) === 0) {
254f61105deSAdrian Lang            // Produce valid XHTML (ul needs a child)
255f61105deSAdrian Lang            $this->setupLocale();
256f61105deSAdrian Lang            $ret .= '<li><div class="li">' . $this->lang['js']['no' . $type . 's'] . '</div></li>';
257f61105deSAdrian Lang        } else {
258f61105deSAdrian Lang            $tags = $this->cloudData($tags);
259f61105deSAdrian Lang            foreach ($tags as $val => $size) {
260a66f6715SAndreas Gohr                // skip hidden tags for users that can't edit
261*df43a7beSAndreas Gohr                if (
262*df43a7beSAndreas Gohr                    $type === 'tag' &&
263*df43a7beSAndreas Gohr                    $hidden_len &&
264*df43a7beSAndreas Gohr                    substr($val, 0, $hidden_len) == $hidden_str &&
265a66f6715SAndreas Gohr                    !($this->getUser() && $INFO['writable'])
266a66f6715SAndreas Gohr                ) {
267a66f6715SAndreas Gohr                    continue;
268a66f6715SAndreas Gohr                }
269a66f6715SAndreas Gohr
270f61105deSAdrian Lang                $ret .= '<li class="t' . $size . '"><div class="li">';
2715540f91dSAndreas Gohr                $ret .= call_user_func($func, $val, $ns);
272f61105deSAdrian Lang                $ret .= '</div></li>';
273f61105deSAdrian Lang            }
274f61105deSAdrian Lang        }
2750cfde7e9SMichael Große        if ($wrap) {
2760cfde7e9SMichael Große            $ret .= '</ul>';
2770cfde7e9SMichael Große        }
2780cfde7e9SMichael Große        if ($return) {
2790cfde7e9SMichael Große            return $ret;
2800cfde7e9SMichael Große        }
281f61105deSAdrian Lang        echo $ret;
282ca455b8eSMichael Große
2835540f91dSAndreas Gohr        return '';
284f61105deSAdrian Lang    }
285f61105deSAdrian Lang
2865540f91dSAndreas Gohr    /**
2870b6fad27Ssandos187     * Display a List of Page Links
2880b6fad27Ssandos187     *
2890b6fad27Ssandos187     * @param array    $pids   list of pids => count
2900b6fad27Ssandos187     * @return string
2910b6fad27Ssandos187     */
292*df43a7beSAndreas Gohr    public function html_page_list($pids)
293*df43a7beSAndreas Gohr    {
2940b6fad27Ssandos187        $ret = '<div class="search_quickresult">';
2950b6fad27Ssandos187        $ret .= '<ul class="search_quickhits">';
2960b6fad27Ssandos187
2970b6fad27Ssandos187        if (count($pids) === 0) {
2980b6fad27Ssandos187            // Produce valid XHTML (ul needs a child)
2990b6fad27Ssandos187            $ret .= '<li><div class="li">' . $this->lang['js']['nopages'] . '</div></li>';
3000b6fad27Ssandos187        } else {
301bdf1ecf0SAnna Dabrowska            foreach (array_keys($pids) as $val) {
3020b6fad27Ssandos187                $ret .= '<li><div class="li">';
303db3ab356SAnna Dabrowska                $ret .= html_wikilink(":$val");
3040b6fad27Ssandos187                $ret .= '</div></li>';
3050b6fad27Ssandos187            }
3060b6fad27Ssandos187        }
3070b6fad27Ssandos187
3080b6fad27Ssandos187        $ret .= '</ul>';
3090b6fad27Ssandos187        $ret .= '</div>';
3100b6fad27Ssandos187        $ret .= '<div class="clearer"></div>';
3110b6fad27Ssandos187
3120b6fad27Ssandos187        return $ret;
3130b6fad27Ssandos187    }
3140b6fad27Ssandos187
3150b6fad27Ssandos187    /**
3165540f91dSAndreas Gohr     * Get the link to a search for the given tag
3175540f91dSAndreas Gohr     *
3185540f91dSAndreas Gohr     * @param string $tag search for this tag
3195540f91dSAndreas Gohr     * @param string $ns  limit search to this namespace
3200cfde7e9SMichael Große     *
3215540f91dSAndreas Gohr     * @return string
3225540f91dSAndreas Gohr     */
323*df43a7beSAndreas Gohr    protected function linkToSearch($tag, $ns = '')
324*df43a7beSAndreas Gohr    {
3255540f91dSAndreas Gohr        return '<a href="' . hsc($this->getTagSearchURL($tag, $ns)) . '">' . $tag . '</a>';
326f61105deSAdrian Lang    }
327f61105deSAdrian Lang
328fb1d0583SAndreas Gohr    /**
329fb1d0583SAndreas Gohr     * Display the Tags for the current page and prepare the tag editing form
3303496cc8aSAndreas Gohr     *
3313496cc8aSAndreas Gohr     * @param bool $print Should the HTML be printed or returned?
3320cfde7e9SMichael Große     *
3333496cc8aSAndreas Gohr     * @return string
334fb1d0583SAndreas Gohr     */
335*df43a7beSAndreas Gohr    public function tpl_tags($print = true)
336*df43a7beSAndreas Gohr    {
337f61105deSAdrian Lang        global $INFO;
338f61105deSAdrian Lang        global $lang;
3393bf0e2f1SMichael Große
340*df43a7beSAndreas Gohr        $filter = ['pid' => $INFO['id']];
3413bf0e2f1SMichael Große        if ($this->getConf('singleusermode')) {
3423bf0e2f1SMichael Große            $filter['tagger'] = 'auto';
3433bf0e2f1SMichael Große        }
3443bf0e2f1SMichael Große
3453bf0e2f1SMichael Große        $tags = $this->findItems($filter, 'tag');
3463496cc8aSAndreas Gohr
347*df43a7beSAndreas Gohr        $ret = '<div class="plugin_tagging_edit">';
348*df43a7beSAndreas Gohr        $ret .= $this->html_cloud($tags, 'tag', [$this, 'linkToSearch'], true, true);
349f61105deSAdrian Lang
3502ace74f4SAndreas Gohr        if ($this->getUser() && $INFO['writable']) {
351f61105deSAdrian Lang            $lang['btn_tagging_edit'] = $lang['btn_secedit'];
352e5b42768SSzymon Olewniczak            $ret .= '<div id="tagging__edit_buttons_group">';
353*df43a7beSAndreas Gohr            $ret .= html_btn('tagging_edit', $INFO['id'], '', []);
354dd52fd45SSzymon Olewniczak            if (auth_isadmin()) {
35526f61833SAnna Dabrowska                $ret .= '<label>'
35626f61833SAnna Dabrowska                    . $this->getLang('toggle admin mode')
35726f61833SAnna Dabrowska                    . '<input type="checkbox" id="tagging__edit_toggle_admin" /></label>';
358dd52fd45SSzymon Olewniczak            }
359e5b42768SSzymon Olewniczak            $ret .= '</div>';
360*df43a7beSAndreas Gohr            $form = new Form();
3612819ffcdSSzymon Olewniczak            $form->id('tagging__edit');
3622819ffcdSSzymon Olewniczak            $form->setHiddenField('tagging[id]', $INFO['id']);
3632819ffcdSSzymon Olewniczak            $form->setHiddenField('call', 'plugin_tagging_save');
364*df43a7beSAndreas Gohr            $tags = $this->findItems(['pid'    => $INFO['id'], 'tagger' => $this->getUser()], 'tag');
36526f61833SAnna Dabrowska            $form->addTextarea('tagging[tags]')
36626f61833SAnna Dabrowska                ->val(implode(', ', array_keys($tags)))
36726f61833SAnna Dabrowska                ->addClass('edit')
36826f61833SAnna Dabrowska                ->attr('rows', 4);
369cf52ec2dSSzymon Olewniczak            $form->addButton('', $lang['btn_save'])->id('tagging__edit_save');
370cf52ec2dSSzymon Olewniczak            $form->addButton('', $lang['btn_cancel'])->id('tagging__edit_cancel');
3712819ffcdSSzymon Olewniczak            $ret .= $form->toHTML();
372f61105deSAdrian Lang        }
3733496cc8aSAndreas Gohr        $ret .= '</div>';
3743496cc8aSAndreas Gohr
3750cfde7e9SMichael Große        if ($print) {
3760cfde7e9SMichael Große            echo $ret;
3770cfde7e9SMichael Große        }
378ca455b8eSMichael Große
3793496cc8aSAndreas Gohr        return $ret;
380f61105deSAdrian Lang    }
381872edc7cSRené Corinth
3828a1a3846SAndreas Gohr    /**
383a99b66c1SSzymon Olewniczak     * @param string $namespace empty for entire wiki
384a99b66c1SSzymon Olewniczak     *
38540b94b1aSAnna Dabrowska     * @param string $order_by
38640b94b1aSAnna Dabrowska     * @param bool $desc
38740b94b1aSAnna Dabrowska     * @param array $filters
3888a1a3846SAndreas Gohr     * @return array
3898a1a3846SAndreas Gohr     */
390*df43a7beSAndreas Gohr    public function getAllTags($namespace = '', $order_by = 'tid', $desc = false, $filters = [])
391*df43a7beSAndreas Gohr    {
392*df43a7beSAndreas Gohr        $order_fields = ['pid', 'tid', 'taggers', 'ns', 'count'];
393f0084ee1SSzymon Olewniczak        if (!in_array($order_by, $order_fields)) {
394f0084ee1SSzymon Olewniczak            msg('cannot sort by ' . $order_by . ' field does not exists', -1);
395f0084ee1SSzymon Olewniczak            $order_by = 'tag';
396f0084ee1SSzymon Olewniczak        }
397872edc7cSRené Corinth
398*df43a7beSAndreas Gohr        [$having, $params] = $this->getFilterSql($filters);
39940b94b1aSAnna Dabrowska
400a2246f69SAnna Dabrowska        $db = $this->getDB();
401872edc7cSRené Corinth
402f0084ee1SSzymon Olewniczak        $query = 'SELECT    "pid",
403ca455b8eSMichael Große                            CLEANTAG("tag") AS "tid",
404f0084ee1SSzymon Olewniczak                            GROUP_SORT(GROUP_CONCAT("tagger"), \', \') AS "taggers",
40540b94b1aSAnna Dabrowska                            GROUP_SORT(GROUP_CONCAT(GET_NS("pid")), \', \') AS "ns",
40689ed97adSAnna Dabrowska                            GROUP_SORT(GROUP_CONCAT("pid"), \', \') AS "pids",
407193a767dSSzymon Olewniczak                            COUNT(*) AS "count"
40857e45304SSzymon Olewniczak                        FROM "taggings"
4094227fca4SAnna Dabrowska                        WHERE "pid" GLOB ? AND GETACCESSLEVEL(pid) >= ' . AUTH_READ
4104227fca4SAnna Dabrowska                        . ' GROUP BY "tid"';
41140b94b1aSAnna Dabrowska        $query .= $having;
41240b94b1aSAnna Dabrowska        $query .=      'ORDER BY ' . $order_by;
413ca455b8eSMichael Große        if ($desc) {
414ca455b8eSMichael Große            $query .= ' DESC';
415ca455b8eSMichael Große        }
416cb469644SSzymon Olewniczak
41740b94b1aSAnna Dabrowska        array_unshift($params, $this->globNamespace($namespace));
41840b94b1aSAnna Dabrowska        $res = $db->query($query, $params);
419872edc7cSRené Corinth
4207e05e623SSzymon Olewniczak        return $db->res2arr($res);
421872edc7cSRené Corinth    }
422872edc7cSRené Corinth
4238a1a3846SAndreas Gohr    /**
42472431326SMichael Große     * Get all pages with tags and their tags
42572431326SMichael Große     *
426790ca788SAndreas Gohr     * @return array ['pid' => ['tag1','tag2','tag3']]
42772431326SMichael Große     */
428*df43a7beSAndreas Gohr    public function getAllTagsByPage()
429*df43a7beSAndreas Gohr    {
43072431326SMichael Große        $query = '
43172431326SMichael Große        SELECT pid, GROUP_CONCAT(tag) AS tags
43272431326SMichael Große        FROM taggings
43372431326SMichael Große        GROUP BY pid
43472431326SMichael Große        ';
43572431326SMichael Große        $db = $this->getDb();
43672431326SMichael Große        $res = $db->query($query);
437790ca788SAndreas Gohr        return array_map(
438*df43a7beSAndreas Gohr            fn($i) => explode(',', $i),
439790ca788SAndreas Gohr            array_column($db->res2arr($res), 'tags', 'pid')
440790ca788SAndreas Gohr        );
44172431326SMichael Große    }
44272431326SMichael Große
44372431326SMichael Große    /**
4448a1a3846SAndreas Gohr     * Renames a tag
4458a1a3846SAndreas Gohr     *
4468a1a3846SAndreas Gohr     * @param string $formerTagName
4474227fca4SAnna Dabrowska     * @param string $newTagNames
4488a1a3846SAndreas Gohr     */
449*df43a7beSAndreas Gohr    public function renameTag($formerTagName, $newTagNames)
450*df43a7beSAndreas Gohr    {
451872edc7cSRené Corinth
4524227fca4SAnna Dabrowska        if (empty($formerTagName) || empty($newTagNames)) {
4538a1a3846SAndreas Gohr            msg($this->getLang("admin enter tag names"), -1);
4548a1a3846SAndreas Gohr            return;
455872edc7cSRené Corinth        }
456872edc7cSRené Corinth
457870d77ddSAnna Dabrowska        $keepFormerTag = false;
458870d77ddSAnna Dabrowska
4594227fca4SAnna Dabrowska        // enable splitting tags on rename
460*df43a7beSAndreas Gohr        $newTagNames = array_map(fn($tag) => $this->cleanTag($tag), explode(',', $newTagNames));
4614227fca4SAnna Dabrowska
4624227fca4SAnna Dabrowska        $db = $this->getDB();
463872edc7cSRené Corinth
4644227fca4SAnna Dabrowska        // non-admins can rename only their own tags
4654227fca4SAnna Dabrowska        if (!auth_isadmin()) {
4664227fca4SAnna Dabrowska            $queryTagger = ' AND tagger = ?';
4674227fca4SAnna Dabrowska            $tagger = $this->getUser();
4684227fca4SAnna Dabrowska        } else {
4694227fca4SAnna Dabrowska            $queryTagger = '';
4704227fca4SAnna Dabrowska            $tagger = '';
4714227fca4SAnna Dabrowska        }
4724227fca4SAnna Dabrowska
4730ec63874SAnna Dabrowska        $insertQuery = 'INSERT INTO taggings ';
4740ec63874SAnna Dabrowska        $insertQuery .= 'SELECT pid, ?, tagger, lang FROM taggings';
4750ec63874SAnna Dabrowska        $where = ' WHERE CLEANTAG(tag) = ?';
4760ec63874SAnna Dabrowska        $where .= ' AND GETACCESSLEVEL(pid) >= ' . AUTH_EDIT;
4770ec63874SAnna Dabrowska        $where .= $queryTagger;
4780ec63874SAnna Dabrowska
4790ec63874SAnna Dabrowska        $db->query('BEGIN TRANSACTION');
4800ec63874SAnna Dabrowska
4810ec63874SAnna Dabrowska        // insert new tags first
4820ec63874SAnna Dabrowska        foreach ($newTagNames as $newTag) {
483870d77ddSAnna Dabrowska            if ($newTag === $this->cleanTag($formerTagName)) {
484870d77ddSAnna Dabrowska                $keepFormerTag = true;
485870d77ddSAnna Dabrowska                continue;
486870d77ddSAnna Dabrowska            }
487870d77ddSAnna Dabrowska            $params = [$newTag, $this->cleanTag($formerTagName)];
488*df43a7beSAndreas Gohr            if ($tagger) $params[] = $tagger;
4890ec63874SAnna Dabrowska            $res = $db->query($insertQuery . $where, $params);
4900ec63874SAnna Dabrowska            if ($res === false) {
4910ec63874SAnna Dabrowska                $db->query('ROLLBACK TRANSACTION');
4920ec63874SAnna Dabrowska                return;
4934227fca4SAnna Dabrowska            }
4940ec63874SAnna Dabrowska            $db->res_close($res);
4950ec63874SAnna Dabrowska        }
4960ec63874SAnna Dabrowska
497870d77ddSAnna Dabrowska        // finally delete the renamed tags
498870d77ddSAnna Dabrowska        if (!$keepFormerTag) {
4990ec63874SAnna Dabrowska            $deleteQuery = 'DELETE FROM taggings';
5000ec63874SAnna Dabrowska            $params = [$this->cleanTag($formerTagName)];
501*df43a7beSAndreas Gohr            if ($tagger) $params[] = $tagger;
5020ec63874SAnna Dabrowska            if ($db->query($deleteQuery . $where, $params) === false) {
5030ec63874SAnna Dabrowska                $db->query('ROLLBACK TRANSACTION');
5040ec63874SAnna Dabrowska                return;
5050ec63874SAnna Dabrowska            }
506870d77ddSAnna Dabrowska        }
5070ec63874SAnna Dabrowska
5080ec63874SAnna Dabrowska        $db->query('COMMIT TRANSACTION');
509872edc7cSRené Corinth
510fb1d0583SAndreas Gohr        msg($this->getLang("admin renamed"), 1);
511872edc7cSRené Corinth    }
512872edc7cSRené Corinth
5138f630140SSzymon Olewniczak    /**
514dd52fd45SSzymon Olewniczak     * Rename or delete a tag for all users
515dd52fd45SSzymon Olewniczak     *
516dd52fd45SSzymon Olewniczak     * @param string $pid
517dd52fd45SSzymon Olewniczak     * @param string $formerTagName
518dd52fd45SSzymon Olewniczak     * @param string $newTagName
519dd52fd45SSzymon Olewniczak     *
520dd52fd45SSzymon Olewniczak     * @return array
521dd52fd45SSzymon Olewniczak     */
522*df43a7beSAndreas Gohr    public function modifyPageTag($pid, $formerTagName, $newTagName)
523*df43a7beSAndreas Gohr    {
524dd52fd45SSzymon Olewniczak
525dd52fd45SSzymon Olewniczak        $db = $this->getDb();
526dd52fd45SSzymon Olewniczak
52726f61833SAnna Dabrowska        $res = $db->query(
52826f61833SAnna Dabrowska            'SELECT pid FROM taggings WHERE CLEANTAG(tag) = ? AND pid = ?',
52926f61833SAnna Dabrowska            $this->cleanTag($formerTagName),
53026f61833SAnna Dabrowska            $pid
53126f61833SAnna Dabrowska        );
532dd52fd45SSzymon Olewniczak        $check = $db->res2arr($res);
533dd52fd45SSzymon Olewniczak
534dd52fd45SSzymon Olewniczak        if (empty($check)) {
535*df43a7beSAndreas Gohr            return [true, $this->getLang('admin tag does not exists')];
536dd52fd45SSzymon Olewniczak        }
537dd52fd45SSzymon Olewniczak
538dd52fd45SSzymon Olewniczak        if (empty($newTagName)) {
53926f61833SAnna Dabrowska            $res = $db->query(
54026f61833SAnna Dabrowska                'DELETE FROM taggings WHERE pid = ? AND CLEANTAG(tag) = ?',
54126f61833SAnna Dabrowska                $pid,
54226f61833SAnna Dabrowska                $this->cleanTag($formerTagName)
54326f61833SAnna Dabrowska            );
544dd52fd45SSzymon Olewniczak        } else {
54526f61833SAnna Dabrowska            $res = $db->query(
54626f61833SAnna Dabrowska                'UPDATE taggings SET tag = ? WHERE pid = ? AND CLEANTAG(tag) = ?',
54726f61833SAnna Dabrowska                $newTagName,
54826f61833SAnna Dabrowska                $pid,
54926f61833SAnna Dabrowska                $this->cleanTag($formerTagName)
55026f61833SAnna Dabrowska            );
551dd52fd45SSzymon Olewniczak        }
552dd52fd45SSzymon Olewniczak        $db->res2arr($res);
553dd52fd45SSzymon Olewniczak
554*df43a7beSAndreas Gohr        return [false, $this->getLang('admin renamed')];
555dd52fd45SSzymon Olewniczak    }
556dd52fd45SSzymon Olewniczak
557dd52fd45SSzymon Olewniczak    /**
5588f630140SSzymon Olewniczak     * Deletes a tag
5598f630140SSzymon Olewniczak     *
5608f630140SSzymon Olewniczak     * @param array  $tags
56131396860SSzymon Olewniczak     * @param string $namespace current namespace context as in getAllTags()
5628f630140SSzymon Olewniczak     */
563*df43a7beSAndreas Gohr    public function deleteTags($tags, $namespace = '')
564*df43a7beSAndreas Gohr    {
565ca455b8eSMichael Große        if (empty($tags)) {
566ca455b8eSMichael Große            return;
567ca455b8eSMichael Große        }
5688f630140SSzymon Olewniczak
56931396860SSzymon Olewniczak        $namespace = cleanId($namespace);
57031396860SSzymon Olewniczak
5711f5991a7SMichael Große        $db = $this->getDB();
5728f630140SSzymon Olewniczak
573de379874SAnna Dabrowska        $queryBody = 'FROM taggings WHERE pid GLOB ? AND (' .
57431396860SSzymon Olewniczak            implode(' OR ', array_fill(0, count($tags), 'CLEANTAG(tag) = ?')) . ')';
575*df43a7beSAndreas Gohr        $args = array_map([$this, 'cleanTag'], $tags);
57631396860SSzymon Olewniczak        array_unshift($args, $this->globNamespace($namespace));
5778f630140SSzymon Olewniczak
5784227fca4SAnna Dabrowska        // non-admins can delete only their own tags
5794227fca4SAnna Dabrowska        if (!auth_isadmin()) {
5804227fca4SAnna Dabrowska            $queryBody .= ' AND tagger = ?';
581*df43a7beSAndreas Gohr            $args[] = $this->getUser();
5824227fca4SAnna Dabrowska        }
583ca455b8eSMichael Große
5841f5991a7SMichael Große        $affectedPagesQuery = 'SELECT DISTINCT pid ' . $queryBody;
5851f5991a7SMichael Große        $resAffectedPages = $db->query($affectedPagesQuery, $args);
5861f5991a7SMichael Große        $numAffectedPages = count($resAffectedPages->fetchAll());
5871f5991a7SMichael Große
5881f5991a7SMichael Große        $deleteQuery = 'DELETE ' . $queryBody;
5891f5991a7SMichael Große        $db->query($deleteQuery, $args);
5901f5991a7SMichael Große
5911f5991a7SMichael Große        msg(sprintf($this->getLang("admin deleted"), count($tags), $numAffectedPages), 1);
5928f630140SSzymon Olewniczak    }
593cd08599fSAnna Dabrowska
594cd08599fSAnna Dabrowska    /**
595ec4796e4SAnna Dabrowska     * Delete taggings of nonexistent pages
596ec4796e4SAnna Dabrowska     */
597ec4796e4SAnna Dabrowska    public function deleteInvalidTaggings()
598ec4796e4SAnna Dabrowska    {
599ec4796e4SAnna Dabrowska        $db = $this->getDB();
600ec4796e4SAnna Dabrowska        $query = 'DELETE    FROM "taggings"
6011815f49fSAnna Dabrowska                            WHERE NOT PAGEEXISTS(pid)
602ec4796e4SAnna Dabrowska                 ';
603ec4796e4SAnna Dabrowska        $res = $db->query($query);
604ec4796e4SAnna Dabrowska        $db->res_close($res);
605ec4796e4SAnna Dabrowska    }
606ec4796e4SAnna Dabrowska
607ec4796e4SAnna Dabrowska    /**
608cd08599fSAnna Dabrowska     * Updates tags with a new page name
609cd08599fSAnna Dabrowska     *
610cd08599fSAnna Dabrowska     * @param string $oldName
611cd08599fSAnna Dabrowska     * @param string $newName
612cd08599fSAnna Dabrowska     */
613*df43a7beSAndreas Gohr    public function renamePage($oldName, $newName)
614*df43a7beSAndreas Gohr    {
615f6568bcbSAnna Dabrowska        $db = $this->getDB();
616cd08599fSAnna Dabrowska        $db->query('UPDATE taggings SET pid = ? WHERE pid = ?', $newName, $oldName);
617cd08599fSAnna Dabrowska    }
618972f6adfSAnna Dabrowska
619972f6adfSAnna Dabrowska    /**
6201b4b4fa9SAnna Dabrowska     * Extracts tags from search query
6211b4b4fa9SAnna Dabrowska     *
6221b4b4fa9SAnna Dabrowska     * @param array $parsedQuery
6231b4b4fa9SAnna Dabrowska     * @return array
6241b4b4fa9SAnna Dabrowska     */
625739c5360SAnna Dabrowska    public function extractFromQuery($parsedQuery)
6261b4b4fa9SAnna Dabrowska    {
6271b4b4fa9SAnna Dabrowska        $tags = [];
6281b4b4fa9SAnna Dabrowska        if (isset($parsedQuery['phrases'][0])) {
6291b4b4fa9SAnna Dabrowska            $tags = $parsedQuery['phrases'];
6301b4b4fa9SAnna Dabrowska        } elseif (isset($parsedQuery['and'][0])) {
6311b4b4fa9SAnna Dabrowska            $tags = $parsedQuery['and'];
6321b4b4fa9SAnna Dabrowska        } elseif (isset($parsedQuery['tag'])) {
6331b4b4fa9SAnna Dabrowska            // handle autocomplete call
6341b4b4fa9SAnna Dabrowska            $tags[] = $parsedQuery['tag'];
6351b4b4fa9SAnna Dabrowska        }
6361b4b4fa9SAnna Dabrowska        return $tags;
6371b4b4fa9SAnna Dabrowska    }
6381b4b4fa9SAnna Dabrowska
6391b4b4fa9SAnna Dabrowska    /**
6404a7da0a5SAnna Dabrowska     * Search for tagged pages
641972f6adfSAnna Dabrowska     *
642739c5360SAnna Dabrowska     * @param array $tagFiler
6434a7da0a5SAnna Dabrowska     * @return array
644972f6adfSAnna Dabrowska     */
645739c5360SAnna Dabrowska    public function searchPages($tagFiler)
646972f6adfSAnna Dabrowska    {
6471b4b4fa9SAnna Dabrowska        global $INPUT;
6481b4b4fa9SAnna Dabrowska        global $QUERY;
649*df43a7beSAndreas Gohr        $parsedQuery = ft_queryParser(new Indexer(), $QUERY);
650972f6adfSAnna Dabrowska
651*df43a7beSAndreas Gohr        $queryBuilder = new helper_plugin_tagging_querybuilder();
652972f6adfSAnna Dabrowska
6534a7da0a5SAnna Dabrowska        $queryBuilder->setField('pid');
654cbe7b4baSAnna Dabrowska        $queryBuilder->setTags($tagFiler);
655f014dfc9SAnna Dabrowska        $queryBuilder->setLogicalAnd($INPUT->str('tagging-logic') === 'and');
6564a7da0a5SAnna Dabrowska        if (isset($parsedQuery['ns'])) $queryBuilder->includeNS($parsedQuery['ns']);
6574a7da0a5SAnna Dabrowska        if (isset($parsedQuery['notns'])) $queryBuilder->excludeNS($parsedQuery['notns']);
6584a7da0a5SAnna Dabrowska        if (isset($parsedQuery['tagger'])) $queryBuilder->setTagger($parsedQuery['tagger']);
6594a7da0a5SAnna Dabrowska        if (isset($parsedQuery['pid'])) $queryBuilder->setPid($parsedQuery['pid']);
660972f6adfSAnna Dabrowska
6614a7da0a5SAnna Dabrowska        return $this->queryDb($queryBuilder->getPages());
662972f6adfSAnna Dabrowska    }
663972f6adfSAnna Dabrowska
6641b4b4fa9SAnna Dabrowska    /**
6654227fca4SAnna Dabrowska     * Syntax to allow users to manage tags on regular pages, respects ACLs
6664227fca4SAnna Dabrowska     * @param string $ns
6674227fca4SAnna Dabrowska     * @return string
6684227fca4SAnna Dabrowska     */
6694227fca4SAnna Dabrowska    public function manageTags($ns)
6704227fca4SAnna Dabrowska    {
6714227fca4SAnna Dabrowska        global $INPUT;
6724227fca4SAnna Dabrowska
673f6568bcbSAnna Dabrowska        $this->setDefaultSort();
6744227fca4SAnna Dabrowska
6754227fca4SAnna Dabrowska        // initially set namespace filter to what is defined in syntax
6764227fca4SAnna Dabrowska        if ($ns && !$INPUT->has('tagging__filters')) {
6774227fca4SAnna Dabrowska            $INPUT->set('tagging__filters', ['ns' => $ns]);
6784227fca4SAnna Dabrowska        }
6794227fca4SAnna Dabrowska
6804227fca4SAnna Dabrowska        return $this->html_table();
6814227fca4SAnna Dabrowska    }
6824227fca4SAnna Dabrowska
6834227fca4SAnna Dabrowska    /**
684a2246f69SAnna Dabrowska     * HTML list of tagged pages
685a2246f69SAnna Dabrowska     *
686a2246f69SAnna Dabrowska     * @param string $tid
687a2246f69SAnna Dabrowska     * @return string
688a2246f69SAnna Dabrowska     */
689a2246f69SAnna Dabrowska    public function getPagesHtml($tid)
690a2246f69SAnna Dabrowska    {
691a2246f69SAnna Dabrowska        $html = '';
692a2246f69SAnna Dabrowska
693a2246f69SAnna Dabrowska        $db = $this->getDB();
69431bddc5fSAnna Dabrowska        $sql = 'SELECT pid from taggings where CLEANTAG(tag) = CLEANTAG(?)';
695a2246f69SAnna Dabrowska        $res =  $db->query($sql, $tid);
696a2246f69SAnna Dabrowska        $pages = $db->res2arr($res);
697a2246f69SAnna Dabrowska
698a2246f69SAnna Dabrowska        if ($pages) {
699a2246f69SAnna Dabrowska            $html .= '<ul>';
700a2246f69SAnna Dabrowska            foreach ($pages as $page) {
701a2246f69SAnna Dabrowska                $pid = $page['pid'];
702a2246f69SAnna Dabrowska                $html .= '<li><a href="' . wl($pid) . '" target="_blank">' . $pid . '</li>';
703a2246f69SAnna Dabrowska            }
704a2246f69SAnna Dabrowska            $html .= '</ul>';
705a2246f69SAnna Dabrowska        }
706a2246f69SAnna Dabrowska
707a2246f69SAnna Dabrowska        return $html;
708a2246f69SAnna Dabrowska    }
709a2246f69SAnna Dabrowska
710a2246f69SAnna Dabrowska    /**
71189ed97adSAnna Dabrowska     * Display tag management table
71289ed97adSAnna Dabrowska     */
713*df43a7beSAndreas Gohr    public function html_table()
714*df43a7beSAndreas Gohr    {
71589ed97adSAnna Dabrowska        global $ID, $INPUT;
71689ed97adSAnna Dabrowska
717*df43a7beSAndreas Gohr        $headers = [
718*df43a7beSAndreas Gohr            ['value' => $this->getLang('admin tag'), 'sort_by' => 'tid'],
719*df43a7beSAndreas Gohr            ['value' => $this->getLang('admin occurrence'), 'sort_by' => 'count']
720*df43a7beSAndreas Gohr        ];
72126c4179dSAnna Dabrowska
72226c4179dSAnna Dabrowska        if (!$this->conf['hidens']) {
723*df43a7beSAndreas Gohr            $headers[] = ['value' => $this->getLang('admin namespaces'), 'sort_by' => 'ns'];
72426c4179dSAnna Dabrowska        }
725*df43a7beSAndreas Gohr        $headers[] = ['value' => $this->getLang('admin taggers'), 'sort_by' => 'taggers'];
726*df43a7beSAndreas Gohr        $headers[] = ['value' => $this->getLang('admin actions'), 'sort_by' => false];
72789ed97adSAnna Dabrowska
728f6568bcbSAnna Dabrowska        $sort = explode(',', $this->getParam('sort'));
72989ed97adSAnna Dabrowska        $order_by = $sort[0];
73089ed97adSAnna Dabrowska        $desc = false;
73189ed97adSAnna Dabrowska        if (isset($sort[1]) && $sort[1] === 'desc') {
73289ed97adSAnna Dabrowska            $desc = true;
73389ed97adSAnna Dabrowska        }
73489ed97adSAnna Dabrowska        $filters = $INPUT->arr('tagging__filters');
73589ed97adSAnna Dabrowska
73689ed97adSAnna Dabrowska        $tags = $this->getAllTags($INPUT->str('filter'), $order_by, $desc, $filters);
73789ed97adSAnna Dabrowska
738*df43a7beSAndreas Gohr        $form = new Form();
739a2246f69SAnna Dabrowska        // required in admin mode
74089ed97adSAnna Dabrowska        $form->setHiddenField('page', 'tagging');
74189ed97adSAnna Dabrowska        $form->setHiddenField('id', $ID);
742f6568bcbSAnna Dabrowska        $form->setHiddenField('[tagging]sort', $this->getParam('sort'));
74389ed97adSAnna Dabrowska
74489ed97adSAnna Dabrowska        /**
74589ed97adSAnna Dabrowska         * Actions dialog
74689ed97adSAnna Dabrowska         */
74789ed97adSAnna Dabrowska        $form->addTagOpen('div')->id('tagging__action-dialog')->attr('style', "display:none;");
74889ed97adSAnna Dabrowska        $form->addTagClose('div');
74989ed97adSAnna Dabrowska
75089ed97adSAnna Dabrowska        /**
75189ed97adSAnna Dabrowska         * Tag pages dialog
75289ed97adSAnna Dabrowska         */
75389ed97adSAnna Dabrowska        $form->addTagOpen('div')->id('tagging__taggedpages-dialog')->attr('style', "display:none;");
75489ed97adSAnna Dabrowska        $form->addTagClose('div');
75589ed97adSAnna Dabrowska
75689ed97adSAnna Dabrowska        /**
75789ed97adSAnna Dabrowska         * Tag management table
75889ed97adSAnna Dabrowska         */
75989ed97adSAnna Dabrowska        $form->addTagOpen('table')->addClass('inline plugin_tagging');
76089ed97adSAnna Dabrowska
761*df43a7beSAndreas Gohr        $nscol = $this->conf['hidens'] ? '' : '<col class="wide-col" />';
762a305ec34SAnna Dabrowska        $form->addHTML(
763a305ec34SAnna Dabrowska            '<colgroup>
764*df43a7beSAndreas Gohr                <col />
765*df43a7beSAndreas Gohr                <col class="narrow-col" />'
76626c4179dSAnna Dabrowska                . $nscol .
767*df43a7beSAndreas Gohr                '<col />
768*df43a7beSAndreas Gohr                <col class="narrow-col" />
769a305ec34SAnna Dabrowska            </colgroup>'
770a305ec34SAnna Dabrowska        );
771a305ec34SAnna Dabrowska
77289ed97adSAnna Dabrowska        /**
77389ed97adSAnna Dabrowska         * Table headers
77489ed97adSAnna Dabrowska         */
77589ed97adSAnna Dabrowska        $form->addTagOpen('tr');
77689ed97adSAnna Dabrowska        foreach ($headers as $header) {
77789ed97adSAnna Dabrowska            $form->addTagOpen('th');
77889ed97adSAnna Dabrowska            if ($header['sort_by'] !== false) {
77989ed97adSAnna Dabrowska                $param = $header['sort_by'];
78089ed97adSAnna Dabrowska                $icon = 'arrow-both';
78189ed97adSAnna Dabrowska                $title = $this->getLang('admin sort ascending');
78289ed97adSAnna Dabrowska                if ($header['sort_by'] === $order_by) {
78389ed97adSAnna Dabrowska                    if ($desc === false) {
78489ed97adSAnna Dabrowska                        $icon = 'arrow-up';
78589ed97adSAnna Dabrowska                        $title = $this->getLang('admin sort descending');
78689ed97adSAnna Dabrowska                        $param .= ',desc';
78789ed97adSAnna Dabrowska                    } else {
78889ed97adSAnna Dabrowska                        $icon = 'arrow-down';
78989ed97adSAnna Dabrowska                    }
79089ed97adSAnna Dabrowska                }
79126f61833SAnna Dabrowska                $form->addButtonHTML(
792f6568bcbSAnna Dabrowska                    "tagging[sort]",
793*df43a7beSAndreas Gohr                    $header['value'] . ' ' . inlineSVG(__DIR__ . "/images/$icon.svg")
794*df43a7beSAndreas Gohr                )
79589ed97adSAnna Dabrowska                    ->addClass('plugin_tagging sort_button')
796f6568bcbSAnna Dabrowska                    ->attr('title', $title)
797f6568bcbSAnna Dabrowska                    ->val($param);
79889ed97adSAnna Dabrowska            } else {
79989ed97adSAnna Dabrowska                $form->addHTML($header['value']);
80089ed97adSAnna Dabrowska            }
80189ed97adSAnna Dabrowska            $form->addTagClose('th');
80289ed97adSAnna Dabrowska        }
80389ed97adSAnna Dabrowska        $form->addTagClose('tr');
80489ed97adSAnna Dabrowska
80589ed97adSAnna Dabrowska        /**
80689ed97adSAnna Dabrowska         * Table filters for all sortable columns
80789ed97adSAnna Dabrowska         */
80889ed97adSAnna Dabrowska        $form->addTagOpen('tr');
80989ed97adSAnna Dabrowska        foreach ($headers as $header) {
81089ed97adSAnna Dabrowska            $form->addTagOpen('th');
81189ed97adSAnna Dabrowska            if ($header['sort_by'] !== false) {
81289ed97adSAnna Dabrowska                $field = $header['sort_by'];
8137c96ae87SAnna Dabrowska                $input = $form->addTextInput("tagging__filters[$field]");
814a305ec34SAnna Dabrowska                $input->addClass('full-col');
81589ed97adSAnna Dabrowska            }
81689ed97adSAnna Dabrowska            $form->addTagClose('th');
81789ed97adSAnna Dabrowska        }
81889ed97adSAnna Dabrowska        $form->addTagClose('tr');
81989ed97adSAnna Dabrowska
82089ed97adSAnna Dabrowska
82189ed97adSAnna Dabrowska        foreach ($tags as $taginfo) {
82289ed97adSAnna Dabrowska            $tagname = $taginfo['tid'];
82389ed97adSAnna Dabrowska            $taggers = $taginfo['taggers'];
82489ed97adSAnna Dabrowska            $ns = $taginfo['ns'];
82589ed97adSAnna Dabrowska            $pids = explode(',', $taginfo['pids']);
82689ed97adSAnna Dabrowska
82789ed97adSAnna Dabrowska            $form->addTagOpen('tr');
82826f61833SAnna Dabrowska            $form->addHTML('<td>');
829a2246f69SAnna Dabrowska            $form->addHTML('<a class="tagslist" href="#" data-tid="' . $taginfo['tid'] . '">');
83026f61833SAnna Dabrowska            $form->addHTML(hsc($tagname) . '</a>');
83126f61833SAnna Dabrowska            $form->addHTML('</td>');
83289ed97adSAnna Dabrowska            $form->addHTML('<td>' . $taginfo['count'] . '</td>');
83326c4179dSAnna Dabrowska            if (!$this->conf['hidens']) {
83489ed97adSAnna Dabrowska                $form->addHTML('<td>' . hsc($ns) . '</td>');
83526c4179dSAnna Dabrowska            }
83689ed97adSAnna Dabrowska            $form->addHTML('<td>' . hsc($taggers) . '</td>');
83789ed97adSAnna Dabrowska
83889ed97adSAnna Dabrowska            /**
83989ed97adSAnna Dabrowska             * action buttons
84089ed97adSAnna Dabrowska             */
84189ed97adSAnna Dabrowska            $form->addHTML('<td>');
84289ed97adSAnna Dabrowska
84389ed97adSAnna Dabrowska            // check ACLs
84489ed97adSAnna Dabrowska            $userEdit = false;
84589ed97adSAnna Dabrowska            foreach ($pids as $pid) {
846749c70e5SAndreas Gohr                if (auth_quickaclcheck($pid) >= AUTH_EDIT) {
84789ed97adSAnna Dabrowska                    $userEdit = true;
848*df43a7beSAndreas Gohr                    break;
84989ed97adSAnna Dabrowska                }
85089ed97adSAnna Dabrowska            }
85189ed97adSAnna Dabrowska
85289ed97adSAnna Dabrowska            if ($userEdit) {
85326f61833SAnna Dabrowska                $form->addButtonHTML(
854f6568bcbSAnna Dabrowska                    'tagging[actions][rename][' . $taginfo['tid'] . ']',
855*df43a7beSAndreas Gohr                    inlineSVG(__DIR__ . '/images/edit.svg')
856*df43a7beSAndreas Gohr                )
85726f61833SAnna Dabrowska                    ->addClass('plugin_tagging action_button')
85826f61833SAnna Dabrowska                    ->attr('data-action', 'rename')
85926f61833SAnna Dabrowska                    ->attr('data-tid', $taginfo['tid']);
86026f61833SAnna Dabrowska                $form->addButtonHTML(
861f6568bcbSAnna Dabrowska                    'tagging[actions][delete][' . $taginfo['tid'] . ']',
862*df43a7beSAndreas Gohr                    inlineSVG(__DIR__ . '/images/delete.svg')
863*df43a7beSAndreas Gohr                )
86426f61833SAnna Dabrowska                    ->addClass('plugin_tagging action_button')
86526f61833SAnna Dabrowska                    ->attr('data-action', 'delete')
86626f61833SAnna Dabrowska                    ->attr('data-tid', $taginfo['tid']);
86789ed97adSAnna Dabrowska            }
86889ed97adSAnna Dabrowska
86989ed97adSAnna Dabrowska            $form->addHTML('</td>');
87089ed97adSAnna Dabrowska            $form->addTagClose('tr');
87189ed97adSAnna Dabrowska        }
87289ed97adSAnna Dabrowska
87389ed97adSAnna Dabrowska        $form->addTagClose('table');
8740b033188SAnna Dabrowska        return '<div class="table">' . $form->toHTML() . '</div>';
87589ed97adSAnna Dabrowska    }
87689ed97adSAnna Dabrowska
87789ed97adSAnna Dabrowska    /**
878ec4796e4SAnna Dabrowska     * Display tag cleaner
879ec4796e4SAnna Dabrowska     *
880ec4796e4SAnna Dabrowska     * @return string
881ec4796e4SAnna Dabrowska     */
882ec4796e4SAnna Dabrowska    public function html_clean()
883ec4796e4SAnna Dabrowska    {
884ec4796e4SAnna Dabrowska        $invalid = $this->getInvalidTaggings();
885ec4796e4SAnna Dabrowska
886ec4796e4SAnna Dabrowska        if (!$invalid) {
887ec4796e4SAnna Dabrowska            return '<p><strong>' . $this->getLang('admin no invalid') . '</strong></p>';
888ec4796e4SAnna Dabrowska        }
889ec4796e4SAnna Dabrowska
890ec4796e4SAnna Dabrowska        $form = new Form();
891ec4796e4SAnna Dabrowska        $form->setHiddenField('do', 'admin');
892ec4796e4SAnna Dabrowska        $form->setHiddenField('page', $this->getPluginName());
893ec4796e4SAnna Dabrowska        $form->addButton('cmd[clean]', $this->getLang('admin clean'));
894ec4796e4SAnna Dabrowska
895ec4796e4SAnna Dabrowska        $html = $form->toHTML();
896ec4796e4SAnna Dabrowska
897ec4796e4SAnna Dabrowska        $html .= '<div class="table"><table class="inline plugin_tagging">';
898ec4796e4SAnna Dabrowska        $html .= '<thead><tr><th>' .
899ec4796e4SAnna Dabrowska            $this->getLang('admin nonexistent page') .
900ec4796e4SAnna Dabrowska            '</th><th>' .
901ec4796e4SAnna Dabrowska            $this->getLang('admin tags') .
902ec4796e4SAnna Dabrowska            '</th></tr></thead><tbody>';
903ec4796e4SAnna Dabrowska
904ec4796e4SAnna Dabrowska        foreach ($invalid as $row) {
905ec4796e4SAnna Dabrowska            $html .= '<tr><td>' . $row['pid'] . '</td><td>' . $row['tags'] . '</td></tr>';
906ec4796e4SAnna Dabrowska        }
907ec4796e4SAnna Dabrowska
908ec4796e4SAnna Dabrowska        $html .= '</tbody></table></div>';
909ec4796e4SAnna Dabrowska
910ec4796e4SAnna Dabrowska        return $html;
911ec4796e4SAnna Dabrowska    }
912ec4796e4SAnna Dabrowska
913ec4796e4SAnna Dabrowska    /**
914f6568bcbSAnna Dabrowska     * Returns all tagging parameters from the query string
915f6568bcbSAnna Dabrowska     *
916f6568bcbSAnna Dabrowska     * @return mixed
917f6568bcbSAnna Dabrowska     */
918f6568bcbSAnna Dabrowska    public function getParams()
919f6568bcbSAnna Dabrowska    {
920f6568bcbSAnna Dabrowska        global $INPUT;
921f6568bcbSAnna Dabrowska        return $INPUT->param('tagging', []);
922f6568bcbSAnna Dabrowska    }
923f6568bcbSAnna Dabrowska
924f6568bcbSAnna Dabrowska    /**
925f6568bcbSAnna Dabrowska     * Get a tagging parameter, empty string if not set
926f6568bcbSAnna Dabrowska     *
927f6568bcbSAnna Dabrowska     * @param string $name
928*df43a7beSAndreas Gohr     * @return string
929f6568bcbSAnna Dabrowska     */
930f6568bcbSAnna Dabrowska    public function getParam($name)
931f6568bcbSAnna Dabrowska    {
932f6568bcbSAnna Dabrowska        $params = $this->getParams();
933f6568bcbSAnna Dabrowska        if ($params) {
934f6568bcbSAnna Dabrowska            return $params[$name] ?: '';
935f6568bcbSAnna Dabrowska        }
936*df43a7beSAndreas Gohr        return '';
937f6568bcbSAnna Dabrowska    }
938f6568bcbSAnna Dabrowska
939f6568bcbSAnna Dabrowska    /**
940f6568bcbSAnna Dabrowska     * Sets a tagging parameter
941f6568bcbSAnna Dabrowska     *
942f6568bcbSAnna Dabrowska     * @param string $name
943f6568bcbSAnna Dabrowska     * @param string|array $value
944f6568bcbSAnna Dabrowska     */
945f6568bcbSAnna Dabrowska    public function setParam($name, $value)
946f6568bcbSAnna Dabrowska    {
947f6568bcbSAnna Dabrowska        global $INPUT;
948f6568bcbSAnna Dabrowska        $params = $this->getParams();
949f6568bcbSAnna Dabrowska        $params = array_merge($params, [$name => $value]);
950*df43a7beSAndreas Gohr
951f6568bcbSAnna Dabrowska        $INPUT->set('tagging', $params);
952f6568bcbSAnna Dabrowska    }
953f6568bcbSAnna Dabrowska
954f6568bcbSAnna Dabrowska    /**
955f6568bcbSAnna Dabrowska     * Default sorting by tag id
956f6568bcbSAnna Dabrowska     */
957f6568bcbSAnna Dabrowska    public function setDefaultSort()
958f6568bcbSAnna Dabrowska    {
959f6568bcbSAnna Dabrowska        if (!$this->getParam('sort')) {
960f6568bcbSAnna Dabrowska            $this->setParam('sort', 'tid');
961f6568bcbSAnna Dabrowska        }
962f6568bcbSAnna Dabrowska    }
963f6568bcbSAnna Dabrowska
964f6568bcbSAnna Dabrowska    /**
9654a7da0a5SAnna Dabrowska     * Executes the query and returns the results as array
9661b4b4fa9SAnna Dabrowska     *
96799122157SAnna Dabrowska     * @param array $query
9681b4b4fa9SAnna Dabrowska     * @return array
9691b4b4fa9SAnna Dabrowska     */
9704a7da0a5SAnna Dabrowska    protected function queryDb($query)
9711b4b4fa9SAnna Dabrowska    {
9724a7da0a5SAnna Dabrowska        $db = $this->getDB();
9734a7da0a5SAnna Dabrowska        if (!$db) {
9744a7da0a5SAnna Dabrowska            return [];
975972f6adfSAnna Dabrowska        }
9764a7da0a5SAnna Dabrowska
97799122157SAnna Dabrowska        $res = $db->query($query[0], $query[1]);
9784a7da0a5SAnna Dabrowska        $res = $db->res2arr($res);
9794a7da0a5SAnna Dabrowska
9804a7da0a5SAnna Dabrowska        $ret = [];
9814a7da0a5SAnna Dabrowska        foreach ($res as $row) {
9824a7da0a5SAnna Dabrowska            $ret[$row['item']] = $row['cnt'];
9834a7da0a5SAnna Dabrowska        }
9844a7da0a5SAnna Dabrowska        return $ret;
985972f6adfSAnna Dabrowska    }
98640b94b1aSAnna Dabrowska
98740b94b1aSAnna Dabrowska    /**
98840b94b1aSAnna Dabrowska     * Construct the HAVING part of the search query
98940b94b1aSAnna Dabrowska     *
99040b94b1aSAnna Dabrowska     * @param array $filters
99140b94b1aSAnna Dabrowska     * @return array
99240b94b1aSAnna Dabrowska     */
99340b94b1aSAnna Dabrowska    protected function getFilterSql($filters)
99440b94b1aSAnna Dabrowska    {
99540b94b1aSAnna Dabrowska        $having = '';
99640b94b1aSAnna Dabrowska        $parts = [];
99740b94b1aSAnna Dabrowska        $params = [];
99840b94b1aSAnna Dabrowska        $filters = array_filter($filters);
999*df43a7beSAndreas Gohr        if ($filters !== []) {
100040b94b1aSAnna Dabrowska            $having = ' HAVING ';
100140b94b1aSAnna Dabrowska            foreach ($filters as $filter => $value) {
100240b94b1aSAnna Dabrowska                $parts[] = " $filter LIKE ? ";
100340b94b1aSAnna Dabrowska                $params[] = "%$value%";
100440b94b1aSAnna Dabrowska            }
100540b94b1aSAnna Dabrowska            $having .= implode(' AND ', $parts);
100640b94b1aSAnna Dabrowska        }
100740b94b1aSAnna Dabrowska        return [$having, $params];
100840b94b1aSAnna Dabrowska    }
1009ec4796e4SAnna Dabrowska
1010ec4796e4SAnna Dabrowska    /**
1011ec4796e4SAnna Dabrowska     * Returns taggings of nonexistent pages
1012ec4796e4SAnna Dabrowska     *
1013ec4796e4SAnna Dabrowska     * @return array
1014ec4796e4SAnna Dabrowska     */
1015ec4796e4SAnna Dabrowska    protected function getInvalidTaggings()
1016ec4796e4SAnna Dabrowska    {
1017ec4796e4SAnna Dabrowska        $db = $this->getDB();
1018ec4796e4SAnna Dabrowska        $query = 'SELECT    "pid",
1019ec4796e4SAnna Dabrowska                            GROUP_CONCAT(CLEANTAG("tag")) AS "tags"
1020ec4796e4SAnna Dabrowska                            FROM "taggings"
1021f24ac95eSAnna Dabrowska                            WHERE NOT PAGEEXISTS(pid)
1022ec4796e4SAnna Dabrowska                            GROUP BY pid
1023ec4796e4SAnna Dabrowska                 ';
1024ec4796e4SAnna Dabrowska        $res = $db->query($query);
1025ec4796e4SAnna Dabrowska        return $db->res2arr($res);
1026ec4796e4SAnna Dabrowska    }
1027cd08599fSAnna Dabrowska}
1028