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