1<?php 2namespace dokuwiki\Search; 3 4use dokuwiki\Extension\Event; 5use dokuwiki\Search\Indexer; 6use dokuwiki\Search\QueryParser; 7 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 * Metadata Search constructor. prevent direct object creation 19 */ 20 protected function __construct() {} 21 22 /** 23 * Returns the backlinks for a given page 24 * 25 * Uses the metadata index. 26 * 27 * @param string $id The id for which links shall be returned 28 * @param bool $ignore_perms Ignore the fact that pages are hidden or read-protected 29 * @return array The pages that contain links to the given page 30 */ 31 public static function backlinks($id, $ignore_perms = false) 32 { 33 $Indexer = Indexer::getInstance(); 34 $result = $Indexer->lookupKey('relation_references', $id); 35 36 if (!count($result)) return $result; 37 38 // check ACL permissions 39 foreach (array_keys($result) as $idx) { 40 if (($ignore_perms !== true 41 && (isHiddenPage($result[$idx]) || auth_quickaclcheck($result[$idx]) < AUTH_READ) 42 ) || !page_exists($result[$idx], '', false) 43 ) { 44 unset($result[$idx]); 45 } 46 } 47 48 sort($result); 49 return $result; 50 } 51 52 /** 53 * Returns the pages that use a given media file 54 * 55 * Uses the relation media metadata property and the metadata index. 56 * 57 * Note that before 2013-07-31 the second parameter was the maximum number 58 * of results and permissions were ignored. That's why the parameter is now 59 * checked to be explicitely set to true (with type bool) in order to be 60 * compatible with older uses of the function. 61 * 62 * @param string $id The media id to look for 63 * @param bool $ignore_perms Ignore hidden pages and acls (optional, default: false) 64 * @return array A list of pages that use the given media file 65 */ 66 public static function mediause($id, $ignore_perms = false) 67 { 68 $Indexer = Indexer::getInstance(); 69 $result = $Indexer->lookupKey('relation_media', $id); 70 71 if (!count($result)) return $result; 72 73 // check ACL permissions 74 foreach (array_keys($result) as $idx) { 75 if (($ignore_perms !== true 76 && (isHiddenPage($result[$idx]) || auth_quickaclcheck($result[$idx]) < AUTH_READ) 77 ) || !page_exists($result[$idx], '', false) 78 ) { 79 unset($result[$idx]); 80 } 81 } 82 83 sort($result); 84 return $result; 85 } 86 87 88 /** 89 * Quicksearch for pagenames 90 * 91 * By default it only matches the pagename and ignores the namespace. 92 * This can be changed with the second parameter. 93 * The third parameter allows to search in titles as well. 94 * 95 * The function always returns titles as well 96 * 97 * @triggers SEARCH_QUERY_PAGELOOKUP 98 * @author Andreas Gohr <andi@splitbrain.org> 99 * @author Adrian Lang <lang@cosmocode.de> 100 * 101 * @param string $id page id 102 * @param bool $in_ns match against namespace as well? 103 * @param bool $in_title search in title? 104 * @param int|string $after only show results with mtime after this date, 105 * accepts timestap or strtotime arguments 106 * @param int|string $before only show results with mtime before this date, 107 * accepts timestap or strtotime arguments 108 * 109 * @return string[] 110 */ 111 public static function pageLookup($id, $in_ns = false, $in_title = false, $after = null, $before = null) 112 { 113 $data = [ 114 'id' => $id, 115 'in_ns' => $in_ns, 116 'in_title' => $in_title, 117 'after' => $after, 118 'before' => $before 119 ]; 120 $data['has_titles'] = true; // for plugin backward compatibility check 121 $action = static::class.'::callback_pageLookup'; 122 return Event::createAndTrigger('SEARCH_QUERY_PAGELOOKUP', $data, $action); 123 } 124 125 /** 126 * Returns list of pages as array(pageid => First Heading) 127 * 128 * @param array $data event data 129 * @return string[] 130 */ 131 public static function callback_pageLookup($data) 132 { 133 $Indexer = Indexer::getInstance(); 134 135 // split out original parameters 136 $id = $data['id']; 137 $parsedQuery = QueryParser::convert($id); 138 139 if (count($parsedQuery['ns']) > 0) { 140 $ns = cleanID($parsedQuery['ns'][0]) . ':'; 141 $id = implode(' ', $parsedQuery['highlight']); 142 } 143 144 $in_ns = $data['in_ns']; 145 $in_title = $data['in_title']; 146 $cleaned = cleanID($id); 147 148 $pages = array(); 149 if ($id !== '' && $cleaned !== '') { 150 $page_idx = $Indexer->getPages(); 151 foreach ($page_idx as $p_id) { 152 if ((strpos($in_ns ? $p_id : noNSorNS($p_id), $cleaned) !== false)) { 153 if (!isset($pages[$p_id])) { 154 $pages[$p_id] = p_get_first_heading($p_id, METADATA_DONT_RENDER); 155 } 156 } 157 } 158 if ($in_title) { 159 $func = static::class.'::pageLookupTitleCompare'; 160 foreach ($Indexer->lookupKey('title', $id, $func) as $p_id) { 161 if (!isset($pages[$p_id])) { 162 $pages[$p_id] = p_get_first_heading($p_id, METADATA_DONT_RENDER); 163 } 164 } 165 } 166 } 167 168 if (isset($ns)) { 169 foreach (array_keys($pages) as $p_id) { 170 if (strpos($p_id, $ns) !== 0) { 171 unset($pages[$p_id]); 172 } 173 } 174 } 175 176 // discard hidden pages 177 // discard nonexistent pages 178 // check ACL permissions 179 foreach (array_keys($pages) as $idx) { 180 if (!isVisiblePage($idx) || !page_exists($idx) || auth_quickaclcheck($idx) < AUTH_READ) { 181 unset($pages[$idx]); 182 } 183 } 184 185 $pages = static::filterResultsByTime($pages, $data['after'], $data['before']); 186 187 uksort($pages, static::class.'::pagesorter'); 188 return $pages; 189 } 190 191 /** 192 * Tiny helper function for comparing the searched title with the title 193 * from the search index. This function is a wrapper around stripos with 194 * adapted argument order and return value. 195 * 196 * @param string $search searched title 197 * @param string $title title from index 198 * @return bool 199 */ 200 protected static function pageLookupTitleCompare($search, $title) 201 { 202 return stripos($title, $search) !== false; 203 } 204 205 /** 206 * Sort pages based on their namespace level first, then on their string 207 * values. This makes higher hierarchy pages rank higher than lower hierarchy 208 * pages. 209 * 210 * @param string $a 211 * @param string $b 212 * @return int Returns < 0 if $a is less than $b; > 0 if $a is greater than $b, 213 * and 0 if they are equal. 214 */ 215 protected static function pagesorter($a, $b) 216 { 217 $ac = count(explode(':',$a)); 218 $bc = count(explode(':',$b)); 219 if ($ac < $bc) { 220 return -1; 221 } elseif ($ac > $bc) { 222 return 1; 223 } 224 return strcmp ($a,$b); 225 } 226 227 /** 228 * @param array $results search results in the form pageid => value 229 * @param int|string $after only returns results with mtime after this date, 230 * accepts timestap or strtotime arguments 231 * @param int|string $before only returns results with mtime after this date, 232 * accepts timestap or strtotime arguments 233 * 234 * @return array 235 */ 236 protected static function filterResultsByTime(array $results, $after, $before) 237 { 238 if ($after || $before) { 239 $after = is_int($after) ? $after : strtotime($after); 240 $before = is_int($before) ? $before : strtotime($before); 241 242 foreach ($results as $id => $value) { 243 $mTime = filemtime(wikiFN($id)); 244 if ($after && $after > $mTime) { 245 unset($results[$id]); 246 continue; 247 } 248 if ($before && $before < $mTime) { 249 unset($results[$id]); 250 } 251 } 252 } 253 return $results; 254 } 255} 256