xref: /plugin/tagging/helper/querybuilder.php (revision df43a7be9125b55733c41a4f1a13770bf4033169)
11b4b4fa9SAnna Dabrowska<?php
2*df43a7beSAndreas Gohr
3*df43a7beSAndreas Gohruse dokuwiki\Extension\Plugin;
4*df43a7beSAndreas Gohr
51b4b4fa9SAnna Dabrowska/**
61b4b4fa9SAnna Dabrowska * Tagging Plugin (helper component)
71b4b4fa9SAnna Dabrowska */
8*df43a7beSAndreas Gohrclass helper_plugin_tagging_querybuilder extends Plugin
9*df43a7beSAndreas Gohr{
101b4b4fa9SAnna Dabrowska    /** @var string */
111b4b4fa9SAnna Dabrowska    protected $field;
121b4b4fa9SAnna Dabrowska    /** @var bool */
131b4b4fa9SAnna Dabrowska    protected $logicalAnd = false;
141b4b4fa9SAnna Dabrowska    /** @var array */
151b4b4fa9SAnna Dabrowska    protected $tags = [];
161b4b4fa9SAnna Dabrowska    /** @var array  */
171b4b4fa9SAnna Dabrowska    protected $ns = [];
181b4b4fa9SAnna Dabrowska    /** @var array */
191b4b4fa9SAnna Dabrowska    protected $notns = [];
201b4b4fa9SAnna Dabrowska    /** @var string */
211b4b4fa9SAnna Dabrowska    protected $pid;
22cbe7b4baSAnna Dabrowska
23cbe7b4baSAnna Dabrowska    /**
24cbe7b4baSAnna Dabrowska     * FIXME consolidate pid (current page query) and pids (global search query)
25cbe7b4baSAnna Dabrowska     * @var array
26cbe7b4baSAnna Dabrowska     */
27cbe7b4baSAnna Dabrowska    protected $pids;
28cbe7b4baSAnna Dabrowska
291b4b4fa9SAnna Dabrowska    /** @var string */
301b4b4fa9SAnna Dabrowska    protected $tagger = '';
3165d49a60SAnna Dabrowska    /** @var int */
3265d49a60SAnna Dabrowska    protected $limit;
331b4b4fa9SAnna Dabrowska    /** @var string */
341b4b4fa9SAnna Dabrowska    protected $orderby;
351b4b4fa9SAnna Dabrowska    /** @var string */
361b4b4fa9SAnna Dabrowska    protected $groupby;
371b4b4fa9SAnna Dabrowska    /** @var string */
381b4b4fa9SAnna Dabrowska    protected $having = '';
394a7da0a5SAnna Dabrowska    /** @var array */
404a7da0a5SAnna Dabrowska    protected $values = [];
411b4b4fa9SAnna Dabrowska
421b4b4fa9SAnna Dabrowska    /**
434a7da0a5SAnna Dabrowska     * Shorthand method: calls the appropriate getter deduced from $this->field
441b4b4fa9SAnna Dabrowska     *
4599122157SAnna Dabrowska     * @return array
461b4b4fa9SAnna Dabrowska     */
471b4b4fa9SAnna Dabrowska    public function getQuery()
481b4b4fa9SAnna Dabrowska    {
491b4b4fa9SAnna Dabrowska        if (!$this->field) {
501b4b4fa9SAnna Dabrowska            throw new \RuntimeException('Failed to build a query, no field specified');
511b4b4fa9SAnna Dabrowska        }
521b4b4fa9SAnna Dabrowska        return ($this->field === 'pid') ? $this->getPages() : $this->getTags();
531b4b4fa9SAnna Dabrowska    }
541b4b4fa9SAnna Dabrowska
551b4b4fa9SAnna Dabrowska    /**
564a7da0a5SAnna Dabrowska     * Processes all parts of the query for fetching tagged pages
571b4b4fa9SAnna Dabrowska     *
589097c6c0SAnna Dabrowska     * Returns SQL and query parameter values
594a7da0a5SAnna Dabrowska     *
6099122157SAnna Dabrowska     * @return array
611b4b4fa9SAnna Dabrowska     */
621b4b4fa9SAnna Dabrowska    public function getPages()
631b4b4fa9SAnna Dabrowska    {
641b4b4fa9SAnna Dabrowska        $this->groupby = 'pid';
651b4b4fa9SAnna Dabrowska        $this->orderby = "cnt DESC, pid";
661b4b4fa9SAnna Dabrowska        if ($this->tags && $this->logicalAnd) $this->having = ' HAVING cnt = ' . count($this->tags);
671b4b4fa9SAnna Dabrowska
6899122157SAnna Dabrowska        return [$this->getSql(), $this->values];
691b4b4fa9SAnna Dabrowska    }
701b4b4fa9SAnna Dabrowska
711b4b4fa9SAnna Dabrowska    /**
724a7da0a5SAnna Dabrowska     * Processes all parts of the query for fetching tags
731b4b4fa9SAnna Dabrowska     *
749097c6c0SAnna Dabrowska     * Returns SQL and query parameter values
754a7da0a5SAnna Dabrowska     *
7699122157SAnna Dabrowska     * @return array
771b4b4fa9SAnna Dabrowska     */
781b4b4fa9SAnna Dabrowska    public function getTags()
791b4b4fa9SAnna Dabrowska    {
801b4b4fa9SAnna Dabrowska        $this->groupby = 'CLEANTAG(tag)';
811b4b4fa9SAnna Dabrowska        $this->orderby = 'CLEANTAG(tag)';
821b4b4fa9SAnna Dabrowska
8399122157SAnna Dabrowska        return [$this->getSql(), $this->values];
844a7da0a5SAnna Dabrowska    }
854a7da0a5SAnna Dabrowska
864a7da0a5SAnna Dabrowska    /**
874a7da0a5SAnna Dabrowska     * Tags to search for
881b4b4fa9SAnna Dabrowska     * @param array $tags
891b4b4fa9SAnna Dabrowska     */
901b4b4fa9SAnna Dabrowska    public function setTags($tags)
911b4b4fa9SAnna Dabrowska    {
921b4b4fa9SAnna Dabrowska        $this->tags = $tags;
931b4b4fa9SAnna Dabrowska    }
941b4b4fa9SAnna Dabrowska
951b4b4fa9SAnna Dabrowska    /**
961b4b4fa9SAnna Dabrowska     * Namespaces to limit search to
971b4b4fa9SAnna Dabrowska     * @param array $ns
981b4b4fa9SAnna Dabrowska     */
991b4b4fa9SAnna Dabrowska    public function includeNS($ns)
1001b4b4fa9SAnna Dabrowska    {
1014a7da0a5SAnna Dabrowska        $this->ns = $this->globNS($ns);
1021b4b4fa9SAnna Dabrowska    }
1031b4b4fa9SAnna Dabrowska
1041b4b4fa9SAnna Dabrowska    /**
1051b4b4fa9SAnna Dabrowska     * Namespaces to exclude from search
1061b4b4fa9SAnna Dabrowska     * @param array $ns
1071b4b4fa9SAnna Dabrowska     */
1081b4b4fa9SAnna Dabrowska    public function excludeNS($ns)
1091b4b4fa9SAnna Dabrowska    {
1104a7da0a5SAnna Dabrowska        $this->notns = $this->globNS($ns);
1111b4b4fa9SAnna Dabrowska    }
1121b4b4fa9SAnna Dabrowska
1131b4b4fa9SAnna Dabrowska    /**
1144a7da0a5SAnna Dabrowska     * Sets the logical operator used in tag search to AND
1151b4b4fa9SAnna Dabrowska     * @param bool $and
1161b4b4fa9SAnna Dabrowska     */
1171b4b4fa9SAnna Dabrowska    public function setLogicalAnd($and)
1181b4b4fa9SAnna Dabrowska    {
11965d49a60SAnna Dabrowska        $this->logicalAnd = (bool)$and;
1201b4b4fa9SAnna Dabrowska    }
1211b4b4fa9SAnna Dabrowska
1221b4b4fa9SAnna Dabrowska    /**
1234a7da0a5SAnna Dabrowska     * Result limit
12465d49a60SAnna Dabrowska     * @param int $limit
1251b4b4fa9SAnna Dabrowska     */
1261b4b4fa9SAnna Dabrowska    public function setLimit($limit)
1271b4b4fa9SAnna Dabrowska    {
12865d49a60SAnna Dabrowska        $this->limit = $limit;
1291b4b4fa9SAnna Dabrowska    }
1301b4b4fa9SAnna Dabrowska
1311b4b4fa9SAnna Dabrowska    /**
1324a7da0a5SAnna Dabrowska     * Database field to select
1331b4b4fa9SAnna Dabrowska     * @param string $field
1341b4b4fa9SAnna Dabrowska     */
1351b4b4fa9SAnna Dabrowska    public function setField($field)
1361b4b4fa9SAnna Dabrowska    {
1371b4b4fa9SAnna Dabrowska        $this->field = $field;
1381b4b4fa9SAnna Dabrowska    }
1391b4b4fa9SAnna Dabrowska
1401b4b4fa9SAnna Dabrowska    /**
1414a7da0a5SAnna Dabrowska     * Limit search to this page id
1421b4b4fa9SAnna Dabrowska     * @param string $pid
1431b4b4fa9SAnna Dabrowska     */
1441b4b4fa9SAnna Dabrowska    public function setPid($pid)
1451b4b4fa9SAnna Dabrowska    {
1461b4b4fa9SAnna Dabrowska        $this->pid = $pid;
1471b4b4fa9SAnna Dabrowska    }
1481b4b4fa9SAnna Dabrowska
1491b4b4fa9SAnna Dabrowska    /**
150cbe7b4baSAnna Dabrowska     * Limit search to certain pages
151cbe7b4baSAnna Dabrowska     *
152cbe7b4baSAnna Dabrowska     * @param array $pids
153cbe7b4baSAnna Dabrowska     */
154cbe7b4baSAnna Dabrowska    public function setPids($pids)
155cbe7b4baSAnna Dabrowska    {
156cbe7b4baSAnna Dabrowska        $this->pids = $pids;
157cbe7b4baSAnna Dabrowska    }
158cbe7b4baSAnna Dabrowska
159cbe7b4baSAnna Dabrowska    /**
1604a7da0a5SAnna Dabrowska     * Limit results to this tagger
1611b4b4fa9SAnna Dabrowska     * @param string $tagger
1621b4b4fa9SAnna Dabrowska     */
1631b4b4fa9SAnna Dabrowska    public function setTagger($tagger)
1641b4b4fa9SAnna Dabrowska    {
1651b4b4fa9SAnna Dabrowska        $this->tagger = $tagger;
1661b4b4fa9SAnna Dabrowska    }
1671b4b4fa9SAnna Dabrowska
1681b4b4fa9SAnna Dabrowska    /**
1694a7da0a5SAnna Dabrowska     * Returns full query SQL
1701b4b4fa9SAnna Dabrowska     * @return string
1711b4b4fa9SAnna Dabrowska     */
17299122157SAnna Dabrowska    protected function getSql()
1731b4b4fa9SAnna Dabrowska    {
1741b4b4fa9SAnna Dabrowska        $sql = "SELECT $this->field AS item, COUNT(*) AS cnt
1751b4b4fa9SAnna Dabrowska                FROM taggings
1769097c6c0SAnna Dabrowska                WHERE " . $this->getWhere() .
1779097c6c0SAnna Dabrowska                " GROUP BY $this->groupby
1781b4b4fa9SAnna Dabrowska                $this->having
1791b4b4fa9SAnna Dabrowska                ORDER BY $this->orderby
1801b4b4fa9SAnna Dabrowska                ";
1811b4b4fa9SAnna Dabrowska
18265d49a60SAnna Dabrowska        if ($this->limit) {
18365d49a60SAnna Dabrowska            $sql .= ' LIMIT ?';
18465d49a60SAnna Dabrowska            $this->values[] = $this->limit;
18565d49a60SAnna Dabrowska        }
18665d49a60SAnna Dabrowska
1871b4b4fa9SAnna Dabrowska        return $sql;
1881b4b4fa9SAnna Dabrowska    }
1891b4b4fa9SAnna Dabrowska
1901b4b4fa9SAnna Dabrowska    /**
1914a7da0a5SAnna Dabrowska     * Builds the WHERE part of query string
1921b4b4fa9SAnna Dabrowska     * @return string
1931b4b4fa9SAnna Dabrowska     */
1941b4b4fa9SAnna Dabrowska    protected function getWhere()
1951b4b4fa9SAnna Dabrowska    {
1961b4b4fa9SAnna Dabrowska        $where = '1=1';
1971b4b4fa9SAnna Dabrowska
1981b4b4fa9SAnna Dabrowska        if ($this->pid) {
19965d49a60SAnna Dabrowska            $where .= ' AND pid';
20065d49a60SAnna Dabrowska            $where .= $this->useLike($this->pid) ? ' GLOB' : ' =';
20165d49a60SAnna Dabrowska            $where .= '  ?';
2024a7da0a5SAnna Dabrowska            $this->values[] = $this->pid;
2031b4b4fa9SAnna Dabrowska        }
2041b4b4fa9SAnna Dabrowska
205cbe7b4baSAnna Dabrowska        if ($this->pids) {
206cbe7b4baSAnna Dabrowska            $where .= ' AND pid';
207cbe7b4baSAnna Dabrowska            $where .=  ' IN(';
208cbe7b4baSAnna Dabrowska            foreach ($this->pids as $pid) {
209cbe7b4baSAnna Dabrowska                $where .= '  ?,';
210cbe7b4baSAnna Dabrowska                $this->values[] = $pid;
211cbe7b4baSAnna Dabrowska            }
212cbe7b4baSAnna Dabrowska            $where = rtrim($where, ',') . ')';
213cbe7b4baSAnna Dabrowska        }
214cbe7b4baSAnna Dabrowska
2151b4b4fa9SAnna Dabrowska        if ($this->tagger) {
2161b4b4fa9SAnna Dabrowska            $where .= ' AND tagger = ?';
2174a7da0a5SAnna Dabrowska            $this->values[] = $this->tagger;
2181b4b4fa9SAnna Dabrowska        }
2191b4b4fa9SAnna Dabrowska
2201b4b4fa9SAnna Dabrowska        if ($this->ns) {
2211b4b4fa9SAnna Dabrowska            $where .= ' AND ';
2221b4b4fa9SAnna Dabrowska
2231b4b4fa9SAnna Dabrowska            $nsCnt = count($this->ns);
2241b4b4fa9SAnna Dabrowska            $i = 0;
2251b4b4fa9SAnna Dabrowska            foreach ($this->ns as $ns) {
2261b4b4fa9SAnna Dabrowska                $where .= ' pid';
2271b4b4fa9SAnna Dabrowska                $where .= ' GLOB';
2281b4b4fa9SAnna Dabrowska                $where .= ' ?';
2291b4b4fa9SAnna Dabrowska                if (++$i < $nsCnt) $where .= ' OR';
2304a7da0a5SAnna Dabrowska                $this->values[] = $ns;
2311b4b4fa9SAnna Dabrowska            }
2321b4b4fa9SAnna Dabrowska        }
2331b4b4fa9SAnna Dabrowska
2341b4b4fa9SAnna Dabrowska        if ($this->notns) {
2351b4b4fa9SAnna Dabrowska            $where .= ' AND ';
2361b4b4fa9SAnna Dabrowska
2371b4b4fa9SAnna Dabrowska            $nsCnt = count($this->notns);
2381b4b4fa9SAnna Dabrowska            $i = 0;
2391b4b4fa9SAnna Dabrowska            foreach ($this->notns as $notns) {
2401b4b4fa9SAnna Dabrowska                $where .= ' pid';
2411b4b4fa9SAnna Dabrowska                $where .= ' NOT GLOB';
2421b4b4fa9SAnna Dabrowska                $where .= ' ?';
2431b4b4fa9SAnna Dabrowska                if (++$i < $nsCnt) $where .= ' AND';
2444a7da0a5SAnna Dabrowska                $this->values[] = $notns;
2451b4b4fa9SAnna Dabrowska            }
2461b4b4fa9SAnna Dabrowska        }
2471b4b4fa9SAnna Dabrowska
2481b4b4fa9SAnna Dabrowska        if ($this->tags) {
2491b4b4fa9SAnna Dabrowska            $where .= ' AND ';
2501b4b4fa9SAnna Dabrowska
2511b4b4fa9SAnna Dabrowska            $tagCnt = count($this->tags);
2521b4b4fa9SAnna Dabrowska            $i = 0;
2531b4b4fa9SAnna Dabrowska            foreach ($this->tags as $tag) {
2541b4b4fa9SAnna Dabrowska                $where .= ' CLEANTAG(tag)';
2551b4b4fa9SAnna Dabrowska                $where .= $this->useLike($tag) ? ' GLOB' : ' =';
2561b4b4fa9SAnna Dabrowska                $where .= ' CLEANTAG(?)';
2571b4b4fa9SAnna Dabrowska                if (++$i < $tagCnt) $where .= ' OR';
2584a7da0a5SAnna Dabrowska                $this->values[] = $tag;
2591b4b4fa9SAnna Dabrowska            }
2601b4b4fa9SAnna Dabrowska        }
2611b4b4fa9SAnna Dabrowska
2629dd6a05bSAnna Dabrowska        // bypass page access check when called by a command line tool
263*df43a7beSAndreas Gohr        if (PHP_SAPI !== 'cli' || defined('DOKU_UNITTEST')) {
2641b4b4fa9SAnna Dabrowska            $where .= ' AND GETACCESSLEVEL(pid) >= ' . AUTH_READ;
2659dd6a05bSAnna Dabrowska        }
2661b4b4fa9SAnna Dabrowska
2671b4b4fa9SAnna Dabrowska        return $where;
2681b4b4fa9SAnna Dabrowska    }
2691b4b4fa9SAnna Dabrowska
2701b4b4fa9SAnna Dabrowska    /**
2711b4b4fa9SAnna Dabrowska     * Check if the given string is a LIKE statement
2721b4b4fa9SAnna Dabrowska     *
2731b4b4fa9SAnna Dabrowska     * @param string $value
2741b4b4fa9SAnna Dabrowska     * @return bool
2751b4b4fa9SAnna Dabrowska     */
276*df43a7beSAndreas Gohr    protected function useLike($value)
277*df43a7beSAndreas Gohr    {
278*df43a7beSAndreas Gohr        return str_starts_with($value, '*') || strrpos($value, '*') === strlen($value) - 1;
2791b4b4fa9SAnna Dabrowska    }
2804a7da0a5SAnna Dabrowska
2814a7da0a5SAnna Dabrowska    /**
2824a7da0a5SAnna Dabrowska     * Converts namespaces into a wildcard form suitable for SQL queries
2834a7da0a5SAnna Dabrowska     *
2844a7da0a5SAnna Dabrowska     * @param array $item
2854a7da0a5SAnna Dabrowska     * @return array
2864a7da0a5SAnna Dabrowska     */
2874a7da0a5SAnna Dabrowska    protected function globNS(array $item)
2884a7da0a5SAnna Dabrowska    {
289*df43a7beSAndreas Gohr        return array_map(fn($ns) => cleanId($ns) . '*', $item);
2904a7da0a5SAnna Dabrowska    }
2911b4b4fa9SAnna Dabrowska}
292