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