Merge "Add attributes parameter to ShowSearchHitTitle"
[lhc/web/wiklou.git] / includes / widget / search / FullSearchResultWidget.php
1 <?php
2
3 namespace MediaWiki\Widget\Search;
4
5 use Category;
6 use Hooks;
7 use HtmlArmor;
8 use MediaWiki\Linker\LinkRenderer;
9 use SearchResult;
10 use SpecialSearch;
11 use Title;
12
13 /**
14 * Renders a 'full' multi-line search result with metadata.
15 *
16 * The Title
17 * some *highlighted* *text* about the search result
18 * 5KB (651 words) - 12:40, 6 Aug 2016
19 */
20 class FullSearchResultWidget implements SearchResultWidget {
21 /** @var SpecialSearch */
22 protected $specialPage;
23 /** @var LinkRenderer */
24 protected $linkRenderer;
25
26 public function __construct( SpecialSearch $specialPage, LinkRenderer $linkRenderer ) {
27 $this->specialPage = $specialPage;
28 $this->linkRenderer = $linkRenderer;
29 }
30
31 /**
32 * @param SearchResult $result The result to render
33 * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet)
34 * @param int $position The result position, including offset
35 * @return string HTML
36 */
37 public function render( SearchResult $result, $terms, $position ) {
38 // If the page doesn't *exist*... our search index is out of date.
39 // The least confusing at this point is to drop the result.
40 // You may get less results, but... on well. :P
41 if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
42 return '';
43 }
44
45 $link = $this->generateMainLinkHtml( $result, $terms, $position );
46 // If page content is not readable, just return ths title.
47 // This is not quite safe, but better than showing excerpts from
48 // non-readable pages. Note that hiding the entry entirely would
49 // screw up paging (really?).
50 if ( !$result->getTitle()->userCan( 'read', $this->specialPage->getUser() ) ) {
51 return "<li>{$link}</li>";
52 }
53
54 $redirect = $this->generateRedirectHtml( $result );
55 $section = $this->generateSectionHtml( $result );
56 $category = $this->generateCategoryHtml( $result );
57 $date = $this->specialPage->getLanguage()->userTimeAndDate(
58 $result->getTimestamp(),
59 $this->specialPage->getUser()
60 );
61 list( $file, $desc, $thumb ) = $this->generateFileHtml( $result );
62 $snippet = $result->getTextSnippet( $terms );
63 if ( $snippet ) {
64 $extract = "<div class='searchresult'>$snippet</div>";
65 } else {
66 $extract = '';
67 }
68
69 if ( $thumb === null ) {
70 // If no thumb, then the description is about size
71 $desc = $this->generateSizeHtml( $result );
72
73 // Let hooks do their own final construction if desired.
74 // FIXME: Not sure why this is only for results without thumbnails,
75 // but keeping it as-is for now to prevent breaking hook consumers.
76 $html = null;
77 $score = '';
78 $related = '';
79 if ( !Hooks::run( 'ShowSearchHit', [
80 $this->specialPage, $result, $terms,
81 &$link, &$redirect, &$section, &$extract,
82 &$score, &$size, &$date, &$related, &$html
83 ] ) ) {
84 return $html;
85 }
86 }
87
88 // All the pieces have been collected. Now generate the final HTML
89 $joined = "{$link} {$redirect} {$category} {$section} {$file}";
90 $meta = $this->buildMeta( $desc, $date );
91
92 if ( $thumb === null ) {
93 $html =
94 "<div class='mw-search-result-heading'>{$joined}</div>" .
95 "{$extract} {$meta}";
96 } else {
97 $html =
98 "<table class='searchResultImage'>" .
99 "<tr>" .
100 "<td style='width: 120px; text-align: center; vertical-align: top'>" .
101 $thumb .
102 "</td>" .
103 "<td style='vertical-align: top'>" .
104 "{$joined} {$extract} {$meta}" .
105 "</td>" .
106 "</tr>" .
107 "</table>";
108 }
109
110 return "<li>{$html}</li>";
111 }
112
113 /**
114 * Generates HTML for the primary call to action. It is
115 * typically the article title, but the search engine can
116 * return an exact snippet to use (typically the article
117 * title with highlighted words).
118 *
119 * @param SearchResult $result
120 * @param string $terms
121 * @param int $position
122 * @return string HTML
123 */
124 protected function generateMainLinkHtml( SearchResult $result, $terms, $position ) {
125 $snippet = $result->getTitleSnippet();
126 if ( $snippet === '' ) {
127 $snippet = null;
128 } else {
129 $snippet = new HtmlArmor( $snippet );
130 }
131
132 // clone to prevent hook from changing the title stored inside $result
133 $title = clone $result->getTitle();
134 $query = [];
135
136 $attributes = [ 'data-serp-pos' => $position ];
137 Hooks::run( 'ShowSearchHitTitle',
138 [ &$title, &$snippet, $result, $terms, $this->specialPage, &$query, &$attributes ] );
139
140 $link = $this->linkRenderer->makeLink(
141 $title,
142 $snippet,
143 $attributes,
144 $query
145 );
146
147 return $link;
148 }
149
150 /**
151 * Generates an alternate title link, such as (redirect from <a>Foo</a>).
152 *
153 * @param string $msgKey i18n message used to wrap title
154 * @param Title|null $title The title to link to, or null to generate
155 * the message without a link. In that case $text must be non-null.
156 * @param string|null $text The text snippet to display, or null
157 * to use the title
158 * @return string HTML
159 */
160 protected function generateAltTitleHtml( $msgKey, Title $title = null, $text ) {
161 $inner = $title === null
162 ? $text
163 : $this->linkRenderer->makeLink( $title, $text ? new HtmlArmor( $text ) : null );
164
165 return "<span class='searchalttitle'>" .
166 $this->specialPage->msg( $msgKey )->rawParams( $inner )->text()
167 . "</span>";
168 }
169
170 /**
171 * @param SearchResult $result
172 * @return string HTML
173 */
174 protected function generateRedirectHtml( SearchResult $result ) {
175 $title = $result->getRedirectTitle();
176 return $title === null
177 ? ''
178 : $this->generateAltTitleHtml( 'search-redirect', $title, $result->getRedirectSnippet() );
179 }
180
181 /**
182 * @param SearchResult $result
183 * @return string HTML
184 */
185 protected function generateSectionHtml( SearchResult $result ) {
186 $title = $result->getSectionTitle();
187 return $title === null
188 ? ''
189 : $this->generateAltTitleHtml( 'search-section', $title, $result->getSectionSnippet() );
190 }
191
192 /**
193 * @param SearchResult $result
194 * @return string HTML
195 */
196 protected function generateCategoryHtml( SearchResult $result ) {
197 $snippet = $result->getCategorySnippet();
198 return $snippet
199 ? $this->generateAltTitleHtml( 'search-category', null, $snippet )
200 : '';
201 }
202
203 /**
204 * @param SearchResult $result
205 * @return string HTML
206 */
207 protected function generateSizeHtml( SearchResult $result ) {
208 $title = $result->getTitle();
209 if ( $title->getNamespace() === NS_CATEGORY ) {
210 $cat = Category::newFromTitle( $title );
211 return $this->specialPage->msg( 'search-result-category-size' )
212 ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() )
213 ->escaped();
214 // TODO: This is a bit odd...but requires changing the i18n message to fix
215 } elseif ( $result->getByteSize() !== null || $result->getWordCount() > 0 ) {
216 $lang = $this->specialPage->getLanguage();
217 $bytes = $lang->formatSize( $result->getByteSize() );
218 $words = $result->getWordCount();
219
220 return $this->specialPage->msg( 'search-result-size', $bytes )
221 ->numParams( $words )
222 ->escaped();
223 }
224
225 return '';
226 }
227
228 /**
229 * @param SearchResult $result
230 * @return array Three element array containing the main file html,
231 * a text description of the file, and finally the thumbnail html.
232 * If no thumbnail is available the second and third will be null.
233 */
234 protected function generateFileHtml( SearchResult $result ) {
235 $title = $result->getTitle();
236 if ( $title->getNamespace() !== NS_FILE ) {
237 return [ '', null, null ];
238 }
239
240 if ( $result->isFileMatch() ) {
241 $html = "<span class='searchalttitle'>" .
242 $this->specialPage->msg( 'search-file-match' )->escaped() .
243 "</span>";
244 } else {
245 $html = '';
246 }
247
248 $descHtml = null;
249 $thumbHtml = null;
250
251 $img = $result->getFile() ?: wfFindFile( $title );
252 if ( $img ) {
253 $thumb = $img->transform( [ 'width' => 120, 'height' => 120 ] );
254 if ( $thumb ) {
255 $descHtml = $this->specialPage->msg( 'parentheses' )
256 ->rawParams( $img->getShortDesc() )
257 ->escaped();
258 $thumbHtml = $thumb->toHtml( [ 'desc-link' => true ] );
259 }
260 }
261
262 return [ $html, $descHtml, $thumbHtml ];
263 }
264
265 /**
266 * @param string $desc HTML description of result, ex: size in bytes, or empty string
267 * @param string $date HTML representation of last edit date, or empty string
268 * @return string HTML A div combining $desc and $date with a separator in a <div>.
269 * If either is missing only one will be represented. If both are missing an empty
270 * string will be returned.
271 */
272 protected function buildMeta( $desc, $date ) {
273 if ( $desc && $date ) {
274 $meta = "{$desc} - {$date}";
275 } elseif ( $desc ) {
276 $meta = $desc;
277 } elseif ( $date ) {
278 $meta = $date;
279 } else {
280 return '';
281 }
282
283 return "<div class='mw-search-result-data'>{$meta}</div>";
284 }
285 }