MediaWiki  master
PrefixSearch.php
Go to the documentation of this file.
1 <?php
24 
33 abstract class PrefixSearch {
43  public function search( $search, $limit, $namespaces = [], $offset = 0 ) {
44  $search = trim( $search );
45  if ( $search == '' ) {
46  return []; // Return empty result
47  }
48 
49  $hasNamespace = SearchEngine::parseNamespacePrefixes( $search, false, true );
50  if ( $hasNamespace !== false ) {
51  list( $search, $namespaces ) = $hasNamespace;
52  }
53 
54  return $this->searchBackend( $namespaces, $search, $limit, $offset );
55  }
56 
66  public function searchWithVariants( $search, $limit, array $namespaces, $offset = 0 ) {
67  $searches = $this->search( $search, $limit, $namespaces, $offset );
68 
69  // if the content language has variants, try to retrieve fallback results
70  $fallbackLimit = $limit - count( $searches );
71  if ( $fallbackLimit > 0 ) {
72  $services = MediaWikiServices::getInstance();
73  $fallbackSearches = $services->getLanguageConverterFactory()
74  ->getLanguageConverter( $services->getContentLanguage() )
75  ->autoConvertToAllVariants( $search );
76  $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] );
77 
78  foreach ( $fallbackSearches as $fbs ) {
79  $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces );
80  $searches = array_merge( $searches, $fallbackSearchResult );
81  $fallbackLimit -= count( $fallbackSearchResult );
82 
83  if ( $fallbackLimit == 0 ) {
84  break;
85  }
86  }
87  }
88  return $searches;
89  }
90 
98  abstract protected function titles( array $titles );
99 
107  abstract protected function strings( array $strings );
108 
117  protected function searchBackend( $namespaces, $search, $limit, $offset ) {
118  if ( count( $namespaces ) == 1 ) {
119  $ns = $namespaces[0];
120  if ( $ns == NS_MEDIA ) {
121  $namespaces = [ NS_FILE ];
122  } elseif ( $ns == NS_SPECIAL ) {
123  return $this->titles( $this->specialSearch( $search, $limit, $offset ) );
124  }
125  }
126  $srchres = [];
127  if ( Hooks::runner()->onPrefixSearchBackend(
128  $namespaces, $search, $limit, $srchres, $offset )
129  ) {
130  return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit, $offset ) );
131  }
132  return $this->strings(
133  $this->handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) );
134  }
135 
136  private function handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) {
137  if ( $offset === 0 ) {
138  // Only perform exact db match if offset === 0
139  // This is still far from perfect but at least we avoid returning the
140  // same title again and again when the user is scrolling with a query
141  // that matches a title in the db.
142  $rescorer = new SearchExactMatchRescorer();
143  $srchres = $rescorer->rescore( $search, $namespaces, $srchres, $limit );
144  }
145  return $srchres;
146  }
147 
156  protected function specialSearch( $search, $limit, $offset ) {
157  $searchParts = explode( '/', $search, 2 );
158  $searchKey = $searchParts[0];
159  $subpageSearch = $searchParts[1] ?? null;
160 
161  // Handle subpage search separately.
162  $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
163  if ( $subpageSearch !== null ) {
164  // Try matching the full search string as a page name
165  $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey );
166  if ( !$specialTitle ) {
167  return [];
168  }
169  $special = $spFactory->getPage( $specialTitle->getText() );
170  if ( $special ) {
171  $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit, $offset );
172  return array_map( [ $specialTitle, 'getSubpage' ], $subpages );
173  } else {
174  return [];
175  }
176  }
177 
178  # normalize searchKey, so aliases with spaces can be found - T27675
179  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
180  $searchKey = str_replace( ' ', '_', $searchKey );
181  $searchKey = $contLang->caseFold( $searchKey );
182 
183  // Unlike SpecialPage itself, we want the canonical forms of both
184  // canonical and alias title forms...
185  $keys = [];
186  foreach ( $spFactory->getNames() as $page ) {
187  $keys[$contLang->caseFold( $page )] = [ 'page' => $page, 'rank' => 0 ];
188  }
189 
190  foreach ( $contLang->getSpecialPageAliases() as $page => $aliases ) {
191  if ( !in_array( $page, $spFactory->getNames() ) ) {# T22885
192  continue;
193  }
194 
195  foreach ( $aliases as $key => $alias ) {
196  $keys[$contLang->caseFold( $alias )] = [ 'page' => $alias, 'rank' => $key ];
197  }
198  }
199  ksort( $keys );
200 
201  $matches = [];
202  foreach ( $keys as $pageKey => $page ) {
203  if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
204  // T29671: Don't use SpecialPage::getTitleFor() here because it
205  // localizes its input leading to searches for e.g. Special:All
206  // returning Spezial:MediaWiki-Systemnachrichten and returning
207  // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de'
208  $matches[$page['rank']][] = Title::makeTitleSafe( NS_SPECIAL, $page['page'] );
209 
210  if ( isset( $matches[0] ) && count( $matches[0] ) >= $limit + $offset ) {
211  // We have enough items in primary rank, no use to continue
212  break;
213  }
214  }
215 
216  }
217 
218  // Ensure keys are in order
219  ksort( $matches );
220  // Flatten the array
221  $matches = array_reduce( $matches, 'array_merge', [] );
222 
223  return array_slice( $matches, $offset, $limit );
224  }
225 
238  public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
239  if ( !$namespaces ) {
240  $namespaces = [ NS_MAIN ];
241  }
242 
243  if ( in_array( NS_SPECIAL, $namespaces ) ) {
244  // For now, if special is included, ignore the other namespaces
245  return $this->specialSearch( $search, $limit, $offset );
246  }
247 
248  // Construct suitable prefix for each namespace. They differ in cases where
249  // some namespaces always capitalize and some don't.
250  $prefixes = [];
251  // Allow to do a prefix search for e.g. "Talk:"
252  if ( $search === '' ) {
253  $prefixes[$search] = $namespaces;
254  } else {
255  // Don't just ignore input like "[[Foo]]", but try to search for "Foo"
256  $search = preg_replace( MediaWikiTitleCodec::getTitleInvalidRegex(), '', $search );
257  foreach ( $namespaces as $namespace ) {
258  $title = Title::makeTitleSafe( $namespace, $search );
259  if ( $title ) {
260  $prefixes[ $title->getDBkey() ][] = $namespace;
261  }
262  }
263  }
264  if ( !$prefixes ) {
265  return [];
266  }
267 
268  $dbr = wfGetDB( DB_REPLICA );
269  // Often there is only one prefix that applies to all requested namespaces,
270  // but sometimes there are two if some namespaces do not always capitalize.
271  $conds = [];
272  foreach ( $prefixes as $prefix => $namespaces ) {
273  $condition = [ 'page_namespace' => $namespaces ];
274  if ( $prefix !== '' ) {
275  $condition[] = 'page_title' . $dbr->buildLike( $prefix, $dbr->anyString() );
276  }
277  $conds[] = $dbr->makeList( $condition, LIST_AND );
278  }
279 
280  $table = 'page';
281  $fields = [ 'page_id', 'page_namespace', 'page_title' ];
282  $conds = $dbr->makeList( $conds, LIST_OR );
283  $options = [
284  'LIMIT' => $limit,
285  'ORDER BY' => [ 'page_title', 'page_namespace' ],
286  'OFFSET' => $offset
287  ];
288 
289  $res = $dbr->select( $table, $fields, $conds, __METHOD__, $options );
290 
291  return iterator_to_array( TitleArray::newFromResult( $res ) );
292  }
293 
300  protected function validateNamespaces( $namespaces ) {
301  // We will look at each given namespace against content language namespaces
302  $validNamespaces = MediaWikiServices::getInstance()->getContentLanguage()->getNamespaces();
303  if ( is_array( $namespaces ) && count( $namespaces ) > 0 ) {
304  $valid = [];
305  foreach ( $namespaces as $ns ) {
306  if ( is_numeric( $ns ) && array_key_exists( $ns, $validNamespaces ) ) {
307  $valid[] = $ns;
308  }
309  }
310  if ( count( $valid ) > 0 ) {
311  return $valid;
312  }
313  }
314 
315  return [ NS_MAIN ];
316  }
317 }
const NS_FILE
Definition: Defines.php:70
const NS_MAIN
Definition: Defines.php:64
const NS_SPECIAL
Definition: Defines.php:53
const LIST_OR
Definition: Defines.php:46
const NS_MEDIA
Definition: Defines.php:52
const LIST_AND
Definition: Defines.php:43
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
$matches
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
static getTitleInvalidRegex()
Returns a simple regex that will match on characters and sequences invalid in titles.
Service locator for MediaWiki core services.
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.
validateNamespaces( $namespaces)
Validate an array of numerical namespace indexes.
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...
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.
static parseNamespacePrefixes( $query, $withAllKeyword=true, $withPrefixSearchExtractNamespaceHook=false)
Parse some common prefixes: all (search everything) or namespace names.
An utility class to rescore search results by looking for an exact match in the db and add the page f...
static newFromResult( $res)
Definition: TitleArray.php:44
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:664
const DB_REPLICA
Definition: defines.php:26