MediaWiki  master
FullSearchResultWidget.php
Go to the documentation of this file.
1 <?php
2 
4 
5 use Category;
6 use Html;
7 use HtmlArmor;
12 use SearchResult;
13 use SpecialSearch;
14 use Title;
15 
25  protected $specialPage;
27  protected $linkRenderer;
29  private $hookRunner;
30 
32  HookContainer $hookContainer
33  ) {
34  $this->specialPage = $specialPage;
35  $this->linkRenderer = $linkRenderer;
36  $this->hookRunner = new HookRunner( $hookContainer );
37  }
38 
44  public function render( SearchResult $result, $position ) {
45  // If the page doesn't *exist*... our search index is out of date.
46  // The least confusing at this point is to drop the result.
47  // You may get less results, but... on well. :P
48  if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
49  return '';
50  }
51 
52  $link = $this->generateMainLinkHtml( $result, $position );
53  // If page content is not readable, just return ths title.
54  // This is not quite safe, but better than showing excerpts from
55  // non-readable pages. Note that hiding the entry entirely would
56  // screw up paging (really?).
57  if ( !$this->specialPage->getAuthority()->definitelyCan( 'read', $result->getTitle() ) ) {
58  return Html::rawElement( 'li', [], $link );
59  }
60 
61  $redirect = $this->generateRedirectHtml( $result );
62  $section = $this->generateSectionHtml( $result );
63  $category = $this->generateCategoryHtml( $result );
64  $date = htmlspecialchars(
65  $this->specialPage->getLanguage()->userTimeAndDate(
66  $result->getTimestamp(),
67  $this->specialPage->getUser()
68  )
69  );
70  list( $file, $desc, $thumb ) = $this->generateFileHtml( $result );
71  $snippet = $result->getTextSnippet();
72  if ( $snippet ) {
73  $extract = Html::rawElement( 'div', [ 'class' => 'searchresult' ], $snippet );
74  } else {
75  $extract = '';
76  }
77 
78  if ( $thumb === null ) {
79  // If no thumb, then the description is about size
80  $desc = $this->generateSizeHtml( $result );
81 
82  // Let hooks do their own final construction if desired.
83  // FIXME: Not sure why this is only for results without thumbnails,
84  // but keeping it as-is for now to prevent breaking hook consumers.
85  $html = null;
86  $score = '';
87  $related = '';
88  // TODO: remove this instanceof and always pass [], let implementors do the cast if
89  // they want to be SearchDatabase specific
90  $terms = $result instanceof \SqlSearchResult ? $result->getTermMatches() : [];
91  if ( !$this->hookRunner->onShowSearchHit( $this->specialPage, $result,
92  $terms, $link, $redirect, $section, $extract, $score,
93  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
94  $desc, $date, $related, $html )
95  ) {
96  return $html;
97  }
98  }
99 
100  // All the pieces have been collected. Now generate the final HTML
101  $joined = "{$link} {$redirect} {$category} {$section} {$file}";
102  $meta = $this->buildMeta( $desc, $date );
103 
104  if ( $thumb === null ) {
105  $html = Html::rawElement(
106  'div',
107  [ 'class' => 'mw-search-result-heading' ],
108  $joined
109  );
110  $html .= $extract . ' ' . $meta;
111  } else {
112  $tableCells = Html::rawElement(
113  'td',
114  [ 'style' => 'width: 120px; text-align: center; vertical-align: top' ],
115  $thumb
116  ) . Html::rawElement(
117  'td',
118  [ 'style' => 'vertical-align: top' ],
119  "$joined $extract $meta"
120  );
121  $html = Html::rawElement(
122  'table',
123  [ 'class' => 'searchResultImage' ],
125  'tr',
126  [],
127  $tableCells
128  )
129  );
130  }
131 
132  return Html::rawElement( 'li', [ 'class' => 'mw-search-result' ], $html );
133  }
134 
145  protected function generateMainLinkHtml( SearchResult $result, $position ) {
146  $snippet = $result->getTitleSnippet();
147  if ( $snippet === '' ) {
148  $snippet = null;
149  } else {
150  $snippet = new HtmlArmor( $snippet );
151  }
152 
153  // clone to prevent hook from changing the title stored inside $result
154  $title = clone $result->getTitle();
155  $query = [];
156 
157  $attributes = [ 'data-serp-pos' => $position ];
158  $this->hookRunner->onShowSearchHitTitle( $title, $snippet, $result,
159  $result instanceof \SqlSearchResult ? $result->getTermMatches() : [],
160  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
161  $this->specialPage, $query, $attributes );
162 
163  $link = $this->linkRenderer->makeLink(
164  $title,
165  $snippet,
166  $attributes,
167  $query
168  );
169 
170  return $link;
171  }
172 
183  protected function generateAltTitleHtml( $msgKey, ?Title $title, $text ) {
184  $inner = $title === null
185  ? $text
186  : $this->linkRenderer->makeLink( $title, $text ? new HtmlArmor( $text ) : null );
187 
188  return "<span class='searchalttitle'>" .
189  $this->specialPage->msg( $msgKey )->rawParams( $inner )->parse()
190  . "</span>";
191  }
192 
197  protected function generateRedirectHtml( SearchResult $result ) {
198  $title = $result->getRedirectTitle();
199  return $title === null
200  ? ''
201  : $this->generateAltTitleHtml( 'search-redirect', $title, $result->getRedirectSnippet() );
202  }
203 
208  protected function generateSectionHtml( SearchResult $result ) {
209  $title = $result->getSectionTitle();
210  return $title === null
211  ? ''
212  : $this->generateAltTitleHtml( 'search-section', $title, $result->getSectionSnippet() );
213  }
214 
219  protected function generateCategoryHtml( SearchResult $result ) {
220  $snippet = $result->getCategorySnippet();
221  return $snippet
222  ? $this->generateAltTitleHtml( 'search-category', null, $snippet )
223  : '';
224  }
225 
230  protected function generateSizeHtml( SearchResult $result ) {
231  $title = $result->getTitle();
232  if ( $title->getNamespace() === NS_CATEGORY ) {
233  $cat = Category::newFromTitle( $title );
234  return $this->specialPage->msg( 'search-result-category-size' )
235  ->numParams( $cat->getMemberCount(), $cat->getSubcatCount(), $cat->getFileCount() )
236  ->escaped();
237  // TODO: This is a bit odd...but requires changing the i18n message to fix
238  } elseif ( $result->getByteSize() !== null || $result->getWordCount() > 0 ) {
239  return $this->specialPage->msg( 'search-result-size' )
240  ->sizeParams( $result->getByteSize() )
241  ->numParams( $result->getWordCount() )
242  ->escaped();
243  }
244 
245  return '';
246  }
247 
254  protected function generateFileHtml( SearchResult $result ) {
255  $title = $result->getTitle();
256  if ( $title->getNamespace() !== NS_FILE ) {
257  return [ '', null, null ];
258  }
259 
260  if ( $result->isFileMatch() ) {
261  $html = Html::rawElement(
262  'span',
263  [ 'class' => 'searchalttitle' ],
264  $this->specialPage->msg( 'search-file-match' )->escaped()
265  );
266  } else {
267  $html = '';
268  }
269 
270  $descHtml = null;
271  $thumbHtml = null;
272 
273  $img = $result->getFile() ?: MediaWikiServices::getInstance()->getRepoGroup()
274  ->findFile( $title );
275  if ( $img ) {
276  $thumb = $img->transform( [ 'width' => 120, 'height' => 120 ] );
277  if ( $thumb ) {
278  $descHtml = $this->specialPage->msg( 'parentheses' )
279  ->rawParams( $img->getShortDesc() )
280  ->escaped();
281  $thumbHtml = $thumb->toHtml( [ 'desc-link' => true ] );
282  }
283  }
284 
285  return [ $html, $descHtml, $thumbHtml ];
286  }
287 
295  protected function buildMeta( $desc, $date ) {
296  if ( $desc && $date ) {
297  $meta = "{$desc} - {$date}";
298  } elseif ( $desc ) {
299  $meta = $desc;
300  } elseif ( $date ) {
301  $meta = $date;
302  } else {
303  return '';
304  }
305 
306  return "<div class='mw-search-result-data'>{$meta}</div>";
307  }
308 }
const NS_FILE
Definition: Defines.php:70
const NS_CATEGORY
Definition: Defines.php:78
Category objects are immutable, strictly speaking.
Definition: Category.php:33
static newFromTitle(PageIdentity $page)
Factory function.
Definition: Category.php:170
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
This class is a collection of static functions that serve two purposes:
Definition: Html.php:51
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
Class that generates HTML anchor link elements for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
Renders a 'full' multi-line search result with metadata.
generateAltTitleHtml( $msgKey, ?Title $title, $text)
Generates an alternate title link, such as (redirect from Foo).
generateMainLinkHtml(SearchResult $result, $position)
Generates HTML for the primary call to action.
__construct(SpecialSearch $specialPage, LinkRenderer $linkRenderer, HookContainer $hookContainer)
NOTE: this class is being refactored into an abstract base class.
implements Special:Search - Run text & title search and display the output
Represents a title within MediaWiki.
Definition: Title.php:48
Renders a single search result to HTML.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42