specialPage = $specialPage; $this->linkRenderer = $linkRenderer; $this->hookRunner = new HookRunner( $hookContainer ); } /** * @param SearchResult $result The result to render * @param int $position The result position, including offset * @return string HTML */ public function render( SearchResult $result, $position ) { // If the page doesn't *exist*... our search index is out of date. // The least confusing at this point is to drop the result. // You may get less results, but... on well. :P if ( $result->isBrokenTitle() || $result->isMissingRevision() ) { return ''; } $link = $this->generateMainLinkHtml( $result, $position ); // If page content is not readable, just return ths title. // This is not quite safe, but better than showing excerpts from // non-readable pages. Note that hiding the entry entirely would // screw up paging (really?). $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); if ( !$permissionManager->userCan( 'read', $this->specialPage->getUser(), $result->getTitle() ) ) { return "
  • {$link}
  • "; } $redirect = $this->generateRedirectHtml( $result ); $section = $this->generateSectionHtml( $result ); $category = $this->generateCategoryHtml( $result ); $date = $this->specialPage->getLanguage()->userTimeAndDate( $result->getTimestamp(), $this->specialPage->getUser() ); list( $file, $desc, $thumb ) = $this->generateFileHtml( $result ); $snippet = $result->getTextSnippet(); if ( $snippet ) { $extract = "
    $snippet
    "; } else { $extract = ''; } if ( $thumb === null ) { // If no thumb, then the description is about size $desc = $this->generateSizeHtml( $result ); // Let hooks do their own final construction if desired. // FIXME: Not sure why this is only for results without thumbnails, // but keeping it as-is for now to prevent breaking hook consumers. $html = null; $score = ''; $related = ''; // TODO: remove this instanceof and always pass [], let implementors do the cast if // they want to be SearchDatabase specific $terms = $result instanceof \SqlSearchResult ? $result->getTermMatches() : []; if ( !$this->hookRunner->onShowSearchHit( $this->specialPage, $result, $terms, $link, $redirect, $section, $extract, $score, $desc, $date, $related, $html ) ) { return $html; } } // All the pieces have been collected. Now generate the final HTML $joined = "{$link} {$redirect} {$category} {$section} {$file}"; $meta = $this->buildMeta( $desc, $date ); if ( $thumb === null ) { $html = "
    {$joined}
    " . "{$extract} {$meta}"; } else { $html = "" . "" . "" . "" . "" . "
    " . $thumb . "" . "{$joined} {$extract} {$meta}" . "
    "; } return "
  • {$html}
  • "; } /** * Generates HTML for the primary call to action. It is * typically the article title, but the search engine can * return an exact snippet to use (typically the article * title with highlighted words). * * @param SearchResult $result * @param int $position * @return string HTML */ protected function generateMainLinkHtml( SearchResult $result, $position ) { $snippet = $result->getTitleSnippet(); if ( $snippet === '' ) { $snippet = null; } else { $snippet = new HtmlArmor( $snippet ); } // clone to prevent hook from changing the title stored inside $result $title = clone $result->getTitle(); $query = []; $attributes = [ 'data-serp-pos' => $position ]; $this->hookRunner->onShowSearchHitTitle( $title, $snippet, $result, $result instanceof \SqlSearchResult ? $result->getTermMatches() : [], $this->specialPage, $query, $attributes ); $link = $this->linkRenderer->makeLink( $title, $snippet, $attributes, $query ); return $link; } /** * Generates an alternate title link, such as (redirect from Foo). * * @param string $msgKey i18n message used to wrap title * @param Title|null $title The title to link to, or null to generate * the message without a link. In that case $text must be non-null. * @param string|null $text The text snippet to display, or null * to use the title * @return string HTML */ protected function generateAltTitleHtml( $msgKey, ?Title $title, $text ) { $inner = $title === null ? $text : $this->linkRenderer->makeLink( $title, $text ? new HtmlArmor( $text ) : null ); return "" . $this->specialPage->msg( $msgKey )->rawParams( $inner )->parse() . ""; } /** * @param SearchResult $result * @return string HTML */ protected function generateRedirectHtml( SearchResult $result ) { $title = $result->getRedirectTitle(); return $title === null ? '' : $this->generateAltTitleHtml( 'search-redirect', $title, $result->getRedirectSnippet() ); } /** * @param SearchResult $result * @return string HTML */ protected function generateSectionHtml( SearchResult $result ) { $title = $result->getSectionTitle(); return $title === null ? '' : $this->generateAltTitleHtml( 'search-section', $title, $result->getSectionSnippet() ); } /** * @param SearchResult $result * @return string HTML */ protected function generateCategoryHtml( SearchResult $result ) { $snippet = $result->getCategorySnippet(); return $snippet ? $this->generateAltTitleHtml( 'search-category', null, $snippet ) : ''; } /** * @param SearchResult $result * @return string HTML */ protected function generateSizeHtml( SearchResult $result ) { $title = $result->getTitle(); if ( $title->getNamespace() === NS_CATEGORY ) { $cat = Category::newFromTitle( $title ); return $this->specialPage->msg( 'search-result-category-size' ) ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() ) ->escaped(); // TODO: This is a bit odd...but requires changing the i18n message to fix } elseif ( $result->getByteSize() !== null || $result->getWordCount() > 0 ) { $lang = $this->specialPage->getLanguage(); $bytes = $lang->formatSize( $result->getByteSize() ); $words = $result->getWordCount(); return $this->specialPage->msg( 'search-result-size', $bytes ) ->numParams( $words ) ->escaped(); } return ''; } /** * @param SearchResult $result * @return array Three element array containing the main file html, * a text description of the file, and finally the thumbnail html. * If no thumbnail is available the second and third will be null. */ protected function generateFileHtml( SearchResult $result ) { $title = $result->getTitle(); if ( $title->getNamespace() !== NS_FILE ) { return [ '', null, null ]; } if ( $result->isFileMatch() ) { $html = "" . $this->specialPage->msg( 'search-file-match' )->escaped() . ""; } else { $html = ''; } $descHtml = null; $thumbHtml = null; $img = $result->getFile() ?: MediaWikiServices::getInstance()->getRepoGroup() ->findFile( $title ); if ( $img ) { $thumb = $img->transform( [ 'width' => 120, 'height' => 120 ] ); if ( $thumb ) { $descHtml = $this->specialPage->msg( 'parentheses' ) ->rawParams( $img->getShortDesc() ) ->escaped(); $thumbHtml = $thumb->toHtml( [ 'desc-link' => true ] ); } } return [ $html, $descHtml, $thumbHtml ]; } /** * @param string $desc HTML description of result, ex: size in bytes, or empty string * @param string $date HTML representation of last edit date, or empty string * @return string HTML A div combining $desc and $date with a separator in a
    . * If either is missing only one will be represented. If both are missing an empty * string will be returned. */ protected function buildMeta( $desc, $date ) { if ( $desc && $date ) { $meta = "{$desc} - {$date}"; } elseif ( $desc ) { $meta = $desc; } elseif ( $date ) { $meta = $date; } else { return ''; } return "
    {$meta}
    "; } }