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