StringPrefixSearch   A
last analyzed

Complexity

Total Complexity 2

Size/Duplication

Total Lines 12
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 2

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 10
c 0
b 0
f 0
wmc 2
lcom 0
cbo 2

2 Methods

Rating   Name   Duplication   Size   Complexity  
A titles() 0 5 1
A strings() 0 3 1
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::prefixSearchSubpages 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;
0 ignored issues
show
Deprecated Code introduced by
The class StringPrefixSearch has been deprecated with message: Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
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 View Code Duplication
		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 View Code Duplication
		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 - bug 25675
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;
243
		}
244
245
		foreach ( $wgContLang->getSpecialPageAliases() as $page => $aliases ) {
246
			if ( !in_array( $page, SpecialPageFactory::getNames() ) ) {# bug 20885
247
				continue;
248
			}
249
250
			foreach ( $aliases as $alias ) {
251
				$keys[$wgContLang->caseFold( $alias )] = $alias;
252
			}
253
		}
254
		ksort( $keys );
255
256
		$srchres = [];
257
		$skipped = 0;
258
		foreach ( $keys as $pageKey => $page ) {
259
			if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
260
				// bug 27671: Don't use SpecialPage::getTitleFor() here because it
261
				// localizes its input leading to searches for e.g. Special:All
262
				// returning Spezial:MediaWiki-Systemnachrichten and returning
263
				// Spezial:Alle_Seiten twice when $wgLanguageCode == 'de'
264
				if ( $offset > 0 && $skipped < $offset ) {
265
					$skipped++;
266
					continue;
267
				}
268
				$srchres[] = Title::makeTitleSafe( NS_SPECIAL, $page );
269
			}
270
271
			if ( count( $srchres ) >= $limit ) {
272
				break;
273
			}
274
		}
275
276
		return $srchres;
277
	}
278
279
	/**
280
	 * Unless overridden by PrefixSearchBackend hook...
281
	 * This is case-sensitive (First character may
282
	 * be automatically capitalized by Title::secureAndSpit()
283
	 * later on depending on $wgCapitalLinks)
284
	 *
285
	 * @param array|null $namespaces Namespaces to search in
286
	 * @param string $search Term
287
	 * @param int $limit Max number of items to return
288
	 * @param int $offset Number of items to skip
289
	 * @return Title[] Array of Title objects
290
	 */
291
	public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
292
		// Backwards compatability with old code. Default to NS_MAIN if no namespaces provided.
293
		if ( $namespaces === null ) {
294
			$namespaces = [];
295
		}
296
		if ( !$namespaces ) {
297
			$namespaces[] = NS_MAIN;
298
		}
299
300
		// Construct suitable prefix for each namespace. They differ in cases where
301
		// some namespaces always capitalize and some don't.
302
		$prefixes = [];
303
		foreach ( $namespaces as $namespace ) {
304
			// For now, if special is included, ignore the other namespaces
305
			if ( $namespace == NS_SPECIAL ) {
306
				return $this->specialSearch( $search, $limit, $offset );
307
			}
308
309
			$title = Title::makeTitleSafe( $namespace, $search );
310
			// Why does the prefix default to empty?
311
			$prefix = $title ? $title->getDBkey() : '';
312
			$prefixes[$prefix][] = $namespace;
313
		}
314
315
		$dbr = wfGetDB( DB_REPLICA );
316
		// Often there is only one prefix that applies to all requested namespaces,
317
		// but sometimes there are two if some namespaces do not always capitalize.
318
		$conds = [];
319
		foreach ( $prefixes as $prefix => $namespaces ) {
320
			$condition = [
321
				'page_namespace' => $namespaces,
322
				'page_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
323
			];
324
			$conds[] = $dbr->makeList( $condition, LIST_AND );
325
		}
326
327
		$table = 'page';
328
		$fields = [ 'page_id', 'page_namespace', 'page_title' ];
329
		$conds = $dbr->makeList( $conds, LIST_OR );
330
		$options = [
331
			'LIMIT' => $limit,
332
			'ORDER BY' => [ 'page_title', 'page_namespace' ],
333
			'OFFSET' => $offset
334
		];
335
336
		$res = $dbr->select( $table, $fields, $conds, __METHOD__, $options );
337
338
		return iterator_to_array( TitleArray::newFromResult( $res ) );
339
	}
340
341
	/**
342
	 * Validate an array of numerical namespace indexes
343
	 *
344
	 * @param array $namespaces
345
	 * @return array (default: contains only NS_MAIN)
346
	 */
347
	protected function validateNamespaces( $namespaces ) {
348
		global $wgContLang;
349
350
		// We will look at each given namespace against wgContLang namespaces
351
		$validNamespaces = $wgContLang->getNamespaces();
352
		if ( is_array( $namespaces ) && count( $namespaces ) > 0 ) {
353
			$valid = [];
354
			foreach ( $namespaces as $ns ) {
355
				if ( is_numeric( $ns ) && array_key_exists( $ns, $validNamespaces ) ) {
356
					$valid[] = $ns;
357
				}
358
			}
359
			if ( count( $valid ) > 0 ) {
360
				return $valid;
361
			}
362
		}
363
364
		return [ NS_MAIN ];
365
	}
366
}
367
368
/**
369
 * Performs prefix search, returning Title objects
370
 * @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch
371
 * @ingroup Search
372
 */
373
class TitlePrefixSearch extends PrefixSearch {
0 ignored issues
show
Deprecated Code introduced by
The class PrefixSearch has been deprecated with message: Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
374
375
	protected function titles( array $titles ) {
376
		return $titles;
377
	}
378
379
	protected function strings( array $strings ) {
380
		$titles = array_map( 'Title::newFromText', $strings );
381
		$lb = new LinkBatch( $titles );
382
		$lb->setCaller( __METHOD__ );
383
		$lb->execute();
384
		return $titles;
385
	}
386
}
387
388
/**
389
 * Performs prefix search, returning strings
390
 * @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch
391
 * @ingroup Search
392
 */
393
class StringPrefixSearch extends PrefixSearch {
0 ignored issues
show
Deprecated Code introduced by
The class PrefixSearch has been deprecated with message: Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
394
395
	protected function titles( array $titles ) {
396
		return array_map( function ( Title $t ) {
397
			return $t->getPrefixedText();
398
		}, $titles );
399
	}
400
401
	protected function strings( array $strings ) {
402
		return $strings;
403
	}
404
}
405