MediaWiki master
PrefixSearch.php
Go to the documentation of this file.
1<?php
29
38abstract class PrefixSearch {
48 public function search( $search, $limit, $namespaces = [], $offset = 0 ) {
49 $search = trim( $search );
50 if ( $search == '' ) {
51 return []; // Return empty result
52 }
53
54 $hasNamespace = SearchEngine::parseNamespacePrefixes( $search, false, true );
55 if ( $hasNamespace !== false ) {
56 [ $search, $namespaces ] = $hasNamespace;
57 }
58
59 return $this->searchBackend( $namespaces, $search, $limit, $offset );
60 }
61
71 public function searchWithVariants( $search, $limit, array $namespaces, $offset = 0 ) {
72 $searches = $this->search( $search, $limit, $namespaces, $offset );
73
74 // if the content language has variants, try to retrieve fallback results
75 $fallbackLimit = $limit - count( $searches );
76 if ( $fallbackLimit > 0 ) {
77 $services = MediaWikiServices::getInstance();
78 $fallbackSearches = $services->getLanguageConverterFactory()
79 ->getLanguageConverter( $services->getContentLanguage() )
80 ->autoConvertToAllVariants( $search );
81 $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] );
82
83 foreach ( $fallbackSearches as $fbs ) {
84 $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces );
85 $searches = array_merge( $searches, $fallbackSearchResult );
86 $fallbackLimit -= count( $fallbackSearchResult );
87
88 if ( $fallbackLimit == 0 ) {
89 break;
90 }
91 }
92 }
93 return $searches;
94 }
95
103 abstract protected function titles( array $titles );
104
112 abstract protected function strings( array $strings );
113
122 protected function searchBackend( $namespaces, $search, $limit, $offset ) {
123 if ( count( $namespaces ) == 1 ) {
124 $ns = $namespaces[0];
125 if ( $ns == NS_MEDIA ) {
126 $namespaces = [ NS_FILE ];
127 } elseif ( $ns == NS_SPECIAL ) {
128 return $this->titles( $this->specialSearch( $search, $limit, $offset ) );
129 }
130 }
131 $srchres = [];
132 if ( ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onPrefixSearchBackend(
133 $namespaces, $search, $limit, $srchres, $offset )
134 ) {
135 return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit, $offset ) );
136 }
137 return $this->strings(
138 $this->handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) );
139 }
140
141 private function handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) {
142 if ( $offset === 0 ) {
143 // Only perform exact db match if offset === 0
144 // This is still far from perfect but at least we avoid returning the
145 // same title again and again when the user is scrolling with a query
146 // that matches a title in the db.
147 $rescorer = new SearchExactMatchRescorer();
148 $srchres = $rescorer->rescore( $search, $namespaces, $srchres, $limit );
149 }
150 return $srchres;
151 }
152
161 protected function specialSearch( $search, $limit, $offset ) {
162 $searchParts = explode( '/', $search, 2 );
163 $searchKey = $searchParts[0];
164 $subpageSearch = $searchParts[1] ?? null;
165
166 // Handle subpage search separately.
167 $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
168 if ( $subpageSearch !== null ) {
169 // Try matching the full search string as a page name
170 $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey );
171 if ( !$specialTitle ) {
172 return [];
173 }
174 $special = $spFactory->getPage( $specialTitle->getText() );
175 if ( $special ) {
176 $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit, $offset );
177 return array_map( [ $specialTitle, 'getSubpage' ], $subpages );
178 } else {
179 return [];
180 }
181 }
182
183 # normalize searchKey, so aliases with spaces can be found - T27675
184 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
185 $searchKey = str_replace( ' ', '_', $searchKey );
186 $searchKey = $contLang->caseFold( $searchKey );
187
188 // Unlike SpecialPage itself, we want the canonical forms of both
189 // canonical and alias title forms...
190 $keys = [];
191 $listedPages = $spFactory->getListedPages();
192 foreach ( $listedPages as $page => $_obj ) {
193 $keys[$contLang->caseFold( $page )] = [ 'page' => $page, 'rank' => 0 ];
194 }
195
196 foreach ( $contLang->getSpecialPageAliases() as $page => $aliases ) {
197 // Exclude localisation aliases for pages that are not defined (T22885),
198 // e.g. if an extension registers a page based on site configuration.
199 if ( !in_array( $page, $spFactory->getNames() ) ) {
200 continue;
201 }
202 // Exclude aliases for unlisted pages
203 if ( !isset( $listedPages[ $page ] ) ) {
204 continue;
205 }
206
207 foreach ( $aliases as $key => $alias ) {
208 $keys[$contLang->caseFold( $alias )] = [ 'page' => $alias, 'rank' => $key ];
209 }
210 }
211 ksort( $keys );
212
213 $matches = [];
214 foreach ( $keys as $pageKey => $page ) {
215 if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
216 // T29671: Don't use SpecialPage::getTitleFor() here because it
217 // localizes its input leading to searches for e.g. Special:All
218 // returning Spezial:MediaWiki-Systemnachrichten and returning
219 // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de'
220 $matches[$page['rank']][] = Title::makeTitleSafe( NS_SPECIAL, $page['page'] );
221
222 if ( isset( $matches[0] ) && count( $matches[0] ) >= $limit + $offset ) {
223 // We have enough items in primary rank, no use to continue
224 break;
225 }
226 }
227
228 }
229
230 // Ensure keys are in order
231 ksort( $matches );
232 // Flatten the array
233 $matches = array_reduce( $matches, 'array_merge', [] );
234
235 return array_slice( $matches, $offset, $limit );
236 }
237
250 public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
251 if ( !$namespaces ) {
252 $namespaces = [ NS_MAIN ];
253 }
254
255 if ( in_array( NS_SPECIAL, $namespaces ) ) {
256 // For now, if special is included, ignore the other namespaces
257 return $this->specialSearch( $search, $limit, $offset );
258 }
259
260 // Construct suitable prefix for each namespace. They differ in cases where
261 // some namespaces always capitalize and some don't.
262 $prefixes = [];
263 // Allow to do a prefix search for e.g. "Talk:"
264 if ( $search === '' ) {
265 $prefixes[$search] = $namespaces;
266 } else {
267 // Don't just ignore input like "[[Foo]]", but try to search for "Foo"
268 $search = preg_replace( MediaWikiTitleCodec::getTitleInvalidRegex(), '', $search );
269 foreach ( $namespaces as $namespace ) {
270 $title = Title::makeTitleSafe( $namespace, $search );
271 if ( $title ) {
272 $prefixes[ $title->getDBkey() ][] = $namespace;
273 }
274 }
275 }
276 if ( !$prefixes ) {
277 return [];
278 }
279
280 $services = MediaWikiServices::getInstance();
281 $dbr = $services->getConnectionProvider()->getReplicaDatabase();
282 // Often there is only one prefix that applies to all requested namespaces,
283 // but sometimes there are two if some namespaces do not always capitalize.
284 $conds = [];
285 foreach ( $prefixes as $prefix => $namespaces ) {
286 $expr = $dbr->expr( 'page_namespace', '=', $namespaces );
287 if ( $prefix !== '' ) {
288 $expr = $expr->and(
289 'page_title',
290 IExpression::LIKE,
291 new LikeValue( (string)$prefix, $dbr->anyString() )
292 );
293 }
294 $conds[] = $expr;
295 }
296
297 $queryBuilder = $dbr->newSelectQueryBuilder()
298 ->select( [ 'page_id', 'page_namespace', 'page_title' ] )
299 ->from( 'page' )
300 ->where( $dbr->orExpr( $conds ) )
301 ->orderBy( [ 'page_title', 'page_namespace' ] )
302 ->limit( $limit )
303 ->offset( $offset );
304 $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
305
306 return iterator_to_array( $services->getTitleFactory()->newTitleArrayFromResult( $res ) );
307 }
308}
const NS_FILE
Definition Defines.php:71
const NS_MAIN
Definition Defines.php:65
const NS_SPECIAL
Definition Defines.php:54
const NS_MEDIA
Definition Defines.php:53
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Service locator for MediaWiki core services.
A codec for MediaWiki page titles.
Represents a title within MediaWiki.
Definition Title.php:78
Handles searching prefixes of titles and finding any page names that match.
searchWithVariants( $search, $limit, array $namespaces, $offset=0)
Do a prefix search for all possible variants of the prefix.
search( $search, $limit, $namespaces=[], $offset=0)
Do a prefix search of titles and return a list of matching page names.
specialSearch( $search, $limit, $offset)
Prefix search special-case for Special: namespace.
titles(array $titles)
When implemented in a descendant class, receives an array of Title objects and returns either an unmo...
defaultSearchBackend( $namespaces, $search, $limit, $offset)
Unless overridden by PrefixSearchBackend hook... This is case-sensitive (First character may be autom...
strings(array $strings)
When implemented in a descendant class, receives an array of titles as strings and returns either an ...
searchBackend( $namespaces, $search, $limit, $offset)
Do a prefix search of titles and return a list of matching page names.
An utility class to rescore search results by looking for an exact match in the db and add the page f...
Content of like value.
Definition LikeValue.php:14