Completed
Branch master (1655eb)
by
unknown
22:24
created

SpecialSearch::shouldRunSuggestedQuery()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 2
nop 1
dl 0
loc 11
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * Implements Special:Search
4
 *
5
 * Copyright © 2004 Brion Vibber <[email protected]>
6
 *
7
 * This program is free software; you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 2 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License along
18
 * with this program; if not, write to the Free Software Foundation, Inc.,
19
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
 * http://www.gnu.org/copyleft/gpl.html
21
 *
22
 * @file
23
 * @ingroup SpecialPage
24
 */
25
26
use MediaWiki\MediaWikiServices;
27
28
/**
29
 * implements Special:Search - Run text & title search and display the output
30
 * @ingroup SpecialPage
31
 */
32
class SpecialSearch extends SpecialPage {
33
	/**
34
	 * Current search profile. Search profile is just a name that identifies
35
	 * the active search tab on the search page (content, discussions...)
36
	 * For users tt replaces the set of enabled namespaces from the query
37
	 * string when applicable. Extensions can add new profiles with hooks
38
	 * with custom search options just for that profile.
39
	 * @var null|string
40
	 */
41
	protected $profile;
42
43
	/** @var SearchEngine Search engine */
44
	protected $searchEngine;
45
46
	/** @var string Search engine type, if not default */
47
	protected $searchEngineType;
48
49
	/** @var array For links */
50
	protected $extraParams = [];
51
52
	/**
53
	 * @var string The prefix url parameter. Set on the searcher and the
54
	 * is expected to treat it as prefix filter on titles.
55
	 */
56
	protected $mPrefix;
57
58
	/**
59
	 * @var int
60
	 */
61
	protected $limit, $offset;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
62
63
	/**
64
	 * @var array
65
	 */
66
	protected $namespaces;
67
68
	/**
69
	 * @var string
70
	 */
71
	protected $fulltext;
72
73
	/**
74
	 * @var bool
75
	 */
76
	protected $runSuggestion = true;
77
78
	/**
79
	 * Names of the wikis, in format: Interwiki prefix -> caption
80
	 * @var array
81
	 */
82
	protected $customCaptions;
83
84
	/**
85
	 * Search engine configurations.
86
	 * @var SearchEngineConfig
87
	 */
88
	protected $searchConfig;
89
90
	const NAMESPACES_CURRENT = 'sense';
91
92
	public function __construct() {
93
		parent::__construct( 'Search' );
94
		$this->searchConfig = MediaWikiServices::getInstance()->getSearchEngineConfig();
95
	}
96
97
	/**
98
	 * Entry point
99
	 *
100
	 * @param string $par
101
	 */
102
	public function execute( $par ) {
103
		$request = $this->getRequest();
104
105
		// Fetch the search term
106
		$search = str_replace( "\n", " ", $request->getText( 'search' ) );
107
108
		// Historically search terms have been accepted not only in the search query
109
		// parameter, but also as part of the primary url. This can have PII implications
110
		// in releasing page view data. As such issue a 301 redirect to the correct
111
		// URL.
112
		if ( strlen( $par ) && !strlen( $search ) ) {
113
			$query = $request->getValues();
114
			unset( $query['title'] );
115
			// Strip underscores from title parameter; most of the time we'll want
116
			// text form here. But don't strip underscores from actual text params!
117
			$query['search'] = str_replace( '_', ' ', $par );
118
			$this->getOutput()->redirect( $this->getPageTitle()->getFullURL( $query ), 301 );
119
			return;
120
		}
121
122
		$this->setHeaders();
123
		$this->outputHeader();
124
		$out = $this->getOutput();
125
		$out->allowClickjacking();
126
		$out->addModuleStyles( [
127
			'mediawiki.special', 'mediawiki.special.search.styles', 'mediawiki.ui', 'mediawiki.ui.button',
128
			'mediawiki.ui.input', 'mediawiki.widgets.SearchInputWidget.styles',
129
		] );
130
		$this->addHelpLink( 'Help:Searching' );
131
132
		$this->load();
133
		if ( !is_null( $request->getVal( 'nsRemember' ) ) ) {
134
			$this->saveNamespaces();
135
			// Remove the token from the URL to prevent the user from inadvertently
136
			// exposing it (e.g. by pasting it into a public wiki page) or undoing
137
			// later settings changes (e.g. by reloading the page).
138
			$query = $request->getValues();
139
			unset( $query['title'], $query['nsRemember'] );
140
			$out->redirect( $this->getPageTitle()->getFullURL( $query ) );
141
			return;
142
		}
143
144
		$out->addJsConfigVars( [ 'searchTerm' => $search ] );
145
		$this->searchEngineType = $request->getVal( 'srbackend' );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->searchEngineType is correct as $request->getVal('srbackend') (which targets WebRequest::getVal()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
146
147
		if ( $request->getVal( 'fulltext' )
148
			|| !is_null( $request->getVal( 'offset' ) )
149
		) {
150
			$this->showResults( $search );
151
		} else {
152
			$this->goResult( $search );
153
		}
154
	}
155
156
	/**
157
	 * Set up basic search parameters from the request and user settings.
158
	 *
159
	 * @see tests/phpunit/includes/specials/SpecialSearchTest.php
160
	 */
161
	public function load() {
162
		$request = $this->getRequest();
163
		list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, '' );
164
		$this->mPrefix = $request->getVal( 'prefix', '' );
165
166
		$user = $this->getUser();
167
168
		# Extract manually requested namespaces
169
		$nslist = $this->powerSearch( $request );
170
		if ( !count( $nslist ) ) {
171
			# Fallback to user preference
172
			$nslist = $this->searchConfig->userNamespaces( $user );
173
		}
174
175
		$profile = null;
176
		if ( !count( $nslist ) ) {
177
			$profile = 'default';
178
		}
179
180
		$profile = $request->getVal( 'profile', $profile );
181
		$profiles = $this->getSearchProfiles();
182
		if ( $profile === null ) {
183
			// BC with old request format
184
			$profile = 'advanced';
185
			foreach ( $profiles as $key => $data ) {
186
				if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
187
					$profile = $key;
188
				}
189
			}
190
			$this->namespaces = $nslist;
191
		} elseif ( $profile === 'advanced' ) {
192
			$this->namespaces = $nslist;
193
		} else {
194
			if ( isset( $profiles[$profile]['namespaces'] ) ) {
195
				$this->namespaces = $profiles[$profile]['namespaces'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $profiles[$profile]['namespaces'] can also be of type string. However, the property $namespaces is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
196
			} else {
197
				// Unknown profile requested
198
				$profile = 'default';
199
				$this->namespaces = $profiles['default']['namespaces'];
200
			}
201
		}
202
203
		$this->fulltext = $request->getVal( 'fulltext' );
204
		$this->runSuggestion = (bool)$request->getVal( 'runsuggestion', true );
205
		$this->profile = $profile;
206
	}
207
208
	/**
209
	 * If an exact title match can be found, jump straight ahead to it.
210
	 *
211
	 * @param string $term
212
	 */
213
	public function goResult( $term ) {
214
		$this->setupPage( $term );
215
		# Try to go to page as entered.
216
		$title = Title::newFromText( $term );
217
		# If the string cannot be used to create a title
218
		if ( is_null( $title ) ) {
219
			$this->showResults( $term );
220
221
			return;
222
		}
223
		# If there's an exact or very near match, jump right there.
224
		$title = $this->getSearchEngine()
225
			->getNearMatcher( $this->getConfig() )->getNearMatch( $term );
226
227
		if ( !is_null( $title ) &&
228
			Hooks::run( 'SpecialSearchGoResult', [ $term, $title, &$url ] )
229
		) {
230
			if ( $url === null ) {
0 ignored issues
show
Bug introduced by
The variable $url seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
231
				$url = $title->getFullURL();
232
			}
233
			$this->getOutput()->redirect( $url );
0 ignored issues
show
Bug introduced by
The variable $url does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
234
235
			return;
236
		}
237
		$this->showResults( $term );
238
	}
239
240
	/**
241
	 * @param string $term
242
	 */
243
	public function showResults( $term ) {
244
		global $wgContLang;
245
246
		$search = $this->getSearchEngine();
247
		$search->setFeatureData( 'rewrite', $this->runSuggestion );
248
		$search->setLimitOffset( $this->limit, $this->offset );
249
		$search->setNamespaces( $this->namespaces );
250
		$search->prefix = $this->mPrefix;
251
		$term = $search->transformSearchTerm( $term );
252
253
		Hooks::run( 'SpecialSearchSetupEngine', [ $this, $this->profile, $search ] );
254
255
		$this->setupPage( $term );
256
257
		$out = $this->getOutput();
258
259
		if ( $this->getConfig()->get( 'DisableTextSearch' ) ) {
260
			$searchFowardUrl = $this->getConfig()->get( 'SearchForwardUrl' );
261
			if ( $searchFowardUrl ) {
262
				$url = str_replace( '$1', urlencode( $term ), $searchFowardUrl );
263
				$out->redirect( $url );
264
			} else {
265
				$out->addHTML(
266
					Xml::openElement( 'fieldset' ) .
267
					Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) .
268
					Xml::element(
269
						'p',
270
						[ 'class' => 'mw-searchdisabled' ],
271
						$this->msg( 'searchdisabled' )->text()
272
					) .
273
					$this->msg( 'googlesearch' )->rawParams(
274
						htmlspecialchars( $term ),
275
						'UTF-8',
276
						$this->msg( 'searchbutton' )->escaped()
277
					)->text() .
278
					Xml::closeElement( 'fieldset' )
279
				);
280
			}
281
282
			return;
283
		}
284
285
		$title = Title::newFromText( $term );
286
		$showSuggestion = $title === null || !$title->isKnown();
287
		$search->setShowSuggestion( $showSuggestion );
288
289
		// fetch search results
290
		$rewritten = $search->replacePrefixes( $term );
291
292
		$titleMatches = $search->searchTitle( $rewritten );
293
		$textMatches = $search->searchText( $rewritten );
294
295
		$textStatus = null;
296
		if ( $textMatches instanceof Status ) {
297
			$textStatus = $textMatches;
298
			$textMatches = null;
299
		}
300
301
		// did you mean... suggestions
302
		$didYouMeanHtml = '';
303
		if ( $showSuggestion && $textMatches && !$textStatus ) {
304
			if ( $textMatches->hasRewrittenQuery() ) {
305
				$didYouMeanHtml = $this->getDidYouMeanRewrittenHtml( $term, $textMatches );
306
			} elseif ( $textMatches->hasSuggestion() ) {
307
				$didYouMeanHtml = $this->getDidYouMeanHtml( $textMatches );
308
			}
309
		}
310
311
		if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) {
312
			# Hook requested termination
313
			return;
314
		}
315
316
		// start rendering the page
317
		$out->addHTML(
318
			Xml::openElement(
319
				'form',
320
				[
321
					'id' => ( $this->isPowerSearch() ? 'powersearch' : 'search' ),
322
					'method' => 'get',
323
					'action' => wfScript(),
324
				]
325
			)
326
		);
327
328
		// Get number of results
329
		$titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0;
330
		if ( $titleMatches ) {
331
			$titleMatchesNum = $titleMatches->numRows();
332
			$numTitleMatches = $titleMatches->getTotalHits();
333
		}
334
		if ( $textMatches ) {
335
			$textMatchesNum = $textMatches->numRows();
336
			$numTextMatches = $textMatches->getTotalHits();
337
		}
338
		$num = $titleMatchesNum + $textMatchesNum;
339
		$totalRes = $numTitleMatches + $numTextMatches;
340
341
		$out->enableOOUI();
342
		$out->addHTML(
343
			# This is an awful awful ID name. It's not a table, but we
344
			# named it poorly from when this was a table so now we're
345
			# stuck with it
346
			Xml::openElement( 'div', [ 'id' => 'mw-search-top-table' ] ) .
347
			$this->shortDialog( $term, $num, $totalRes ) .
348
			Xml::closeElement( 'div' ) .
349
			$this->searchProfileTabs( $term ) .
350
			$this->searchOptions( $term ) .
351
			Xml::closeElement( 'form' ) .
352
			$didYouMeanHtml
353
		);
354
355
		$filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':';
356
		if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
357
			// Empty query -- straight view of search form
358
			return;
359
		}
360
361
		$out->addHTML( "<div class='searchresults'>" );
362
363
		// prev/next links
364
		$prevnext = null;
365
		if ( $num || $this->offset ) {
366
			// Show the create link ahead
367
			$this->showCreateLink( $title, $num, $titleMatches, $textMatches );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($term) on line 285 can be null; however, SpecialSearch::showCreateLink() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
368
			if ( $totalRes > $this->limit || $this->offset ) {
369
				if ( $this->searchEngineType !== null ) {
370
					$this->setExtraParam( 'srbackend', $this->searchEngineType );
371
				}
372
				$prevnext = $this->getLanguage()->viewPrevNext(
373
					$this->getPageTitle(),
374
					$this->offset,
375
					$this->limit,
376
					$this->powerSearchOptions() + [ 'search' => $term ],
377
					$this->limit + $this->offset >= $totalRes
378
				);
379
			}
380
		}
381
		Hooks::run( 'SpecialSearchResults', [ $term, &$titleMatches, &$textMatches ] );
382
383
		$out->parserOptions()->setEditSection( false );
384
		if ( $titleMatches ) {
385
			if ( $numTitleMatches > 0 ) {
386
				$out->wrapWikiMsg( "==$1==\n", 'titlematches' );
387
				$out->addHTML( $this->showMatches( $titleMatches ) );
388
			}
389
			$titleMatches->free();
390
		}
391
		if ( $textMatches && !$textStatus ) {
392
			// output appropriate heading
393
			if ( $numTextMatches > 0 && $numTitleMatches > 0 ) {
394
				$out->addHTML( '<div class="mw-search-visualclear"></div>' );
395
				// if no title matches the heading is redundant
396
				$out->wrapWikiMsg( "==$1==\n", 'textmatches' );
397
			}
398
399
			// show results
400
			if ( $numTextMatches > 0 ) {
401
				$search->augmentSearchResults( $textMatches );
402
				$out->addHTML( $this->showMatches( $textMatches ) );
403
			}
404
405
			// show secondary interwiki results if any
406
			if ( $textMatches->hasInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) ) {
407
				$out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(
0 ignored issues
show
Bug introduced by
It seems like $textMatches->getInterwi...Set::SECONDARY_RESULTS) targeting SearchResultSet::getInterwikiResults() can also be of type null; however, SpecialSearch::showInterwiki() does only seem to accept object<SearchResultSet>|array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
408
						SearchResultSet::SECONDARY_RESULTS ), $term ) );
409
			}
410
		}
411
412
		$hasOtherResults = $textMatches &&
413
			$textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS );
414
415
		if ( $num === 0 ) {
416
			if ( $textStatus ) {
417
				$out->addHTML( '<div class="error">' .
418
					$textStatus->getMessage( 'search-error' ) . '</div>' );
419
			} else {
420
				if ( !$this->offset ) {
421
					// If we have an offset the create link was rendered earlier in this function.
422
					// This class needs a good de-spaghettification, but for now this will
423
					// do the job.
424
					$this->showCreateLink( $title, $num, $titleMatches, $textMatches );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($term) on line 285 can be null; however, SpecialSearch::showCreateLink() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
425
				}
426
				$out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>",
427
					[ $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound',
428
							wfEscapeWikiText( $term )
429
					] );
430
			}
431
		}
432
433
		if ( $hasOtherResults ) {
434
			foreach ( $textMatches->getInterwikiResults( SearchResultSet::INLINE_RESULTS )
0 ignored issues
show
Bug introduced by
The expression $textMatches->getInterwi...ultSet::INLINE_RESULTS) of type array<integer,object<SearchResultSet>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
435
						as $interwiki => $interwikiResult ) {
436
				if ( $interwikiResult instanceof Status || $interwikiResult->numRows() == 0 ) {
437
					// ignore bad interwikis for now
438
					continue;
439
				}
440
				// TODO: wiki header
441
				$out->addHTML( $this->showMatches( $interwikiResult, $interwiki ) );
442
			}
443
		}
444
445
		if ( $textMatches ) {
446
			$textMatches->free();
447
		}
448
449
		$out->addHTML( '<div class="mw-search-visualclear"></div>' );
450
451
		if ( $prevnext ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $prevnext of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
452
			$out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
453
		}
454
455
		$out->addHTML( "</div>" );
456
457
		Hooks::run( 'SpecialSearchResultsAppend', [ $this, $out, $term ] );
458
	}
459
460
	/**
461
	 * Produce wiki header for interwiki results
462
	 * @param string $interwiki Interwiki name
463
	 * @param SearchResultSet $interwikiResult The result set
464
	 * @return string
465
	 */
466
	protected function interwikiHeader( $interwiki, $interwikiResult ) {
467
		// TODO: we need to figure out how to name wikis correctly
468
		$wikiMsg = $this->msg( 'search-interwiki-results-' . $interwiki )->parse();
469
		return "<p class=\"mw-search-interwiki-header mw-search-visualclear\">\n$wikiMsg</p>";
470
	}
471
472
	/**
473
	 * Generates HTML shown to the user when we have a suggestion about a query
474
	 * that might give more results than their current query.
475
	 */
476
	protected function getDidYouMeanHtml( SearchResultSet $textMatches ) {
477
		# mirror Go/Search behavior of original request ..
478
		$params = [ 'search' => $textMatches->getSuggestionQuery() ];
479 View Code Duplication
		if ( $this->fulltext === null ) {
480
			$params['fulltext'] = 'Search';
481
		} else {
482
			$params['fulltext'] = $this->fulltext;
483
		}
484
		$stParams = array_merge( $params, $this->powerSearchOptions() );
485
486
		$suggest = Linker::linkKnown(
487
			$this->getPageTitle(),
488
			$textMatches->getSuggestionSnippet() ?: null,
489
			[ 'id' => 'mw-search-DYM-suggestion' ],
490
			$stParams
491
		);
492
493
		# HTML of did you mean... search suggestion link
494
		return Html::rawElement(
495
			'div',
496
			[ 'class' => 'searchdidyoumean' ],
497
			$this->msg( 'search-suggest' )->rawParams( $suggest )->parse()
498
		);
499
	}
500
501
	/**
502
	 * Generates HTML shown to user when their query has been internally rewritten,
503
	 * and the results of the rewritten query are being returned.
504
	 *
505
	 * @param string $term The users search input
506
	 * @param SearchResultSet $textMatches The response to the users initial search request
507
	 * @return string HTML linking the user to their original $term query, and the one
508
	 *  suggested by $textMatches.
509
	 */
510
	protected function getDidYouMeanRewrittenHtml( $term, SearchResultSet $textMatches ) {
511
		// Showing results for '$rewritten'
512
		// Search instead for '$orig'
513
514
		$params = [ 'search' => $textMatches->getQueryAfterRewrite() ];
515 View Code Duplication
		if ( $this->fulltext === null ) {
516
			$params['fulltext'] = 'Search';
517
		} else {
518
			$params['fulltext'] = $this->fulltext;
519
		}
520
		$stParams = array_merge( $params, $this->powerSearchOptions() );
521
522
		$rewritten = Linker::linkKnown(
523
			$this->getPageTitle(),
524
			$textMatches->getQueryAfterRewriteSnippet() ?: null,
525
			[ 'id' => 'mw-search-DYM-rewritten' ],
526
			$stParams
527
		);
528
529
		$stParams['search'] = $term;
530
		$stParams['runsuggestion'] = 0;
531
		$original = Linker::linkKnown(
532
			$this->getPageTitle(),
533
			htmlspecialchars( $term ),
534
			[ 'id' => 'mw-search-DYM-original' ],
535
			$stParams
536
		);
537
538
		return Html::rawElement(
539
			'div',
540
			[ 'class' => 'searchdidyoumean' ],
541
			$this->msg( 'search-rewritten' )->rawParams( $rewritten, $original )->escaped()
542
		);
543
	}
544
545
	/**
546
	 * @param Title $title
547
	 * @param int $num The number of search results found
548
	 * @param null|SearchResultSet $titleMatches Results from title search
549
	 * @param null|SearchResultSet $textMatches Results from text search
550
	 */
551
	protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) {
552
		// show direct page/create link if applicable
553
554
		// Check DBkey !== '' in case of fragment link only.
555
		if ( is_null( $title ) || $title->getDBkey() === ''
556
			|| ( $titleMatches !== null && $titleMatches->searchContainedSyntax() )
557
			|| ( $textMatches !== null && $textMatches->searchContainedSyntax() )
558
		) {
559
			// invalid title
560
			// preserve the paragraph for margins etc...
561
			$this->getOutput()->addHTML( '<p></p>' );
562
563
			return;
564
		}
565
566
		$messageName = 'searchmenu-new-nocreate';
567
		$linkClass = 'mw-search-createlink';
568
569
		if ( !$title->isExternal() ) {
570
			if ( $title->isKnown() ) {
571
				$messageName = 'searchmenu-exists';
572
				$linkClass = 'mw-search-exists';
573
			} elseif ( $title->quickUserCan( 'create', $this->getUser() ) ) {
574
				$messageName = 'searchmenu-new';
575
			}
576
		}
577
578
		$params = [
579
			$messageName,
580
			wfEscapeWikiText( $title->getPrefixedText() ),
581
			Message::numParam( $num )
582
		];
583
		Hooks::run( 'SpecialSearchCreateLink', [ $title, &$params ] );
584
585
		// Extensions using the hook might still return an empty $messageName
586
		if ( $messageName ) {
587
			$this->getOutput()->wrapWikiMsg( "<p class=\"$linkClass\">\n$1</p>", $params );
588
		} else {
589
			// preserve the paragraph for margins etc...
590
			$this->getOutput()->addHTML( '<p></p>' );
591
		}
592
	}
593
594
	/**
595
	 * @param string $term
596
	 */
597
	protected function setupPage( $term ) {
598
		$out = $this->getOutput();
599
		if ( strval( $term ) !== '' ) {
600
			$out->setPageTitle( $this->msg( 'searchresults' ) );
601
			$out->setHTMLTitle( $this->msg( 'pagetitle' )
602
				->rawParams( $this->msg( 'searchresults-title' )->rawParams( $term )->text() )
603
				->inContentLanguage()->text()
604
			);
605
		}
606
		// add javascript specific to special:search
607
		$out->addModules( 'mediawiki.special.search' );
608
	}
609
610
	/**
611
	 * Return true if current search is a power (advanced) search
612
	 *
613
	 * @return bool
614
	 */
615
	protected function isPowerSearch() {
616
		return $this->profile === 'advanced';
617
	}
618
619
	/**
620
	 * Extract "power search" namespace settings from the request object,
621
	 * returning a list of index numbers to search.
622
	 *
623
	 * @param WebRequest $request
624
	 * @return array
625
	 */
626
	protected function powerSearch( &$request ) {
627
		$arr = [];
628
		foreach ( $this->searchConfig->searchableNamespaces() as $ns => $name ) {
629
			if ( $request->getCheck( 'ns' . $ns ) ) {
630
				$arr[] = $ns;
631
			}
632
		}
633
634
		return $arr;
635
	}
636
637
	/**
638
	 * Reconstruct the 'power search' options for links
639
	 *
640
	 * @return array
641
	 */
642
	protected function powerSearchOptions() {
643
		$opt = [];
644
		if ( !$this->isPowerSearch() ) {
645
			$opt['profile'] = $this->profile;
646
		} else {
647
			foreach ( $this->namespaces as $n ) {
648
				$opt['ns' . $n] = 1;
649
			}
650
		}
651
652
		return $opt + $this->extraParams;
653
	}
654
655
	/**
656
	 * Save namespace preferences when we're supposed to
657
	 *
658
	 * @return bool Whether we wrote something
659
	 */
660
	protected function saveNamespaces() {
661
		$user = $this->getUser();
662
		$request = $this->getRequest();
663
664
		if ( $user->isLoggedIn() &&
665
			$user->matchEditToken(
666
				$request->getVal( 'nsRemember' ),
667
				'searchnamespace',
668
				$request
669
			) && !wfReadOnly()
670
		) {
671
			// Reset namespace preferences: namespaces are not searched
672
			// when they're not mentioned in the URL parameters.
673
			foreach ( MWNamespace::getValidNamespaces() as $n ) {
674
				$user->setOption( 'searchNs' . $n, false );
675
			}
676
			// The request parameters include all the namespaces to be searched.
677
			// Even if they're the same as an existing profile, they're not eaten.
678
			foreach ( $this->namespaces as $n ) {
679
				$user->setOption( 'searchNs' . $n, true );
680
			}
681
682
			DeferredUpdates::addCallableUpdate( function () use ( $user ) {
683
				$user->saveSettings();
684
			} );
685
686
			return true;
687
		}
688
689
		return false;
690
	}
691
692
	/**
693
	 * Show whole set of results
694
	 *
695
	 * @param SearchResultSet $matches
696
	 * @param string $interwiki Interwiki name
697
	 *
698
	 * @return string
699
	 */
700
	protected function showMatches( $matches, $interwiki = null ) {
701
		global $wgContLang;
702
703
		$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
704
		$out = '';
705
		$result = $matches->next();
706
		$pos = $this->offset;
707
708
		if ( $result && $interwiki ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $interwiki of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
709
			$out .= $this->interwikiHeader( $interwiki, $matches );
710
		}
711
712
		$out .= "<ul class='mw-search-results'>\n";
713
		while ( $result ) {
714
			$out .= $this->showHit( $result, $terms, $pos++ );
715
			$result = $matches->next();
716
		}
717
		$out .= "</ul>\n";
718
719
		// convert the whole thing to desired language variant
720
		$out = $wgContLang->convert( $out );
721
722
		return $out;
723
	}
724
725
	/**
726
	 * Format a single hit result
727
	 *
728
	 * @param SearchResult $result
729
	 * @param array $terms Terms to highlight
730
	 * @param int $position Position within the search results, including offset.
731
	 *
732
	 * @return string
733
	 */
734
	protected function showHit( SearchResult $result, $terms, $position ) {
735
		if ( $result->isBrokenTitle() ) {
736
			return '';
737
		}
738
739
		$title = $result->getTitle();
740
741
		$titleSnippet = $result->getTitleSnippet();
742
743
		if ( $titleSnippet == '' ) {
744
			$titleSnippet = null;
745
		}
746
747
		$link_t = clone $title;
748
		$query = [];
749
750
		Hooks::run( 'ShowSearchHitTitle',
751
			[ &$link_t, &$titleSnippet, $result, $terms, $this, &$query ] );
752
753
		$link = Linker::linkKnown(
754
			$link_t,
755
			$titleSnippet,
756
			[ 'data-serp-pos' => $position ], // HTML attributes
757
			$query
758
		);
759
760
		// If page content is not readable, just return the title.
761
		// This is not quite safe, but better than showing excerpts from non-readable pages
762
		// Note that hiding the entry entirely would screw up paging.
763
		if ( !$title->userCan( 'read', $this->getUser() ) ) {
764
			return "<li>{$link}</li>\n";
765
		}
766
767
		// If the page doesn't *exist*... our search index is out of date.
768
		// The least confusing at this point is to drop the result.
769
		// You may get less results, but... oh well. :P
770
		if ( $result->isMissingRevision() ) {
771
			return '';
772
		}
773
774
		// format redirects / relevant sections
775
		$redirectTitle = $result->getRedirectTitle();
776
		$redirectText = $result->getRedirectSnippet();
777
		$sectionTitle = $result->getSectionTitle();
778
		$sectionText = $result->getSectionSnippet();
779
		$categorySnippet = $result->getCategorySnippet();
780
781
		$redirect = '';
782 View Code Duplication
		if ( !is_null( $redirectTitle ) ) {
783
			if ( $redirectText == '' ) {
784
				$redirectText = null;
785
			}
786
787
			$redirect = "<span class='searchalttitle'>" .
788
				$this->msg( 'search-redirect' )->rawParams(
789
					Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
790
				"</span>";
791
		}
792
793
		$section = '';
794 View Code Duplication
		if ( !is_null( $sectionTitle ) ) {
795
			if ( $sectionText == '' ) {
796
				$sectionText = null;
797
			}
798
799
			$section = "<span class='searchalttitle'>" .
800
				$this->msg( 'search-section' )->rawParams(
801
					Linker::linkKnown( $sectionTitle, $sectionText ) )->text() .
802
				"</span>";
803
		}
804
805
		$category = '';
806
		if ( $categorySnippet ) {
807
			$category = "<span class='searchalttitle'>" .
808
				$this->msg( 'search-category' )->rawParams( $categorySnippet )->text() .
809
				"</span>";
810
		}
811
812
		// format text extract
813
		$extract = "<div class='searchresult'>" . $result->getTextSnippet( $terms ) . "</div>";
814
815
		$lang = $this->getLanguage();
816
817
		// format description
818
		$byteSize = $result->getByteSize();
819
		$wordCount = $result->getWordCount();
820
		$timestamp = $result->getTimestamp();
821
		$size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) )
822
			->numParams( $wordCount )->escaped();
823
824
		if ( $title->getNamespace() == NS_CATEGORY ) {
825
			$cat = Category::newFromTitle( $title );
826
			$size = $this->msg( 'search-result-category-size' )
827
				->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() )
828
				->escaped();
829
		}
830
831
		$date = $lang->userTimeAndDate( $timestamp, $this->getUser() );
832
833
		$fileMatch = '';
834
		// Include a thumbnail for media files...
835
		if ( $title->getNamespace() == NS_FILE ) {
836
			$img = $result->getFile();
837
			$img = $img ?: wfFindFile( $title );
838
			if ( $result->isFileMatch() ) {
839
				$fileMatch = "<span class='searchalttitle'>" .
840
					$this->msg( 'search-file-match' )->escaped() . "</span>";
841
			}
842
			if ( $img ) {
843
				$thumb = $img->transform( [ 'width' => 120, 'height' => 120 ] );
844
				if ( $thumb ) {
845
					$desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped();
846
					// Float doesn't seem to interact well with the bullets.
847
					// Table messes up vertical alignment of the bullets.
848
					// Bullets are therefore disabled (didn't look great anyway).
849
					return "<li>" .
850
						'<table class="searchResultImage">' .
851
						'<tr>' .
852
						'<td style="width: 120px; text-align: center; vertical-align: top;">' .
853
						$thumb->toHtml( [ 'desc-link' => true ] ) .
854
						'</td>' .
855
						'<td style="vertical-align: top;">' .
856
						"{$link} {$redirect} {$category} {$section} {$fileMatch}" .
857
						$extract .
858
						"<div class='mw-search-result-data'>{$desc} - {$date}</div>" .
859
						'</td>' .
860
						'</tr>' .
861
						'</table>' .
862
						"</li>\n";
863
				}
864
			}
865
		}
866
867
		$html = null;
868
869
		$score = '';
870
		$related = '';
871
		if ( Hooks::run( 'ShowSearchHit', [
872
			$this, $result, $terms,
873
			&$link, &$redirect, &$section, &$extract,
874
			&$score, &$size, &$date, &$related,
875
			&$html
876
		] ) ) {
877
			$html = "<li><div class='mw-search-result-heading'>" .
878
				"{$link} {$redirect} {$category} {$section} {$fileMatch}</div> {$extract}\n" .
879
				"<div class='mw-search-result-data'>{$size} - {$date}</div>" .
880
				"</li>\n";
881
		}
882
883
		return $html;
884
	}
885
886
	/**
887
	 * Extract custom captions from search-interwiki-custom message
888
	 */
889
	protected function getCustomCaptions() {
890
		if ( is_null( $this->customCaptions ) ) {
891
			$this->customCaptions = [];
892
			// format per line <iwprefix>:<caption>
893
			$customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() );
894
			foreach ( $customLines as $line ) {
895
				$parts = explode( ":", $line, 2 );
896
				if ( count( $parts ) == 2 ) { // validate line
897
					$this->customCaptions[$parts[0]] = $parts[1];
898
				}
899
			}
900
		}
901
	}
902
903
	/**
904
	 * Show results from other wikis
905
	 *
906
	 * @param SearchResultSet|array $matches
907
	 * @param string $query
908
	 *
909
	 * @return string
910
	 */
911
	protected function showInterwiki( $matches, $query ) {
912
		global $wgContLang;
913
914
		$out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>" .
915
			$this->msg( 'search-interwiki-caption' )->text() . "</div>\n";
916
		$out .= "<ul class='mw-search-iwresults'>\n";
917
918
		// work out custom project captions
919
		$this->getCustomCaptions();
920
921
		if ( !is_array( $matches ) ) {
922
			$matches = [ $matches ];
923
		}
924
925
		foreach ( $matches as $set ) {
926
			$prev = null;
927
			$result = $set->next();
928
			while ( $result ) {
929
				$out .= $this->showInterwikiHit( $result, $prev, $query );
930
				$prev = $result->getInterwikiPrefix();
931
				$result = $set->next();
932
			}
933
		}
934
935
		// @todo Should support paging in a non-confusing way (not sure how though, maybe via ajax)..
936
		$out .= "</ul></div>\n";
937
938
		// convert the whole thing to desired language variant
939
		$out = $wgContLang->convert( $out );
940
941
		return $out;
942
	}
943
944
	/**
945
	 * Show single interwiki link
946
	 *
947
	 * @param SearchResult $result
948
	 * @param string $lastInterwiki
949
	 * @param string $query
950
	 *
951
	 * @return string
952
	 */
953
	protected function showInterwikiHit( $result, $lastInterwiki, $query ) {
954
		if ( $result->isBrokenTitle() ) {
955
			return '';
956
		}
957
958
		$title = $result->getTitle();
959
960
		$titleSnippet = $result->getTitleSnippet();
961
962
		if ( $titleSnippet == '' ) {
963
			$titleSnippet = null;
964
		}
965
966
		$link = Linker::linkKnown(
967
			$title,
968
			$titleSnippet
969
		);
970
971
		// format redirect if any
972
		$redirectTitle = $result->getRedirectTitle();
973
		$redirectText = $result->getRedirectSnippet();
974
		$redirect = '';
975 View Code Duplication
		if ( !is_null( $redirectTitle ) ) {
976
			if ( $redirectText == '' ) {
977
				$redirectText = null;
978
			}
979
980
			$redirect = "<span class='searchalttitle'>" .
981
				$this->msg( 'search-redirect' )->rawParams(
982
					Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
983
				"</span>";
984
		}
985
986
		$out = "";
987
		// display project name
988
		if ( is_null( $lastInterwiki ) || $lastInterwiki != $title->getInterwiki() ) {
989
			if ( array_key_exists( $title->getInterwiki(), $this->customCaptions ) ) {
990
				// captions from 'search-interwiki-custom'
991
				$caption = $this->customCaptions[$title->getInterwiki()];
992
			} else {
993
				// default is to show the hostname of the other wiki which might suck
994
				// if there are many wikis on one hostname
995
				$parsed = wfParseUrl( $title->getFullURL() );
996
				$caption = $this->msg( 'search-interwiki-default', $parsed['host'] )->text();
997
			}
998
			// "more results" link (special page stuff could be localized, but we might not know target lang)
999
			$searchTitle = Title::newFromText( $title->getInterwiki() . ":Special:Search" );
1000
			$searchLink = Linker::linkKnown(
1001
				$searchTitle,
1002
				$this->msg( 'search-interwiki-more' )->text(),
1003
				[],
1004
				[
1005
					'search' => $query,
1006
					'fulltext' => 'Search'
1007
				]
1008
			);
1009
			$out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>
1010
				{$searchLink}</span>{$caption}</div>\n<ul>";
1011
		}
1012
1013
		$out .= "<li>{$link} {$redirect}</li>\n";
1014
1015
		return $out;
1016
	}
1017
1018
	/**
1019
	 * Generates the power search box at [[Special:Search]]
1020
	 *
1021
	 * @param string $term Search term
1022
	 * @param array $opts
1023
	 * @return string HTML form
1024
	 */
1025
	protected function powerSearchBox( $term, $opts ) {
1026
		global $wgContLang;
1027
1028
		// Groups namespaces into rows according to subject
1029
		$rows = [];
1030
		foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) {
1031
			$subject = MWNamespace::getSubject( $namespace );
1032
			if ( !array_key_exists( $subject, $rows ) ) {
1033
				$rows[$subject] = "";
1034
			}
1035
1036
			$name = $wgContLang->getConverter()->convertNamespace( $namespace );
1037
			if ( $name == '' ) {
1038
				$name = $this->msg( 'blanknamespace' )->text();
1039
			}
1040
1041
			$rows[$subject] .=
1042
				Xml::openElement( 'td' ) .
1043
				Xml::checkLabel(
1044
					$name,
1045
					"ns{$namespace}",
1046
					"mw-search-ns{$namespace}",
1047
					in_array( $namespace, $this->namespaces )
1048
				) .
1049
				Xml::closeElement( 'td' );
1050
		}
1051
1052
		$rows = array_values( $rows );
1053
		$numRows = count( $rows );
1054
1055
		// Lays out namespaces in multiple floating two-column tables so they'll
1056
		// be arranged nicely while still accommodating different screen widths
1057
		$namespaceTables = '';
1058
		for ( $i = 0; $i < $numRows; $i += 4 ) {
1059
			$namespaceTables .= Xml::openElement( 'table' );
1060
1061
			for ( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
1062
				$namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
1063
			}
1064
1065
			$namespaceTables .= Xml::closeElement( 'table' );
1066
		}
1067
1068
		$showSections = [ 'namespaceTables' => $namespaceTables ];
1069
1070
		Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] );
1071
1072
		$hidden = '';
1073
		foreach ( $opts as $key => $value ) {
1074
			$hidden .= Html::hidden( $key, $value );
1075
		}
1076
1077
		# Stuff to feed saveNamespaces()
1078
		$remember = '';
1079
		$user = $this->getUser();
1080
		if ( $user->isLoggedIn() ) {
1081
			$remember .= Xml::checkLabel(
1082
				$this->msg( 'powersearch-remember' )->text(),
1083
				'nsRemember',
1084
				'mw-search-powersearch-remember',
1085
				false,
1086
				// The token goes here rather than in a hidden field so it
1087
				// is only sent when necessary (not every form submission).
1088
				[ 'value' => $user->getEditToken(
1089
					'searchnamespace',
1090
					$this->getRequest()
1091
				) ]
1092
			);
1093
		}
1094
1095
		// Return final output
1096
		return Xml::openElement( 'fieldset', [ 'id' => 'mw-searchoptions' ] ) .
1097
			Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) .
1098
			Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) .
1099
			Xml::element( 'div', [ 'id' => 'mw-search-togglebox' ], '', false ) .
1100
			Xml::element( 'div', [ 'class' => 'divider' ], '', false ) .
1101
			implode( Xml::element( 'div', [ 'class' => 'divider' ], '', false ), $showSections ) .
1102
			$hidden .
1103
			Xml::element( 'div', [ 'class' => 'divider' ], '', false ) .
1104
			$remember .
1105
			Xml::closeElement( 'fieldset' );
1106
	}
1107
1108
	/**
1109
	 * @return array
1110
	 */
1111
	protected function getSearchProfiles() {
1112
		// Builds list of Search Types (profiles)
1113
		$nsAllSet = array_keys( $this->searchConfig->searchableNamespaces() );
1114
		$defaultNs = $this->searchConfig->defaultNamespaces();
1115
		$profiles = [
1116
			'default' => [
1117
				'message' => 'searchprofile-articles',
1118
				'tooltip' => 'searchprofile-articles-tooltip',
1119
				'namespaces' => $defaultNs,
1120
				'namespace-messages' => $this->searchConfig->namespacesAsText(
1121
					$defaultNs
1122
				),
1123
			],
1124
			'images' => [
1125
				'message' => 'searchprofile-images',
1126
				'tooltip' => 'searchprofile-images-tooltip',
1127
				'namespaces' => [ NS_FILE ],
1128
			],
1129
			'all' => [
1130
				'message' => 'searchprofile-everything',
1131
				'tooltip' => 'searchprofile-everything-tooltip',
1132
				'namespaces' => $nsAllSet,
1133
			],
1134
			'advanced' => [
1135
				'message' => 'searchprofile-advanced',
1136
				'tooltip' => 'searchprofile-advanced-tooltip',
1137
				'namespaces' => self::NAMESPACES_CURRENT,
1138
			]
1139
		];
1140
1141
		Hooks::run( 'SpecialSearchProfiles', [ &$profiles ] );
1142
1143
		foreach ( $profiles as &$data ) {
1144
			if ( !is_array( $data['namespaces'] ) ) {
1145
				continue;
1146
			}
1147
			sort( $data['namespaces'] );
1148
		}
1149
1150
		return $profiles;
1151
	}
1152
1153
	/**
1154
	 * @param string $term
1155
	 * @return string
1156
	 */
1157
	protected function searchProfileTabs( $term ) {
1158
		$out = Html::element( 'div', [ 'class' => 'mw-search-visualclear' ] ) .
1159
			Xml::openElement( 'div', [ 'class' => 'mw-search-profile-tabs' ] );
1160
1161
		$bareterm = $term;
1162
		if ( $this->startsWithImage( $term ) ) {
1163
			// Deletes prefixes
1164
			$bareterm = substr( $term, strpos( $term, ':' ) + 1 );
1165
		}
1166
1167
		$profiles = $this->getSearchProfiles();
1168
		$lang = $this->getLanguage();
1169
1170
		// Outputs XML for Search Types
1171
		$out .= Xml::openElement( 'div', [ 'class' => 'search-types' ] );
1172
		$out .= Xml::openElement( 'ul' );
1173
		foreach ( $profiles as $id => $profile ) {
1174
			if ( !isset( $profile['parameters'] ) ) {
1175
				$profile['parameters'] = [];
1176
			}
1177
			$profile['parameters']['profile'] = $id;
1178
1179
			$tooltipParam = isset( $profile['namespace-messages'] ) ?
1180
				$lang->commaList( $profile['namespace-messages'] ) : null;
1181
			$out .= Xml::tags(
1182
				'li',
1183
				[
1184
					'class' => $this->profile === $id ? 'current' : 'normal'
1185
				],
1186
				$this->makeSearchLink(
1187
					$bareterm,
1188
					[],
1189
					$this->msg( $profile['message'] )->text(),
1190
					$this->msg( $profile['tooltip'], $tooltipParam )->text(),
1191
					$profile['parameters']
0 ignored issues
show
Bug introduced by
It seems like $profile['parameters'] can also be of type string; however, SpecialSearch::makeSearchLink() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1192
				)
1193
			);
1194
		}
1195
		$out .= Xml::closeElement( 'ul' );
1196
		$out .= Xml::closeElement( 'div' );
1197
		$out .= Xml::element( 'div', [ 'style' => 'clear:both' ], '', false );
1198
		$out .= Xml::closeElement( 'div' );
1199
1200
		return $out;
1201
	}
1202
1203
	/**
1204
	 * @param string $term Search term
1205
	 * @return string
1206
	 */
1207
	protected function searchOptions( $term ) {
1208
		$out = '';
1209
		$opts = [];
1210
		$opts['profile'] = $this->profile;
1211
1212
		if ( $this->isPowerSearch() ) {
1213
			$out .= $this->powerSearchBox( $term, $opts );
1214
		} else {
1215
			$form = '';
1216
			Hooks::run( 'SpecialSearchProfileForm', [ $this, &$form, $this->profile, $term, $opts ] );
1217
			$out .= $form;
1218
		}
1219
1220
		return $out;
1221
	}
1222
1223
	/**
1224
	 * @param string $term
1225
	 * @param int $resultsShown
1226
	 * @param int $totalNum
1227
	 * @return string
1228
	 */
1229
	protected function shortDialog( $term, $resultsShown, $totalNum ) {
1230
		$searchWidget = new MediaWiki\Widget\SearchInputWidget( [
1231
			'id' => 'searchText',
1232
			'name' => 'search',
1233
			'autofocus' => trim( $term ) === '',
1234
			'value' => $term,
1235
			'dataLocation' => 'content',
1236
			'infusable' => true,
1237
		] );
1238
1239
		$layout = new OOUI\ActionFieldLayout( $searchWidget, new OOUI\ButtonInputWidget( [
1240
			'type' => 'submit',
1241
			'label' => $this->msg( 'searchbutton' )->text(),
1242
			'flags' => [ 'progressive', 'primary' ],
1243
		] ), [
1244
			'align' => 'top',
1245
		] );
1246
1247
		$out =
1248
			Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
1249
			Html::hidden( 'profile', $this->profile ) .
1250
			Html::hidden( 'fulltext', 'Search' ) .
1251
			$layout;
1252
1253
		// Results-info
1254
		if ( $totalNum > 0 && $this->offset < $totalNum ) {
1255
			$top = $this->msg( 'search-showingresults' )
1256
				->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum )
1257
				->numParams( $resultsShown )
1258
				->parse();
1259
			$out .= Xml::tags( 'div', [ 'class' => 'results-info' ], $top );
1260
		}
1261
1262
		return $out;
1263
	}
1264
1265
	/**
1266
	 * Make a search link with some target namespaces
1267
	 *
1268
	 * @param string $term
1269
	 * @param array $namespaces Ignored
1270
	 * @param string $label Link's text
1271
	 * @param string $tooltip Link's tooltip
1272
	 * @param array $params Query string parameters
1273
	 * @return string HTML fragment
1274
	 */
1275
	protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = [] ) {
1276
		$opt = $params;
1277
		foreach ( $namespaces as $n ) {
1278
			$opt['ns' . $n] = 1;
1279
		}
1280
1281
		$stParams = array_merge(
1282
			[
1283
				'search' => $term,
1284
				'fulltext' => $this->msg( 'search' )->text()
1285
			],
1286
			$opt
1287
		);
1288
1289
		return Xml::element(
1290
			'a',
1291
			[
1292
				'href' => $this->getPageTitle()->getLocalURL( $stParams ),
1293
				'title' => $tooltip
1294
			],
1295
			$label
1296
		);
1297
	}
1298
1299
	/**
1300
	 * Check if query starts with image: prefix
1301
	 *
1302
	 * @param string $term The string to check
1303
	 * @return bool
1304
	 */
1305
	protected function startsWithImage( $term ) {
1306
		global $wgContLang;
1307
1308
		$parts = explode( ':', $term );
1309
		if ( count( $parts ) > 1 ) {
1310
			return $wgContLang->getNsIndex( $parts[0] ) == NS_FILE;
1311
		}
1312
1313
		return false;
1314
	}
1315
1316
	/**
1317
	 * @since 1.18
1318
	 *
1319
	 * @return SearchEngine
1320
	 */
1321
	public function getSearchEngine() {
1322
		if ( $this->searchEngine === null ) {
1323
			$this->searchEngine = $this->searchEngineType ?
1324
				MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $this->searchEngineType ) :
1325
				MediaWikiServices::getInstance()->newSearchEngine();
1326
		}
1327
1328
		return $this->searchEngine;
1329
	}
1330
1331
	/**
1332
	 * Current search profile.
1333
	 * @return null|string
1334
	 */
1335
	function getProfile() {
1336
		return $this->profile;
1337
	}
1338
1339
	/**
1340
	 * Current namespaces.
1341
	 * @return array
1342
	 */
1343
	function getNamespaces() {
1344
		return $this->namespaces;
1345
	}
1346
1347
	/**
1348
	 * Users of hook SpecialSearchSetupEngine can use this to
1349
	 * add more params to links to not lose selection when
1350
	 * user navigates search results.
1351
	 * @since 1.18
1352
	 *
1353
	 * @param string $key
1354
	 * @param mixed $value
1355
	 */
1356
	public function setExtraParam( $key, $value ) {
1357
		$this->extraParams[$key] = $value;
1358
	}
1359
1360
	protected function getGroupName() {
1361
		return 'pages';
1362
	}
1363
}
1364