Completed
Branch master (9259dd)
by
unknown
27:26
created

SpecialSearch   F

Complexity

Total Complexity 170

Size/Duplication

Total Lines 1360
Duplicated Lines 2.94 %

Coupling/Cohesion

Components 1
Dependencies 27

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 40
loc 1360
rs 0.5217
wmc 170
lcom 1
cbo 27

33 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B execute() 0 43 4
C load() 0 46 9
B goResult() 0 31 6
F showResults() 0 211 39
A interwikiHeader() 0 5 1
B shouldRunSuggestedQuery() 0 11 5
B getDidYouMeanHtml() 5 24 3
B getDidYouMeanRewrittenHtml() 5 34 3
C showCreateLink() 0 42 11
A setupPage() 0 12 2
A isPowerSearch() 0 3 1
A powerSearch() 0 10 3
A powerSearchOptions() 0 12 3
B saveNamespaces() 0 31 6
B showMatches() 0 24 4
F showHit() 20 152 17
A getCustomCaptions() 0 13 4
B showInterwiki() 0 32 4
C showInterwikiHit() 10 65 8
C powerSearchBox() 0 82 9
B getSearchProfiles() 0 41 3
B searchProfileTabs() 0 45 6
A searchOptions() 0 15 2
B shortDialog() 0 34 3
A makeSearchLink() 0 23 2
A startsWithImage() 0 10 2
A startsWithAll() 0 11 2
A getSearchEngine() 0 9 3
A getProfile() 0 3 1
A getNamespaces() 0 3 1
A setExtraParam() 0 3 1
A getGroupName() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SpecialSearch often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SpecialSearch, and based on these observations, apply Extract Interface, too.

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
		$this->setHeaders();
104
		$this->outputHeader();
105
		$out = $this->getOutput();
106
		$out->allowClickjacking();
107
		$out->addModuleStyles( [
108
			'mediawiki.special', 'mediawiki.special.search.styles', 'mediawiki.ui', 'mediawiki.ui.button',
109
			'mediawiki.ui.input', 'mediawiki.widgets.SearchInputWidget.styles',
110
		] );
111
		$this->addHelpLink( 'Help:Searching' );
112
113
		// Strip underscores from title parameter; most of the time we'll want
114
		// text form here. But don't strip underscores from actual text params!
115
		$titleParam = str_replace( '_', ' ', $par );
116
117
		$request = $this->getRequest();
118
119
		// Fetch the search term
120
		$search = str_replace( "\n", " ", $request->getText( 'search', $titleParam ) );
121
122
		$this->load();
123
		if ( !is_null( $request->getVal( 'nsRemember' ) ) ) {
124
			$this->saveNamespaces();
125
			// Remove the token from the URL to prevent the user from inadvertently
126
			// exposing it (e.g. by pasting it into a public wiki page) or undoing
127
			// later settings changes (e.g. by reloading the page).
128
			$query = $request->getValues();
129
			unset( $query['title'], $query['nsRemember'] );
130
			$out->redirect( $this->getPageTitle()->getFullURL( $query ) );
131
			return;
132
		}
133
134
		$out->addJsConfigVars( [ 'searchTerm' => $search ] );
135
		$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...
136
137
		if ( $request->getVal( 'fulltext' )
138
			|| !is_null( $request->getVal( 'offset' ) )
139
		) {
140
			$this->showResults( $search );
141
		} else {
142
			$this->goResult( $search );
143
		}
144
	}
145
146
	/**
147
	 * Set up basic search parameters from the request and user settings.
148
	 *
149
	 * @see tests/phpunit/includes/specials/SpecialSearchTest.php
150
	 */
151
	public function load() {
152
		$request = $this->getRequest();
153
		list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, '' );
154
		$this->mPrefix = $request->getVal( 'prefix', '' );
155
156
		$user = $this->getUser();
157
158
		# Extract manually requested namespaces
159
		$nslist = $this->powerSearch( $request );
160
		if ( !count( $nslist ) ) {
161
			# Fallback to user preference
162
			$nslist = $this->searchConfig->userNamespaces( $user );
163
		}
164
165
		$profile = null;
166
		if ( !count( $nslist ) ) {
167
			$profile = 'default';
168
		}
169
170
		$profile = $request->getVal( 'profile', $profile );
171
		$profiles = $this->getSearchProfiles();
172
		if ( $profile === null ) {
173
			// BC with old request format
174
			$profile = 'advanced';
175
			foreach ( $profiles as $key => $data ) {
176
				if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
177
					$profile = $key;
178
				}
179
			}
180
			$this->namespaces = $nslist;
181
		} elseif ( $profile === 'advanced' ) {
182
			$this->namespaces = $nslist;
183
		} else {
184
			if ( isset( $profiles[$profile]['namespaces'] ) ) {
185
				$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...
186
			} else {
187
				// Unknown profile requested
188
				$profile = 'default';
189
				$this->namespaces = $profiles['default']['namespaces'];
190
			}
191
		}
192
193
		$this->fulltext = $request->getVal( 'fulltext' );
194
		$this->runSuggestion = (bool)$request->getVal( 'runsuggestion', true );
195
		$this->profile = $profile;
196
	}
197
198
	/**
199
	 * If an exact title match can be found, jump straight ahead to it.
200
	 *
201
	 * @param string $term
202
	 */
203
	public function goResult( $term ) {
204
		$this->setupPage( $term );
205
		# Try to go to page as entered.
206
		$title = Title::newFromText( $term );
207
		# If the string cannot be used to create a title
208
		if ( is_null( $title ) ) {
209
			$this->showResults( $term );
210
211
			return;
212
		}
213
		# If there's an exact or very near match, jump right there.
214
		$title = $this->getSearchEngine()
215
			->getNearMatcher( $this->getConfig() )->getNearMatch( $term );
216
217
		if ( !is_null( $title ) &&
218
			Hooks::run( 'SpecialSearchGoResult', [ $term, $title, &$url ] )
219
		) {
220
			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...
221
				$url = $title->getFullURL();
222
			}
223
			$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...
224
225
			return;
226
		}
227
		# No match, generate an edit URL
228
		$title = Title::newFromText( $term );
229
		if ( !is_null( $title ) ) {
230
			Hooks::run( 'SpecialSearchNogomatch', [ &$title ] );
231
		}
232
		$this->showResults( $term );
233
	}
234
235
	/**
236
	 * @param string $term
237
	 */
238
	public function showResults( $term ) {
239
		global $wgContLang;
240
241
		$search = $this->getSearchEngine();
242
		$search->setFeatureData( 'rewrite', $this->runSuggestion );
243
		$search->setLimitOffset( $this->limit, $this->offset );
244
		$search->setNamespaces( $this->namespaces );
245
		$search->prefix = $this->mPrefix;
246
		$term = $search->transformSearchTerm( $term );
247
248
		Hooks::run( 'SpecialSearchSetupEngine', [ $this, $this->profile, $search ] );
249
250
		$this->setupPage( $term );
251
252
		$out = $this->getOutput();
253
254
		if ( $this->getConfig()->get( 'DisableTextSearch' ) ) {
255
			$searchFowardUrl = $this->getConfig()->get( 'SearchForwardUrl' );
256
			if ( $searchFowardUrl ) {
257
				$url = str_replace( '$1', urlencode( $term ), $searchFowardUrl );
258
				$out->redirect( $url );
259
			} else {
260
				$out->addHTML(
261
					Xml::openElement( 'fieldset' ) .
262
					Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) .
263
					Xml::element(
264
						'p',
265
						[ 'class' => 'mw-searchdisabled' ],
266
						$this->msg( 'searchdisabled' )->text()
267
					) .
268
					$this->msg( 'googlesearch' )->rawParams(
269
						htmlspecialchars( $term ),
270
						'UTF-8',
271
						$this->msg( 'searchbutton' )->escaped()
272
					)->text() .
273
					Xml::closeElement( 'fieldset' )
274
				);
275
			}
276
277
			return;
278
		}
279
280
		$title = Title::newFromText( $term );
281
		$showSuggestion = $title === null || !$title->isKnown();
282
		$search->setShowSuggestion( $showSuggestion );
283
284
		// fetch search results
285
		$rewritten = $search->replacePrefixes( $term );
286
287
		$titleMatches = $search->searchTitle( $rewritten );
288
		$textMatches = $search->searchText( $rewritten );
289
290
		$textStatus = null;
291
		if ( $textMatches instanceof Status ) {
292
			$textStatus = $textMatches;
293
			$textMatches = null;
294
		}
295
296
		// did you mean... suggestions
297
		$didYouMeanHtml = '';
298
		if ( $showSuggestion && $textMatches && !$textStatus ) {
299
			if ( $textMatches->hasRewrittenQuery() ) {
300
				$didYouMeanHtml = $this->getDidYouMeanRewrittenHtml( $term, $textMatches );
301
			} elseif ( $textMatches->hasSuggestion() ) {
302
				$didYouMeanHtml = $this->getDidYouMeanHtml( $textMatches );
303
			}
304
		}
305
306
		if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) {
307
			# Hook requested termination
308
			return;
309
		}
310
311
		// start rendering the page
312
		$out->addHTML(
313
			Xml::openElement(
314
				'form',
315
				[
316
					'id' => ( $this->isPowerSearch() ? 'powersearch' : 'search' ),
317
					'method' => 'get',
318
					'action' => wfScript(),
319
				]
320
			)
321
		);
322
323
		// Get number of results
324
		$titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0;
325
		if ( $titleMatches ) {
326
			$titleMatchesNum = $titleMatches->numRows();
327
			$numTitleMatches = $titleMatches->getTotalHits();
328
		}
329
		if ( $textMatches ) {
330
			$textMatchesNum = $textMatches->numRows();
331
			$numTextMatches = $textMatches->getTotalHits();
332
		}
333
		$num = $titleMatchesNum + $textMatchesNum;
334
		$totalRes = $numTitleMatches + $numTextMatches;
335
336
		$out->enableOOUI();
337
		$out->addHTML(
338
			# This is an awful awful ID name. It's not a table, but we
339
			# named it poorly from when this was a table so now we're
340
			# stuck with it
341
			Xml::openElement( 'div', [ 'id' => 'mw-search-top-table' ] ) .
342
			$this->shortDialog( $term, $num, $totalRes ) .
343
			Xml::closeElement( 'div' ) .
344
			$this->searchProfileTabs( $term ) .
345
			$this->searchOptions( $term ) .
346
			Xml::closeElement( 'form' ) .
347
			$didYouMeanHtml
348
		);
349
350
		$filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':';
351
		if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
352
			// Empty query -- straight view of search form
353
			return;
354
		}
355
356
		$out->addHTML( "<div class='searchresults'>" );
357
358
		// prev/next links
359
		$prevnext = null;
360
		if ( $num || $this->offset ) {
361
			// Show the create link ahead
362
			$this->showCreateLink( $title, $num, $titleMatches, $textMatches );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($term) on line 280 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...
363
			if ( $totalRes > $this->limit || $this->offset ) {
364
				if ( $this->searchEngineType !== null ) {
365
					$this->setExtraParam( 'srbackend', $this->searchEngineType );
366
				}
367
				$prevnext = $this->getLanguage()->viewPrevNext(
368
					$this->getPageTitle(),
369
					$this->offset,
370
					$this->limit,
371
					$this->powerSearchOptions() + [ 'search' => $term ],
372
					$this->limit + $this->offset >= $totalRes
373
				);
374
			}
375
		}
376
		Hooks::run( 'SpecialSearchResults', [ $term, &$titleMatches, &$textMatches ] );
377
378
		$out->parserOptions()->setEditSection( false );
379
		if ( $titleMatches ) {
380
			if ( $numTitleMatches > 0 ) {
381
				$out->wrapWikiMsg( "==$1==\n", 'titlematches' );
382
				$out->addHTML( $this->showMatches( $titleMatches ) );
383
			}
384
			$titleMatches->free();
385
		}
386
		if ( $textMatches && !$textStatus ) {
387
			// output appropriate heading
388
			if ( $numTextMatches > 0 && $numTitleMatches > 0 ) {
389
				$out->addHTML( '<div class="visualClear"></div>' );
390
				// if no title matches the heading is redundant
391
				$out->wrapWikiMsg( "==$1==\n", 'textmatches' );
392
			}
393
394
			// show results
395
			if ( $numTextMatches > 0 ) {
396
				$out->addHTML( $this->showMatches( $textMatches ) );
397
			}
398
399
			// show secondary interwiki results if any
400
			if ( $textMatches->hasInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) ) {
401
				$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...
402
						SearchResultSet::SECONDARY_RESULTS ), $term ) );
403
			}
404
		}
405
406
		$hasOtherResults = $textMatches &&
407
			$textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS );
408
409
		if ( $num === 0 ) {
410
			if ( $textStatus ) {
411
				$out->addHTML( '<div class="error">' .
412
					$textStatus->getMessage( 'search-error' ) . '</div>' );
413
			} else {
414
				$this->showCreateLink( $title, $num, $titleMatches, $textMatches );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::newFromText($term) on line 280 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...
415
				$out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>",
416
					[ $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound',
417
							wfEscapeWikiText( $term )
418
					] );
419
			}
420
		}
421
422
		if ( $hasOtherResults ) {
423
			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...
424
						as $interwiki => $interwikiResult ) {
425
				if ( $interwikiResult instanceof Status || $interwikiResult->numRows() == 0 ) {
426
					// ignore bad interwikis for now
427
					continue;
428
				}
429
				// TODO: wiki header
430
				$out->addHTML( $this->showMatches( $interwikiResult, $interwiki ) );
431
			}
432
		}
433
434
		if ( $textMatches ) {
435
			$textMatches->free();
436
		}
437
438
		$out->addHTML( '<div class="visualClear"></div>' );
439
440
		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...
441
			$out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
442
		}
443
444
		$out->addHTML( "</div>" );
445
446
		Hooks::run( 'SpecialSearchResultsAppend', [ $this, $out, $term ] );
447
448
	}
449
450
	/**
451
	 * Produce wiki header for interwiki results
452
	 * @param string $interwiki Interwiki name
453
	 * @param SearchResultSet $interwikiResult The result set
454
	 * @return string
455
	 */
456
	protected function interwikiHeader( $interwiki, $interwikiResult ) {
457
		// TODO: we need to figure out how to name wikis correctly
458
		$wikiMsg = $this->msg( 'search-interwiki-results-' . $interwiki )->parse();
459
		return "<p class=\"mw-search-interwiki-header\">\n$wikiMsg</p>";
460
	}
461
462
	/**
463
	 * Decide if the suggested query should be run, and it's results returned
464
	 * instead of the provided $textMatches
465
	 *
466
	 * @param SearchResultSet $textMatches The results of a users query
467
	 * @return bool
468
	 */
469
	protected function shouldRunSuggestedQuery( SearchResultSet $textMatches ) {
470
		if ( !$this->runSuggestion ||
471
			!$textMatches->hasSuggestion() ||
472
			$textMatches->numRows() > 0 ||
473
			$textMatches->searchContainedSyntax()
474
		) {
475
			return false;
476
		}
477
478
		return $this->getConfig()->get( 'SearchRunSuggestedQuery' );
479
	}
480
481
	/**
482
	 * Generates HTML shown to the user when we have a suggestion about a query
483
	 * that might give more results than their current query.
484
	 */
485
	protected function getDidYouMeanHtml( SearchResultSet $textMatches ) {
486
		# mirror Go/Search behavior of original request ..
487
		$params = [ 'search' => $textMatches->getSuggestionQuery() ];
488 View Code Duplication
		if ( $this->fulltext === null ) {
489
			$params['fulltext'] = 'Search';
490
		} else {
491
			$params['fulltext'] = $this->fulltext;
492
		}
493
		$stParams = array_merge( $params, $this->powerSearchOptions() );
494
495
		$suggest = Linker::linkKnown(
496
			$this->getPageTitle(),
497
			$textMatches->getSuggestionSnippet() ?: null,
498
			[ 'id' => 'mw-search-DYM-suggestion' ],
499
			$stParams
500
		);
501
502
		# HTML of did you mean... search suggestion link
503
		return Html::rawElement(
504
			'div',
505
			[ 'class' => 'searchdidyoumean' ],
506
			$this->msg( 'search-suggest' )->rawParams( $suggest )->parse()
507
		);
508
	}
509
510
	/**
511
	 * Generates HTML shown to user when their query has been internally rewritten,
512
	 * and the results of the rewritten query are being returned.
513
	 *
514
	 * @param string $term The users search input
515
	 * @param SearchResultSet $textMatches The response to the users initial search request
516
	 * @return string HTML linking the user to their original $term query, and the one
517
	 *  suggested by $textMatches.
518
	 */
519
	protected function getDidYouMeanRewrittenHtml( $term, SearchResultSet $textMatches ) {
520
		// Showing results for '$rewritten'
521
		// Search instead for '$orig'
522
523
		$params = [ 'search' => $textMatches->getQueryAfterRewrite() ];
524 View Code Duplication
		if ( $this->fulltext === null ) {
525
			$params['fulltext'] = 'Search';
526
		} else {
527
			$params['fulltext'] = $this->fulltext;
528
		}
529
		$stParams = array_merge( $params, $this->powerSearchOptions() );
530
531
		$rewritten = Linker::linkKnown(
532
			$this->getPageTitle(),
533
			$textMatches->getQueryAfterRewriteSnippet() ?: null,
534
			[ 'id' => 'mw-search-DYM-rewritten' ],
535
			$stParams
536
		);
537
538
		$stParams['search'] = $term;
539
		$stParams['runsuggestion'] = 0;
540
		$original = Linker::linkKnown(
541
			$this->getPageTitle(),
542
			htmlspecialchars( $term ),
543
			[ 'id' => 'mw-search-DYM-original' ],
544
			$stParams
545
		);
546
547
		return Html::rawElement(
548
			'div',
549
			[ 'class' => 'searchdidyoumean' ],
550
			$this->msg( 'search-rewritten' )->rawParams( $rewritten, $original )->escaped()
551
		);
552
	}
553
554
	/**
555
	 * @param Title $title
556
	 * @param int $num The number of search results found
557
	 * @param null|SearchResultSet $titleMatches Results from title search
558
	 * @param null|SearchResultSet $textMatches Results from text search
559
	 */
560
	protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) {
561
		// show direct page/create link if applicable
562
563
		// Check DBkey !== '' in case of fragment link only.
564
		if ( is_null( $title ) || $title->getDBkey() === ''
565
			|| ( $titleMatches !== null && $titleMatches->searchContainedSyntax() )
566
			|| ( $textMatches !== null && $textMatches->searchContainedSyntax() )
567
		) {
568
			// invalid title
569
			// preserve the paragraph for margins etc...
570
			$this->getOutput()->addHTML( '<p></p>' );
571
572
			return;
573
		}
574
575
		$messageName = 'searchmenu-new-nocreate';
576
		$linkClass = 'mw-search-createlink';
577
578
		if ( !$title->isExternal() ) {
579
			if ( $title->isKnown() ) {
580
				$messageName = 'searchmenu-exists';
581
				$linkClass = 'mw-search-exists';
582
			} elseif ( $title->quickUserCan( 'create', $this->getUser() ) ) {
583
				$messageName = 'searchmenu-new';
584
			}
585
		}
586
587
		$params = [
588
			$messageName,
589
			wfEscapeWikiText( $title->getPrefixedText() ),
590
			Message::numParam( $num )
591
		];
592
		Hooks::run( 'SpecialSearchCreateLink', [ $title, &$params ] );
593
594
		// Extensions using the hook might still return an empty $messageName
595
		if ( $messageName ) {
596
			$this->getOutput()->wrapWikiMsg( "<p class=\"$linkClass\">\n$1</p>", $params );
597
		} else {
598
			// preserve the paragraph for margins etc...
599
			$this->getOutput()->addHTML( '<p></p>' );
600
		}
601
	}
602
603
	/**
604
	 * @param string $term
605
	 */
606
	protected function setupPage( $term ) {
607
		$out = $this->getOutput();
608
		if ( strval( $term ) !== '' ) {
609
			$out->setPageTitle( $this->msg( 'searchresults' ) );
610
			$out->setHTMLTitle( $this->msg( 'pagetitle' )
611
				->rawParams( $this->msg( 'searchresults-title' )->rawParams( $term )->text() )
612
				->inContentLanguage()->text()
613
			);
614
		}
615
		// add javascript specific to special:search
616
		$out->addModules( 'mediawiki.special.search' );
617
	}
618
619
	/**
620
	 * Return true if current search is a power (advanced) search
621
	 *
622
	 * @return bool
623
	 */
624
	protected function isPowerSearch() {
625
		return $this->profile === 'advanced';
626
	}
627
628
	/**
629
	 * Extract "power search" namespace settings from the request object,
630
	 * returning a list of index numbers to search.
631
	 *
632
	 * @param WebRequest $request
633
	 * @return array
634
	 */
635
	protected function powerSearch( &$request ) {
636
		$arr = [];
637
		foreach ( $this->searchConfig->searchableNamespaces() as $ns => $name ) {
638
			if ( $request->getCheck( 'ns' . $ns ) ) {
639
				$arr[] = $ns;
640
			}
641
		}
642
643
		return $arr;
644
	}
645
646
	/**
647
	 * Reconstruct the 'power search' options for links
648
	 *
649
	 * @return array
650
	 */
651
	protected function powerSearchOptions() {
652
		$opt = [];
653
		if ( !$this->isPowerSearch() ) {
654
			$opt['profile'] = $this->profile;
655
		} else {
656
			foreach ( $this->namespaces as $n ) {
657
				$opt['ns' . $n] = 1;
658
			}
659
		}
660
661
		return $opt + $this->extraParams;
662
	}
663
664
	/**
665
	 * Save namespace preferences when we're supposed to
666
	 *
667
	 * @return bool Whether we wrote something
668
	 */
669
	protected function saveNamespaces() {
670
		$user = $this->getUser();
671
		$request = $this->getRequest();
672
673
		if ( $user->isLoggedIn() &&
674
			$user->matchEditToken(
675
				$request->getVal( 'nsRemember' ),
676
				'searchnamespace',
677
				$request
678
			) && !wfReadOnly()
679
		) {
680
			// Reset namespace preferences: namespaces are not searched
681
			// when they're not mentioned in the URL parameters.
682
			foreach ( MWNamespace::getValidNamespaces() as $n ) {
683
				$user->setOption( 'searchNs' . $n, false );
684
			}
685
			// The request parameters include all the namespaces to be searched.
686
			// Even if they're the same as an existing profile, they're not eaten.
687
			foreach ( $this->namespaces as $n ) {
688
				$user->setOption( 'searchNs' . $n, true );
689
			}
690
691
			DeferredUpdates::addCallableUpdate( function () use ( $user ) {
692
				$user->saveSettings();
693
			} );
694
695
			return true;
696
		}
697
698
		return false;
699
	}
700
701
	/**
702
	 * Show whole set of results
703
	 *
704
	 * @param SearchResultSet $matches
705
	 * @param string $interwiki Interwiki name
706
	 *
707
	 * @return string
708
	 */
709
	protected function showMatches( &$matches, $interwiki = null ) {
710
		global $wgContLang;
711
712
		$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
713
		$out = '';
714
		$result = $matches->next();
715
		$pos = $this->offset;
716
717
		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...
718
			$out .= $this->interwikiHeader( $interwiki, $result );
719
		}
720
721
		$out .= "<ul class='mw-search-results'>\n";
722
		while ( $result ) {
723
			$out .= $this->showHit( $result, $terms, ++$pos );
724
			$result = $matches->next();
725
		}
726
		$out .= "</ul>\n";
727
728
		// convert the whole thing to desired language variant
729
		$out = $wgContLang->convert( $out );
730
731
		return $out;
732
	}
733
734
	/**
735
	 * Format a single hit result
736
	 *
737
	 * @param SearchResult $result
738
	 * @param array $terms Terms to highlight
739
	 * @param int $position Position within the search results, including offset.
740
	 *
741
	 * @return string
742
	 */
743
	protected function showHit( $result, $terms, $position ) {
744
745
		if ( $result->isBrokenTitle() ) {
746
			return '';
747
		}
748
749
		$title = $result->getTitle();
750
751
		$titleSnippet = $result->getTitleSnippet();
752
753
		if ( $titleSnippet == '' ) {
754
			$titleSnippet = null;
755
		}
756
757
		$link_t = clone $title;
758
		$query = [];
759
760
		Hooks::run( 'ShowSearchHitTitle',
761
			[ &$link_t, &$titleSnippet, $result, $terms, $this, &$query ] );
762
763
		$link = Linker::linkKnown(
764
			$link_t,
765
			$titleSnippet,
766
			[ 'data-serp-pos' => $position ], // HTML attributes
767
			$query
768
		);
769
770
		// If page content is not readable, just return the title.
771
		// This is not quite safe, but better than showing excerpts from non-readable pages
772
		// Note that hiding the entry entirely would screw up paging.
773
		if ( !$title->userCan( 'read', $this->getUser() ) ) {
774
			return "<li>{$link}</li>\n";
775
		}
776
777
		// If the page doesn't *exist*... our search index is out of date.
778
		// The least confusing at this point is to drop the result.
779
		// You may get less results, but... oh well. :P
780
		if ( $result->isMissingRevision() ) {
781
			return '';
782
		}
783
784
		// format redirects / relevant sections
785
		$redirectTitle = $result->getRedirectTitle();
786
		$redirectText = $result->getRedirectSnippet();
787
		$sectionTitle = $result->getSectionTitle();
788
		$sectionText = $result->getSectionSnippet();
789
		$categorySnippet = $result->getCategorySnippet();
790
791
		$redirect = '';
792 View Code Duplication
		if ( !is_null( $redirectTitle ) ) {
793
			if ( $redirectText == '' ) {
794
				$redirectText = null;
795
			}
796
797
			$redirect = "<span class='searchalttitle'>" .
798
				$this->msg( 'search-redirect' )->rawParams(
799
					Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
800
				"</span>";
801
		}
802
803
		$section = '';
804 View Code Duplication
		if ( !is_null( $sectionTitle ) ) {
805
			if ( $sectionText == '' ) {
806
				$sectionText = null;
807
			}
808
809
			$section = "<span class='searchalttitle'>" .
810
				$this->msg( 'search-section' )->rawParams(
811
					Linker::linkKnown( $sectionTitle, $sectionText ) )->text() .
812
				"</span>";
813
		}
814
815
		$category = '';
816
		if ( $categorySnippet ) {
817
			$category = "<span class='searchalttitle'>" .
818
				$this->msg( 'search-category' )->rawParams( $categorySnippet )->text() .
819
				"</span>";
820
		}
821
822
		// format text extract
823
		$extract = "<div class='searchresult'>" . $result->getTextSnippet( $terms ) . "</div>";
824
825
		$lang = $this->getLanguage();
826
827
		// format description
828
		$byteSize = $result->getByteSize();
829
		$wordCount = $result->getWordCount();
830
		$timestamp = $result->getTimestamp();
831
		$size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) )
832
			->numParams( $wordCount )->escaped();
833
834
		if ( $title->getNamespace() == NS_CATEGORY ) {
835
			$cat = Category::newFromTitle( $title );
836
			$size = $this->msg( 'search-result-category-size' )
837
				->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() )
838
				->escaped();
839
		}
840
841
		$date = $lang->userTimeAndDate( $timestamp, $this->getUser() );
842
843
		$fileMatch = '';
844
		// Include a thumbnail for media files...
845
		if ( $title->getNamespace() == NS_FILE ) {
846
			$img = $result->getFile();
847
			$img = $img ?: wfFindFile( $title );
848
			if ( $result->isFileMatch() ) {
849
				$fileMatch = "<span class='searchalttitle'>" .
850
					$this->msg( 'search-file-match' )->escaped() . "</span>";
851
			}
852
			if ( $img ) {
853
				$thumb = $img->transform( [ 'width' => 120, 'height' => 120 ] );
854
				if ( $thumb ) {
855
					$desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped();
856
					// Float doesn't seem to interact well with the bullets.
857
					// Table messes up vertical alignment of the bullets.
858
					// Bullets are therefore disabled (didn't look great anyway).
859
					return "<li>" .
860
						'<table class="searchResultImage">' .
861
						'<tr>' .
862
						'<td style="width: 120px; text-align: center; vertical-align: top;">' .
863
						$thumb->toHtml( [ 'desc-link' => true ] ) .
864
						'</td>' .
865
						'<td style="vertical-align: top;">' .
866
						"{$link} {$redirect} {$category} {$section} {$fileMatch}" .
867
						$extract .
868
						"<div class='mw-search-result-data'>{$desc} - {$date}</div>" .
869
						'</td>' .
870
						'</tr>' .
871
						'</table>' .
872
						"</li>\n";
873
				}
874
			}
875
		}
876
877
		$html = null;
878
879
		$score = '';
880
		$related = '';
881
		if ( Hooks::run( 'ShowSearchHit', [
882
			$this, $result, $terms,
883
			&$link, &$redirect, &$section, &$extract,
884
			&$score, &$size, &$date, &$related,
885
			&$html
886
		] ) ) {
887
			$html = "<li><div class='mw-search-result-heading'>" .
888
				"{$link} {$redirect} {$category} {$section} {$fileMatch}</div> {$extract}\n" .
889
				"<div class='mw-search-result-data'>{$size} - {$date}</div>" .
890
				"</li>\n";
891
		}
892
893
		return $html;
894
	}
895
896
	/**
897
	 * Extract custom captions from search-interwiki-custom message
898
	 */
899
	protected function getCustomCaptions() {
900
		if ( is_null( $this->customCaptions ) ) {
901
			$this->customCaptions = [];
902
			// format per line <iwprefix>:<caption>
903
			$customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() );
904
			foreach ( $customLines as $line ) {
905
				$parts = explode( ":", $line, 2 );
906
				if ( count( $parts ) == 2 ) { // validate line
907
					$this->customCaptions[$parts[0]] = $parts[1];
908
				}
909
			}
910
		}
911
	}
912
913
	/**
914
	 * Show results from other wikis
915
	 *
916
	 * @param SearchResultSet|array $matches
917
	 * @param string $query
918
	 *
919
	 * @return string
920
	 */
921
	protected function showInterwiki( $matches, $query ) {
922
		global $wgContLang;
923
924
		$out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>" .
925
			$this->msg( 'search-interwiki-caption' )->text() . "</div>\n";
926
		$out .= "<ul class='mw-search-iwresults'>\n";
927
928
		// work out custom project captions
929
		$this->getCustomCaptions();
930
931
		if ( !is_array( $matches ) ) {
932
			$matches = [ $matches ];
933
		}
934
935
		foreach ( $matches as $set ) {
936
			$prev = null;
937
			$result = $set->next();
938
			while ( $result ) {
939
				$out .= $this->showInterwikiHit( $result, $prev, $query );
940
				$prev = $result->getInterwikiPrefix();
941
				$result = $set->next();
942
			}
943
		}
944
945
		// @todo Should support paging in a non-confusing way (not sure how though, maybe via ajax)..
946
		$out .= "</ul></div>\n";
947
948
		// convert the whole thing to desired language variant
949
		$out = $wgContLang->convert( $out );
950
951
		return $out;
952
	}
953
954
	/**
955
	 * Show single interwiki link
956
	 *
957
	 * @param SearchResult $result
958
	 * @param string $lastInterwiki
959
	 * @param string $query
960
	 *
961
	 * @return string
962
	 */
963
	protected function showInterwikiHit( $result, $lastInterwiki, $query ) {
964
965
		if ( $result->isBrokenTitle() ) {
966
			return '';
967
		}
968
969
		$title = $result->getTitle();
970
971
		$titleSnippet = $result->getTitleSnippet();
972
973
		if ( $titleSnippet == '' ) {
974
			$titleSnippet = null;
975
		}
976
977
		$link = Linker::linkKnown(
978
			$title,
979
			$titleSnippet
980
		);
981
982
		// format redirect if any
983
		$redirectTitle = $result->getRedirectTitle();
984
		$redirectText = $result->getRedirectSnippet();
985
		$redirect = '';
986 View Code Duplication
		if ( !is_null( $redirectTitle ) ) {
987
			if ( $redirectText == '' ) {
988
				$redirectText = null;
989
			}
990
991
			$redirect = "<span class='searchalttitle'>" .
992
				$this->msg( 'search-redirect' )->rawParams(
993
					Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
994
				"</span>";
995
		}
996
997
		$out = "";
998
		// display project name
999
		if ( is_null( $lastInterwiki ) || $lastInterwiki != $title->getInterwiki() ) {
1000
			if ( array_key_exists( $title->getInterwiki(), $this->customCaptions ) ) {
1001
				// captions from 'search-interwiki-custom'
1002
				$caption = $this->customCaptions[$title->getInterwiki()];
1003
			} else {
1004
				// default is to show the hostname of the other wiki which might suck
1005
				// if there are many wikis on one hostname
1006
				$parsed = wfParseUrl( $title->getFullURL() );
1007
				$caption = $this->msg( 'search-interwiki-default', $parsed['host'] )->text();
1008
			}
1009
			// "more results" link (special page stuff could be localized, but we might not know target lang)
1010
			$searchTitle = Title::newFromText( $title->getInterwiki() . ":Special:Search" );
1011
			$searchLink = Linker::linkKnown(
1012
				$searchTitle,
1013
				$this->msg( 'search-interwiki-more' )->text(),
1014
				[],
1015
				[
1016
					'search' => $query,
1017
					'fulltext' => 'Search'
1018
				]
1019
			);
1020
			$out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>
1021
				{$searchLink}</span>{$caption}</div>\n<ul>";
1022
		}
1023
1024
		$out .= "<li>{$link} {$redirect}</li>\n";
1025
1026
		return $out;
1027
	}
1028
1029
	/**
1030
	 * Generates the power search box at [[Special:Search]]
1031
	 *
1032
	 * @param string $term Search term
1033
	 * @param array $opts
1034
	 * @return string HTML form
1035
	 */
1036
	protected function powerSearchBox( $term, $opts ) {
1037
		global $wgContLang;
1038
1039
		// Groups namespaces into rows according to subject
1040
		$rows = [];
1041
		foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) {
1042
			$subject = MWNamespace::getSubject( $namespace );
1043
			if ( !array_key_exists( $subject, $rows ) ) {
1044
				$rows[$subject] = "";
1045
			}
1046
1047
			$name = $wgContLang->getConverter()->convertNamespace( $namespace );
1048
			if ( $name == '' ) {
1049
				$name = $this->msg( 'blanknamespace' )->text();
1050
			}
1051
1052
			$rows[$subject] .=
1053
				Xml::openElement( 'td' ) .
1054
				Xml::checkLabel(
1055
					$name,
1056
					"ns{$namespace}",
1057
					"mw-search-ns{$namespace}",
1058
					in_array( $namespace, $this->namespaces )
1059
				) .
1060
				Xml::closeElement( 'td' );
1061
		}
1062
1063
		$rows = array_values( $rows );
1064
		$numRows = count( $rows );
1065
1066
		// Lays out namespaces in multiple floating two-column tables so they'll
1067
		// be arranged nicely while still accommodating different screen widths
1068
		$namespaceTables = '';
1069
		for ( $i = 0; $i < $numRows; $i += 4 ) {
1070
			$namespaceTables .= Xml::openElement( 'table' );
1071
1072
			for ( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
1073
				$namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
1074
			}
1075
1076
			$namespaceTables .= Xml::closeElement( 'table' );
1077
		}
1078
1079
		$showSections = [ 'namespaceTables' => $namespaceTables ];
1080
1081
		Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] );
1082
1083
		$hidden = '';
1084
		foreach ( $opts as $key => $value ) {
1085
			$hidden .= Html::hidden( $key, $value );
1086
		}
1087
1088
		# Stuff to feed saveNamespaces()
1089
		$remember = '';
1090
		$user = $this->getUser();
1091
		if ( $user->isLoggedIn() ) {
1092
			$remember .= Xml::checkLabel(
1093
				$this->msg( 'powersearch-remember' )->text(),
1094
				'nsRemember',
1095
				'mw-search-powersearch-remember',
1096
				false,
1097
				// The token goes here rather than in a hidden field so it
1098
				// is only sent when necessary (not every form submission).
1099
				[ 'value' => $user->getEditToken(
1100
					'searchnamespace',
1101
					$this->getRequest()
1102
				) ]
1103
			);
1104
		}
1105
1106
		// Return final output
1107
		return Xml::openElement( 'fieldset', [ 'id' => 'mw-searchoptions' ] ) .
1108
			Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) .
1109
			Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) .
1110
			Xml::element( 'div', [ 'id' => 'mw-search-togglebox' ], '', false ) .
1111
			Xml::element( 'div', [ 'class' => 'divider' ], '', false ) .
1112
			implode( Xml::element( 'div', [ 'class' => 'divider' ], '', false ), $showSections ) .
1113
			$hidden .
1114
			Xml::element( 'div', [ 'class' => 'divider' ], '', false ) .
1115
			$remember .
1116
			Xml::closeElement( 'fieldset' );
1117
	}
1118
1119
	/**
1120
	 * @return array
1121
	 */
1122
	protected function getSearchProfiles() {
1123
		// Builds list of Search Types (profiles)
1124
		$nsAllSet = array_keys( $this->searchConfig->searchableNamespaces() );
1125
		$defaultNs = $this->searchConfig->defaultNamespaces();
1126
		$profiles = [
1127
			'default' => [
1128
				'message' => 'searchprofile-articles',
1129
				'tooltip' => 'searchprofile-articles-tooltip',
1130
				'namespaces' => $defaultNs,
1131
				'namespace-messages' => $this->searchConfig->namespacesAsText(
1132
					$defaultNs
1133
				),
1134
			],
1135
			'images' => [
1136
				'message' => 'searchprofile-images',
1137
				'tooltip' => 'searchprofile-images-tooltip',
1138
				'namespaces' => [ NS_FILE ],
1139
			],
1140
			'all' => [
1141
				'message' => 'searchprofile-everything',
1142
				'tooltip' => 'searchprofile-everything-tooltip',
1143
				'namespaces' => $nsAllSet,
1144
			],
1145
			'advanced' => [
1146
				'message' => 'searchprofile-advanced',
1147
				'tooltip' => 'searchprofile-advanced-tooltip',
1148
				'namespaces' => self::NAMESPACES_CURRENT,
1149
			]
1150
		];
1151
1152
		Hooks::run( 'SpecialSearchProfiles', [ &$profiles ] );
1153
1154
		foreach ( $profiles as &$data ) {
1155
			if ( !is_array( $data['namespaces'] ) ) {
1156
				continue;
1157
			}
1158
			sort( $data['namespaces'] );
1159
		}
1160
1161
		return $profiles;
1162
	}
1163
1164
	/**
1165
	 * @param string $term
1166
	 * @return string
1167
	 */
1168
	protected function searchProfileTabs( $term ) {
1169
		$out = Html::element( 'div', [ 'class' => 'visualClear' ] ) .
1170
			Xml::openElement( 'div', [ 'class' => 'mw-search-profile-tabs' ] );
1171
1172
		$bareterm = $term;
1173
		if ( $this->startsWithImage( $term ) ) {
1174
			// Deletes prefixes
1175
			$bareterm = substr( $term, strpos( $term, ':' ) + 1 );
1176
		}
1177
1178
		$profiles = $this->getSearchProfiles();
1179
		$lang = $this->getLanguage();
1180
1181
		// Outputs XML for Search Types
1182
		$out .= Xml::openElement( 'div', [ 'class' => 'search-types' ] );
1183
		$out .= Xml::openElement( 'ul' );
1184
		foreach ( $profiles as $id => $profile ) {
1185
			if ( !isset( $profile['parameters'] ) ) {
1186
				$profile['parameters'] = [];
1187
			}
1188
			$profile['parameters']['profile'] = $id;
1189
1190
			$tooltipParam = isset( $profile['namespace-messages'] ) ?
1191
				$lang->commaList( $profile['namespace-messages'] ) : null;
1192
			$out .= Xml::tags(
1193
				'li',
1194
				[
1195
					'class' => $this->profile === $id ? 'current' : 'normal'
1196
				],
1197
				$this->makeSearchLink(
1198
					$bareterm,
1199
					[],
1200
					$this->msg( $profile['message'] )->text(),
1201
					$this->msg( $profile['tooltip'], $tooltipParam )->text(),
1202
					$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...
1203
				)
1204
			);
1205
		}
1206
		$out .= Xml::closeElement( 'ul' );
1207
		$out .= Xml::closeElement( 'div' );
1208
		$out .= Xml::element( 'div', [ 'style' => 'clear:both' ], '', false );
1209
		$out .= Xml::closeElement( 'div' );
1210
1211
		return $out;
1212
	}
1213
1214
	/**
1215
	 * @param string $term Search term
1216
	 * @return string
1217
	 */
1218
	protected function searchOptions( $term ) {
1219
		$out = '';
1220
		$opts = [];
1221
		$opts['profile'] = $this->profile;
1222
1223
		if ( $this->isPowerSearch() ) {
1224
			$out .= $this->powerSearchBox( $term, $opts );
1225
		} else {
1226
			$form = '';
1227
			Hooks::run( 'SpecialSearchProfileForm', [ $this, &$form, $this->profile, $term, $opts ] );
1228
			$out .= $form;
1229
		}
1230
1231
		return $out;
1232
	}
1233
1234
	/**
1235
	 * @param string $term
1236
	 * @param int $resultsShown
1237
	 * @param int $totalNum
1238
	 * @return string
1239
	 */
1240
	protected function shortDialog( $term, $resultsShown, $totalNum ) {
1241
		$searchWidget = new MediaWiki\Widget\SearchInputWidget( [
1242
			'id' => 'searchText',
1243
			'name' => 'search',
1244
			'autofocus' => trim( $term ) === '',
1245
			'value' => $term,
1246
			'dataLocation' => 'content',
1247
		] );
1248
1249
		$layout = new OOUI\ActionFieldLayout( $searchWidget, new OOUI\ButtonInputWidget( [
1250
			'type' => 'submit',
1251
			'label' => $this->msg( 'searchbutton' )->text(),
1252
			'flags' => [ 'progressive', 'primary' ],
1253
		] ), [
1254
			'align' => 'top',
1255
		] );
1256
1257
		$out =
1258
			Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
1259
			Html::hidden( 'profile', $this->profile ) .
1260
			Html::hidden( 'fulltext', 'Search' ) .
1261
			$layout;
1262
1263
		// Results-info
1264
		if ( $totalNum > 0 && $this->offset < $totalNum ) {
1265
			$top = $this->msg( 'search-showingresults' )
1266
				->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum )
1267
				->numParams( $resultsShown )
1268
				->parse();
1269
			$out .= Xml::tags( 'div', [ 'class' => 'results-info' ], $top );
1270
		}
1271
1272
		return $out;
1273
	}
1274
1275
	/**
1276
	 * Make a search link with some target namespaces
1277
	 *
1278
	 * @param string $term
1279
	 * @param array $namespaces Ignored
1280
	 * @param string $label Link's text
1281
	 * @param string $tooltip Link's tooltip
1282
	 * @param array $params Query string parameters
1283
	 * @return string HTML fragment
1284
	 */
1285
	protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = [] ) {
1286
		$opt = $params;
1287
		foreach ( $namespaces as $n ) {
1288
			$opt['ns' . $n] = 1;
1289
		}
1290
1291
		$stParams = array_merge(
1292
			[
1293
				'search' => $term,
1294
				'fulltext' => $this->msg( 'search' )->text()
1295
			],
1296
			$opt
1297
		);
1298
1299
		return Xml::element(
1300
			'a',
1301
			[
1302
				'href' => $this->getPageTitle()->getLocalURL( $stParams ),
1303
				'title' => $tooltip
1304
			],
1305
			$label
1306
		);
1307
	}
1308
1309
	/**
1310
	 * Check if query starts with image: prefix
1311
	 *
1312
	 * @param string $term The string to check
1313
	 * @return bool
1314
	 */
1315
	protected function startsWithImage( $term ) {
1316
		global $wgContLang;
1317
1318
		$parts = explode( ':', $term );
1319
		if ( count( $parts ) > 1 ) {
1320
			return $wgContLang->getNsIndex( $parts[0] ) == NS_FILE;
1321
		}
1322
1323
		return false;
1324
	}
1325
1326
	/**
1327
	 * Check if query starts with all: prefix
1328
	 *
1329
	 * @param string $term The string to check
1330
	 * @return bool
1331
	 */
1332
	protected function startsWithAll( $term ) {
1333
1334
		$allkeyword = $this->msg( 'searchall' )->inContentLanguage()->text();
1335
1336
		$parts = explode( ':', $term );
1337
		if ( count( $parts ) > 1 ) {
1338
			return $parts[0] == $allkeyword;
1339
		}
1340
1341
		return false;
1342
	}
1343
1344
	/**
1345
	 * @since 1.18
1346
	 *
1347
	 * @return SearchEngine
1348
	 */
1349
	public function getSearchEngine() {
1350
		if ( $this->searchEngine === null ) {
1351
			$this->searchEngine = $this->searchEngineType ?
1352
				MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $this->searchEngineType ) :
1353
				MediaWikiServices::getInstance()->newSearchEngine();
1354
		}
1355
1356
		return $this->searchEngine;
1357
	}
1358
1359
	/**
1360
	 * Current search profile.
1361
	 * @return null|string
1362
	 */
1363
	function getProfile() {
1364
		return $this->profile;
1365
	}
1366
1367
	/**
1368
	 * Current namespaces.
1369
	 * @return array
1370
	 */
1371
	function getNamespaces() {
1372
		return $this->namespaces;
1373
	}
1374
1375
	/**
1376
	 * Users of hook SpecialSearchSetupEngine can use this to
1377
	 * add more params to links to not lose selection when
1378
	 * user navigates search results.
1379
	 * @since 1.18
1380
	 *
1381
	 * @param string $key
1382
	 * @param mixed $value
1383
	 */
1384
	public function setExtraParam( $key, $value ) {
1385
		$this->extraParams[$key] = $value;
1386
	}
1387
1388
	protected function getGroupName() {
1389
		return 'pages';
1390
	}
1391
}
1392