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(
142 array $srchres, array $namespaces, string $search, int $limit, int $offset
143 ): array {
144 if ( $offset === 0 ) {
145 // Only perform exact db match if offset === 0
146 // This is still far from perfect but at least we avoid returning the
147 // same title again and again when the user is scrolling with a query
148 // that matches a title in the db.
149 $rescorer = new SearchExactMatchRescorer();
150 $srchres = $rescorer->rescore( $search, $namespaces, $srchres, $limit );
151 }
152 return $srchres;
153 }
154
163 protected function specialSearch( $search, $limit, $offset ) {
164 $searchParts = explode( '/', $search, 2 );
165 $searchKey = $searchParts[0];
166 $subpageSearch = $searchParts[1] ?? null;
167
168 // Handle subpage search separately.
169 $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
170 if ( $subpageSearch !== null ) {
171 // Try matching the full search string as a page name
172 $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey );
173 if ( !$specialTitle ) {
174 return [];
175 }
176 $special = $spFactory->getPage( $specialTitle->getText() );
177 if ( $special ) {
178 $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit, $offset );
179 return array_map( [ $specialTitle, 'getSubpage' ], $subpages );
180 } else {
181 return [];
182 }
183 }
184
185 # normalize searchKey, so aliases with spaces can be found - T27675
186 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
187 $searchKey = str_replace( ' ', '_', $searchKey );
188 $searchKey = $contLang->caseFold( $searchKey );
189
190 // Unlike SpecialPage itself, we want the canonical forms of both
191 // canonical and alias title forms...
192 $keys = [];
193 $listedPages = $spFactory->getListedPages();
194 foreach ( $listedPages as $page => $_obj ) {
195 $keys[$contLang->caseFold( $page )] = [ 'page' => $page, 'rank' => 0 ];
196 }
197
198 foreach ( $contLang->getSpecialPageAliases() as $page => $aliases ) {
199 // Exclude localisation aliases for pages that are not defined (T22885),
200 // e.g. if an extension registers a page based on site configuration.
201 if ( !in_array( $page, $spFactory->getNames() ) ) {
202 continue;
203 }
204 // Exclude aliases for unlisted pages
205 if ( !isset( $listedPages[ $page ] ) ) {
206 continue;
207 }
208
209 foreach ( $aliases as $key => $alias ) {
210 $keys[$contLang->caseFold( $alias )] = [ 'page' => $alias, 'rank' => $key ];
211 }
212 }
213 ksort( $keys );
214
215 $matches = [];
216 foreach ( $keys as $pageKey => $page ) {
217 if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
218 // T29671: Don't use SpecialPage::getTitleFor() here because it
219 // localizes its input leading to searches for e.g. Special:All
220 // returning Spezial:MediaWiki-Systemnachrichten and returning
221 // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de'
222 $matches[$page['rank']][] = Title::makeTitleSafe( NS_SPECIAL, $page['page'] );
223
224 if ( isset( $matches[0] ) && count( $matches[0] ) >= $limit + $offset ) {
225 // We have enough items in primary rank, no use to continue
226 break;
227 }
228 }
229
230 }
231
232 // Ensure keys are in order
233 ksort( $matches );
234 // Flatten the array
235 $matches = array_reduce( $matches, 'array_merge', [] );
236
237 return array_slice( $matches, $offset, $limit );
238 }
239
252 public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
253 if ( !$namespaces ) {
254 $namespaces = [ NS_MAIN ];
255 }
256
257 if ( in_array( NS_SPECIAL, $namespaces ) ) {
258 // For now, if special is included, ignore the other namespaces
259 return $this->specialSearch( $search, $limit, $offset );
260 }
261
262 // Construct suitable prefix for each namespace. They differ in cases where
263 // some namespaces always capitalize and some don't.
264 $prefixes = [];
265 // Allow to do a prefix search for e.g. "Talk:"
266 if ( $search === '' ) {
267 $prefixes[$search] = $namespaces;
268 } else {
269 // Don't just ignore input like "[[Foo]]", but try to search for "Foo"
270 $search = preg_replace( TitleParser::getTitleInvalidRegex(), '', $search );
271 foreach ( $namespaces as $namespace ) {
272 $title = Title::makeTitleSafe( $namespace, $search );
273 if ( $title ) {
274 $prefixes[ $title->getDBkey() ][] = $namespace;
275 }
276 }
277 }
278 if ( !$prefixes ) {
279 return [];
280 }
281
282 $services = MediaWikiServices::getInstance();
283 $dbr = $services->getConnectionProvider()->getReplicaDatabase();
284 // Often there is only one prefix that applies to all requested namespaces,
285 // but sometimes there are two if some namespaces do not always capitalize.
286 $conds = [];
287 foreach ( $prefixes as $prefix => $namespaces ) {
288 $expr = $dbr->expr( 'page_namespace', '=', $namespaces );
289 if ( $prefix !== '' ) {
290 $expr = $expr->and(
291 'page_title',
292 IExpression::LIKE,
293 new LikeValue( (string)$prefix, $dbr->anyString() )
294 );
295 }
296 $conds[] = $expr;
297 }
298
299 $queryBuilder = $dbr->newSelectQueryBuilder()
300 ->select( [ 'page_id', 'page_namespace', 'page_title' ] )
301 ->from( 'page' )
302 ->where( $dbr->orExpr( $conds ) )
303 ->orderBy( [ 'page_title', 'page_namespace' ] )
304 ->limit( $limit )
305 ->offset( $offset );
306 $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
307
308 return iterator_to_array( $services->getTitleFactory()->newTitleArrayFromResult( $res ) );
309 }
310}
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
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:82
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Service locator for MediaWiki core services.
A title parser service for MediaWiki.
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