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}
";
}
}