Merge "Rename Parser_DiffTest class to ParserDiffTest"
[mediawiki.git] / includes / PrefixSearch.php
blob31bc60054e9653905d2d09a6ba1fcb525f01c5ca
1 <?php
2 /**
3 * Prefix search of page names.
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.
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.
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
20 * @file
23 /**
24 * Handles searching prefixes of titles and finding any page
25 * names that match. Used largely by the OpenSearch implementation.
27 * @ingroup Search
29 abstract class PrefixSearch {
30 /**
31 * Do a prefix search of titles and return a list of matching page names.
32 * @deprecated: Since 1.23, use TitlePrefixSearch or StringPrefixSearch classes
34 * @param string $search
35 * @param int $limit
36 * @param array $namespaces Used if query is not explicitly prefixed
37 * @return array Array of strings
39 public static function titleSearch( $search, $limit, $namespaces = array() ) {
40 $prefixSearch = new StringPrefixSearch;
41 return $prefixSearch->search( $search, $limit, $namespaces );
44 /**
45 * Do a prefix search of titles and return a list of matching page names.
47 * @param string $search
48 * @param int $limit
49 * @param array $namespaces Used if query is not explicitly prefixed
50 * @return array Array of strings or Title objects
52 public function search( $search, $limit, $namespaces = array() ) {
53 $search = trim( $search );
54 if ( $search === '' ) {
55 return array();
58 $namespaces = $this->validateNamespaces( $namespaces );
60 // Is this a namespace prefix? Start listing all pages in it.
61 $title = Title::newFromText( $search . 'Dummy' );
62 if ( $title
63 && $title->getText() === 'Dummy'
64 && !$title->inNamespace( NS_MAIN )
65 && !$title->isExternal()
66 ) {
67 $this->searchBackend( array( $title->getNamespace() ), '', $limit );
70 // Explicit namespace prefix? Limit search to that namespace.
71 $title = Title::newFromText( $search );
72 if ( $title
73 && !$title->isExternal()
74 && !$title->inNamespace( NS_MAIN )
75 ) {
76 // This will convert first letter to uppercase if appropriate for the namespace
77 $this->searchBackend( array( $title->getNamespace() ), $title->getText(), $limit );
80 // Search in all requested namespaces
81 return $this->searchBackend( $namespaces, $search, $limit );
84 /**
85 * Do a prefix search for all possible variants of the prefix
86 * @param string $search
87 * @param int $limit
88 * @param array $namespaces
90 * @return array
92 public function searchWithVariants( $search, $limit, array $namespaces ) {
93 wfProfileIn( __METHOD__ );
94 $searches = $this->search( $search, $limit, $namespaces );
96 // if the content language has variants, try to retrieve fallback results
97 $fallbackLimit = $limit - count( $searches );
98 if ( $fallbackLimit > 0 ) {
99 global $wgContLang;
101 $fallbackSearches = $wgContLang->autoConvertToAllVariants( $search );
102 $fallbackSearches = array_diff( array_unique( $fallbackSearches ), array( $search ) );
104 foreach ( $fallbackSearches as $fbs ) {
105 $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces );
106 $searches = array_merge( $searches, $fallbackSearchResult );
107 $fallbackLimit -= count( $fallbackSearchResult );
109 if ( $fallbackLimit == 0 ) {
110 break;
114 wfProfileOut( __METHOD__ );
115 return $searches;
119 * When implemented in a descendant class, receives an array of Title objects and returns
120 * either an unmodified array or an array of strings corresponding to titles passed to it.
122 * @param array $titles
123 * @return array
125 abstract protected function titles( array $titles );
128 * When implemented in a descendant class, receives an array of titles as strings and returns
129 * either an unmodified array or an array of Title objects corresponding to strings received.
131 * @param array $strings
133 * @return array
135 abstract protected function strings( array $strings );
138 * Do a prefix search of titles and return a list of matching page names.
139 * @param array $namespaces
140 * @param string $search
141 * @param int $limit
142 * @return array Array of strings
144 protected function searchBackend( $namespaces, $search, $limit ) {
145 if ( count( $namespaces ) == 1 ) {
146 $ns = $namespaces[0];
147 if ( $ns == NS_MEDIA ) {
148 $namespaces = array( NS_FILE );
149 } elseif ( $ns == NS_SPECIAL ) {
150 return $this->titles( $this->specialSearch( $search, $limit ) );
153 $srchres = array();
154 if ( wfRunHooks( 'PrefixSearchBackend', array( $namespaces, $search, $limit, &$srchres ) ) ) {
155 return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit ) );
157 return $this->strings( $srchres );
161 * Prefix search special-case for Special: namespace.
163 * @param string $search Term
164 * @param int $limit Max number of items to return
165 * @return array
167 protected function specialSearch( $search, $limit ) {
168 global $wgContLang;
170 list( $searchKey, $subpageSearch ) = explode( '/', $search, 2 );
172 // Handle subpage search separately.
173 if ( $subpageSearch !== null ) {
174 // Try matching the full search string as a page name
175 $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey );
176 $special = SpecialPageFactory::getPage( $specialTitle->getText() );
177 if ( $special ) {
178 $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit );
179 return array_map( function ( $sub ) use ( $specialTitle ) {
180 return $specialTitle->getSubpage( $sub );
181 }, $subpages );
182 } else {
183 return array();
187 # normalize searchKey, so aliases with spaces can be found - bug 25675
188 $searchKey = str_replace( ' ', '_', $searchKey );
189 $searchKey = $wgContLang->caseFold( $searchKey );
191 // Unlike SpecialPage itself, we want the canonical forms of both
192 // canonical and alias title forms...
193 $keys = array();
194 foreach ( SpecialPageFactory::getList() as $page => $class ) {
195 $keys[$wgContLang->caseFold( $page )] = $page;
198 foreach ( $wgContLang->getSpecialPageAliases() as $page => $aliases ) {
199 if ( !array_key_exists( $page, SpecialPageFactory::getList() ) ) {# bug 20885
200 continue;
203 foreach ( $aliases as $alias ) {
204 $keys[$wgContLang->caseFold( $alias )] = $alias;
207 ksort( $keys );
209 $srchres = array();
210 foreach ( $keys as $pageKey => $page ) {
211 if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
212 // bug 27671: Don't use SpecialPage::getTitleFor() here because it
213 // localizes its input leading to searches for e.g. Special:All
214 // returning Spezial:MediaWiki-Systemnachrichten and returning
215 // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de'
216 $srchres[] = Title::makeTitleSafe( NS_SPECIAL, $page );
219 if ( count( $srchres ) >= $limit ) {
220 break;
224 return $srchres;
228 * Unless overridden by PrefixSearchBackend hook...
229 * This is case-sensitive (First character may
230 * be automatically capitalized by Title::secureAndSpit()
231 * later on depending on $wgCapitalLinks)
233 * @param array $namespaces Namespaces to search in
234 * @param string $search Term
235 * @param int $limit Max number of items to return
236 * @return array Array of Title objects
238 protected function defaultSearchBackend( $namespaces, $search, $limit ) {
239 $dbr = wfGetDB( DB_SLAVE );
241 // Construct suitable prefix for each namespace, they might differ
242 $prefixes = array();
243 foreach ( $namespaces as $ns ) {
244 $title = Title::makeTitleSafe( $ns, $search );
245 $prefix = $title ? $title->getDBkey() : '';
246 $prefixes[$prefix][] = $ns;
249 $conds = array();
250 foreach ( $prefixes as $prefix => $nss ) {
251 $conds[] = $dbr->makeList( array(
252 'page_namespace' => $nss,
253 'page_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
254 ), LIST_AND );
257 $res = $dbr->select( 'page',
258 array( 'page_id', 'page_namespace', 'page_title' ),
259 $dbr->makeList( $conds, LIST_OR ),
260 __METHOD__,
261 array( 'LIMIT' => $limit, 'ORDER BY' => 'page_title' )
264 // Shorter than a loop, and doesn't break class api
265 return iterator_to_array( TitleArray::newFromResult( $res ) );
269 * Validate an array of numerical namespace indexes
271 * @param array $namespaces
272 * @return array (default: contains only NS_MAIN)
274 protected function validateNamespaces( $namespaces ) {
275 global $wgContLang;
277 // We will look at each given namespace against wgContLang namespaces
278 $validNamespaces = $wgContLang->getNamespaces();
279 if ( is_array( $namespaces ) && count( $namespaces ) > 0 ) {
280 $valid = array();
281 foreach ( $namespaces as $ns ) {
282 if ( is_numeric( $ns ) && array_key_exists( $ns, $validNamespaces ) ) {
283 $valid[] = $ns;
286 if ( count( $valid ) > 0 ) {
287 return $valid;
291 return array( NS_MAIN );
296 * Performs prefix search, returning Title objects
297 * @ingroup Search
299 class TitlePrefixSearch extends PrefixSearch {
301 protected function titles( array $titles ) {
302 return $titles;
305 protected function strings( array $strings ) {
306 $titles = array_map( 'Title::newFromText', $strings );
307 $lb = new LinkBatch( $titles );
308 $lb->setCaller( __METHOD__ );
309 $lb->execute();
310 return $titles;
315 * Performs prefix search, returning strings
316 * @ingroup Search
318 class StringPrefixSearch extends PrefixSearch {
320 protected function titles( array $titles ) {
321 return array_map( function ( Title $t ) {
322 return $t->getPrefixedText();
323 }, $titles );
326 protected function strings( array $strings ) {
327 return $strings;