1<?php 2 3namespace dokuwiki\Search; 4 5use dokuwiki\Extension\Event; 6use dokuwiki\Search\MetadataIndex; 7use dokuwiki\Search\QueryParser; 8 9/** 10 * Class DokuWiki Metadata Search 11 * 12 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 13 * @author Andreas Gohr <andi@splitbrain.org> 14 */ 15class MetadataSearch 16{ 17 /** 18 * Quicksearch for pagenames 19 * 20 * By default it only matches the pagename and ignores the namespace. 21 * This can be changed with the second parameter. 22 * The third parameter allows to search in titles as well. 23 * 24 * The function always returns titles as well 25 * 26 * @triggers SEARCH_QUERY_PAGELOOKUP 27 * @author Andreas Gohr <andi@splitbrain.org> 28 * @author Adrian Lang <lang@cosmocode.de> 29 * 30 * @param string $id page id 31 * @param bool $in_ns match against namespace as well? 32 * @param bool $in_title search in title? 33 * @param int|string $after only show results with mtime after this date, 34 * accepts timestap or strtotime arguments 35 * @param int|string $before only show results with mtime before this date, 36 * accepts timestap or strtotime arguments 37 * 38 * @return string[] 39 */ 40 public function pageLookup($id, $in_ns = false, $in_title = false, $after = null, $before = null) 41 { 42 $data = [ 43 'id' => $id, 44 'in_ns' => $in_ns, 45 'in_title' => $in_title, 46 'after' => $after, 47 'before' => $before 48 ]; 49 $data['has_titles'] = true; // for plugin backward compatibility check 50 $action = [$this, 'pageLookupCallBack']; 51 return Event::createAndTrigger('SEARCH_QUERY_PAGELOOKUP', $data, $action); 52 } 53 54 /** 55 * Returns list of pages as array(pageid => First Heading) 56 * 57 * @param array $data event data 58 * @return string[] 59 */ 60 public function pageLookupCallBack(&$data) 61 { 62 // split out original parameters 63 $id = $data['id']; 64 $parsedQuery = (new QueryParser)->convert($id); 65 66 if (count($parsedQuery['ns']) > 0) { 67 $ns = cleanID($parsedQuery['ns'][0]) . ':'; 68 $id = implode(' ', $parsedQuery['highlight']); 69 } 70 if (count($parsedQuery['notns']) > 0) { 71 $notns = cleanID($parsedQuery['notns'][0]) . ':'; 72 $id = implode(' ', $parsedQuery['highlight']); 73 } 74 75 $in_ns = $data['in_ns']; 76 $in_title = $data['in_title']; 77 $cleaned = cleanID($id); 78 79 $MetadataIndex = new MetadataIndex(); 80 $page_idx = $MetadataIndex->getPages(); 81 82 $pages = array(); 83 if ($id !== '' && $cleaned !== '') { 84 foreach ($page_idx as $p_id) { 85 if ((strpos($in_ns ? $p_id : noNSorNS($p_id), $cleaned) !== false)) { 86 if (!isset($pages[$p_id])) { 87 $pages[$p_id] = p_get_first_heading($p_id, METADATA_DONT_RENDER); 88 } 89 } 90 } 91 if ($in_title) { 92 $func = [$this, 'pageLookupTitleCompare']; 93 foreach ($MetadataIndex->lookupKey('title', $id, $func) as $p_id) { 94 if (!isset($pages[$p_id])) { 95 $pages[$p_id] = p_get_first_heading($p_id, METADATA_DONT_RENDER); 96 } 97 } 98 } 99 } 100 101 if (isset($ns)) { 102 foreach (array_keys($pages) as $p_id) { 103 if (strpos($p_id, $ns) !== 0) { 104 unset($pages[$p_id]); 105 } 106 } 107 } 108 if (isset($notns)) { 109 foreach (array_keys($pages) as $p_id) { 110 if (strpos($p_id, $notns) === 0) { 111 unset($pages[$p_id]); 112 } 113 } 114 } 115 116 // discard hidden pages 117 // discard nonexistent pages 118 // check ACL permissions 119 foreach (array_keys($pages) as $idx) { 120 if (!isVisiblePage($idx) || !page_exists($idx) || auth_quickaclcheck($idx) < AUTH_READ) { 121 unset($pages[$idx]); 122 } 123 } 124 125 $pages = $this->filterResultsByTime($pages, $data['after'], $data['before']); 126 127 uksort($pages, [$this, 'pagesorter']); 128 return $pages; 129 } 130 131 /** 132 * Tiny helper function for comparing the searched title with the title 133 * from the search index. This function is a wrapper around stripos with 134 * adapted argument order and return value. 135 * 136 * @param string $search searched title 137 * @param string $title title from index 138 * @return bool 139 */ 140 protected function pageLookupTitleCompare($search, $title) 141 { 142 return stripos($title, $search) !== false; 143 } 144 145 /** 146 * Sort pages based on their namespace level first, then on their string 147 * values. This makes higher hierarchy pages rank higher than lower hierarchy 148 * pages. 149 * 150 * @param string $a 151 * @param string $b 152 * @return int Returns < 0 if $a is less than $b; > 0 if $a is greater than $b, 153 * and 0 if they are equal. 154 */ 155 protected function pagesorter($a, $b) 156 { 157 $ac = count(explode(':',$a)); 158 $bc = count(explode(':',$b)); 159 if ($ac < $bc) { 160 return -1; 161 } elseif ($ac > $bc) { 162 return 1; 163 } 164 return strcmp ($a,$b); 165 } 166 167 /** 168 * @param array $results search results in the form pageid => value 169 * @param int|string $after only returns results with mtime after this date, 170 * accepts timestap or strtotime arguments 171 * @param int|string $before only returns results with mtime after this date, 172 * accepts timestap or strtotime arguments 173 * 174 * @return array 175 */ 176 protected function filterResultsByTime(array $results, $after, $before) 177 { 178 if ($after || $before) { 179 $after = is_int($after) ? $after : strtotime($after); 180 $before = is_int($before) ? $before : strtotime($before); 181 182 foreach ($results as $id => $value) { 183 $mTime = filemtime(wikiFN($id)); 184 if ($after && $after > $mTime) { 185 unset($results[$id]); 186 continue; 187 } 188 if ($before && $before < $mTime) { 189 unset($results[$id]); 190 } 191 } 192 } 193 return $results; 194 } 195} 196