11b4b4fa9SAnna Dabrowska<?php 2*df43a7beSAndreas Gohr 3*df43a7beSAndreas Gohruse dokuwiki\Extension\Plugin; 4*df43a7beSAndreas Gohr 51b4b4fa9SAnna Dabrowska/** 61b4b4fa9SAnna Dabrowska * Tagging Plugin (helper component) 71b4b4fa9SAnna Dabrowska */ 8*df43a7beSAndreas Gohrclass helper_plugin_tagging_querybuilder extends Plugin 9*df43a7beSAndreas Gohr{ 101b4b4fa9SAnna Dabrowska /** @var string */ 111b4b4fa9SAnna Dabrowska protected $field; 121b4b4fa9SAnna Dabrowska /** @var bool */ 131b4b4fa9SAnna Dabrowska protected $logicalAnd = false; 141b4b4fa9SAnna Dabrowska /** @var array */ 151b4b4fa9SAnna Dabrowska protected $tags = []; 161b4b4fa9SAnna Dabrowska /** @var array */ 171b4b4fa9SAnna Dabrowska protected $ns = []; 181b4b4fa9SAnna Dabrowska /** @var array */ 191b4b4fa9SAnna Dabrowska protected $notns = []; 201b4b4fa9SAnna Dabrowska /** @var string */ 211b4b4fa9SAnna Dabrowska protected $pid; 22cbe7b4baSAnna Dabrowska 23cbe7b4baSAnna Dabrowska /** 24cbe7b4baSAnna Dabrowska * FIXME consolidate pid (current page query) and pids (global search query) 25cbe7b4baSAnna Dabrowska * @var array 26cbe7b4baSAnna Dabrowska */ 27cbe7b4baSAnna Dabrowska protected $pids; 28cbe7b4baSAnna Dabrowska 291b4b4fa9SAnna Dabrowska /** @var string */ 301b4b4fa9SAnna Dabrowska protected $tagger = ''; 3165d49a60SAnna Dabrowska /** @var int */ 3265d49a60SAnna Dabrowska protected $limit; 331b4b4fa9SAnna Dabrowska /** @var string */ 341b4b4fa9SAnna Dabrowska protected $orderby; 351b4b4fa9SAnna Dabrowska /** @var string */ 361b4b4fa9SAnna Dabrowska protected $groupby; 371b4b4fa9SAnna Dabrowska /** @var string */ 381b4b4fa9SAnna Dabrowska protected $having = ''; 394a7da0a5SAnna Dabrowska /** @var array */ 404a7da0a5SAnna Dabrowska protected $values = []; 411b4b4fa9SAnna Dabrowska 421b4b4fa9SAnna Dabrowska /** 434a7da0a5SAnna Dabrowska * Shorthand method: calls the appropriate getter deduced from $this->field 441b4b4fa9SAnna Dabrowska * 4599122157SAnna Dabrowska * @return array 461b4b4fa9SAnna Dabrowska */ 471b4b4fa9SAnna Dabrowska public function getQuery() 481b4b4fa9SAnna Dabrowska { 491b4b4fa9SAnna Dabrowska if (!$this->field) { 501b4b4fa9SAnna Dabrowska throw new \RuntimeException('Failed to build a query, no field specified'); 511b4b4fa9SAnna Dabrowska } 521b4b4fa9SAnna Dabrowska return ($this->field === 'pid') ? $this->getPages() : $this->getTags(); 531b4b4fa9SAnna Dabrowska } 541b4b4fa9SAnna Dabrowska 551b4b4fa9SAnna Dabrowska /** 564a7da0a5SAnna Dabrowska * Processes all parts of the query for fetching tagged pages 571b4b4fa9SAnna Dabrowska * 589097c6c0SAnna Dabrowska * Returns SQL and query parameter values 594a7da0a5SAnna Dabrowska * 6099122157SAnna Dabrowska * @return array 611b4b4fa9SAnna Dabrowska */ 621b4b4fa9SAnna Dabrowska public function getPages() 631b4b4fa9SAnna Dabrowska { 641b4b4fa9SAnna Dabrowska $this->groupby = 'pid'; 651b4b4fa9SAnna Dabrowska $this->orderby = "cnt DESC, pid"; 661b4b4fa9SAnna Dabrowska if ($this->tags && $this->logicalAnd) $this->having = ' HAVING cnt = ' . count($this->tags); 671b4b4fa9SAnna Dabrowska 6899122157SAnna Dabrowska return [$this->getSql(), $this->values]; 691b4b4fa9SAnna Dabrowska } 701b4b4fa9SAnna Dabrowska 711b4b4fa9SAnna Dabrowska /** 724a7da0a5SAnna Dabrowska * Processes all parts of the query for fetching tags 731b4b4fa9SAnna Dabrowska * 749097c6c0SAnna Dabrowska * Returns SQL and query parameter values 754a7da0a5SAnna Dabrowska * 7699122157SAnna Dabrowska * @return array 771b4b4fa9SAnna Dabrowska */ 781b4b4fa9SAnna Dabrowska public function getTags() 791b4b4fa9SAnna Dabrowska { 801b4b4fa9SAnna Dabrowska $this->groupby = 'CLEANTAG(tag)'; 811b4b4fa9SAnna Dabrowska $this->orderby = 'CLEANTAG(tag)'; 821b4b4fa9SAnna Dabrowska 8399122157SAnna Dabrowska return [$this->getSql(), $this->values]; 844a7da0a5SAnna Dabrowska } 854a7da0a5SAnna Dabrowska 864a7da0a5SAnna Dabrowska /** 874a7da0a5SAnna Dabrowska * Tags to search for 881b4b4fa9SAnna Dabrowska * @param array $tags 891b4b4fa9SAnna Dabrowska */ 901b4b4fa9SAnna Dabrowska public function setTags($tags) 911b4b4fa9SAnna Dabrowska { 921b4b4fa9SAnna Dabrowska $this->tags = $tags; 931b4b4fa9SAnna Dabrowska } 941b4b4fa9SAnna Dabrowska 951b4b4fa9SAnna Dabrowska /** 961b4b4fa9SAnna Dabrowska * Namespaces to limit search to 971b4b4fa9SAnna Dabrowska * @param array $ns 981b4b4fa9SAnna Dabrowska */ 991b4b4fa9SAnna Dabrowska public function includeNS($ns) 1001b4b4fa9SAnna Dabrowska { 1014a7da0a5SAnna Dabrowska $this->ns = $this->globNS($ns); 1021b4b4fa9SAnna Dabrowska } 1031b4b4fa9SAnna Dabrowska 1041b4b4fa9SAnna Dabrowska /** 1051b4b4fa9SAnna Dabrowska * Namespaces to exclude from search 1061b4b4fa9SAnna Dabrowska * @param array $ns 1071b4b4fa9SAnna Dabrowska */ 1081b4b4fa9SAnna Dabrowska public function excludeNS($ns) 1091b4b4fa9SAnna Dabrowska { 1104a7da0a5SAnna Dabrowska $this->notns = $this->globNS($ns); 1111b4b4fa9SAnna Dabrowska } 1121b4b4fa9SAnna Dabrowska 1131b4b4fa9SAnna Dabrowska /** 1144a7da0a5SAnna Dabrowska * Sets the logical operator used in tag search to AND 1151b4b4fa9SAnna Dabrowska * @param bool $and 1161b4b4fa9SAnna Dabrowska */ 1171b4b4fa9SAnna Dabrowska public function setLogicalAnd($and) 1181b4b4fa9SAnna Dabrowska { 11965d49a60SAnna Dabrowska $this->logicalAnd = (bool)$and; 1201b4b4fa9SAnna Dabrowska } 1211b4b4fa9SAnna Dabrowska 1221b4b4fa9SAnna Dabrowska /** 1234a7da0a5SAnna Dabrowska * Result limit 12465d49a60SAnna Dabrowska * @param int $limit 1251b4b4fa9SAnna Dabrowska */ 1261b4b4fa9SAnna Dabrowska public function setLimit($limit) 1271b4b4fa9SAnna Dabrowska { 12865d49a60SAnna Dabrowska $this->limit = $limit; 1291b4b4fa9SAnna Dabrowska } 1301b4b4fa9SAnna Dabrowska 1311b4b4fa9SAnna Dabrowska /** 1324a7da0a5SAnna Dabrowska * Database field to select 1331b4b4fa9SAnna Dabrowska * @param string $field 1341b4b4fa9SAnna Dabrowska */ 1351b4b4fa9SAnna Dabrowska public function setField($field) 1361b4b4fa9SAnna Dabrowska { 1371b4b4fa9SAnna Dabrowska $this->field = $field; 1381b4b4fa9SAnna Dabrowska } 1391b4b4fa9SAnna Dabrowska 1401b4b4fa9SAnna Dabrowska /** 1414a7da0a5SAnna Dabrowska * Limit search to this page id 1421b4b4fa9SAnna Dabrowska * @param string $pid 1431b4b4fa9SAnna Dabrowska */ 1441b4b4fa9SAnna Dabrowska public function setPid($pid) 1451b4b4fa9SAnna Dabrowska { 1461b4b4fa9SAnna Dabrowska $this->pid = $pid; 1471b4b4fa9SAnna Dabrowska } 1481b4b4fa9SAnna Dabrowska 1491b4b4fa9SAnna Dabrowska /** 150cbe7b4baSAnna Dabrowska * Limit search to certain pages 151cbe7b4baSAnna Dabrowska * 152cbe7b4baSAnna Dabrowska * @param array $pids 153cbe7b4baSAnna Dabrowska */ 154cbe7b4baSAnna Dabrowska public function setPids($pids) 155cbe7b4baSAnna Dabrowska { 156cbe7b4baSAnna Dabrowska $this->pids = $pids; 157cbe7b4baSAnna Dabrowska } 158cbe7b4baSAnna Dabrowska 159cbe7b4baSAnna Dabrowska /** 1604a7da0a5SAnna Dabrowska * Limit results to this tagger 1611b4b4fa9SAnna Dabrowska * @param string $tagger 1621b4b4fa9SAnna Dabrowska */ 1631b4b4fa9SAnna Dabrowska public function setTagger($tagger) 1641b4b4fa9SAnna Dabrowska { 1651b4b4fa9SAnna Dabrowska $this->tagger = $tagger; 1661b4b4fa9SAnna Dabrowska } 1671b4b4fa9SAnna Dabrowska 1681b4b4fa9SAnna Dabrowska /** 1694a7da0a5SAnna Dabrowska * Returns full query SQL 1701b4b4fa9SAnna Dabrowska * @return string 1711b4b4fa9SAnna Dabrowska */ 17299122157SAnna Dabrowska protected function getSql() 1731b4b4fa9SAnna Dabrowska { 1741b4b4fa9SAnna Dabrowska $sql = "SELECT $this->field AS item, COUNT(*) AS cnt 1751b4b4fa9SAnna Dabrowska FROM taggings 1769097c6c0SAnna Dabrowska WHERE " . $this->getWhere() . 1779097c6c0SAnna Dabrowska " GROUP BY $this->groupby 1781b4b4fa9SAnna Dabrowska $this->having 1791b4b4fa9SAnna Dabrowska ORDER BY $this->orderby 1801b4b4fa9SAnna Dabrowska "; 1811b4b4fa9SAnna Dabrowska 18265d49a60SAnna Dabrowska if ($this->limit) { 18365d49a60SAnna Dabrowska $sql .= ' LIMIT ?'; 18465d49a60SAnna Dabrowska $this->values[] = $this->limit; 18565d49a60SAnna Dabrowska } 18665d49a60SAnna Dabrowska 1871b4b4fa9SAnna Dabrowska return $sql; 1881b4b4fa9SAnna Dabrowska } 1891b4b4fa9SAnna Dabrowska 1901b4b4fa9SAnna Dabrowska /** 1914a7da0a5SAnna Dabrowska * Builds the WHERE part of query string 1921b4b4fa9SAnna Dabrowska * @return string 1931b4b4fa9SAnna Dabrowska */ 1941b4b4fa9SAnna Dabrowska protected function getWhere() 1951b4b4fa9SAnna Dabrowska { 1961b4b4fa9SAnna Dabrowska $where = '1=1'; 1971b4b4fa9SAnna Dabrowska 1981b4b4fa9SAnna Dabrowska if ($this->pid) { 19965d49a60SAnna Dabrowska $where .= ' AND pid'; 20065d49a60SAnna Dabrowska $where .= $this->useLike($this->pid) ? ' GLOB' : ' ='; 20165d49a60SAnna Dabrowska $where .= ' ?'; 2024a7da0a5SAnna Dabrowska $this->values[] = $this->pid; 2031b4b4fa9SAnna Dabrowska } 2041b4b4fa9SAnna Dabrowska 205cbe7b4baSAnna Dabrowska if ($this->pids) { 206cbe7b4baSAnna Dabrowska $where .= ' AND pid'; 207cbe7b4baSAnna Dabrowska $where .= ' IN('; 208cbe7b4baSAnna Dabrowska foreach ($this->pids as $pid) { 209cbe7b4baSAnna Dabrowska $where .= ' ?,'; 210cbe7b4baSAnna Dabrowska $this->values[] = $pid; 211cbe7b4baSAnna Dabrowska } 212cbe7b4baSAnna Dabrowska $where = rtrim($where, ',') . ')'; 213cbe7b4baSAnna Dabrowska } 214cbe7b4baSAnna Dabrowska 2151b4b4fa9SAnna Dabrowska if ($this->tagger) { 2161b4b4fa9SAnna Dabrowska $where .= ' AND tagger = ?'; 2174a7da0a5SAnna Dabrowska $this->values[] = $this->tagger; 2181b4b4fa9SAnna Dabrowska } 2191b4b4fa9SAnna Dabrowska 2201b4b4fa9SAnna Dabrowska if ($this->ns) { 2211b4b4fa9SAnna Dabrowska $where .= ' AND '; 2221b4b4fa9SAnna Dabrowska 2231b4b4fa9SAnna Dabrowska $nsCnt = count($this->ns); 2241b4b4fa9SAnna Dabrowska $i = 0; 2251b4b4fa9SAnna Dabrowska foreach ($this->ns as $ns) { 2261b4b4fa9SAnna Dabrowska $where .= ' pid'; 2271b4b4fa9SAnna Dabrowska $where .= ' GLOB'; 2281b4b4fa9SAnna Dabrowska $where .= ' ?'; 2291b4b4fa9SAnna Dabrowska if (++$i < $nsCnt) $where .= ' OR'; 2304a7da0a5SAnna Dabrowska $this->values[] = $ns; 2311b4b4fa9SAnna Dabrowska } 2321b4b4fa9SAnna Dabrowska } 2331b4b4fa9SAnna Dabrowska 2341b4b4fa9SAnna Dabrowska if ($this->notns) { 2351b4b4fa9SAnna Dabrowska $where .= ' AND '; 2361b4b4fa9SAnna Dabrowska 2371b4b4fa9SAnna Dabrowska $nsCnt = count($this->notns); 2381b4b4fa9SAnna Dabrowska $i = 0; 2391b4b4fa9SAnna Dabrowska foreach ($this->notns as $notns) { 2401b4b4fa9SAnna Dabrowska $where .= ' pid'; 2411b4b4fa9SAnna Dabrowska $where .= ' NOT GLOB'; 2421b4b4fa9SAnna Dabrowska $where .= ' ?'; 2431b4b4fa9SAnna Dabrowska if (++$i < $nsCnt) $where .= ' AND'; 2444a7da0a5SAnna Dabrowska $this->values[] = $notns; 2451b4b4fa9SAnna Dabrowska } 2461b4b4fa9SAnna Dabrowska } 2471b4b4fa9SAnna Dabrowska 2481b4b4fa9SAnna Dabrowska if ($this->tags) { 2491b4b4fa9SAnna Dabrowska $where .= ' AND '; 2501b4b4fa9SAnna Dabrowska 2511b4b4fa9SAnna Dabrowska $tagCnt = count($this->tags); 2521b4b4fa9SAnna Dabrowska $i = 0; 2531b4b4fa9SAnna Dabrowska foreach ($this->tags as $tag) { 2541b4b4fa9SAnna Dabrowska $where .= ' CLEANTAG(tag)'; 2551b4b4fa9SAnna Dabrowska $where .= $this->useLike($tag) ? ' GLOB' : ' ='; 2561b4b4fa9SAnna Dabrowska $where .= ' CLEANTAG(?)'; 2571b4b4fa9SAnna Dabrowska if (++$i < $tagCnt) $where .= ' OR'; 2584a7da0a5SAnna Dabrowska $this->values[] = $tag; 2591b4b4fa9SAnna Dabrowska } 2601b4b4fa9SAnna Dabrowska } 2611b4b4fa9SAnna Dabrowska 2629dd6a05bSAnna Dabrowska // bypass page access check when called by a command line tool 263*df43a7beSAndreas Gohr if (PHP_SAPI !== 'cli' || defined('DOKU_UNITTEST')) { 2641b4b4fa9SAnna Dabrowska $where .= ' AND GETACCESSLEVEL(pid) >= ' . AUTH_READ; 2659dd6a05bSAnna Dabrowska } 2661b4b4fa9SAnna Dabrowska 2671b4b4fa9SAnna Dabrowska return $where; 2681b4b4fa9SAnna Dabrowska } 2691b4b4fa9SAnna Dabrowska 2701b4b4fa9SAnna Dabrowska /** 2711b4b4fa9SAnna Dabrowska * Check if the given string is a LIKE statement 2721b4b4fa9SAnna Dabrowska * 2731b4b4fa9SAnna Dabrowska * @param string $value 2741b4b4fa9SAnna Dabrowska * @return bool 2751b4b4fa9SAnna Dabrowska */ 276*df43a7beSAndreas Gohr protected function useLike($value) 277*df43a7beSAndreas Gohr { 278*df43a7beSAndreas Gohr return str_starts_with($value, '*') || strrpos($value, '*') === strlen($value) - 1; 2791b4b4fa9SAnna Dabrowska } 2804a7da0a5SAnna Dabrowska 2814a7da0a5SAnna Dabrowska /** 2824a7da0a5SAnna Dabrowska * Converts namespaces into a wildcard form suitable for SQL queries 2834a7da0a5SAnna Dabrowska * 2844a7da0a5SAnna Dabrowska * @param array $item 2854a7da0a5SAnna Dabrowska * @return array 2864a7da0a5SAnna Dabrowska */ 2874a7da0a5SAnna Dabrowska protected function globNS(array $item) 2884a7da0a5SAnna Dabrowska { 289*df43a7beSAndreas Gohr return array_map(fn($ns) => cleanId($ns) . '*', $item); 2904a7da0a5SAnna Dabrowska } 2911b4b4fa9SAnna Dabrowska} 292