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