121fcef82SMichael Große<?php 221fcef82SMichael Große 321fcef82SMichael Großenamespace dokuwiki\Ui; 421fcef82SMichael Große 5cbb44eabSAndreas Gohruse dokuwiki\Extension\Event; 664159a61SAndreas Gohruse dokuwiki\Form\Form; 70cba610bSSatoshi Saharause dokuwiki\Search\FulltextSearch; 80cba610bSSatoshi Saharause dokuwiki\Search\QueryParser; 92d85e841SAndreas Gohruse dokuwiki\Utf8\Sort; 100cba610bSSatoshi Sahara 110cba610bSSatoshi Saharause const dokuwiki\Search\FT_SNIPPET_NUMBER; 12427ed988SMichael Große 13*b9c8f036SAndreas Gohr 1421fcef82SMichael Großeclass Search extends Ui 1521fcef82SMichael Große{ 1621fcef82SMichael Große protected $query; 174c924eb8SMichael Große protected $parsedQuery; 1818856c5dSMichael Große protected $searchState; 1921fcef82SMichael Große protected $pageLookupResults = array(); 2021fcef82SMichael Große protected $fullTextResults = array(); 2121fcef82SMichael Große protected $highlight = array(); 2221fcef82SMichael Große 2321fcef82SMichael Große /** 2421fcef82SMichael Große * Search constructor. 256639a152SMichael Große * 26fc46ed58SMichael Große * @param array $pageLookupResults pagename lookup results in the form [pagename => pagetitle] 27fc46ed58SMichael Große * @param array $fullTextResults fulltext search results in the form [pagename => #hits] 28fc46ed58SMichael Große * @param array $highlight array of strings to be highlighted 2921fcef82SMichael Große */ 306639a152SMichael Große public function __construct(array $pageLookupResults, array $fullTextResults, $highlight) 3121fcef82SMichael Große { 32d09b5b64SMichael Große global $QUERY; 33d09b5b64SMichael Große 34d09b5b64SMichael Große $this->query = $QUERY; 359329b002SSatoshi Sahara $this->parsedQuery = (new QueryParser)->convert($QUERY); 3618856c5dSMichael Große $this->searchState = new SearchState($this->parsedQuery); 3721fcef82SMichael Große 386639a152SMichael Große $this->pageLookupResults = $pageLookupResults; 396639a152SMichael Große $this->fullTextResults = $fullTextResults; 4021fcef82SMichael Große $this->highlight = $highlight; 41b3cfe85aSMichael Große } 42bbc1da2eSMichael Große 43b3cfe85aSMichael Große /** 4421fcef82SMichael Große * display the search result 4521fcef82SMichael Große * 4621fcef82SMichael Große * @return void 4721fcef82SMichael Große */ 4821fcef82SMichael Große public function show() 4921fcef82SMichael Große { 5021fcef82SMichael Große $searchHTML = ''; 5121fcef82SMichael Große 5221fcef82SMichael Große $searchHTML .= $this->getSearchIntroHTML($this->query); 5321fcef82SMichael Große 542ce8affcSMichael Große $searchHTML .= $this->getSearchFormHTML($this->query); 552ce8affcSMichael Große 5621fcef82SMichael Große $searchHTML .= $this->getPageLookupHTML($this->pageLookupResults); 5721fcef82SMichael Große 5821fcef82SMichael Große $searchHTML .= $this->getFulltextResultsHTML($this->fullTextResults, $this->highlight); 5921fcef82SMichael Große 6021fcef82SMichael Große echo $searchHTML; 6121fcef82SMichael Große } 6221fcef82SMichael Große 6321fcef82SMichael Große /** 64427ed988SMichael Große * Get a form which can be used to adjust/refine the search 65427ed988SMichael Große * 66427ed988SMichael Große * @param string $query 67427ed988SMichael Große * 68427ed988SMichael Große * @return string 69427ed988SMichael Große */ 70427ed988SMichael Große protected function getSearchFormHTML($query) 71427ed988SMichael Große { 72bbc1da2eSMichael Große global $lang, $ID, $INPUT; 73427ed988SMichael Große 747fa270bcSMichael Große $searchForm = (new Form(['method' => 'get'], true))->addClass('search-results-form'); 75bb8ef867SMichael Große $searchForm->setHiddenField('do', 'search'); 76d22b78c8SMichael Große $searchForm->setHiddenField('id', $ID); 771265b193SMichael Große $searchForm->setHiddenField('sf', '1'); 78422bbbc6SMichael Große if ($INPUT->has('min')) { 79422bbbc6SMichael Große $searchForm->setHiddenField('min', $INPUT->str('min')); 80bbc1da2eSMichael Große } 81422bbbc6SMichael Große if ($INPUT->has('max')) { 82422bbbc6SMichael Große $searchForm->setHiddenField('max', $INPUT->str('max')); 83bbc1da2eSMichael Große } 841265b193SMichael Große if ($INPUT->has('srt')) { 851265b193SMichael Große $searchForm->setHiddenField('srt', $INPUT->str('srt')); 868d0e286aSMichael Große } 874bdf82b5SAndreas Gohr $searchForm->addFieldsetOpen()->addClass('search-form'); 88d22b78c8SMichael Große $searchForm->addTextInput('q')->val($query)->useInput(false); 89427ed988SMichael Große $searchForm->addButton('', $lang['btn_search'])->attr('type', 'submit'); 90bb8ef867SMichael Große 9118856c5dSMichael Große $this->addSearchAssistanceElements($searchForm); 92bb8ef867SMichael Große 93427ed988SMichael Große $searchForm->addFieldsetClose(); 94427ed988SMichael Große 95cbb44eabSAndreas Gohr Event::createAndTrigger('FORM_SEARCH_OUTPUT', $searchForm); 9681a0edd9SMichael Große 97427ed988SMichael Große return $searchForm->toHTML(); 98427ed988SMichael Große } 99427ed988SMichael Große 100be76738bSMichael Große /** 101be76738bSMichael Große * Add elements to adjust how the results are sorted 102be76738bSMichael Große * 103be76738bSMichael Große * @param Form $searchForm 104be76738bSMichael Große */ 105b005809cSMichael Große protected function addSortTool(Form $searchForm) 106b005809cSMichael Große { 107b005809cSMichael Große global $INPUT, $lang; 108b005809cSMichael Große 109b005809cSMichael Große $options = [ 110b005809cSMichael Große 'hits' => [ 111b005809cSMichael Große 'label' => $lang['search_sort_by_hits'], 112b005809cSMichael Große 'sort' => '', 113b005809cSMichael Große ], 114b005809cSMichael Große 'mtime' => [ 115b005809cSMichael Große 'label' => $lang['search_sort_by_mtime'], 116b005809cSMichael Große 'sort' => 'mtime', 117b005809cSMichael Große ], 118b005809cSMichael Große ]; 119b005809cSMichael Große $activeOption = 'hits'; 120b005809cSMichael Große 1211265b193SMichael Große if ($INPUT->str('srt') === 'mtime') { 122b005809cSMichael Große $activeOption = 'mtime'; 123b005809cSMichael Große } 124b005809cSMichael Große 1252171f9cbSAndreas Gohr $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true'); 126b005809cSMichael Große // render current 1274bdf82b5SAndreas Gohr $currentWrapper = $searchForm->addTagOpen('div')->addClass('current'); 128b005809cSMichael Große if ($activeOption !== 'hits') { 1294bdf82b5SAndreas Gohr $currentWrapper->addClass('changed'); 130b005809cSMichael Große } 131b005809cSMichael Große $searchForm->addHTML($options[$activeOption]['label']); 132b005809cSMichael Große $searchForm->addTagClose('div'); 133b005809cSMichael Große 134b005809cSMichael Große // render options list 1352171f9cbSAndreas Gohr $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false'); 136b005809cSMichael Große 137b005809cSMichael Große foreach ($options as $key => $option) { 1384bdf82b5SAndreas Gohr $listItem = $searchForm->addTagOpen('li'); 139b005809cSMichael Große 140b005809cSMichael Große if ($key === $activeOption) { 1414bdf82b5SAndreas Gohr $listItem->addClass('active'); 142b005809cSMichael Große $searchForm->addHTML($option['label']); 143b005809cSMichael Große } else { 14452d4cd42SMichael Große $link = $this->searchState->withSorting($option['sort'])->getSearchLink($option['label']); 14552d4cd42SMichael Große $searchForm->addHTML($link); 146b005809cSMichael Große } 147b005809cSMichael Große $searchForm->addTagClose('li'); 148b005809cSMichael Große } 149b005809cSMichael Große $searchForm->addTagClose('ul'); 150b005809cSMichael Große 151b005809cSMichael Große $searchForm->addTagClose('div'); 152b005809cSMichael Große 153b005809cSMichael Große } 154b005809cSMichael Große 155be76738bSMichael Große /** 156be76738bSMichael Große * Check if the query is simple enough to modify its namespace limitations without breaking the rest of the query 157be76738bSMichael Große * 158be76738bSMichael Große * @param array $parsedQuery 159be76738bSMichael Große * 160be76738bSMichael Große * @return bool 161be76738bSMichael Große */ 162df977249SMichael Große protected function isNamespaceAssistanceAvailable(array $parsedQuery) { 163df977249SMichael Große if (preg_match('/[\(\)\|]/', $parsedQuery['query']) === 1) { 164bb8ef867SMichael Große return false; 165bb8ef867SMichael Große } 166df977249SMichael Große 167df977249SMichael Große return true; 168df977249SMichael Große } 169df977249SMichael Große 170be76738bSMichael Große /** 171be76738bSMichael Große * Check if the query is simple enough to modify the fragment search behavior without breaking the rest of the query 172be76738bSMichael Große * 173be76738bSMichael Große * @param array $parsedQuery 174be76738bSMichael Große * 175be76738bSMichael Große * @return bool 176be76738bSMichael Große */ 177df977249SMichael Große protected function isFragmentAssistanceAvailable(array $parsedQuery) { 178df977249SMichael Große if (preg_match('/[\(\)\|]/', $parsedQuery['query']) === 1) { 179bb8ef867SMichael Große return false; 180bb8ef867SMichael Große } 181bb8ef867SMichael Große 182bb8ef867SMichael Große if (!empty($parsedQuery['phrases'])) { 183bb8ef867SMichael Große return false; 184bb8ef867SMichael Große } 185bb8ef867SMichael Große 186bb8ef867SMichael Große return true; 187bb8ef867SMichael Große } 188bb8ef867SMichael Große 189bb8ef867SMichael Große /** 190bb8ef867SMichael Große * Add the elements to be used for search assistance 191bb8ef867SMichael Große * 192bb8ef867SMichael Große * @param Form $searchForm 193bb8ef867SMichael Große */ 19418856c5dSMichael Große protected function addSearchAssistanceElements(Form $searchForm) 195bb8ef867SMichael Große { 196bb8ef867SMichael Große $searchForm->addTagOpen('div') 1974bdf82b5SAndreas Gohr ->addClass('advancedOptions') 1982171f9cbSAndreas Gohr ->attr('style', 'display: none;') 1992171f9cbSAndreas Gohr ->attr('aria-hidden', 'true'); 200bb8ef867SMichael Große 20118856c5dSMichael Große $this->addFragmentBehaviorLinks($searchForm); 20218856c5dSMichael Große $this->addNamespaceSelector($searchForm); 20318856c5dSMichael Große $this->addDateSelector($searchForm); 204b005809cSMichael Große $this->addSortTool($searchForm); 205bb8ef867SMichael Große 206bb8ef867SMichael Große $searchForm->addTagClose('div'); 207bb8ef867SMichael Große } 208bb8ef867SMichael Große 209be76738bSMichael Große /** 210be76738bSMichael Große * Add the elements to adjust the fragment search behavior 211be76738bSMichael Große * 212be76738bSMichael Große * @param Form $searchForm 213be76738bSMichael Große */ 21418856c5dSMichael Große protected function addFragmentBehaviorLinks(Form $searchForm) 2154d0cb6e1SMichael Große { 216df977249SMichael Große if (!$this->isFragmentAssistanceAvailable($this->parsedQuery)) { 217df977249SMichael Große return; 218df977249SMichael Große } 219b005809cSMichael Große global $lang; 2204d0cb6e1SMichael Große 221b005809cSMichael Große $options = [ 222b005809cSMichael Große 'exact' => [ 223b005809cSMichael Große 'label' => $lang['search_exact_match'], 224b005809cSMichael Große 'and' => array_map(function ($term) { 225b005809cSMichael Große return trim($term, '*'); 226b005809cSMichael Große }, $this->parsedQuery['and']), 227df977249SMichael Große 'not' => array_map(function ($term) { 228df977249SMichael Große return trim($term, '*'); 229df977249SMichael Große }, $this->parsedQuery['not']), 230b005809cSMichael Große ], 231b005809cSMichael Große 'starts' => [ 232b005809cSMichael Große 'label' => $lang['search_starts_with'], 233b005809cSMichael Große 'and' => array_map(function ($term) { 234b005809cSMichael Große return trim($term, '*') . '*'; 235df977249SMichael Große }, $this->parsedQuery['and']), 236df977249SMichael Große 'not' => array_map(function ($term) { 237df977249SMichael Große return trim($term, '*') . '*'; 238df977249SMichael Große }, $this->parsedQuery['not']), 239b005809cSMichael Große ], 240b005809cSMichael Große 'ends' => [ 241b005809cSMichael Große 'label' => $lang['search_ends_with'], 242b005809cSMichael Große 'and' => array_map(function ($term) { 243b005809cSMichael Große return '*' . trim($term, '*'); 244df977249SMichael Große }, $this->parsedQuery['and']), 245df977249SMichael Große 'not' => array_map(function ($term) { 246df977249SMichael Große return '*' . trim($term, '*'); 247df977249SMichael Große }, $this->parsedQuery['not']), 248b005809cSMichael Große ], 249b005809cSMichael Große 'contains' => [ 250b005809cSMichael Große 'label' => $lang['search_contains'], 251b005809cSMichael Große 'and' => array_map(function ($term) { 252b005809cSMichael Große return '*' . trim($term, '*') . '*'; 253df977249SMichael Große }, $this->parsedQuery['and']), 254df977249SMichael Große 'not' => array_map(function ($term) { 255df977249SMichael Große return '*' . trim($term, '*') . '*'; 256df977249SMichael Große }, $this->parsedQuery['not']), 257b005809cSMichael Große ] 258b005809cSMichael Große ]; 259b005809cSMichael Große 260b005809cSMichael Große // detect current 261c6b5b74aSMichael Große $activeOption = 'custom'; 262b005809cSMichael Große foreach ($options as $key => $option) { 263b005809cSMichael Große if ($this->parsedQuery['and'] === $option['and']) { 264b005809cSMichael Große $activeOption = $key; 265b005809cSMichael Große } 266b005809cSMichael Große } 267c6b5b74aSMichael Große if ($activeOption === 'custom') { 268c6b5b74aSMichael Große $options = array_merge(['custom' => [ 269c6b5b74aSMichael Große 'label' => $lang['search_custom_match'], 270c6b5b74aSMichael Große ]], $options); 271c6b5b74aSMichael Große } 272b005809cSMichael Große 2732171f9cbSAndreas Gohr $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true'); 274b005809cSMichael Große // render current 2754bdf82b5SAndreas Gohr $currentWrapper = $searchForm->addTagOpen('div')->addClass('current'); 276b005809cSMichael Große if ($activeOption !== 'exact') { 2774bdf82b5SAndreas Gohr $currentWrapper->addClass('changed'); 278b005809cSMichael Große } 279b005809cSMichael Große $searchForm->addHTML($options[$activeOption]['label']); 280b005809cSMichael Große $searchForm->addTagClose('div'); 281b005809cSMichael Große 282b005809cSMichael Große // render options list 2832171f9cbSAndreas Gohr $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false'); 284b005809cSMichael Große 285b005809cSMichael Große foreach ($options as $key => $option) { 2864bdf82b5SAndreas Gohr $listItem = $searchForm->addTagOpen('li'); 287b005809cSMichael Große 288b005809cSMichael Große if ($key === $activeOption) { 2894bdf82b5SAndreas Gohr $listItem->addClass('active'); 290b005809cSMichael Große $searchForm->addHTML($option['label']); 291b005809cSMichael Große } else { 29252d4cd42SMichael Große $link = $this->searchState 29352d4cd42SMichael Große ->withFragments($option['and'], $option['not']) 29452d4cd42SMichael Große ->getSearchLink($option['label']) 29552d4cd42SMichael Große ; 29652d4cd42SMichael Große $searchForm->addHTML($link); 297b005809cSMichael Große } 298b005809cSMichael Große $searchForm->addTagClose('li'); 299b005809cSMichael Große } 300b005809cSMichael Große $searchForm->addTagClose('ul'); 3014d0cb6e1SMichael Große 3024d0cb6e1SMichael Große $searchForm->addTagClose('div'); 303b005809cSMichael Große 304b005809cSMichael Große // render options list 3054d0cb6e1SMichael Große } 3064d0cb6e1SMichael Große 307bb8ef867SMichael Große /** 308bb8ef867SMichael Große * Add the elements for the namespace selector 309bb8ef867SMichael Große * 310bb8ef867SMichael Große * @param Form $searchForm 311bb8ef867SMichael Große */ 31218856c5dSMichael Große protected function addNamespaceSelector(Form $searchForm) 313bb8ef867SMichael Große { 314df977249SMichael Große if (!$this->isNamespaceAssistanceAvailable($this->parsedQuery)) { 315df977249SMichael Große return; 316df977249SMichael Große } 317df977249SMichael Große 318b005809cSMichael Große global $lang; 319b005809cSMichael Große 32018856c5dSMichael Große $baseNS = empty($this->parsedQuery['ns']) ? '' : $this->parsedQuery['ns'][0]; 321bbc1da2eSMichael Große $extraNS = $this->getAdditionalNamespacesFromResults($baseNS); 3224d0cb6e1SMichael Große 3232171f9cbSAndreas Gohr $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true'); 324b005809cSMichael Große // render current 3254bdf82b5SAndreas Gohr $currentWrapper = $searchForm->addTagOpen('div')->addClass('current'); 326bbc1da2eSMichael Große if ($baseNS) { 3274bdf82b5SAndreas Gohr $currentWrapper->addClass('changed'); 328b005809cSMichael Große $searchForm->addHTML('@' . $baseNS); 329b005809cSMichael Große } else { 330b005809cSMichael Große $searchForm->addHTML($lang['search_any_ns']); 331b005809cSMichael Große } 332b005809cSMichael Große $searchForm->addTagClose('div'); 333b005809cSMichael Große 334b005809cSMichael Große // render options list 3352171f9cbSAndreas Gohr $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false'); 336b005809cSMichael Große 3374bdf82b5SAndreas Gohr $listItem = $searchForm->addTagOpen('li'); 338b005809cSMichael Große if ($baseNS) { 3394bdf82b5SAndreas Gohr $listItem->addClass('active'); 34052d4cd42SMichael Große $link = $this->searchState->withNamespace('')->getSearchLink($lang['search_any_ns']); 34152d4cd42SMichael Große $searchForm->addHTML($link); 342b005809cSMichael Große } else { 343b005809cSMichael Große $searchForm->addHTML($lang['search_any_ns']); 344bb8ef867SMichael Große } 345b005809cSMichael Große $searchForm->addTagClose('li'); 346bb8ef867SMichael Große 34718856c5dSMichael Große foreach ($extraNS as $ns => $count) { 3484bdf82b5SAndreas Gohr $listItem = $searchForm->addTagOpen('li'); 3491d918893SAndreas Gohr $label = $ns . ($count ? " <bdi>($count)</bdi>" : ''); 3504d0cb6e1SMichael Große 351b005809cSMichael Große if ($ns === $baseNS) { 3524bdf82b5SAndreas Gohr $listItem->addClass('active'); 353b005809cSMichael Große $searchForm->addHTML($label); 354b005809cSMichael Große } else { 35552d4cd42SMichael Große $link = $this->searchState->withNamespace($ns)->getSearchLink($label); 35652d4cd42SMichael Große $searchForm->addHTML($link); 357bb8ef867SMichael Große } 358b005809cSMichael Große $searchForm->addTagClose('li'); 359bb8ef867SMichael Große } 360b005809cSMichael Große $searchForm->addTagClose('ul'); 361bb8ef867SMichael Große 362bb8ef867SMichael Große $searchForm->addTagClose('div'); 363b005809cSMichael Große 364bb8ef867SMichael Große } 365bb8ef867SMichael Große 366bb8ef867SMichael Große /** 367bb8ef867SMichael Große * Parse the full text results for their top namespaces below the given base namespace 368bb8ef867SMichael Große * 369bb8ef867SMichael Große * @param string $baseNS the namespace within which was searched, empty string for root namespace 370bb8ef867SMichael Große * 371bb8ef867SMichael Große * @return array an associative array with namespace => #number of found pages, sorted descending 372bb8ef867SMichael Große */ 373bb8ef867SMichael Große protected function getAdditionalNamespacesFromResults($baseNS) 374bb8ef867SMichael Große { 375bb8ef867SMichael Große $namespaces = []; 376bb8ef867SMichael Große $baseNSLength = strlen($baseNS); 377bb8ef867SMichael Große foreach ($this->fullTextResults as $page => $numberOfHits) { 378bb8ef867SMichael Große $namespace = getNS($page); 379bb8ef867SMichael Große if (!$namespace) { 380bb8ef867SMichael Große continue; 381bb8ef867SMichael Große } 382bb8ef867SMichael Große if ($namespace === $baseNS) { 383bb8ef867SMichael Große continue; 384bb8ef867SMichael Große } 385bb8ef867SMichael Große $firstColon = strpos((string)$namespace, ':', $baseNSLength + 1) ?: strlen($namespace); 386bb8ef867SMichael Große $subtopNS = substr($namespace, 0, $firstColon); 387bb8ef867SMichael Große if (empty($namespaces[$subtopNS])) { 388bb8ef867SMichael Große $namespaces[$subtopNS] = 0; 389bb8ef867SMichael Große } 390bb8ef867SMichael Große $namespaces[$subtopNS] += 1; 391bb8ef867SMichael Große } 3922d85e841SAndreas Gohr Sort::ksort($namespaces); 393bb8ef867SMichael Große arsort($namespaces); 394bb8ef867SMichael Große return $namespaces; 395bb8ef867SMichael Große } 396bb8ef867SMichael Große 397bb8ef867SMichael Große /** 398bbc1da2eSMichael Große * @ToDo: custom date input 399bbc1da2eSMichael Große * 400bbc1da2eSMichael Große * @param Form $searchForm 401bbc1da2eSMichael Große */ 402b005809cSMichael Große protected function addDateSelector(Form $searchForm) 403b005809cSMichael Große { 404b005809cSMichael Große global $INPUT, $lang; 405bbc1da2eSMichael Große 406b005809cSMichael Große $options = [ 407b005809cSMichael Große 'any' => [ 408b005809cSMichael Große 'before' => false, 409b005809cSMichael Große 'after' => false, 410b005809cSMichael Große 'label' => $lang['search_any_time'], 411b005809cSMichael Große ], 412b005809cSMichael Große 'week' => [ 413b005809cSMichael Große 'before' => false, 414b005809cSMichael Große 'after' => '1 week ago', 415b005809cSMichael Große 'label' => $lang['search_past_7_days'], 416b005809cSMichael Große ], 417b005809cSMichael Große 'month' => [ 418b005809cSMichael Große 'before' => false, 419b005809cSMichael Große 'after' => '1 month ago', 420b005809cSMichael Große 'label' => $lang['search_past_month'], 421b005809cSMichael Große ], 422b005809cSMichael Große 'year' => [ 423b005809cSMichael Große 'before' => false, 424b005809cSMichael Große 'after' => '1 year ago', 425b005809cSMichael Große 'label' => $lang['search_past_year'], 426b005809cSMichael Große ], 427b005809cSMichael Große ]; 428b005809cSMichael Große $activeOption = 'any'; 429b005809cSMichael Große foreach ($options as $key => $option) { 430422bbbc6SMichael Große if ($INPUT->str('min') === $option['after']) { 431b005809cSMichael Große $activeOption = $key; 432b005809cSMichael Große break; 433b005809cSMichael Große } 434b005809cSMichael Große } 435b005809cSMichael Große 4362171f9cbSAndreas Gohr $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true'); 437b005809cSMichael Große // render current 4384bdf82b5SAndreas Gohr $currentWrapper = $searchForm->addTagOpen('div')->addClass('current'); 439422bbbc6SMichael Große if ($INPUT->has('max') || $INPUT->has('min')) { 4404bdf82b5SAndreas Gohr $currentWrapper->addClass('changed'); 441bbc1da2eSMichael Große } 442b005809cSMichael Große $searchForm->addHTML($options[$activeOption]['label']); 443b005809cSMichael Große $searchForm->addTagClose('div'); 444bbc1da2eSMichael Große 445b005809cSMichael Große // render options list 4462171f9cbSAndreas Gohr $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false'); 447b005809cSMichael Große 448b005809cSMichael Große foreach ($options as $key => $option) { 4494bdf82b5SAndreas Gohr $listItem = $searchForm->addTagOpen('li'); 450b005809cSMichael Große 451b005809cSMichael Große if ($key === $activeOption) { 4524bdf82b5SAndreas Gohr $listItem->addClass('active'); 453b005809cSMichael Große $searchForm->addHTML($option['label']); 454bbc1da2eSMichael Große } else { 45552d4cd42SMichael Große $link = $this->searchState 45652d4cd42SMichael Große ->withTimeLimitations($option['after'], $option['before']) 45752d4cd42SMichael Große ->getSearchLink($option['label']) 45852d4cd42SMichael Große ; 45952d4cd42SMichael Große $searchForm->addHTML($link); 460bbc1da2eSMichael Große } 461b005809cSMichael Große $searchForm->addTagClose('li'); 462bbc1da2eSMichael Große } 463b005809cSMichael Große $searchForm->addTagClose('ul'); 464bbc1da2eSMichael Große 465bbc1da2eSMichael Große $searchForm->addTagClose('div'); 466bbc1da2eSMichael Große } 467bbc1da2eSMichael Große 468bbc1da2eSMichael Große 469bbc1da2eSMichael Große /** 47021fcef82SMichael Große * Build the intro text for the search page 47121fcef82SMichael Große * 47221fcef82SMichael Große * @param string $query the search query 47321fcef82SMichael Große * 47421fcef82SMichael Große * @return string 47521fcef82SMichael Große */ 47621fcef82SMichael Große protected function getSearchIntroHTML($query) 47721fcef82SMichael Große { 4782ce8affcSMichael Große global $lang; 47921fcef82SMichael Große 48021fcef82SMichael Große $intro = p_locale_xhtml('searchpage'); 4812ce8affcSMichael Große 4822ce8affcSMichael Große $queryPagename = $this->createPagenameFromQuery($this->parsedQuery); 4832ce8affcSMichael Große $createQueryPageLink = html_wikilink($queryPagename . '?do=edit', $queryPagename); 4842ce8affcSMichael Große 4852ce8affcSMichael Große $pagecreateinfo = ''; 4862ce8affcSMichael Große if (auth_quickaclcheck($queryPagename) >= AUTH_CREATE) { 4872ce8affcSMichael Große $pagecreateinfo = sprintf($lang['searchcreatepage'], $createQueryPageLink); 4882ce8affcSMichael Große } 48921fcef82SMichael Große $intro = str_replace( 49021fcef82SMichael Große array('@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'), 49121fcef82SMichael Große array(hsc(rawurlencode($query)), hsc($query), $pagecreateinfo), 49221fcef82SMichael Große $intro 49321fcef82SMichael Große ); 4942ce8affcSMichael Große 49521fcef82SMichael Große return $intro; 49621fcef82SMichael Große } 49721fcef82SMichael Große 49821fcef82SMichael Große /** 4992ce8affcSMichael Große * Create a pagename based the parsed search query 5002ce8affcSMichael Große * 5012ce8affcSMichael Große * @param array $parsedQuery 5022ce8affcSMichael Große * 5032ce8affcSMichael Große * @return string pagename constructed from the parsed query 5042ce8affcSMichael Große */ 50542690e4dSMichael Große public function createPagenameFromQuery($parsedQuery) 5062ce8affcSMichael Große { 507e180e453SPhy $cleanedQuery = cleanID($parsedQuery['query']); // already strtolowered 5088cbc5ee8SAndreas Gohr if ($cleanedQuery === \dokuwiki\Utf8\PhpString::strtolower($parsedQuery['query'])) { 50942690e4dSMichael Große return ':' . $cleanedQuery; 51042690e4dSMichael Große } 5112ce8affcSMichael Große $pagename = ''; 5122ce8affcSMichael Große if (!empty($parsedQuery['ns'])) { 51342690e4dSMichael Große $pagename .= ':' . cleanID($parsedQuery['ns'][0]); 5142ce8affcSMichael Große } 5152ce8affcSMichael Große $pagename .= ':' . cleanID(implode(' ' , $parsedQuery['highlight'])); 5162ce8affcSMichael Große return $pagename; 5172ce8affcSMichael Große } 5182ce8affcSMichael Große 5192ce8affcSMichael Große /** 52021fcef82SMichael Große * Build HTML for a list of pages with matching pagenames 52121fcef82SMichael Große * 52221fcef82SMichael Große * @param array $data search results 52321fcef82SMichael Große * 52421fcef82SMichael Große * @return string 52521fcef82SMichael Große */ 52621fcef82SMichael Große protected function getPageLookupHTML($data) 52721fcef82SMichael Große { 52821fcef82SMichael Große if (empty($data)) { 52921fcef82SMichael Große return ''; 53021fcef82SMichael Große } 53121fcef82SMichael Große 53221fcef82SMichael Große global $lang; 53321fcef82SMichael Große 53421fcef82SMichael Große $html = '<div class="search_quickresult">'; 5356d55fda7SMichael Große $html .= '<h2>' . $lang['quickhits'] . ':</h2>'; 53621fcef82SMichael Große $html .= '<ul class="search_quickhits">'; 53721fcef82SMichael Große foreach ($data as $id => $title) { 5385d87aa31SMichael Große $name = null; 5395d87aa31SMichael Große if (!useHeading('navigation') && $ns = getNS($id)) { 5405d87aa31SMichael Große $name = shorten(noNS($id), ' (' . $ns . ')', 30); 5415d87aa31SMichael Große } 5425d87aa31SMichael Große $link = html_wikilink(':' . $id, $name); 5434eab6f7cSMichael Große $eventData = [ 5444eab6f7cSMichael Große 'listItemContent' => [$link], 5454eab6f7cSMichael Große 'page' => $id, 5464eab6f7cSMichael Große ]; 547cbb44eabSAndreas Gohr Event::createAndTrigger('SEARCH_RESULT_PAGELOOKUP', $eventData); 5484eab6f7cSMichael Große $html .= '<li>' . implode('', $eventData['listItemContent']) . '</li>'; 54921fcef82SMichael Große } 55021fcef82SMichael Große $html .= '</ul> '; 55121fcef82SMichael Große //clear float (see http://www.complexspiral.com/publications/containing-floats/) 55221fcef82SMichael Große $html .= '<div class="clearer"></div>'; 55321fcef82SMichael Große $html .= '</div>'; 55421fcef82SMichael Große 55521fcef82SMichael Große return $html; 55621fcef82SMichael Große } 55721fcef82SMichael Große 55821fcef82SMichael Große /** 55921fcef82SMichael Große * Build HTML for fulltext search results or "no results" message 56021fcef82SMichael Große * 56121fcef82SMichael Große * @param array $data the results of the fulltext search 56221fcef82SMichael Große * @param array $highlight the terms to be highlighted in the results 56321fcef82SMichael Große * 56421fcef82SMichael Große * @return string 56521fcef82SMichael Große */ 56621fcef82SMichael Große protected function getFulltextResultsHTML($data, $highlight) 56721fcef82SMichael Große { 56821fcef82SMichael Große global $lang; 56921fcef82SMichael Große 57021fcef82SMichael Große if (empty($data)) { 57121fcef82SMichael Große return '<div class="nothing">' . $lang['nothingfound'] . '</div>'; 57221fcef82SMichael Große } 57321fcef82SMichael Große 5742ce8affcSMichael Große $html = '<div class="search_fulltextresult">'; 5756d55fda7SMichael Große $html .= '<h2>' . $lang['search_fullresults'] . ':</h2>'; 5762ce8affcSMichael Große 57721fcef82SMichael Große $html .= '<dl class="search_results">'; 5788225e1abSMichael Große $num = 0; 5798225e1abSMichael Große $position = 0; 5809329b002SSatoshi Sahara $FulltextSearch = FulltextSearch::getInstance(); 5814c924eb8SMichael Große 58221fcef82SMichael Große foreach ($data as $id => $cnt) { 58378d786c9SMichael Große $position += 1; 5844eab6f7cSMichael Große $resultLink = html_wikilink(':' . $id, null, $highlight); 5854c924eb8SMichael Große 5864c924eb8SMichael Große $resultHeader = [$resultLink]; 5874c924eb8SMichael Große 5884eab6f7cSMichael Große 5894c924eb8SMichael Große $restrictQueryToNSLink = $this->restrictQueryToNSLink(getNS($id)); 5904c924eb8SMichael Große if ($restrictQueryToNSLink) { 5914c924eb8SMichael Große $resultHeader[] = $restrictQueryToNSLink; 5924c924eb8SMichael Große } 5934c924eb8SMichael Große 5945d06a1e4SMichael Große $resultBody = []; 5959a75abfbSMichael Große $mtime = filemtime(wikiFN($id)); 5965d06a1e4SMichael Große $lastMod = '<span class="lastmod">' . $lang['lastmod'] . '</span> '; 59764159a61SAndreas Gohr $lastMod .= '<time datetime="' . date_iso8601($mtime) . '" title="' . dformat($mtime) . '">' . 59864159a61SAndreas Gohr dformat($mtime, '%f') . 59964159a61SAndreas Gohr '</time>'; 600f0861d1fSMichael Große $resultBody['meta'] = $lastMod; 6015d06a1e4SMichael Große if ($cnt !== 0) { 6028225e1abSMichael Große $num++; 603b12bcb77SAnika Henke $hits = '<span class="hits">' . $cnt . ' ' . $lang['hits'] . '</span>, '; 604f0861d1fSMichael Große $resultBody['meta'] = $hits . $resultBody['meta']; 6058225e1abSMichael Große if ($num <= FT_SNIPPET_NUMBER) { // create snippets for the first number of matches only 6069329b002SSatoshi Sahara $resultBody['snippet'] = $FulltextSearch->snippet($id, $highlight); 6079a75abfbSMichael Große } 6089a75abfbSMichael Große } 6099a75abfbSMichael Große 6104eab6f7cSMichael Große $eventData = [ 6114c924eb8SMichael Große 'resultHeader' => $resultHeader, 6125d06a1e4SMichael Große 'resultBody' => $resultBody, 6134eab6f7cSMichael Große 'page' => $id, 61478d786c9SMichael Große 'position' => $position, 6154eab6f7cSMichael Große ]; 616cbb44eabSAndreas Gohr Event::createAndTrigger('SEARCH_RESULT_FULLPAGE', $eventData); 6175d06a1e4SMichael Große $html .= '<div class="search_fullpage_result">'; 6184eab6f7cSMichael Große $html .= '<dt>' . implode(' ', $eventData['resultHeader']) . '</dt>'; 6195d06a1e4SMichael Große foreach ($eventData['resultBody'] as $class => $htmlContent) { 6205d06a1e4SMichael Große $html .= "<dd class=\"$class\">$htmlContent</dd>"; 6215d06a1e4SMichael Große } 6225d06a1e4SMichael Große $html .= '</div>'; 62321fcef82SMichael Große } 62421fcef82SMichael Große $html .= '</dl>'; 62521fcef82SMichael Große 6262ce8affcSMichael Große $html .= '</div>'; 6272ce8affcSMichael Große 62821fcef82SMichael Große return $html; 62921fcef82SMichael Große } 6304c924eb8SMichael Große 6314c924eb8SMichael Große /** 6324c924eb8SMichael Große * create a link to restrict the current query to a namespace 6334c924eb8SMichael Große * 634ec27794fSMichael Große * @param false|string $ns the namespace to which to restrict the query 6354c924eb8SMichael Große * 636ec27794fSMichael Große * @return false|string 6374c924eb8SMichael Große */ 6384c924eb8SMichael Große protected function restrictQueryToNSLink($ns) 6394c924eb8SMichael Große { 6404c924eb8SMichael Große if (!$ns) { 6414c924eb8SMichael Große return false; 6424c924eb8SMichael Große } 643df977249SMichael Große if (!$this->isNamespaceAssistanceAvailable($this->parsedQuery)) { 6444c924eb8SMichael Große return false; 6454c924eb8SMichael Große } 6464c924eb8SMichael Große if (!empty($this->parsedQuery['ns']) && $this->parsedQuery['ns'][0] === $ns) { 6474c924eb8SMichael Große return false; 6484c924eb8SMichael Große } 64952d4cd42SMichael Große 6504c924eb8SMichael Große $name = '@' . $ns; 65152d4cd42SMichael Große return $this->searchState->withNamespace($ns)->getSearchLink($name); 6524c924eb8SMichael Große } 65321fcef82SMichael Große} 654