123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- <?php
- namespace MediaWiki\Widget\Search;
- use Category;
- use Hooks;
- use HtmlArmor;
- use MediaWiki\Linker\LinkRenderer;
- use MediaWiki\MediaWikiServices;
- use SearchResult;
- use SpecialSearch;
- use Title;
- /**
- * Renders a 'full' multi-line search result with metadata.
- *
- * The Title
- * some *highlighted* *text* about the search result
- * 5KB (651 words) - 12:40, 6 Aug 2016
- */
- class FullSearchResultWidget implements SearchResultWidget {
- /** @var SpecialSearch */
- protected $specialPage;
- /** @var LinkRenderer */
- protected $linkRenderer;
- public function __construct( SpecialSearch $specialPage, LinkRenderer $linkRenderer ) {
- $this->specialPage = $specialPage;
- $this->linkRenderer = $linkRenderer;
- }
- /**
- * @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 "<li>{$link}</li>";
- }
- $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 = "<div class='searchresult'>$snippet</div>";
- } 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 ( !Hooks::run( 'ShowSearchHit', [
- $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 =
- "<div class='mw-search-result-heading'>{$joined}</div>" .
- "{$extract} {$meta}";
- } else {
- $html =
- "<table class='searchResultImage'>" .
- "<tr>" .
- "<td style='width: 120px; text-align: center; vertical-align: top'>" .
- $thumb .
- "</td>" .
- "<td style='vertical-align: top'>" .
- "{$joined} {$extract} {$meta}" .
- "</td>" .
- "</tr>" .
- "</table>";
- }
- return "<li class='mw-search-result'>{$html}</li>";
- }
- /**
- * 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 ];
- Hooks::run( 'ShowSearchHitTitle',
- [ &$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 <a>Foo</a>).
- *
- * @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 = null, $text ) {
- $inner = $title === null
- ? $text
- : $this->linkRenderer->makeLink( $title, $text ? new HtmlArmor( $text ) : null );
- return "<span class='searchalttitle'>" .
- $this->specialPage->msg( $msgKey )->rawParams( $inner )->parse()
- . "</span>";
- }
- /**
- * @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 = "<span class='searchalttitle'>" .
- $this->specialPage->msg( 'search-file-match' )->escaped() .
- "</span>";
- } 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 <div>.
- * 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 "<div class='mw-search-result-data'>{$meta}</div>";
- }
- }
|