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