Fix some bad "Implements Special:*" comments in includes/specials/ files.
[lhc/web/wiklou.git] / includes / specials / SpecialSearch.php
1 <?php
2 /**
3 * Implements Special:Search
4 *
5 * Copyright © 2004 Brion Vibber <brion@pobox.com>
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 /**
27 * implements Special:Search - Run text & title search and display the output
28 * @ingroup SpecialPage
29 */
30 class SpecialSearch extends SpecialPage {
31 /// Current search profile
32 protected $profile;
33
34 /// Search engine
35 protected $searchEngine;
36
37 /// For links
38 protected $extraParams = array();
39
40 /// No idea, apparently used by some other classes
41 protected $mPrefix;
42
43 /**
44 * @var int
45 */
46 protected $limit, $offset;
47
48 /**
49 * @var array
50 */
51 protected $namespaces;
52
53 /**
54 * @var bool
55 */
56 protected $searchRedirects;
57
58 /**
59 * @var string
60 */
61 protected $didYouMeanHtml, $fulltext;
62
63 const NAMESPACES_CURRENT = 'sense';
64
65 public function __construct() {
66 parent::__construct( 'Search' );
67 }
68
69 /**
70 * Entry point
71 *
72 * @param $par String or null
73 */
74 public function execute( $par ) {
75 $this->setHeaders();
76 $this->outputHeader();
77 $out = $this->getOutput();
78 $out->allowClickjacking();
79 $out->addModuleStyles( 'mediawiki.special' );
80
81 // Strip underscores from title parameter; most of the time we'll want
82 // text form here. But don't strip underscores from actual text params!
83 $titleParam = str_replace( '_', ' ', $par );
84
85 $request = $this->getRequest();
86
87 // Fetch the search term
88 $search = str_replace( "\n", " ", $request->getText( 'search', $titleParam ) );
89
90 $this->load();
91
92 if ( $request->getVal( 'fulltext' )
93 || !is_null( $request->getVal( 'offset' ) )
94 || !is_null( $request->getVal( 'searchx' ) ) )
95 {
96 $this->showResults( $search );
97 } else {
98 $this->goResult( $search );
99 }
100 }
101
102 /**
103 * Set up basic search parameters from the request and user settings.
104 */
105 public function load() {
106 $request = $this->getRequest();
107 list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
108 $this->mPrefix = $request->getVal( 'prefix', '' );
109
110 $user = $this->getUser();
111 # Extract manually requested namespaces
112 $nslist = $this->powerSearch( $request );
113 $this->profile = $profile = $request->getVal( 'profile', null );
114 $profiles = $this->getSearchProfiles();
115 if ( $profile === null) {
116 // BC with old request format
117 $this->profile = 'advanced';
118 if ( count( $nslist ) ) {
119 foreach( $profiles as $key => $data ) {
120 if ( $nslist === $data['namespaces'] && $key !== 'advanced') {
121 $this->profile = $key;
122 }
123 }
124 $this->namespaces = $nslist;
125 } else {
126 $this->namespaces = SearchEngine::userNamespaces( $user );
127 }
128 } elseif ( $profile === 'advanced' ) {
129 $this->namespaces = $nslist;
130 } else {
131 if ( isset( $profiles[$profile]['namespaces'] ) ) {
132 $this->namespaces = $profiles[$profile]['namespaces'];
133 } else {
134 // Unknown profile requested
135 $this->profile = 'default';
136 $this->namespaces = $profiles['default']['namespaces'];
137 }
138 }
139
140 // Redirects defaults to true, but we don't know whether it was ticked of or just missing
141 $default = $request->getBool( 'profile' ) ? 0 : 1;
142 $this->searchRedirects = $request->getBool( 'redirs', $default ) ? 1 : 0;
143 $this->didYouMeanHtml = ''; # html of did you mean... link
144 $this->fulltext = $request->getVal('fulltext');
145 }
146
147 /**
148 * If an exact title match can be found, jump straight ahead to it.
149 *
150 * @param $term String
151 */
152 public function goResult( $term ) {
153 $this->setupPage( $term );
154 # Try to go to page as entered.
155 $t = Title::newFromText( $term );
156 # If the string cannot be used to create a title
157 if( is_null( $t ) ) {
158 return $this->showResults( $term );
159 }
160 # If there's an exact or very near match, jump right there.
161 $t = SearchEngine::getNearMatch( $term );
162
163 if ( !wfRunHooks( 'SpecialSearchGo', array( &$t, &$term ) ) ) {
164 # Hook requested termination
165 return;
166 }
167
168 if( !is_null( $t ) ) {
169 $this->getOutput()->redirect( $t->getFullURL() );
170 return;
171 }
172 # No match, generate an edit URL
173 $t = Title::newFromText( $term );
174 if( !is_null( $t ) ) {
175 global $wgGoToEdit;
176 wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
177 wfDebugLog( 'nogomatch', $t->getText(), false );
178
179 # If the feature is enabled, go straight to the edit page
180 if( $wgGoToEdit ) {
181 $this->getOutput()->redirect( $t->getFullURL( array( 'action' => 'edit' ) ) );
182 return;
183 }
184 }
185 return $this->showResults( $term );
186 }
187
188 /**
189 * @param $term String
190 */
191 public function showResults( $term ) {
192 global $wgDisableTextSearch, $wgSearchForwardUrl, $wgContLang, $wgScript;
193 wfProfileIn( __METHOD__ );
194
195 $search = $this->getSearchEngine();
196 $search->setLimitOffset( $this->limit, $this->offset );
197 $search->setNamespaces( $this->namespaces );
198 $search->showRedirects = $this->searchRedirects; // BC
199 $search->setFeatureData( 'list-redirects', $this->searchRedirects );
200 $search->prefix = $this->mPrefix;
201 $term = $search->transformSearchTerm($term);
202
203 wfRunHooks( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) );
204
205 $this->setupPage( $term );
206
207 $out = $this->getOutput();
208
209 if ( $wgDisableTextSearch ) {
210 if ( $wgSearchForwardUrl ) {
211 $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
212 $out->redirect( $url );
213 } else {
214 $out->addHTML(
215 Xml::openElement( 'fieldset' ) .
216 Xml::element( 'legend', null, wfMsg( 'search-external' ) ) .
217 Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) .
218 wfMsg( 'googlesearch',
219 htmlspecialchars( $term ),
220 htmlspecialchars( 'UTF-8' ),
221 htmlspecialchars( wfMsg( 'searchbutton' ) )
222 ) .
223 Xml::closeElement( 'fieldset' )
224 );
225 }
226 wfProfileOut( __METHOD__ );
227 return;
228 }
229
230 $t = Title::newFromText( $term );
231
232 // fetch search results
233 $rewritten = $search->replacePrefixes($term);
234
235 $titleMatches = $search->searchTitle( $rewritten );
236 if( !( $titleMatches instanceof SearchResultTooMany ) ) {
237 $textMatches = $search->searchText( $rewritten );
238 }
239
240 // did you mean... suggestions
241 if( $textMatches && $textMatches->hasSuggestion() ) {
242 $st = SpecialPage::getTitleFor( 'Search' );
243
244 # mirror Go/Search behaviour of original request ..
245 $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() );
246
247 if( $this->fulltext != null ) {
248 $didYouMeanParams['fulltext'] = $this->fulltext;
249 }
250
251 $stParams = array_merge(
252 $didYouMeanParams,
253 $this->powerSearchOptions()
254 );
255
256 $suggestionSnippet = $textMatches->getSuggestionSnippet();
257
258 if( $suggestionSnippet == '' ) {
259 $suggestionSnippet = null;
260 }
261
262 $suggestLink = Linker::linkKnown(
263 $st,
264 $suggestionSnippet,
265 array(),
266 $stParams
267 );
268
269 $this->didYouMeanHtml = '<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>';
270 }
271 // start rendering the page
272 $out->addHtml(
273 Xml::openElement(
274 'form',
275 array(
276 'id' => ( $this->profile === 'advanced' ? 'powersearch' : 'search' ),
277 'method' => 'get',
278 'action' => $wgScript
279 )
280 )
281 );
282 $out->addHtml(
283 Xml::openElement( 'table', array( 'id'=>'mw-search-top-table', 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) .
284 Xml::openElement( 'tr' ) .
285 Xml::openElement( 'td' ) . "\n" .
286 $this->shortDialog( $term ) .
287 Xml::closeElement('td') .
288 Xml::closeElement('tr') .
289 Xml::closeElement('table')
290 );
291
292 // Sometimes the search engine knows there are too many hits
293 if( $titleMatches instanceof SearchResultTooMany ) {
294 $out->wrapWikiMsg( "==$1==\n", 'toomanymatches' );
295 wfProfileOut( __METHOD__ );
296 return;
297 }
298
299 $filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':';
300 if( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
301 $out->addHTML( $this->formHeader( $term, 0, 0 ) );
302 $out->addHtml( $this->getProfileForm( $this->profile, $term ) );
303 $out->addHTML( '</form>' );
304 // Empty query -- straight view of search form
305 wfProfileOut( __METHOD__ );
306 return;
307 }
308
309 // Get number of results
310 $titleMatchesNum = $titleMatches ? $titleMatches->numRows() : 0;
311 $textMatchesNum = $textMatches ? $textMatches->numRows() : 0;
312 // Total initial query matches (possible false positives)
313 $num = $titleMatchesNum + $textMatchesNum;
314
315 // Get total actual results (after second filtering, if any)
316 $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ?
317 $titleMatches->getTotalHits() : $titleMatchesNum;
318 $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ?
319 $textMatches->getTotalHits() : $textMatchesNum;
320
321 // get total number of results if backend can calculate it
322 $totalRes = 0;
323 if($titleMatches && !is_null( $titleMatches->getTotalHits() ) )
324 $totalRes += $titleMatches->getTotalHits();
325 if($textMatches && !is_null( $textMatches->getTotalHits() ))
326 $totalRes += $textMatches->getTotalHits();
327
328 // show number of results and current offset
329 $out->addHTML( $this->formHeader( $term, $num, $totalRes ) );
330 $out->addHtml( $this->getProfileForm( $this->profile, $term ) );
331
332
333 $out->addHtml( Xml::closeElement( 'form' ) );
334 $out->addHtml( "<div class='searchresults'>" );
335
336 // prev/next links
337 if( $num || $this->offset ) {
338 // Show the create link ahead
339 $this->showCreateLink( $t );
340 $prevnext = wfViewPrevNext( $this->offset, $this->limit,
341 SpecialPage::getTitleFor( 'Search' ),
342 wfArrayToCGI( $this->powerSearchOptions(), array( 'search' => $term ) ),
343 max( $titleMatchesNum, $textMatchesNum ) < $this->limit
344 );
345 //$out->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
346 wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
347 } else {
348 wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
349 }
350
351 $out->parserOptions()->setEditSection( false );
352 if( $titleMatches ) {
353 if( $numTitleMatches > 0 ) {
354 $out->wrapWikiMsg( "==$1==\n", 'titlematches' );
355 $out->addHTML( $this->showMatches( $titleMatches ) );
356 }
357 $titleMatches->free();
358 }
359 if( $textMatches ) {
360 // output appropriate heading
361 if( $numTextMatches > 0 && $numTitleMatches > 0 ) {
362 // if no title matches the heading is redundant
363 $out->wrapWikiMsg( "==$1==\n", 'textmatches' );
364 } elseif( $totalRes == 0 ) {
365 # Don't show the 'no text matches' if we received title matches
366 # $out->wrapWikiMsg( "==$1==\n", 'notextmatches' );
367 }
368 // show interwiki results if any
369 if( $textMatches->hasInterwikiResults() ) {
370 $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) );
371 }
372 // show results
373 if( $numTextMatches > 0 ) {
374 $out->addHTML( $this->showMatches( $textMatches ) );
375 }
376
377 $textMatches->free();
378 }
379 if( $num === 0 ) {
380 $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", array( 'search-nonefound', wfEscapeWikiText( $term ) ) );
381 $this->showCreateLink( $t );
382 }
383 $out->addHtml( "</div>" );
384
385 if( $num || $this->offset ) {
386 $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
387 }
388 wfProfileOut( __METHOD__ );
389 }
390
391 /**
392 * @param $t Title
393 */
394 protected function showCreateLink( $t ) {
395 // show direct page/create link if applicable
396 // Check DBkey !== '' in case of fragment link only.
397 $messageName = null;
398 if( !is_null($t) && $t->getDBkey() !== '' ) {
399 if( $t->isKnown() ) {
400 $messageName = 'searchmenu-exists';
401 } elseif( $t->userCan( 'create' ) ) {
402 $messageName = 'searchmenu-new';
403 } else {
404 $messageName = 'searchmenu-new-nocreate';
405 }
406 }
407 if( $messageName ) {
408 $this->getOutput()->wrapWikiMsg( "<p class=\"mw-search-createlink\">\n$1</p>", array( $messageName, wfEscapeWikiText( $t->getPrefixedText() ) ) );
409 } else {
410 // preserve the paragraph for margins etc...
411 $this->getOutput()->addHtml( '<p></p>' );
412 }
413 }
414
415 /**
416 * @param $term string
417 */
418 protected function setupPage( $term ) {
419 # Should advanced UI be used?
420 $this->searchAdvanced = ($this->profile === 'advanced');
421 $out = $this->getOutput();
422 if( strval( $term ) !== '' ) {
423 $out->setPageTitle( wfMsg( 'searchresults') );
424 $out->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term ) ) );
425 }
426 // add javascript specific to special:search
427 $out->addModules( 'mediawiki.special.search' );
428 }
429
430 /**
431 * Extract "power search" namespace settings from the request object,
432 * returning a list of index numbers to search.
433 *
434 * @param $request WebRequest
435 * @return Array
436 */
437 protected function powerSearch( &$request ) {
438 $arr = array();
439 foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
440 if( $request->getCheck( 'ns' . $ns ) ) {
441 $arr[] = $ns;
442 }
443 }
444
445 return $arr;
446 }
447
448 /**
449 * Reconstruct the 'power search' options for links
450 *
451 * @return Array
452 */
453 protected function powerSearchOptions() {
454 $opt = array();
455 $opt['redirs'] = $this->searchRedirects ? 1 : 0;
456 if( $this->profile !== 'advanced' ) {
457 $opt['profile'] = $this->profile;
458 } else {
459 foreach( $this->namespaces as $n ) {
460 $opt['ns' . $n] = 1;
461 }
462 }
463 return $opt + $this->extraParams;
464 }
465
466 /**
467 * Show whole set of results
468 *
469 * @param $matches SearchResultSet
470 *
471 * @return string
472 */
473 protected function showMatches( &$matches ) {
474 global $wgContLang;
475 wfProfileIn( __METHOD__ );
476
477 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
478
479 $out = "";
480 $infoLine = $matches->getInfo();
481 if( !is_null($infoLine) ) {
482 $out .= "\n<!-- {$infoLine} -->\n";
483 }
484 $out .= "<ul class='mw-search-results'>\n";
485 while( $result = $matches->next() ) {
486 $out .= $this->showHit( $result, $terms );
487 }
488 $out .= "</ul>\n";
489
490 // convert the whole thing to desired language variant
491 $out = $wgContLang->convert( $out );
492 wfProfileOut( __METHOD__ );
493 return $out;
494 }
495
496 /**
497 * Format a single hit result
498 *
499 * @param $result SearchResult
500 * @param $terms Array: terms to highlight
501 *
502 * @return string
503 */
504 protected function showHit( $result, $terms ) {
505 wfProfileIn( __METHOD__ );
506
507 if( $result->isBrokenTitle() ) {
508 wfProfileOut( __METHOD__ );
509 return "<!-- Broken link in search result -->\n";
510 }
511
512 $t = $result->getTitle();
513
514 $titleSnippet = $result->getTitleSnippet($terms);
515
516 if( $titleSnippet == '' )
517 $titleSnippet = null;
518
519 $link_t = clone $t;
520
521 wfRunHooks( 'ShowSearchHitTitle',
522 array( &$link_t, &$titleSnippet, $result, $terms, $this ) );
523
524 $link = Linker::linkKnown(
525 $link_t,
526 $titleSnippet
527 );
528
529 //If page content is not readable, just return the title.
530 //This is not quite safe, but better than showing excerpts from non-readable pages
531 //Note that hiding the entry entirely would screw up paging.
532 if( !$t->userCanRead() ) {
533 wfProfileOut( __METHOD__ );
534 return "<li>{$link}</li>\n";
535 }
536
537 // If the page doesn't *exist*... our search index is out of date.
538 // The least confusing at this point is to drop the result.
539 // You may get less results, but... oh well. :P
540 if( $result->isMissingRevision() ) {
541 wfProfileOut( __METHOD__ );
542 return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
543 }
544
545 // format redirects / relevant sections
546 $redirectTitle = $result->getRedirectTitle();
547 $redirectText = $result->getRedirectSnippet($terms);
548 $sectionTitle = $result->getSectionTitle();
549 $sectionText = $result->getSectionSnippet($terms);
550 $redirect = '';
551
552 if( !is_null($redirectTitle) ) {
553 if( $redirectText == '' )
554 $redirectText = null;
555
556 $redirect = "<span class='searchalttitle'>" .
557 wfMsg(
558 'search-redirect',
559 Linker::linkKnown(
560 $redirectTitle,
561 $redirectText
562 )
563 ) .
564 "</span>";
565 }
566
567 $section = '';
568
569 if( !is_null($sectionTitle) ) {
570 if( $sectionText == '' )
571 $sectionText = null;
572
573 $section = "<span class='searchalttitle'>" .
574 wfMsg(
575 'search-section', Linker::linkKnown(
576 $sectionTitle,
577 $sectionText
578 )
579 ) .
580 "</span>";
581 }
582
583 // format text extract
584 $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
585
586 $lang = $this->getLang();
587
588 // format score
589 if( is_null( $result->getScore() ) ) {
590 // Search engine doesn't report scoring info
591 $score = '';
592 } else {
593 $percent = sprintf( '%2.1f', $result->getScore() * 100 );
594 $score = wfMsg( 'search-result-score', $lang->formatNum( $percent ) )
595 . ' - ';
596 }
597
598 // format description
599 $byteSize = $result->getByteSize();
600 $wordCount = $result->getWordCount();
601 $timestamp = $result->getTimestamp();
602 $size = wfMsgExt(
603 'search-result-size',
604 array( 'parsemag', 'escape' ),
605 $lang->formatSize( $byteSize ),
606 $lang->formatNum( $wordCount )
607 );
608
609 if( $t->getNamespace() == NS_CATEGORY ) {
610 $cat = Category::newFromTitle( $t );
611 $size = wfMsgExt(
612 'search-result-category-size',
613 array( 'parsemag', 'escape' ),
614 $lang->formatNum( $cat->getPageCount() ),
615 $lang->formatNum( $cat->getSubcatCount() ),
616 $lang->formatNum( $cat->getFileCount() )
617 );
618 }
619
620 $date = $lang->timeanddate( $timestamp );
621
622 // link to related articles if supported
623 $related = '';
624 if( $result->hasRelated() ) {
625 $st = SpecialPage::getTitleFor( 'Search' );
626 $stParams = array_merge(
627 $this->powerSearchOptions(),
628 array(
629 'search' => wfMsgForContent( 'searchrelated' ) . ':' . $t->getPrefixedText(),
630 'fulltext' => wfMsg( 'search' )
631 )
632 );
633
634 $related = ' -- ' . Linker::linkKnown(
635 $st,
636 wfMsg('search-relatedarticle'),
637 array(),
638 $stParams
639 );
640 }
641
642 // Include a thumbnail for media files...
643 if( $t->getNamespace() == NS_FILE ) {
644 $img = wfFindFile( $t );
645 if( $img ) {
646 $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
647 if( $thumb ) {
648 $desc = wfMsg( 'parentheses', $img->getShortDesc() );
649 wfProfileOut( __METHOD__ );
650 // Float doesn't seem to interact well with the bullets.
651 // Table messes up vertical alignment of the bullets.
652 // Bullets are therefore disabled (didn't look great anyway).
653 return "<li>" .
654 '<table class="searchResultImage">' .
655 '<tr>' .
656 '<td width="120" align="center" valign="top">' .
657 $thumb->toHtml( array( 'desc-link' => true ) ) .
658 '</td>' .
659 '<td valign="top">' .
660 $link .
661 $extract .
662 "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
663 '</td>' .
664 '</tr>' .
665 '</table>' .
666 "</li>\n";
667 }
668 }
669 }
670
671 wfProfileOut( __METHOD__ );
672 return "<li><div class='mw-search-result-heading'>{$link} {$redirect} {$section}</div> {$extract}\n" .
673 "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
674 "</li>\n";
675
676 }
677
678 /**
679 * Show results from other wikis
680 *
681 * @param $matches SearchResultSet
682 * @param $query String
683 *
684 * @return string
685 */
686 protected function showInterwiki( &$matches, $query ) {
687 global $wgContLang;
688 wfProfileIn( __METHOD__ );
689 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
690
691 $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".
692 wfMsg('search-interwiki-caption')."</div>\n";
693 $out .= "<ul class='mw-search-iwresults'>\n";
694
695 // work out custom project captions
696 $customCaptions = array();
697 $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
698 foreach($customLines as $line) {
699 $parts = explode(":",$line,2);
700 if(count($parts) == 2) // validate line
701 $customCaptions[$parts[0]] = $parts[1];
702 }
703
704 $prev = null;
705 while( $result = $matches->next() ) {
706 $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
707 $prev = $result->getInterwikiPrefix();
708 }
709 // TODO: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
710 $out .= "</ul></div>\n";
711
712 // convert the whole thing to desired language variant
713 $out = $wgContLang->convert( $out );
714 wfProfileOut( __METHOD__ );
715 return $out;
716 }
717
718 /**
719 * Show single interwiki link
720 *
721 * @param $result SearchResult
722 * @param $lastInterwiki String
723 * @param $terms Array
724 * @param $query String
725 * @param $customCaptions Array: iw prefix -> caption
726 *
727 * @return string
728 */
729 protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
730 wfProfileIn( __METHOD__ );
731
732 if( $result->isBrokenTitle() ) {
733 wfProfileOut( __METHOD__ );
734 return "<!-- Broken link in search result -->\n";
735 }
736
737 $t = $result->getTitle();
738
739 $titleSnippet = $result->getTitleSnippet($terms);
740
741 if( $titleSnippet == '' )
742 $titleSnippet = null;
743
744 $link = Linker::linkKnown(
745 $t,
746 $titleSnippet
747 );
748
749 // format redirect if any
750 $redirectTitle = $result->getRedirectTitle();
751 $redirectText = $result->getRedirectSnippet($terms);
752 $redirect = '';
753 if( !is_null($redirectTitle) ) {
754 if( $redirectText == '' )
755 $redirectText = null;
756
757 $redirect = "<span class='searchalttitle'>" .
758 wfMsg(
759 'search-redirect',
760 Linker::linkKnown(
761 $redirectTitle,
762 $redirectText
763 )
764 ) .
765 "</span>";
766 }
767
768 $out = "";
769 // display project name
770 if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()) {
771 if( key_exists($t->getInterwiki(),$customCaptions) ) {
772 // captions from 'search-interwiki-custom'
773 $caption = $customCaptions[$t->getInterwiki()];
774 } else {
775 // default is to show the hostname of the other wiki which might suck
776 // if there are many wikis on one hostname
777 $parsed = parse_url($t->getFullURL());
778 $caption = wfMsg('search-interwiki-default', $parsed['host']);
779 }
780 // "more results" link (special page stuff could be localized, but we might not know target lang)
781 $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
782 $searchLink = Linker::linkKnown(
783 $searchTitle,
784 wfMsg('search-interwiki-more'),
785 array(),
786 array(
787 'search' => $query,
788 'fulltext' => 'Search'
789 )
790 );
791 $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>
792 {$searchLink}</span>{$caption}</div>\n<ul>";
793 }
794
795 $out .= "<li>{$link} {$redirect}</li>\n";
796 wfProfileOut( __METHOD__ );
797 return $out;
798 }
799
800 /**
801 * @param $profile
802 * @param $term
803 * @return String
804 */
805 protected function getProfileForm( $profile, $term ) {
806 // Hidden stuff
807 $opts = array();
808 $opts['redirs'] = $this->searchRedirects;
809 $opts['profile'] = $this->profile;
810
811 if ( $profile === 'advanced' ) {
812 return $this->powerSearchBox( $term, $opts );
813 } else {
814 $form = '';
815 wfRunHooks( 'SpecialSearchProfileForm', array( $this, &$form, $profile, $term, $opts ) );
816 return $form;
817 }
818 }
819
820 /**
821 * Generates the power search box at [[Special:Search]]
822 *
823 * @param $term String: search term
824 * @param $opts array
825 * @return String: HTML form
826 */
827 protected function powerSearchBox( $term, $opts ) {
828 // Groups namespaces into rows according to subject
829 $rows = array();
830 foreach( SearchEngine::searchableNamespaces() as $namespace => $name ) {
831 $subject = MWNamespace::getSubject( $namespace );
832 if( !array_key_exists( $subject, $rows ) ) {
833 $rows[$subject] = "";
834 }
835 $name = str_replace( '_', ' ', $name );
836 if( $name == '' ) {
837 $name = wfMsg( 'blanknamespace' );
838 }
839 $rows[$subject] .=
840 Xml::openElement(
841 'td', array( 'style' => 'white-space: nowrap' )
842 ) .
843 Xml::checkLabel(
844 $name,
845 "ns{$namespace}",
846 "mw-search-ns{$namespace}",
847 in_array( $namespace, $this->namespaces )
848 ) .
849 Xml::closeElement( 'td' );
850 }
851 $rows = array_values( $rows );
852 $numRows = count( $rows );
853
854 // Lays out namespaces in multiple floating two-column tables so they'll
855 // be arranged nicely while still accommodating different screen widths
856 $namespaceTables = '';
857 for( $i = 0; $i < $numRows; $i += 4 ) {
858 $namespaceTables .= Xml::openElement(
859 'table',
860 array( 'cellpadding' => 0, 'cellspacing' => 0, 'border' => 0 )
861 );
862 for( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
863 $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
864 }
865 $namespaceTables .= Xml::closeElement( 'table' );
866 }
867 // Show redirects check only if backend supports it
868 $redirects = '';
869 if( $this->getSearchEngine()->supports( 'list-redirects' ) ) {
870 $redirects =
871 Xml::checkLabel( wfMsg( 'powersearch-redir' ), 'redirs', 'redirs', $this->searchRedirects );
872 }
873
874 $hidden = '';
875 unset( $opts['redirs'] );
876 foreach( $opts as $key => $value ) {
877 $hidden .= Html::hidden( $key, $value );
878 }
879 // Return final output
880 return
881 Xml::openElement(
882 'fieldset',
883 array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' )
884 ) .
885 Xml::element( 'legend', null, wfMsg('powersearch-legend') ) .
886 Xml::tags( 'h4', null, wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) ) .
887 Xml::tags(
888 'div',
889 array( 'id' => 'mw-search-togglebox' ),
890 Xml::label( wfMsg( 'powersearch-togglelabel' ), 'mw-search-togglelabel' ) .
891 Xml::element(
892 'input',
893 array(
894 'type'=>'button',
895 'id' => 'mw-search-toggleall',
896 'value' => wfMsg( 'powersearch-toggleall' )
897 )
898 ) .
899 Xml::element(
900 'input',
901 array(
902 'type'=>'button',
903 'id' => 'mw-search-togglenone',
904 'value' => wfMsg( 'powersearch-togglenone' )
905 )
906 )
907 ) .
908 Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
909 $namespaceTables .
910 Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
911 $redirects . $hidden .
912 Xml::closeElement( 'fieldset' );
913 }
914
915 /**
916 * @return array
917 */
918 protected function getSearchProfiles() {
919 // Builds list of Search Types (profiles)
920 $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
921
922 $profiles = array(
923 'default' => array(
924 'message' => 'searchprofile-articles',
925 'tooltip' => 'searchprofile-articles-tooltip',
926 'namespaces' => SearchEngine::defaultNamespaces(),
927 'namespace-messages' => SearchEngine::namespacesAsText(
928 SearchEngine::defaultNamespaces()
929 ),
930 ),
931 'images' => array(
932 'message' => 'searchprofile-images',
933 'tooltip' => 'searchprofile-images-tooltip',
934 'namespaces' => array( NS_FILE ),
935 ),
936 'help' => array(
937 'message' => 'searchprofile-project',
938 'tooltip' => 'searchprofile-project-tooltip',
939 'namespaces' => SearchEngine::helpNamespaces(),
940 'namespace-messages' => SearchEngine::namespacesAsText(
941 SearchEngine::helpNamespaces()
942 ),
943 ),
944 'all' => array(
945 'message' => 'searchprofile-everything',
946 'tooltip' => 'searchprofile-everything-tooltip',
947 'namespaces' => $nsAllSet,
948 ),
949 'advanced' => array(
950 'message' => 'searchprofile-advanced',
951 'tooltip' => 'searchprofile-advanced-tooltip',
952 'namespaces' => self::NAMESPACES_CURRENT,
953 )
954 );
955
956 wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) );
957
958 foreach( $profiles as &$data ) {
959 if ( !is_array( $data['namespaces'] ) ) continue;
960 sort( $data['namespaces'] );
961 }
962
963 return $profiles;
964 }
965
966 /**
967 * @param $term
968 * @param $resultsShown
969 * @param $totalNum
970 * @return string
971 */
972 protected function formHeader( $term, $resultsShown, $totalNum ) {
973 $out = Xml::openElement('div', array( 'class' => 'mw-search-formheader' ) );
974
975 $bareterm = $term;
976 if( $this->startsWithImage( $term ) ) {
977 // Deletes prefixes
978 $bareterm = substr( $term, strpos( $term, ':' ) + 1 );
979 }
980
981 $profiles = $this->getSearchProfiles();
982 $lang = $this->getLang();
983
984 // Outputs XML for Search Types
985 $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) );
986 $out .= Xml::openElement( 'ul' );
987 foreach ( $profiles as $id => $profile ) {
988 if ( !isset( $profile['parameters'] ) ) {
989 $profile['parameters'] = array();
990 }
991 $profile['parameters']['profile'] = $id;
992
993 $tooltipParam = isset( $profile['namespace-messages'] ) ?
994 $lang->commaList( $profile['namespace-messages'] ) : null;
995 $out .= Xml::tags(
996 'li',
997 array(
998 'class' => $this->profile === $id ? 'current' : 'normal'
999 ),
1000 $this->makeSearchLink(
1001 $bareterm,
1002 array(),
1003 wfMsg( $profile['message'] ),
1004 wfMsg( $profile['tooltip'], $tooltipParam ),
1005 $profile['parameters']
1006 )
1007 );
1008 }
1009 $out .= Xml::closeElement( 'ul' );
1010 $out .= Xml::closeElement('div') ;
1011
1012 // Results-info
1013 if ( $resultsShown > 0 ) {
1014 if ( $totalNum > 0 ){
1015 $top = wfMsgExt( 'showingresultsheader', array( 'parseinline' ),
1016 $lang->formatNum( $this->offset + 1 ),
1017 $lang->formatNum( $this->offset + $resultsShown ),
1018 $lang->formatNum( $totalNum ),
1019 wfEscapeWikiText( $term ),
1020 $lang->formatNum( $resultsShown )
1021 );
1022 } elseif ( $resultsShown >= $this->limit ) {
1023 $top = wfShowingResults( $this->offset, $this->limit );
1024 } else {
1025 $top = wfMsgExt( 'showingresultsnum', array( 'parseinline' ),
1026 $lang->formatNum( $this->limit ),
1027 $lang->formatNum( $this->offset + 1 ),
1028 $lang->formatNum( $resultsShown )
1029 );
1030 }
1031 $out .= Xml::tags( 'div', array( 'class' => 'results-info' ),
1032 Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) )
1033 );
1034 }
1035
1036 $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
1037 $out .= Xml::closeElement('div');
1038
1039 return $out;
1040 }
1041
1042 /**
1043 * @param $term string
1044 * @return string
1045 */
1046 protected function shortDialog( $term ) {
1047 $out = Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1048 // Term box
1049 $out .= Html::input( 'search', $term, 'search', array(
1050 'id' => $this->profile === 'advanced' ? 'powerSearchText' : 'searchText',
1051 'size' => '50',
1052 'autofocus'
1053 ) ) . "\n";
1054 $out .= Html::hidden( 'fulltext', 'Search' ) . "\n";
1055 $out .= Xml::submitButton( wfMsg( 'searchbutton' ) ) . "\n";
1056 return $out . $this->didYouMeanHtml;
1057 }
1058
1059 /**
1060 * Make a search link with some target namespaces
1061 *
1062 * @param $term String
1063 * @param $namespaces Array ignored
1064 * @param $label String: link's text
1065 * @param $tooltip String: link's tooltip
1066 * @param $params Array: query string parameters
1067 * @return String: HTML fragment
1068 */
1069 protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = array() ) {
1070 $opt = $params;
1071 foreach( $namespaces as $n ) {
1072 $opt['ns' . $n] = 1;
1073 }
1074 $opt['redirs'] = $this->searchRedirects;
1075
1076 $stParams = array_merge(
1077 array(
1078 'search' => $term,
1079 'fulltext' => wfMsg( 'search' )
1080 ),
1081 $opt
1082 );
1083
1084 return Xml::element(
1085 'a',
1086 array(
1087 'href' => $this->getTitle()->getLocalURL( $stParams ),
1088 'title' => $tooltip),
1089 $label
1090 );
1091 }
1092
1093 /**
1094 * Check if query starts with image: prefix
1095 *
1096 * @param $term String: the string to check
1097 * @return Boolean
1098 */
1099 protected function startsWithImage( $term ) {
1100 global $wgContLang;
1101
1102 $p = explode( ':', $term );
1103 if( count( $p ) > 1 ) {
1104 return $wgContLang->getNsIndex( $p[0] ) == NS_FILE;
1105 }
1106 return false;
1107 }
1108
1109 /**
1110 * Check if query starts with all: prefix
1111 *
1112 * @param $term String: the string to check
1113 * @return Boolean
1114 */
1115 protected function startsWithAll( $term ) {
1116
1117 $allkeyword = wfMsgForContent('searchall');
1118
1119 $p = explode( ':', $term );
1120 if( count( $p ) > 1 ) {
1121 return $p[0] == $allkeyword;
1122 }
1123 return false;
1124 }
1125
1126 /**
1127 * @since 1.18
1128 *
1129 * @return SearchEngine
1130 */
1131 public function getSearchEngine() {
1132 if ( $this->searchEngine === null ) {
1133 $this->searchEngine = SearchEngine::create();
1134 }
1135 return $this->searchEngine;
1136 }
1137
1138 /**
1139 * Users of hook SpecialSearchSetupEngine can use this to
1140 * add more params to links to not lose selection when
1141 * user navigates search results.
1142 * @since 1.18
1143 *
1144 * @param $key
1145 * @param $value
1146 */
1147 public function setExtraParam( $key, $value ) {
1148 $this->extraParams[$key] = $value;
1149 }
1150
1151 }