1<?php
2/**
3 * Tagging Plugin (helper component)
4 */
5class helper_plugin_tagging_querybuilder extends DokuWiki_Plugin {
6
7    /** @var string */
8    protected $field;
9    /** @var bool */
10    protected $logicalAnd = false;
11    /** @var array */
12    protected $tags = [];
13    /** @var array  */
14    protected $ns = [];
15    /** @var array */
16    protected $notns = [];
17    /** @var string */
18    protected $pid;
19
20    /**
21     * FIXME consolidate pid (current page query) and pids (global search query)
22     * @var array
23     */
24    protected $pids;
25
26    /** @var string */
27    protected $tagger = '';
28    /** @var int */
29    protected $limit;
30    /** @var string */
31    protected $orderby;
32    /** @var string */
33    protected $groupby;
34    /** @var string */
35    protected $having = '';
36    /** @var array */
37    protected $values = [];
38
39    /**
40     * Shorthand method: calls the appropriate getter deduced from $this->field
41     *
42     * @return array
43     */
44    public function getQuery()
45    {
46        if (!$this->field) {
47            throw new \RuntimeException('Failed to build a query, no field specified');
48        }
49        return ($this->field === 'pid') ? $this->getPages() : $this->getTags();
50    }
51
52    /**
53     * Processes all parts of the query for fetching tagged pages
54     *
55     * Returns SQL and query parameter values
56     *
57     * @return array
58     */
59    public function getPages()
60    {
61        $this->groupby = 'pid';
62        $this->orderby = "cnt DESC, pid";
63        if ($this->tags && $this->logicalAnd) $this->having = ' HAVING cnt = ' . count($this->tags);
64
65        return [$this->getSql(), $this->values];
66    }
67
68    /**
69     * Processes all parts of the query for fetching tags
70     *
71     * Returns SQL and query parameter values
72     *
73     * @return array
74     */
75    public function getTags()
76    {
77        $this->groupby = 'CLEANTAG(tag)';
78        $this->orderby = 'CLEANTAG(tag)';
79
80        return [$this->getSql(), $this->values];
81    }
82
83    /**
84     * Tags to search for
85     * @param array $tags
86     */
87    public function setTags($tags)
88    {
89        $this->tags = $tags;
90    }
91
92    /**
93     * Namespaces to limit search to
94     * @param array $ns
95     */
96    public function includeNS($ns)
97    {
98        $this->ns = $this->globNS($ns);
99    }
100
101    /**
102     * Namespaces to exclude from search
103     * @param array $ns
104     */
105    public function excludeNS($ns)
106    {
107        $this->notns = $this->globNS($ns);
108    }
109
110    /**
111     * Sets the logical operator used in tag search to AND
112     * @param bool $and
113     */
114    public function setLogicalAnd($and)
115    {
116        $this->logicalAnd = (bool)$and;
117    }
118
119    /**
120     * Result limit
121     * @param int $limit
122     */
123    public function setLimit($limit)
124    {
125        $this->limit = $limit;
126    }
127
128    /**
129     * Database field to select
130     * @param string $field
131     */
132    public function setField($field)
133    {
134        $this->field = $field;
135    }
136
137    /**
138     * Limit search to this page id
139     * @param string $pid
140     */
141    public function setPid($pid)
142    {
143        $this->pid = $pid;
144    }
145
146    /**
147     * Limit search to certain pages
148     *
149     * @param array $pids
150     */
151    public function setPids($pids)
152    {
153        $this->pids = $pids;
154    }
155
156    /**
157     * Limit results to this tagger
158     * @param string $tagger
159     */
160    public function setTagger($tagger)
161    {
162        $this->tagger = $tagger;
163    }
164
165    /**
166     * Returns full query SQL
167     * @return string
168     */
169    protected function getSql()
170    {
171        $sql = "SELECT $this->field AS item, COUNT(*) AS cnt
172                FROM taggings
173                WHERE " . $this->getWhere() .
174                " GROUP BY $this->groupby
175                $this->having
176                ORDER BY $this->orderby
177                ";
178
179        if ($this->limit) {
180            $sql .= ' LIMIT ?';
181            $this->values[] = $this->limit;
182        }
183
184        return $sql;
185    }
186
187    /**
188     * Builds the WHERE part of query string
189     * @return string
190     */
191    protected function getWhere()
192    {
193        $where = '1=1';
194
195        if ($this->pid) {
196            $where .= ' AND pid';
197            $where .= $this->useLike($this->pid) ? ' GLOB' : ' =';
198            $where .= '  ?';
199            $this->values[] = $this->pid;
200        }
201
202        if ($this->pids) {
203            $where .= ' AND pid';
204            $where .=  ' IN(';
205            foreach ($this->pids as $pid) {
206                $where .= '  ?,';
207                $this->values[] = $pid;
208            }
209            $where = rtrim($where, ',') . ')';
210        }
211
212        if ($this->tagger) {
213            $where .= ' AND tagger = ?';
214            $this->values[] = $this->tagger;
215        }
216
217        if ($this->ns) {
218            $where .= ' AND ';
219
220            $nsCnt = count($this->ns);
221            $i = 0;
222            foreach ($this->ns as $ns) {
223                $where .= ' pid';
224                $where .= ' GLOB';
225                $where .= ' ?';
226                if (++$i < $nsCnt) $where .= ' OR';
227                $this->values[] = $ns;
228            }
229        }
230
231        if ($this->notns) {
232            $where .= ' AND ';
233
234            $nsCnt = count($this->notns);
235            $i = 0;
236            foreach ($this->notns as $notns) {
237                $where .= ' pid';
238                $where .= ' NOT GLOB';
239                $where .= ' ?';
240                if (++$i < $nsCnt) $where .= ' AND';
241                $this->values[] = $notns;
242            }
243        }
244
245        if ($this->tags) {
246            $where .= ' AND ';
247
248            $tagCnt = count($this->tags);
249            $i = 0;
250            foreach ($this->tags as $tag) {
251                $where .= ' CLEANTAG(tag)';
252                $where .= $this->useLike($tag) ? ' GLOB' : ' =';
253                $where .= ' CLEANTAG(?)';
254                if (++$i < $tagCnt) $where .= ' OR';
255                $this->values[] = $tag;
256            }
257        }
258
259        // bypass page access check when called by a command line tool
260        if (php_sapi_name() !== 'cli' || defined('DOKU_UNITTEST')) {
261            $where .= ' AND GETACCESSLEVEL(pid) >= ' . AUTH_READ;
262        }
263
264        return $where;
265    }
266
267    /**
268     * Check if the given string is a LIKE statement
269     *
270     * @param string $value
271     * @return bool
272     */
273    protected function useLike($value) {
274        return strpos($value, '*') === 0 || strrpos($value, '*') === strlen($value) - 1;
275    }
276
277    /**
278     * Converts namespaces into a wildcard form suitable for SQL queries
279     *
280     * @param array $item
281     * @return array
282     */
283    protected function globNS(array $item)
284    {
285        return array_map(function($ns) {
286            return cleanId($ns) . '*';
287        }, $item);
288    }
289
290}
291