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