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