[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/specials/ -> SpecialSearch.php (source)

   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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1