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 array 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 array 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->getSql(), $this->values]; 62 } 63 64 /** 65 * Processes all parts of the query for fetching tags 66 * 67 * Returns the query builder object 68 * 69 * @return array 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->getSql(), $this->values]; 78 } 79 80 /** 81 * Tags to search for 82 * @param array $tags 83 */ 84 public function setTags($tags) 85 { 86 $this->tags = $tags; 87 } 88 89 /** 90 * Namespaces to limit search to 91 * @param array $ns 92 */ 93 public function includeNS($ns) 94 { 95 $this->ns = $this->globNS($ns); 96 } 97 98 /** 99 * Namespaces to exclude from search 100 * @param array $ns 101 */ 102 public function excludeNS($ns) 103 { 104 $this->notns = $this->globNS($ns); 105 } 106 107 /** 108 * Sets the logical operator used in tag search to AND 109 * @param bool $and 110 */ 111 public function setLogicalAnd($and) 112 { 113 $this->logicalAnd = (bool)$and; 114 } 115 116 /** 117 * Result limit 118 * @param int $limit 119 */ 120 public function setLimit($limit) 121 { 122 $this->limit = $limit; 123 } 124 125 /** 126 * Database field to select 127 * @param string $field 128 */ 129 public function setField($field) 130 { 131 $this->field = $field; 132 } 133 134 /** 135 * Limit search to this page id 136 * @param string $pid 137 */ 138 public function setPid($pid) 139 { 140 $this->pid = $pid; 141 } 142 143 /** 144 * Limit results to this tagger 145 * @param string $tagger 146 */ 147 public function setTagger($tagger) 148 { 149 $this->tagger = $tagger; 150 } 151 152 /** 153 * Returns full query SQL 154 * @return string 155 */ 156 protected function getSql() 157 { 158 $sql = "SELECT $this->field AS item, COUNT(*) AS cnt 159 FROM taggings 160 WHERE $this->where 161 GROUP BY $this->groupby 162 $this->having 163 ORDER BY $this->orderby 164 "; 165 166 if ($this->limit) { 167 $sql .= ' LIMIT ?'; 168 $this->values[] = $this->limit; 169 } 170 171 return $sql; 172 } 173 174 /** 175 * Builds the WHERE part of query string 176 * @return string 177 */ 178 protected function getWhere() 179 { 180 $where = '1=1'; 181 182 if ($this->pid) { 183 $where .= ' AND pid'; 184 $where .= $this->useLike($this->pid) ? ' GLOB' : ' ='; 185 $where .= ' ?'; 186 $this->values[] = $this->pid; 187 } 188 189 if ($this->tagger) { 190 $where .= ' AND tagger = ?'; 191 $this->values[] = $this->tagger; 192 } 193 194 if ($this->ns) { 195 $where .= ' AND '; 196 197 $nsCnt = count($this->ns); 198 $i = 0; 199 foreach ($this->ns as $ns) { 200 $where .= ' pid'; 201 $where .= ' GLOB'; 202 $where .= ' ?'; 203 if (++$i < $nsCnt) $where .= ' OR'; 204 $this->values[] = $ns; 205 } 206 } 207 208 if ($this->notns) { 209 $where .= ' AND '; 210 211 $nsCnt = count($this->notns); 212 $i = 0; 213 foreach ($this->notns as $notns) { 214 $where .= ' pid'; 215 $where .= ' NOT GLOB'; 216 $where .= ' ?'; 217 if (++$i < $nsCnt) $where .= ' AND'; 218 $this->values[] = $notns; 219 } 220 } 221 222 if ($this->tags) { 223 $where .= ' AND '; 224 225 $tagCnt = count($this->tags); 226 $i = 0; 227 foreach ($this->tags as $tag) { 228 $where .= ' CLEANTAG(tag)'; 229 $where .= $this->useLike($tag) ? ' GLOB' : ' ='; 230 $where .= ' CLEANTAG(?)'; 231 if (++$i < $tagCnt) $where .= ' OR'; 232 $this->values[] = $tag; 233 } 234 } 235 236 $where .= ' AND GETACCESSLEVEL(pid) >= ' . AUTH_READ; 237 238 239 return $where; 240 } 241 242 /** 243 * Check if the given string is a LIKE statement 244 * 245 * @param string $value 246 * @return bool 247 */ 248 protected function useLike($value) { 249 return strpos($value, '*') === 0 || strrpos($value, '*') === strlen($value) - 1; 250 } 251 252 /** 253 * Converts namespaces into a wildcard form suitable for SQL queries 254 * 255 * @param array $item 256 * @return array 257 */ 258 protected function globNS(array $item) 259 { 260 return array_map(function($ns) { 261 return cleanId($ns) . '*'; 262 }, $item); 263 } 264 265} 266