]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/PrefixSearch.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / PrefixSearch.php
1 <?php
2 /**
3  * Prefix search of page names.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  */
22
23 /**
24  * Handles searching prefixes of titles and finding any page
25  * names that match. Used largely by the OpenSearch implementation.
26  * @deprecated Since 1.27, Use SearchEngine::defaultPrefixSearch or SearchEngine::completionSearch
27  *
28  * @ingroup Search
29  */
30 abstract class PrefixSearch {
31         /**
32          * Do a prefix search of titles and return a list of matching page names.
33          * @deprecated Since 1.23, use TitlePrefixSearch or StringPrefixSearch classes
34          *
35          * @param string $search
36          * @param int $limit
37          * @param array $namespaces Used if query is not explicitly prefixed
38          * @param int $offset How many results to offset from the beginning
39          * @return array Array of strings
40          */
41         public static function titleSearch( $search, $limit, $namespaces = [], $offset = 0 ) {
42                 $prefixSearch = new StringPrefixSearch;
43                 return $prefixSearch->search( $search, $limit, $namespaces, $offset );
44         }
45
46         /**
47          * Do a prefix search of titles and return a list of matching page names.
48          *
49          * @param string $search
50          * @param int $limit
51          * @param array $namespaces Used if query is not explicitly prefixed
52          * @param int $offset How many results to offset from the beginning
53          * @return array Array of strings or Title objects
54          */
55         public function search( $search, $limit, $namespaces = [], $offset = 0 ) {
56                 $search = trim( $search );
57                 if ( $search == '' ) {
58                         return []; // Return empty result
59                 }
60
61                 $hasNamespace = $this->extractNamespace( $search );
62                 if ( $hasNamespace ) {
63                         list( $namespace, $search ) = $hasNamespace;
64                         $namespaces = [ $namespace ];
65                 } else {
66                         $namespaces = $this->validateNamespaces( $namespaces );
67                         Hooks::run( 'PrefixSearchExtractNamespace', [ &$namespaces, &$search ] );
68                 }
69
70                 return $this->searchBackend( $namespaces, $search, $limit, $offset );
71         }
72
73         /**
74          * Figure out if given input contains an explicit namespace.
75          *
76          * @param string $input
77          * @return false|array Array of namespace and remaining text, or false if no namespace given.
78          */
79         protected function extractNamespace( $input ) {
80                 if ( strpos( $input, ':' ) === false ) {
81                         return false;
82                 }
83
84                 // Namespace prefix only
85                 $title = Title::newFromText( $input . 'Dummy' );
86                 if (
87                         $title &&
88                         $title->getText() === 'Dummy' &&
89                         !$title->inNamespace( NS_MAIN ) &&
90                         !$title->isExternal()
91                 ) {
92                         return [ $title->getNamespace(), '' ];
93                 }
94
95                 // Namespace prefix with additional input
96                 $title = Title::newFromText( $input );
97                 if (
98                         $title &&
99                         !$title->inNamespace( NS_MAIN ) &&
100                         !$title->isExternal()
101                 ) {
102                         // getText provides correct capitalization
103                         return [ $title->getNamespace(), $title->getText() ];
104                 }
105
106                 return false;
107         }
108
109         /**
110          * Do a prefix search for all possible variants of the prefix
111          * @param string $search
112          * @param int $limit
113          * @param array $namespaces
114          * @param int $offset How many results to offset from the beginning
115          *
116          * @return array
117          */
118         public function searchWithVariants( $search, $limit, array $namespaces, $offset = 0 ) {
119                 $searches = $this->search( $search, $limit, $namespaces, $offset );
120
121                 // if the content language has variants, try to retrieve fallback results
122                 $fallbackLimit = $limit - count( $searches );
123                 if ( $fallbackLimit > 0 ) {
124                         global $wgContLang;
125
126                         $fallbackSearches = $wgContLang->autoConvertToAllVariants( $search );
127                         $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] );
128
129                         foreach ( $fallbackSearches as $fbs ) {
130                                 $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces );
131                                 $searches = array_merge( $searches, $fallbackSearchResult );
132                                 $fallbackLimit -= count( $fallbackSearchResult );
133
134                                 if ( $fallbackLimit == 0 ) {
135                                         break;
136                                 }
137                         }
138                 }
139                 return $searches;
140         }
141
142         /**
143          * When implemented in a descendant class, receives an array of Title objects and returns
144          * either an unmodified array or an array of strings corresponding to titles passed to it.
145          *
146          * @param array $titles
147          * @return array
148          */
149         abstract protected function titles( array $titles );
150
151         /**
152          * When implemented in a descendant class, receives an array of titles as strings and returns
153          * either an unmodified array or an array of Title objects corresponding to strings received.
154          *
155          * @param array $strings
156          *
157          * @return array
158          */
159         abstract protected function strings( array $strings );
160
161         /**
162          * Do a prefix search of titles and return a list of matching page names.
163          * @param array $namespaces
164          * @param string $search
165          * @param int $limit
166          * @param int $offset How many results to offset from the beginning
167          * @return array Array of strings
168          */
169         protected function searchBackend( $namespaces, $search, $limit, $offset ) {
170                 if ( count( $namespaces ) == 1 ) {
171                         $ns = $namespaces[0];
172                         if ( $ns == NS_MEDIA ) {
173                                 $namespaces = [ NS_FILE ];
174                         } elseif ( $ns == NS_SPECIAL ) {
175                                 return $this->titles( $this->specialSearch( $search, $limit, $offset ) );
176                         }
177                 }
178                 $srchres = [];
179                 if ( Hooks::run(
180                         'PrefixSearchBackend',
181                         [ $namespaces, $search, $limit, &$srchres, $offset ]
182                 ) ) {
183                         return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit, $offset ) );
184                 }
185                 return $this->strings(
186                         $this->handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) );
187         }
188
189         private function handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) {
190                 if ( $offset === 0 ) {
191                         // Only perform exact db match if offset === 0
192                         // This is still far from perfect but at least we avoid returning the
193                         // same title afain and again when the user is scrolling with a query
194                         // that matches a title in the db.
195                         $rescorer = new SearchExactMatchRescorer();
196                         $srchres = $rescorer->rescore( $search, $namespaces, $srchres, $limit );
197                 }
198                 return $srchres;
199         }
200
201         /**
202          * Prefix search special-case for Special: namespace.
203          *
204          * @param string $search Term
205          * @param int $limit Max number of items to return
206          * @param int $offset Number of items to offset
207          * @return array
208          */
209         protected function specialSearch( $search, $limit, $offset ) {
210                 global $wgContLang;
211
212                 $searchParts = explode( '/', $search, 2 );
213                 $searchKey = $searchParts[0];
214                 $subpageSearch = isset( $searchParts[1] ) ? $searchParts[1] : null;
215
216                 // Handle subpage search separately.
217                 if ( $subpageSearch !== null ) {
218                         // Try matching the full search string as a page name
219                         $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey );
220                         if ( !$specialTitle ) {
221                                 return [];
222                         }
223                         $special = SpecialPageFactory::getPage( $specialTitle->getText() );
224                         if ( $special ) {
225                                 $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit, $offset );
226                                 return array_map( function ( $sub ) use ( $specialTitle ) {
227                                         return $specialTitle->getSubpage( $sub );
228                                 }, $subpages );
229                         } else {
230                                 return [];
231                         }
232                 }
233
234                 # normalize searchKey, so aliases with spaces can be found - T27675
235                 $searchKey = str_replace( ' ', '_', $searchKey );
236                 $searchKey = $wgContLang->caseFold( $searchKey );
237
238                 // Unlike SpecialPage itself, we want the canonical forms of both
239                 // canonical and alias title forms...
240                 $keys = [];
241                 foreach ( SpecialPageFactory::getNames() as $page ) {
242                         $keys[$wgContLang->caseFold( $page )] = [ 'page' => $page, 'rank' => 0 ];
243                 }
244
245                 foreach ( $wgContLang->getSpecialPageAliases() as $page => $aliases ) {
246                         if ( !in_array( $page, SpecialPageFactory::getNames() ) ) {# T22885
247                                 continue;
248                         }
249
250                         foreach ( $aliases as $key => $alias ) {
251                                 $keys[$wgContLang->caseFold( $alias )] = [ 'page' => $alias, 'rank' => $key ];
252                         }
253                 }
254                 ksort( $keys );
255
256                 $matches = [];
257                 foreach ( $keys as $pageKey => $page ) {
258                         if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
259                                 // T29671: Don't use SpecialPage::getTitleFor() here because it
260                                 // localizes its input leading to searches for e.g. Special:All
261                                 // returning Spezial:MediaWiki-Systemnachrichten and returning
262                                 // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de'
263                                 $matches[$page['rank']][] = Title::makeTitleSafe( NS_SPECIAL, $page['page'] );
264
265                                 if ( isset( $matches[0] ) && count( $matches[0] ) >= $limit + $offset ) {
266                                         // We have enough items in primary rank, no use to continue
267                                         break;
268                                 }
269                         }
270
271                 }
272
273                 // Ensure keys are in order
274                 ksort( $matches );
275                 // Flatten the array
276                 $matches = array_reduce( $matches, 'array_merge', [] );
277
278                 return array_slice( $matches, $offset, $limit );
279         }
280
281         /**
282          * Unless overridden by PrefixSearchBackend hook...
283          * This is case-sensitive (First character may
284          * be automatically capitalized by Title::secureAndSpit()
285          * later on depending on $wgCapitalLinks)
286          *
287          * @param array|null $namespaces Namespaces to search in
288          * @param string $search Term
289          * @param int $limit Max number of items to return
290          * @param int $offset Number of items to skip
291          * @return Title[] Array of Title objects
292          */
293         public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
294                 // Backwards compatability with old code. Default to NS_MAIN if no namespaces provided.
295                 if ( $namespaces === null ) {
296                         $namespaces = [];
297                 }
298                 if ( !$namespaces ) {
299                         $namespaces[] = NS_MAIN;
300                 }
301
302                 // Construct suitable prefix for each namespace. They differ in cases where
303                 // some namespaces always capitalize and some don't.
304                 $prefixes = [];
305                 foreach ( $namespaces as $namespace ) {
306                         // For now, if special is included, ignore the other namespaces
307                         if ( $namespace == NS_SPECIAL ) {
308                                 return $this->specialSearch( $search, $limit, $offset );
309                         }
310
311                         $title = Title::makeTitleSafe( $namespace, $search );
312                         // Why does the prefix default to empty?
313                         $prefix = $title ? $title->getDBkey() : '';
314                         $prefixes[$prefix][] = $namespace;
315                 }
316
317                 $dbr = wfGetDB( DB_REPLICA );
318                 // Often there is only one prefix that applies to all requested namespaces,
319                 // but sometimes there are two if some namespaces do not always capitalize.
320                 $conds = [];
321                 foreach ( $prefixes as $prefix => $namespaces ) {
322                         $condition = [
323                                 'page_namespace' => $namespaces,
324                                 'page_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
325                         ];
326                         $conds[] = $dbr->makeList( $condition, LIST_AND );
327                 }
328
329                 $table = 'page';
330                 $fields = [ 'page_id', 'page_namespace', 'page_title' ];
331                 $conds = $dbr->makeList( $conds, LIST_OR );
332                 $options = [
333                         'LIMIT' => $limit,
334                         'ORDER BY' => [ 'page_title', 'page_namespace' ],
335                         'OFFSET' => $offset
336                 ];
337
338                 $res = $dbr->select( $table, $fields, $conds, __METHOD__, $options );
339
340                 return iterator_to_array( TitleArray::newFromResult( $res ) );
341         }
342
343         /**
344          * Validate an array of numerical namespace indexes
345          *
346          * @param array $namespaces
347          * @return array (default: contains only NS_MAIN)
348          */
349         protected function validateNamespaces( $namespaces ) {
350                 global $wgContLang;
351
352                 // We will look at each given namespace against wgContLang namespaces
353                 $validNamespaces = $wgContLang->getNamespaces();
354                 if ( is_array( $namespaces ) && count( $namespaces ) > 0 ) {
355                         $valid = [];
356                         foreach ( $namespaces as $ns ) {
357                                 if ( is_numeric( $ns ) && array_key_exists( $ns, $validNamespaces ) ) {
358                                         $valid[] = $ns;
359                                 }
360                         }
361                         if ( count( $valid ) > 0 ) {
362                                 return $valid;
363                         }
364                 }
365
366                 return [ NS_MAIN ];
367         }
368 }
369
370 /**
371  * Performs prefix search, returning Title objects
372  * @deprecated Since 1.27, Use SearchEngine::defaultPrefixSearch or SearchEngine::completionSearch
373  * @ingroup Search
374  */
375 class TitlePrefixSearch extends PrefixSearch {
376
377         protected function titles( array $titles ) {
378                 return $titles;
379         }
380
381         protected function strings( array $strings ) {
382                 $titles = array_map( 'Title::newFromText', $strings );
383                 $lb = new LinkBatch( $titles );
384                 $lb->setCaller( __METHOD__ );
385                 $lb->execute();
386                 return $titles;
387         }
388 }
389
390 /**
391  * Performs prefix search, returning strings
392  * @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch
393  * @ingroup Search
394  */
395 class StringPrefixSearch extends PrefixSearch {
396
397         protected function titles( array $titles ) {
398                 return array_map( function ( Title $t ) {
399                         return $t->getPrefixedText();
400                 }, $titles );
401         }
402
403         protected function strings( array $strings ) {
404                 return $strings;
405         }
406 }