Merge "Only list Create account when permissions allow it"
[lhc/web/wiklou.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 *
27 * @ingroup Search
28 */
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
33 *
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
38 */
39 public static function titleSearch( $search, $limit, $namespaces = array() ) {
40 $prefixSearch = new StringPrefixSearch;
41 return $prefixSearch->search( $search, $limit, $namespaces );
42 }
43
44 /**
45 * Do a prefix search of titles and return a list of matching page names.
46 *
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
51 */
52 public function search( $search, $limit, $namespaces = array() ) {
53 $search = trim( $search );
54 if ( $search === '' ) {
55 return array();
56 }
57
58 $namespaces = $this->validateNamespaces( $namespaces );
59
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 );
68 }
69
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 );
78 }
79
80 // Search in all requested namespaces
81 return $this->searchBackend( $namespaces, $search, $limit );
82 }
83
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
89 *
90 * @return array
91 */
92 public function searchWithVariants( $search, $limit, array $namespaces ) {
93 wfProfileIn( __METHOD__ );
94 $searches = $this->search( $search, $limit, $namespaces );
95
96 // if the content language has variants, try to retrieve fallback results
97 $fallbackLimit = $limit - count( $searches );
98 if ( $fallbackLimit > 0 ) {
99 global $wgContLang;
100
101 $fallbackSearches = $wgContLang->autoConvertToAllVariants( $search );
102 $fallbackSearches = array_diff( array_unique( $fallbackSearches ), array( $search ) );
103
104 foreach ( $fallbackSearches as $fbs ) {
105 $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces );
106 $searches = array_merge( $searches, $fallbackSearchResult );
107 $fallbackLimit -= count( $fallbackSearchResult );
108
109 if ( $fallbackLimit == 0 ) {
110 break;
111 }
112 }
113 }
114 wfProfileOut( __METHOD__ );
115 return $searches;
116 }
117
118 /**
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.
121 *
122 * @param array $titles
123 * @return array
124 */
125 abstract protected function titles( array $titles );
126
127 /**
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.
130 *
131 * @param array $strings
132 *
133 * @return array
134 */
135 abstract protected function strings( array $strings );
136
137 /**
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
143 */
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 ) );
151 }
152 }
153 $srchres = array();
154 if ( wfRunHooks( 'PrefixSearchBackend', array( $namespaces, $search, $limit, &$srchres ) ) ) {
155 return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit ) );
156 }
157 return $this->strings( $srchres );
158 }
159
160 /**
161 * Prefix search special-case for Special: namespace.
162 *
163 * @param string $search Term
164 * @param int $limit Max number of items to return
165 * @return array
166 */
167 protected function specialSearch( $search, $limit ) {
168 global $wgContLang;
169
170 list( $searchKey, $subpageSearch ) = explode( '/', $search, 2 );
171
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();
184 }
185 }
186
187 # normalize searchKey, so aliases with spaces can be found - bug 25675
188 $searchKey = str_replace( ' ', '_', $searchKey );
189 $searchKey = $wgContLang->caseFold( $searchKey );
190
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;
196 }
197
198 foreach ( $wgContLang->getSpecialPageAliases() as $page => $aliases ) {
199 if ( !array_key_exists( $page, SpecialPageFactory::getList() ) ) {# bug 20885
200 continue;
201 }
202
203 foreach ( $aliases as $alias ) {
204 $keys[$wgContLang->caseFold( $alias )] = $alias;
205 }
206 }
207 ksort( $keys );
208
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 );
217 }
218
219 if ( count( $srchres ) >= $limit ) {
220 break;
221 }
222 }
223
224 return $srchres;
225 }
226
227 /**
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)
232 *
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
237 */
238 protected function defaultSearchBackend( $namespaces, $search, $limit ) {
239 $dbr = wfGetDB( DB_SLAVE );
240
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;
247 }
248
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 );
255 }
256
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' )
262 );
263
264 // Shorter than a loop, and doesn't break class api
265 return iterator_to_array( TitleArray::newFromResult( $res ) );
266 }
267
268 /**
269 * Validate an array of numerical namespace indexes
270 *
271 * @param array $namespaces
272 * @return array (default: contains only NS_MAIN)
273 */
274 protected function validateNamespaces( $namespaces ) {
275 global $wgContLang;
276
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;
284 }
285 }
286 if ( count( $valid ) > 0 ) {
287 return $valid;
288 }
289 }
290
291 return array( NS_MAIN );
292 }
293 }
294
295 /**
296 * Performs prefix search, returning Title objects
297 * @ingroup Search
298 */
299 class TitlePrefixSearch extends PrefixSearch {
300
301 protected function titles( array $titles ) {
302 return $titles;
303 }
304
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;
311 }
312 }
313
314 /**
315 * Performs prefix search, returning strings
316 * @ingroup Search
317 */
318 class StringPrefixSearch extends PrefixSearch {
319
320 protected function titles( array $titles ) {
321 return array_map( function ( Title $t ) {
322 return $t->getPrefixedText();
323 }, $titles );
324 }
325
326 protected function strings( array $strings ) {
327 return $strings;
328 }
329 }