1<?php 2 3namespace dokuwiki\Search; 4 5use dokuwiki\Debug\DebugHelper; 6use dokuwiki\Search\Collection\CollectionSearch; 7use dokuwiki\Search\Collection\PageFulltextCollection; 8use dokuwiki\Search\Collection\PageMetaCollection; 9use dokuwiki\Search\Collection\PageTitleCollection; 10use dokuwiki\Search\Exception\SearchException; 11use dokuwiki\Search\Index\FileIndex; 12use dokuwiki\Search\Index\TupleOps; 13 14/** 15 * Backward-compatible wrapper around {@see Indexer} 16 * 17 * The refactored {@see Indexer} reports failures by throwing 18 * {@see SearchException} subclasses. Plugins written against the legacy 19 * Doku_Indexer API expect the four mutating methods (addPage, deletePage, 20 * renamePage, clear) to return `true` on success or a string error message 21 * on failure. This class wraps an {@see Indexer} instance and restores that 22 * contract for those four methods. It also hosts the legacy helpers 23 * (lookupKey, getPages, addMetaKeys, renameMetaValue, getPID, lookup) that 24 * used to live on Indexer itself. 25 * 26 * It is returned by the deprecated {@see ::idx_get_indexer()} helper, which 27 * is the entry point most plugins use to obtain an indexer instance. New 28 * code should instantiate {@see Indexer} directly and handle 29 * {@see SearchException} via try/catch. 30 * 31 * Composition (not inheritance) is used because PHP does not allow 32 * overriding a `void` return type with `bool|string`. 33 * 34 * @deprecated 2026-04-07 use {@see Indexer} directly with try/catch 35 * 36 * @method string|int getVersion() 37 * @method string[] getAllPages(bool $existsFilter = false) 38 * @method string[] getPages(?string $key = null) 39 * @method bool needsIndexing(string $page, bool $force = false) 40 * @method void checkIntegrity() 41 * @method bool isIndexEmpty() 42 */ 43class LegacyIndexer 44{ 45 protected Indexer $indexer; 46 47 public function __construct(?Indexer $indexer = null) 48 { 49 $this->indexer = $indexer ?? new Indexer(); 50 } 51 52 /** 53 * Forward any other call (getVersion, getAllPages, getPages, needsIndexing, 54 * checkIntegrity, isIndexEmpty, ...) to the wrapped indexer. 55 * 56 * @deprecated 2026-04-07 call the same method on {@see Indexer} directly 57 */ 58 public function __call(string $name, array $args): mixed 59 { 60 DebugHelper::dbgDeprecatedFunction(Indexer::class . '::' . $name . '()'); 61 return $this->indexer->$name(...$args); 62 } 63 64 /** 65 * @return bool|string true if work was done, false if there was nothing to do, 66 * error message string on failure 67 * 68 * @deprecated 2026-04-07 use {@see Indexer::addPage()} with try/catch instead 69 */ 70 public function addPage(string $page, bool $force = false): bool|string 71 { 72 DebugHelper::dbgDeprecatedFunction(Indexer::class . '::addPage()'); 73 try { 74 return $this->indexer->addPage($page, $force); 75 } catch (SearchException $e) { 76 return $e->getMessage(); 77 } 78 } 79 80 /** 81 * @return bool|string true if work was done, false if there was nothing to do, 82 * error message string on failure 83 * 84 * @deprecated 2026-04-07 use {@see Indexer::deletePage()} with try/catch instead 85 */ 86 public function deletePage(string $page, bool $force = false): bool|string 87 { 88 DebugHelper::dbgDeprecatedFunction(Indexer::class . '::deletePage()'); 89 try { 90 return $this->indexer->deletePage($page, $force); 91 } catch (SearchException $e) { 92 return $e->getMessage(); 93 } 94 } 95 96 /** 97 * @return bool|string true if work was done, false if there was nothing to do, 98 * error message string on failure 99 * 100 * @deprecated 2026-04-07 use {@see Indexer::renamePage()} with try/catch instead 101 */ 102 public function renamePage(string $oldpage, string $newpage): bool|string 103 { 104 DebugHelper::dbgDeprecatedFunction(Indexer::class . '::renamePage()'); 105 try { 106 $result = $this->indexer->renamePage($oldpage, $newpage); 107 // a false result for differing names means the old page was not in the 108 // index; restore the legacy error message that callers expect here 109 if ($result === false && $oldpage !== $newpage) { 110 return 'page is not in index'; 111 } 112 return $result; 113 } catch (SearchException $e) { 114 return $e->getMessage(); 115 } 116 } 117 118 /** 119 * @return true|string true on success, error message on failure 120 * 121 * @deprecated 2026-04-07 use {@see Indexer::clear()} with try/catch instead 122 */ 123 public function clear(): bool|string 124 { 125 DebugHelper::dbgDeprecatedFunction(Indexer::class . '::clear()'); 126 try { 127 $this->indexer->clear(); 128 return true; 129 } catch (SearchException $e) { 130 return $e->getMessage(); 131 } 132 } 133 134 /** 135 * Find pages containing a metadata value 136 * 137 * @param string $key metadata key name 138 * @param string|string[] $value search term(s) 139 * @param callable|null $func ignored, kept for backward compatibility 140 * @return array 141 * 142 * @deprecated 2026-04-07 use MetadataSearch::lookupKey() instead 143 */ 144 public function lookupKey($key, &$value, $func = null) 145 { 146 DebugHelper::dbgDeprecatedFunction(MetadataSearch::class . '::lookupKey()'); 147 return (new MetadataSearch())->lookupKey($key, $value); 148 } 149 150 /** 151 * Add metadata values for a page 152 * 153 * @param string $page page name 154 * @param string $key metadata key name 155 * @param string|string[]|null $value value(s) to add 156 * @return bool 157 * 158 * @deprecated 2026-04-07 use Collection classes directly instead 159 */ 160 public function addMetaKeys($page, $key, $value = null) 161 { 162 DebugHelper::dbgDeprecatedFunction('Collection classes'); 163 try { 164 if ($key === 'title') { 165 $collection = new PageTitleCollection(); 166 } else { 167 $collection = new PageMetaCollection($key); 168 } 169 $values = is_array($value) ? $value : ($value !== null && $value !== '' ? [$value] : []); 170 $collection->lock()->addEntity($page, $values)->unlock(); 171 $this->indexer->updateMetadataRegistry([$key]); 172 return true; 173 } catch (SearchException) { 174 return false; 175 } 176 } 177 178 /** 179 * Rename a metadata value in the index 180 * 181 * @param string $key metadata key name 182 * @param string $oldvalue old value 183 * @param string $newvalue new value 184 * @return bool 185 * 186 * @deprecated 2026-04-07 use Collection classes directly instead 187 */ 188 public function renameMetaValue($key, $oldvalue, $newvalue) 189 { 190 DebugHelper::dbgDeprecatedFunction('Collection classes'); 191 try { 192 $collection = new PageMetaCollection($key); 193 $collection->lock(); 194 195 $tokenIndex = $collection->getTokenIndex(); 196 197 // find old value — search() is read-only, won't create entries 198 $matches = $tokenIndex->search('/^' . preg_quote($oldvalue, '/') . '$/'); 199 if ($matches === []) { 200 $collection->unlock(); 201 return true; 202 } 203 $oldid = array_key_first($matches); 204 205 // check if new value already exists (read-only lookup) 206 $newMatches = $tokenIndex->search('/^' . preg_quote($newvalue, '/') . '$/'); 207 208 if ($newMatches !== []) { 209 // both values exist — merge frequency data from old to new 210 $newid = array_key_first($newMatches); 211 $freqIndex = $collection->getFrequencyIndex(); 212 $reverseIndex = $collection->getReverseIndex(); 213 $oldFreqLine = $freqIndex->retrieveRow($oldid); 214 215 if ($oldFreqLine !== '') { 216 $newFreqLine = $freqIndex->retrieveRow($newid); 217 foreach (TupleOps::parseTuples($oldFreqLine) as $entityId => $count) { 218 $newFreqLine = TupleOps::updateTuple($newFreqLine, $entityId, $count); 219 220 // update reverse index: remove old token, add new 221 $reverseRow = $reverseIndex->retrieveRow((int)$entityId); 222 $keyline = explode(':', $reverseRow); 223 $keyline = array_diff($keyline, [(string)$oldid]); 224 if (!in_array((string)$newid, $keyline)) { 225 $keyline[] = $newid; 226 } 227 $reverseIndex->changeRow( 228 (int)$entityId, 229 implode(':', array_filter($keyline, fn($v) => $v !== '')) 230 ); 231 } 232 $freqIndex->changeRow($oldid, ''); 233 $freqIndex->changeRow($newid, $newFreqLine); 234 } 235 } else { 236 // new value doesn't exist — simple rename 237 $tokenIndex->changeRow($oldid, $newvalue); 238 } 239 240 $collection->unlock(); 241 return true; 242 } catch (SearchException) { 243 return false; 244 } 245 } 246 247 /** 248 * Get the page ID for a page name 249 * 250 * @param string $page page name 251 * @return int|false 252 * 253 * @deprecated 2026-04-07 use FileIndex directly instead 254 */ 255 public function getPID($page) 256 { 257 DebugHelper::dbgDeprecatedFunction(FileIndex::class); 258 try { 259 return (new FileIndex('page', '', true))->accessCachedValue($page); 260 } catch (SearchException) { 261 return false; 262 } 263 } 264 265 /** 266 * Find tokens in the fulltext index 267 * 268 * @param array $tokens list of words to search for 269 * @return array list of pages found [word => [page => count, ...]] 270 * 271 * @deprecated 2026-04-07 use CollectionSearch on PageFulltextCollection instead 272 */ 273 public function lookup($tokens) 274 { 275 DebugHelper::dbgDeprecatedFunction(CollectionSearch::class); 276 $collection = new PageFulltextCollection(); 277 $search = new CollectionSearch($collection); 278 $termMap = []; 279 foreach ($tokens as $token) { 280 if (!Tokenizer::isValidSearchTerm($token)) continue; 281 $term = $search->addTerm($token); 282 $termMap[$token] = $term; 283 } 284 285 if ($termMap === []) return []; 286 $search->execute(); 287 288 $result = []; 289 foreach ($termMap as $word => $term) { 290 $freqs = $term->getEntityFrequencies(); 291 // filter to only existing pages 292 $filtered = array_filter($freqs, fn($page) => page_exists($page, '', false), ARRAY_FILTER_USE_KEY); 293 $result[$word] = $filtered; 294 } 295 return $result; 296 } 297} 298