This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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
|
|||
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
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 The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes. ![]() |
|||
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
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 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;
}
![]() |
|||
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
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, ![]() |
|||
231 | $url = $title->getFullURL(); |
||
232 | } |
||
233 | $this->getOutput()->redirect( $url ); |
||
0 ignored issues
–
show
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
![]() |
|||
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
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);
}
}
![]() |
|||
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
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. ![]() |
|||
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
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);
}
}
![]() |
|||
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
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.
![]() |
|||
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
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 For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
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
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 For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
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
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. ![]() |
|||
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 |
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.