MediaWiki master
PrefixSearch.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Search;
10
16
25abstract class PrefixSearch {
35 public function search( $search, $limit, $namespaces = [], $offset = 0 ) {
36 $search = trim( $search );
37 if ( $search == '' ) {
38 return []; // Return empty result
39 }
40
41 $hasNamespace = SearchEngine::parseNamespacePrefixes( $search, false, true );
42 if ( $hasNamespace !== false ) {
43 [ $search, $namespaces ] = $hasNamespace;
44 }
45
46 return $this->searchBackend( $namespaces, $search, $limit, $offset );
47 }
48
58 public function searchWithVariants( $search, $limit, array $namespaces, $offset = 0 ) {
59 $searches = $this->search( $search, $limit, $namespaces, $offset );
60
61 // if the content language has variants, try to retrieve fallback results
62 $fallbackLimit = $limit - count( $searches );
63 if ( $fallbackLimit > 0 ) {
65 $fallbackSearches = $services->getLanguageConverterFactory()
66 ->getLanguageConverter( $services->getContentLanguage() )
67 ->autoConvertToAllVariants( $search );
68 $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] );
69
70 foreach ( $fallbackSearches as $fbs ) {
71 $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces );
72 $searches = array_merge( $searches, $fallbackSearchResult );
73 $fallbackLimit -= count( $fallbackSearchResult );
74
75 if ( $fallbackLimit == 0 ) {
76 break;
77 }
78 }
79 }
80 return $searches;
81 }
82
90 abstract protected function titles( array $titles );
91
99 abstract protected function strings( array $strings );
100
109 protected function searchBackend( $namespaces, $search, $limit, $offset ) {
110 if ( count( $namespaces ) == 1 ) {
111 $ns = $namespaces[0];
112 if ( $ns == NS_MEDIA ) {
113 $namespaces = [ NS_FILE ];
114 } elseif ( $ns == NS_SPECIAL ) {
115 return $this->titles( $this->specialSearch( $search, $limit, $offset ) );
116 }
117 }
118 return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit, $offset ) );
119 }
120
129 protected function specialSearch( $search, $limit, $offset ) {
130 $searchParts = explode( '/', $search, 2 );
131 $searchKey = $searchParts[0];
132 $subpageSearch = $searchParts[1] ?? null;
133
134 // Handle subpage search separately.
135 $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
136 if ( $subpageSearch !== null ) {
137 // Try matching the full search string as a page name
138 $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey );
139 if ( !$specialTitle ) {
140 return [];
141 }
142 $special = $spFactory->getPage( $specialTitle->getText() );
143 if ( $special ) {
144 $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit, $offset );
145 return array_map( $specialTitle->getSubpage( ... ), $subpages );
146 } else {
147 return [];
148 }
149 }
150
151 # normalize searchKey, so aliases with spaces can be found - T27675
152 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
153 $searchKey = str_replace( ' ', '_', $searchKey );
154 $searchKey = $contLang->caseFold( $searchKey );
155
156 // Unlike SpecialPage itself, we want the canonical forms of both
157 // canonical and alias title forms...
158 $keys = [];
159 $listedPages = $spFactory->getListedPages();
160 foreach ( $listedPages as $page => $_obj ) {
161 $keys[$contLang->caseFold( $page )] = [ 'page' => $page, 'rank' => 0 ];
162 }
163
164 foreach ( $contLang->getSpecialPageAliases() as $page => $aliases ) {
165 // Exclude localisation aliases for pages that are not defined (T22885),
166 // e.g. if an extension registers a page based on site configuration.
167 if ( !in_array( $page, $spFactory->getNames() ) ) {
168 continue;
169 }
170 // Exclude aliases for unlisted pages
171 if ( !isset( $listedPages[$page] ) ) {
172 continue;
173 }
174
175 foreach ( $aliases as $key => $alias ) {
176 $keys[$contLang->caseFold( $alias )] = [ 'page' => $alias, 'rank' => $key ];
177 }
178 }
179 ksort( $keys );
180
181 $matches = [];
182 foreach ( $keys as $pageKey => $page ) {
183 if ( $searchKey === '' || str_starts_with( $pageKey, $searchKey ) ) {
184 // T29671: Don't use SpecialPage::getTitleFor() here because it
185 // localizes its input leading to searches for e.g. Special:All
186 // returning Spezial:MediaWiki-Systemnachrichten and returning
187 // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de'
188 $matches[$page['rank']][] = Title::makeTitleSafe( NS_SPECIAL, $page['page'] );
189
190 if ( isset( $matches[0] ) && count( $matches[0] ) >= $limit + $offset ) {
191 // We have enough items in primary rank, no use to continue
192 break;
193 }
194 }
195
196 }
197
198 // Ensure keys are in order
199 ksort( $matches );
200 // Flatten the array
201 $matches = array_reduce( $matches, 'array_merge', [] );
202
203 return array_slice( $matches, $offset, $limit );
204 }
205
217 public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
218 if ( !$namespaces ) {
219 $namespaces = [ NS_MAIN ];
220 }
221
222 if ( in_array( NS_SPECIAL, $namespaces ) ) {
223 // For now, if special is included, ignore the other namespaces
224 return $this->specialSearch( $search, $limit, $offset );
225 }
226
227 // Construct suitable prefix for each namespace. They differ in cases where
228 // some namespaces always capitalize and some don't.
229 $prefixes = [];
230 // Allow to do a prefix search for e.g. "Talk:"
231 if ( $search === '' ) {
232 $prefixes[$search] = $namespaces;
233 } else {
234 // Don't just ignore input like "[[Foo]]", but try to search for "Foo"
235 $search = preg_replace( TitleParser::getTitleInvalidRegex(), '', $search );
236 foreach ( $namespaces as $namespace ) {
237 $title = Title::makeTitleSafe( $namespace, $search );
238 if ( $title ) {
239 $prefixes[$title->getDBkey()][] = $namespace;
240 }
241 }
242 }
243 if ( !$prefixes ) {
244 return [];
245 }
246
247 $services = MediaWikiServices::getInstance();
248 $dbr = $services->getConnectionProvider()->getReplicaDatabase();
249 // Often there is only one prefix that applies to all requested namespaces,
250 // but sometimes there are two if some namespaces do not always capitalize.
251 $conds = [];
252 foreach ( $prefixes as $prefix => $namespaces ) {
253 $expr = $dbr->expr( 'page_namespace', '=', $namespaces );
254 if ( $prefix !== '' ) {
255 $expr = $expr->and(
256 'page_title',
257 IExpression::LIKE,
258 new LikeValue( (string)$prefix, $dbr->anyString() )
259 );
260 }
261 $conds[] = $expr;
262 }
263
264 $queryBuilder = $dbr->newSelectQueryBuilder()
265 ->select( [ 'page_id', 'page_namespace', 'page_title' ] )
266 ->from( 'page' )
267 ->where( $dbr->orExpr( $conds ) )
268 ->orderBy( [ 'page_title', 'page_namespace' ] )
269 ->limit( $limit )
270 ->offset( $offset );
271 $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
272
273 return iterator_to_array( $services->getTitleFactory()->newTitleArrayFromResult( $res ) );
274 }
275}
276
278class_alias( PrefixSearch::class, 'PrefixSearch' );
const NS_FILE
Definition Defines.php:57
const NS_MAIN
Definition Defines.php:51
const NS_SPECIAL
Definition Defines.php:40
const NS_MEDIA
Definition Defines.php:39
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Handles searching prefixes of titles and finding any page names that match.
strings(array $strings)
When implemented in a descendant class, receives an array of titles as strings and returns either an ...
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)
This is case-sensitive (First character may be automatically capitalized by Title::secureAndSpit() la...
searchWithVariants( $search, $limit, array $namespaces, $offset=0)
Do a prefix search for all possible variants of the prefix.
searchBackend( $namespaces, $search, $limit, $offset)
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.
search( $search, $limit, $namespaces=[], $offset=0)
Do a prefix search of titles and return a list of matching page names.
static parseNamespacePrefixes( $query, $withAllKeyword=true, $withPrefixSearchExtractNamespaceHook=false)
Parse some common prefixes: all (search everything) or namespace names.
A title parser service for MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:69
Content of like value.
Definition LikeValue.php:14
Definition of a mapping for the search index field.