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