14a5f6748SAnna Dabrowska<?php 24a5f6748SAnna Dabrowska 3df43a7beSAndreas Gohruse dokuwiki\Extension\ActionPlugin; 4df43a7beSAndreas Gohruse dokuwiki\Extension\EventHandler; 5df43a7beSAndreas Gohruse dokuwiki\Extension\Event; 6df43a7beSAndreas Gohruse dokuwiki\Ui\SearchState; 7df43a7beSAndreas Gohruse dokuwiki\Form\InputElement; 8df43a7beSAndreas Gohr 94a5f6748SAnna Dabrowska/** 104a5f6748SAnna Dabrowska * Class action_plugin_tagging_search 114a5f6748SAnna Dabrowska */ 12df43a7beSAndreas Gohrclass action_plugin_tagging_search extends ActionPlugin 134a5f6748SAnna Dabrowska{ 144a5f6748SAnna Dabrowska /** 154a5f6748SAnna Dabrowska * @var array 164a5f6748SAnna Dabrowska */ 174a5f6748SAnna Dabrowska protected $tagFilter = []; 184a5f6748SAnna Dabrowska 194a5f6748SAnna Dabrowska /** 204a5f6748SAnna Dabrowska * @var array 214a5f6748SAnna Dabrowska */ 224a5f6748SAnna Dabrowska protected $allTagsByPage = []; 234a5f6748SAnna Dabrowska 244a5f6748SAnna Dabrowska /** 254a5f6748SAnna Dabrowska * @var string 264a5f6748SAnna Dabrowska */ 274a5f6748SAnna Dabrowska protected $originalQuery = ''; 284a5f6748SAnna Dabrowska 294a5f6748SAnna Dabrowska /** 304a5f6748SAnna Dabrowska * Register handlers 314a5f6748SAnna Dabrowska * 32df43a7beSAndreas Gohr * @param EventHandler $controller 334a5f6748SAnna Dabrowska */ 34df43a7beSAndreas Gohr public function register(EventHandler $controller) 354a5f6748SAnna Dabrowska { 364a5f6748SAnna Dabrowska $controller->register_hook( 37df43a7beSAndreas Gohr 'TPL_CONTENT_DISPLAY', 38df43a7beSAndreas Gohr 'BEFORE', 39df43a7beSAndreas Gohr $this, 404a5f6748SAnna Dabrowska 'echo_searchresults' 414a5f6748SAnna Dabrowska ); 424a5f6748SAnna Dabrowska 434a5f6748SAnna Dabrowska $controller->register_hook( 44df43a7beSAndreas Gohr 'TPL_ACT_RENDER', 45df43a7beSAndreas Gohr 'BEFORE', 46df43a7beSAndreas Gohr $this, 474a5f6748SAnna Dabrowska 'setupTagSearchDoku' 484a5f6748SAnna Dabrowska ); 494a5f6748SAnna Dabrowska 504a5f6748SAnna Dabrowska $controller->register_hook( 51df43a7beSAndreas Gohr 'SEARCH_QUERY_FULLPAGE', 52df43a7beSAndreas Gohr 'AFTER', 53df43a7beSAndreas Gohr $this, 544a5f6748SAnna Dabrowska 'filterSearchResults' 554a5f6748SAnna Dabrowska ); 564a5f6748SAnna Dabrowska 574a5f6748SAnna Dabrowska $controller->register_hook( 58df43a7beSAndreas Gohr 'SEARCH_RESULT_FULLPAGE', 59df43a7beSAndreas Gohr 'AFTER', 60df43a7beSAndreas Gohr $this, 614a5f6748SAnna Dabrowska 'tagResults' 624a5f6748SAnna Dabrowska ); 634a5f6748SAnna Dabrowska 644a5f6748SAnna Dabrowska $controller->register_hook( 65df43a7beSAndreas Gohr 'FORM_SEARCH_OUTPUT', 66df43a7beSAndreas Gohr 'BEFORE', 67df43a7beSAndreas Gohr $this, 684a5f6748SAnna Dabrowska 'addSwitchToSearchForm' 694a5f6748SAnna Dabrowska ); 704a5f6748SAnna Dabrowska } 714a5f6748SAnna Dabrowska 724a5f6748SAnna Dabrowska /** 734a5f6748SAnna Dabrowska * Add AND/OR switch to advanced search tools 744a5f6748SAnna Dabrowska * 75df43a7beSAndreas Gohr * @param Event $event 764a5f6748SAnna Dabrowska * @param $param 774a5f6748SAnna Dabrowska */ 78df43a7beSAndreas Gohr public function addSwitchToSearchForm(Event $event, $param) 794a5f6748SAnna Dabrowska { 804a5f6748SAnna Dabrowska global $INPUT; 814a5f6748SAnna Dabrowska 82df43a7beSAndreas Gohr /* @var dokuwiki\Form\Form $searchForm */ 834a5f6748SAnna Dabrowska $searchForm = $event->data; 844a5f6748SAnna Dabrowska $currElemPos = $searchForm->findPositionByAttribute('class', 'advancedOptions'); 854a5f6748SAnna Dabrowska 864a5f6748SAnna Dabrowska // the actual filter is built in Javascript 874a5f6748SAnna Dabrowska $searchForm->addTagOpen('div', ++$currElemPos) 884a5f6748SAnna Dabrowska ->addClass('toggle') 894a5f6748SAnna Dabrowska ->attr('aria-haspopup', 'true') 904a5f6748SAnna Dabrowska ->id('plugin__tagging-tags'); 914a5f6748SAnna Dabrowska // this element needs to be rendered by the backend so that all JS events properly attach 924a5f6748SAnna Dabrowska $searchForm->addTagOpen('div', ++$currElemPos) 934a5f6748SAnna Dabrowska ->addClass('current'); 944a5f6748SAnna Dabrowska $searchForm->addHTML($this->getLang('search_filter_label'), ++$currElemPos); 954a5f6748SAnna Dabrowska $searchForm->addTagClose('div', ++$currElemPos); 964a5f6748SAnna Dabrowska $searchForm->addTagClose('div', ++$currElemPos); 974a5f6748SAnna Dabrowska 984a5f6748SAnna Dabrowska // set active setting 994a5f6748SAnna Dabrowska $active = ''; 1004a5f6748SAnna Dabrowska if ($INPUT->has('tagging-logic')) { 1014a5f6748SAnna Dabrowska $active = $INPUT->str('tagging-logic'); 1024a5f6748SAnna Dabrowska } 1034a5f6748SAnna Dabrowska $searchForm->setHiddenField('tagging-logic', $active); 1044a5f6748SAnna Dabrowska 1054a5f6748SAnna Dabrowska $searchForm->addTagOpen('div', ++$currElemPos) 1064a5f6748SAnna Dabrowska ->addClass('toggle') 1074a5f6748SAnna Dabrowska ->attr('aria-haspopup', 'true') 1084a5f6748SAnna Dabrowska ->id('plugin__tagging-logic'); 1094a5f6748SAnna Dabrowska 1104a5f6748SAnna Dabrowska // popup toggler 1114a5f6748SAnna Dabrowska $toggler = $searchForm->addTagOpen('div', ++$currElemPos)->addClass('current'); 1124a5f6748SAnna Dabrowska 1134a5f6748SAnna Dabrowska // current item 114df43a7beSAndreas Gohr if ($active === 'and') { 1154a5f6748SAnna Dabrowska $currentLabel = $this->getLang('search_all_tags'); 1164a5f6748SAnna Dabrowska $toggler->addClass('changed'); 1174a5f6748SAnna Dabrowska } else { 1184a5f6748SAnna Dabrowska $currentLabel = $this->getLang('search_any_tag'); 1194a5f6748SAnna Dabrowska } 1204a5f6748SAnna Dabrowska 1214a5f6748SAnna Dabrowska $searchForm->addHTML($currentLabel, ++$currElemPos); 1224a5f6748SAnna Dabrowska $searchForm->addTagClose('div', ++$currElemPos); 1234a5f6748SAnna Dabrowska 1244a5f6748SAnna Dabrowska // options 1254a5f6748SAnna Dabrowska $options = [ 1264a5f6748SAnna Dabrowska 'or' => $this->getLang('search_any_tag'), 1274a5f6748SAnna Dabrowska 'and' => $this->getLang('search_all_tags'), 1284a5f6748SAnna Dabrowska ]; 1294a5f6748SAnna Dabrowska $searchForm->addTagOpen('ul', ++$currElemPos)->attr('aria-expanded', 'false'); 1304a5f6748SAnna Dabrowska foreach ($options as $key => $label) { 1314a5f6748SAnna Dabrowska $listItem = $searchForm->addTagOpen('li', ++$currElemPos); 1324a5f6748SAnna Dabrowska if ($active && $key === $active) { 1334a5f6748SAnna Dabrowska $listItem->addClass('active'); 1344a5f6748SAnna Dabrowska } 1354a5f6748SAnna Dabrowska $link = $this->getSettingsLink($label, 'tagging-logic', $key); 1364a5f6748SAnna Dabrowska $searchForm->addHTML($link, ++$currElemPos); 1374a5f6748SAnna Dabrowska $searchForm->addTagClose('li', ++$currElemPos); 1384a5f6748SAnna Dabrowska } 1394a5f6748SAnna Dabrowska $searchForm->addTagClose('ul', ++$currElemPos); 1404a5f6748SAnna Dabrowska 1414a5f6748SAnna Dabrowska $searchForm->addTagClose('div', ++$currElemPos); 1424a5f6748SAnna Dabrowska 1434a5f6748SAnna Dabrowska // restore query with tags in the search form 1444a5f6748SAnna Dabrowska if ($this->tagFilter) { 145df43a7beSAndreas Gohr /** @var InputElement $q */ 1464a5f6748SAnna Dabrowska $q = $searchForm->getElementAt($searchForm->findPositionByAttribute('name', 'q')); 1474a5f6748SAnna Dabrowska $q->val($this->originalQuery); 1484a5f6748SAnna Dabrowska } 1494a5f6748SAnna Dabrowska } 1504a5f6748SAnna Dabrowska 1514a5f6748SAnna Dabrowska /** 1524a5f6748SAnna Dabrowska * Extracts tags from query and temporarily removes them 1534a5f6748SAnna Dabrowska * to prevent running fulltext search on tags as simple terms. 1544a5f6748SAnna Dabrowska * 155df43a7beSAndreas Gohr * @param Event $event 1564a5f6748SAnna Dabrowska * @param $param 1574a5f6748SAnna Dabrowska */ 158df43a7beSAndreas Gohr public function setupTagSearchDoku(Event $event, $param) 1594a5f6748SAnna Dabrowska { 1604a5f6748SAnna Dabrowska if ($event->data !== 'search' || !plugin_isdisabled('elasticsearch')) { 1614a5f6748SAnna Dabrowska return; 1624a5f6748SAnna Dabrowska } 1634a5f6748SAnna Dabrowska 1644a5f6748SAnna Dabrowska // allTagsByPage will be accessed by individual search results in SEARCH_RESULT_FULLPAGE event 1654a5f6748SAnna Dabrowska // and when displaying tag suggestions 1664a5f6748SAnna Dabrowska /** @var helper_plugin_tagging $hlp */ 1674a5f6748SAnna Dabrowska $hlp = plugin_load('helper', 'tagging'); 1684a5f6748SAnna Dabrowska $this->allTagsByPage = $hlp->getAllTagsByPage(); 1694a5f6748SAnna Dabrowska 1704a5f6748SAnna Dabrowska global $QUERY; 171df43a7beSAndreas Gohr if (!str_contains($QUERY, '#')) { 1724a5f6748SAnna Dabrowska return; 1734a5f6748SAnna Dabrowska } 1744a5f6748SAnna Dabrowska 1754a5f6748SAnna Dabrowska $this->originalQuery = $QUERY; 1764a5f6748SAnna Dabrowska 1774a5f6748SAnna Dabrowska // get (hash)tags from query 1784a5f6748SAnna Dabrowska preg_match_all('/(?:#)(\w+)/u', $QUERY, $matches); 1794a5f6748SAnna Dabrowska if (isset($matches[1])) { 1804a5f6748SAnna Dabrowska $this->tagFilter += array_map([$hlp, 'cleanTag'], $matches[1]); 1814a5f6748SAnna Dabrowska } 1824a5f6748SAnna Dabrowska 1834a5f6748SAnna Dabrowska // remove tags from query before search is executed 1844a5f6748SAnna Dabrowska self::removeTagsFromQuery($QUERY); 1854a5f6748SAnna Dabrowska } 1864a5f6748SAnna Dabrowska 1874a5f6748SAnna Dabrowska /** 1884a5f6748SAnna Dabrowska * If tags are found in query, the results are filtered, 1894a5f6748SAnna Dabrowska * or, with an empty query, tag search results are returned. 1904a5f6748SAnna Dabrowska * 191df43a7beSAndreas Gohr * @param Event $event 1924a5f6748SAnna Dabrowska * @param $param 1934a5f6748SAnna Dabrowska */ 194df43a7beSAndreas Gohr public function filterSearchResults(Event $event, $param) 1954a5f6748SAnna Dabrowska { 1964a5f6748SAnna Dabrowska if (!$this->tagFilter) { 1974a5f6748SAnna Dabrowska return; 1984a5f6748SAnna Dabrowska } 1994a5f6748SAnna Dabrowska 2004a5f6748SAnna Dabrowska /** @var helper_plugin_tagging $hlp */ 2014a5f6748SAnna Dabrowska $hlp = plugin_load('helper', 'tagging'); 2024a5f6748SAnna Dabrowska 2034a5f6748SAnna Dabrowska // search for tagged pages 2044a5f6748SAnna Dabrowska $pages = $hlp->searchPages($this->tagFilter); 2054a5f6748SAnna Dabrowska if (!$pages) { 2064a5f6748SAnna Dabrowska $event->result = []; 2074a5f6748SAnna Dabrowska return; 2084a5f6748SAnna Dabrowska } 2094a5f6748SAnna Dabrowska 2104a5f6748SAnna Dabrowska // tag search only, without additional terms 2114a5f6748SAnna Dabrowska if (!trim($event->data['query'])) { 2124a5f6748SAnna Dabrowska $event->result = $pages; 2134a5f6748SAnna Dabrowska } 2144a5f6748SAnna Dabrowska 2154a5f6748SAnna Dabrowska // apply filter 2164a5f6748SAnna Dabrowska $tagged = array_keys($pages); 2174a5f6748SAnna Dabrowska foreach ($event->result as $id => $count) { 2184a5f6748SAnna Dabrowska if (!in_array($id, $tagged)) { 2194a5f6748SAnna Dabrowska unset($event->result[$id]); 2204a5f6748SAnna Dabrowska } 2214a5f6748SAnna Dabrowska } 2224a5f6748SAnna Dabrowska } 2234a5f6748SAnna Dabrowska 2244a5f6748SAnna Dabrowska /** 2254a5f6748SAnna Dabrowska * Add tag links to all search results 2264a5f6748SAnna Dabrowska * 227df43a7beSAndreas Gohr * @param Event $event 2284a5f6748SAnna Dabrowska * @param $param 2294a5f6748SAnna Dabrowska */ 230df43a7beSAndreas Gohr public function tagResults(Event $event, $param) 2314a5f6748SAnna Dabrowska { 2324a5f6748SAnna Dabrowska $page = $event->data['page']; 2330801e51aSAnna Dabrowska $tags = $this->allTagsByPage[$page] ?? null; 2344a5f6748SAnna Dabrowska if ($tags) { 2354a5f6748SAnna Dabrowska foreach ($tags as $tag) { 2364a5f6748SAnna Dabrowska $event->data['resultHeader'][] = $this->getSettingsLink('#' . $tag, 'q', '#' . $tag); 2374a5f6748SAnna Dabrowska } 2384a5f6748SAnna Dabrowska } 2394a5f6748SAnna Dabrowska } 2404a5f6748SAnna Dabrowska 2414a5f6748SAnna Dabrowska /** 2424a5f6748SAnna Dabrowska * Show tags that are similar to the terms used in search 2434a5f6748SAnna Dabrowska * 244df43a7beSAndreas Gohr * @param Event $event 2454a5f6748SAnna Dabrowska * @param $param 2464a5f6748SAnna Dabrowska */ 247df43a7beSAndreas Gohr public function echo_searchresults(Event $event, $param) 248df43a7beSAndreas Gohr { 2494a5f6748SAnna Dabrowska global $ACT; 2504a5f6748SAnna Dabrowska global $QUERY; 2514a5f6748SAnna Dabrowska 2524a5f6748SAnna Dabrowska if ($ACT !== 'search') { 2534a5f6748SAnna Dabrowska return; 2544a5f6748SAnna Dabrowska } 2554a5f6748SAnna Dabrowska 2564a5f6748SAnna Dabrowska if (!plugin_isdisabled('elasticsearch')) return; 2574a5f6748SAnna Dabrowska 2584a5f6748SAnna Dabrowska /** @var helper_plugin_tagging $hlp */ 2594a5f6748SAnna Dabrowska $hlp = plugin_load('helper', 'tagging'); 2604a5f6748SAnna Dabrowska 2614a5f6748SAnna Dabrowska $terms = $hlp->extractFromQuery(ft_queryParser(idx_get_indexer(), $QUERY)); 2624a5f6748SAnna Dabrowska if (!$terms) { 2634a5f6748SAnna Dabrowska return; 2644a5f6748SAnna Dabrowska } 2654a5f6748SAnna Dabrowska 2664a5f6748SAnna Dabrowska $allTags = []; 267df43a7beSAndreas Gohr foreach ($this->allTagsByPage as $tags) { 2684a5f6748SAnna Dabrowska $allTags = array_merge($allTags, $tags); 2694a5f6748SAnna Dabrowska } 2704a5f6748SAnna Dabrowska $allTags = array_unique($allTags); 2714a5f6748SAnna Dabrowska 2724a5f6748SAnna Dabrowska $suggestedTags = []; 2734a5f6748SAnna Dabrowska foreach ($terms as $term) { 2744a5f6748SAnna Dabrowska $term = str_replace('*', '', $term); 275*581cea88SAnna Dabrowska $suggestedTags = array_merge($suggestedTags, preg_grep('/' . preg_quote($term) . '/i', $allTags)); 2764a5f6748SAnna Dabrowska } 2774a5f6748SAnna Dabrowska sort($suggestedTags); 2784a5f6748SAnna Dabrowska 2794a5f6748SAnna Dabrowska if (!$suggestedTags) { 2804a5f6748SAnna Dabrowska $this->originalQuery && $this->restoreSearchQuery(); 2814a5f6748SAnna Dabrowska return; 2824a5f6748SAnna Dabrowska } 2834a5f6748SAnna Dabrowska 2844a5f6748SAnna Dabrowska // create output HTML: tag search links 2854a5f6748SAnna Dabrowska $results = '<div class="search_quickresult">'; 2864a5f6748SAnna Dabrowska $results .= '<h2>' . $this->getLang('search_suggestions') . '</h2>'; 2874a5f6748SAnna Dabrowska $results .= '<ul class="search_quickhits">'; 2884a5f6748SAnna Dabrowska 2894a5f6748SAnna Dabrowska foreach ($suggestedTags as $tag) { 2904a5f6748SAnna Dabrowska $results .= '<li><div class="li">'; 2914a5f6748SAnna Dabrowska $results .= $this->getSettingsLink('#' . $tag, 'q', '#' . $tag); 2924a5f6748SAnna Dabrowska $results .= '</div></li>'; 2934a5f6748SAnna Dabrowska } 2944a5f6748SAnna Dabrowska $results .= '</ul>'; 2954a5f6748SAnna Dabrowska $results .= '<div class="clearer"></div>'; 2964a5f6748SAnna Dabrowska $results .= '</div>'; 2974a5f6748SAnna Dabrowska 2984a5f6748SAnna Dabrowska if (preg_match('/<div class="nothing">.*?<\/div>/', $event->data)) { 2994a5f6748SAnna Dabrowska // there are no other hits, replace the nothing found 3004a5f6748SAnna Dabrowska $event->data = preg_replace('/<div class="nothing">.*?<\/div>/', $results, $event->data, 1); 3014a5f6748SAnna Dabrowska } elseif (preg_match('/(<\/h2>)/', $event->data)) { 3024a5f6748SAnna Dabrowska // insert it right before second level headline 3034a5f6748SAnna Dabrowska $event->data = preg_replace('/(<h2>)/', $results . "\\1\n", $event->data, 1); 3044a5f6748SAnna Dabrowska } else { 3054a5f6748SAnna Dabrowska // unclear what happened, let's just append 3064a5f6748SAnna Dabrowska $event->data .= $results; 3074a5f6748SAnna Dabrowska } 3084a5f6748SAnna Dabrowska 3094a5f6748SAnna Dabrowska // all done, finally restore the original query 3104a5f6748SAnna Dabrowska $this->restoreSearchQuery(); 3114a5f6748SAnna Dabrowska } 3124a5f6748SAnna Dabrowska 3134a5f6748SAnna Dabrowska /** 3144a5f6748SAnna Dabrowska * Remove tags from query 3154a5f6748SAnna Dabrowska * 3164a5f6748SAnna Dabrowska * @param string $q 3174a5f6748SAnna Dabrowska */ 3184a5f6748SAnna Dabrowska public static function removeTagsFromQuery(&$q) 3194a5f6748SAnna Dabrowska { 3204a5f6748SAnna Dabrowska $q = preg_replace('/#\w+/u', '', $q); 3214a5f6748SAnna Dabrowska } 3224a5f6748SAnna Dabrowska 3234a5f6748SAnna Dabrowska /** 3244a5f6748SAnna Dabrowska * Restore original query on exit 3254a5f6748SAnna Dabrowska */ 3264a5f6748SAnna Dabrowska protected function restoreSearchQuery() 3274a5f6748SAnna Dabrowska { 3284a5f6748SAnna Dabrowska global $QUERY; 3294a5f6748SAnna Dabrowska $QUERY = $this->originalQuery; 3304a5f6748SAnna Dabrowska } 3314a5f6748SAnna Dabrowska 3324a5f6748SAnna Dabrowska 3334a5f6748SAnna Dabrowska /** 3344a5f6748SAnna Dabrowska * Returns a link that includes all parameters set by inbuilt search tools 3354a5f6748SAnna Dabrowska * and an optional additional parameter. 3364a5f6748SAnna Dabrowska * If the passed parameter is q, its value will be REPLACED. 3374a5f6748SAnna Dabrowska * 3384a5f6748SAnna Dabrowska * @param string $label 3394a5f6748SAnna Dabrowska * @param string $param 3404a5f6748SAnna Dabrowska * @param string $value 3414a5f6748SAnna Dabrowska * @return string 3424a5f6748SAnna Dabrowska */ 3434a5f6748SAnna Dabrowska protected function getSettingsLink($label, $param = '', $value = '') 3444a5f6748SAnna Dabrowska { 3454a5f6748SAnna Dabrowska global $QUERY; 3464a5f6748SAnna Dabrowska 3474a5f6748SAnna Dabrowska $Indexer = idx_get_indexer(); 3484a5f6748SAnna Dabrowska $parsedQuery = ft_queryParser($Indexer, $QUERY); 349df43a7beSAndreas Gohr $searchState = new SearchState($parsedQuery); 3504a5f6748SAnna Dabrowska $linkTag = $searchState->getSearchLink($label); 3514a5f6748SAnna Dabrowska 3524a5f6748SAnna Dabrowska // manipulate the link string because there is yet no way for inbuilt search to allow plugins 3534a5f6748SAnna Dabrowska // to extend search queries 3544a5f6748SAnna Dabrowska if ($param === '') { 3554a5f6748SAnna Dabrowska return $linkTag; 3564a5f6748SAnna Dabrowska } elseif ($param === 'q') { 3574a5f6748SAnna Dabrowska return preg_replace('/q=[^&\'" ]*/', 'q=' . urlencode($value), $linkTag); 3584a5f6748SAnna Dabrowska } 359df43a7beSAndreas Gohr // FIXME current links have a strange format where href is set in single quotes 360df43a7beSAndreas Gohr // and followed by a space so preg_replace would make more sense 3614a5f6748SAnna Dabrowska return str_replace("' >", '&' . $param . '=' . $value . "'> ", $linkTag); 3624a5f6748SAnna Dabrowska } 3634a5f6748SAnna Dabrowska} 364