MediaWiki master
ChangeTagsFormatter.php
Go to the documentation of this file.
1<?php
8
21use OOUI\ComboBoxInputWidget;
23
31
33 public const array CONSTRUCTOR_OPTIONS = [
35 ];
36
41 private const int TAG_DESC_CHARACTER_LIMIT = 120;
42
43 public function __construct(
44 private readonly ServiceOptions $options,
45 private readonly ChangeTagsStore $changeTagsStore,
46 private readonly WANObjectCache $cache,
47 private readonly LanguageFactory $languageFactory,
48 ) {
49 $this->options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
50 }
51
66 public function formatTagsAsSummaryList(
67 ?string $tags,
68 MessageLocalizer $localizer,
69 Authority $authority
70 ): array {
71 if ( $tags === '' || $tags === null ) {
72 return [ '', [] ];
73 }
74
75 $classes = [];
76
77 $tags = explode( ',', $tags );
78 $tags = $this->changeTagsStore->filterViewableTags( $tags, $authority );
79 $order = array_flip( $this->changeTagsStore->listDefinedTags() );
80 usort( $tags, static function ( $a, $b ) use ( $order ) {
81 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
82 } );
83
84 $displayTags = [];
85 foreach ( $tags as $tag ) {
86 if ( $tag === '' ) {
87 continue;
88 }
89 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
90 $description = $this->getTagDescription( $tag, $localizer );
91 if ( $description === '' ) {
92 continue;
93 }
94 $displayTags[] = Html::rawElement(
95 'span',
96 [ 'class' => 'mw-tag-marker ' . Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
97 $description
98 );
99 }
100
101 if ( !$displayTags ) {
102 return [ '', $classes ];
103 }
104
105 $markers = $localizer->msg( 'tag-list-wrapper' )
106 ->numParams( count( $displayTags ) )
107 ->rawParams( implode( ' ', $displayTags ) )
108 ->parse();
109 $markers = Html::rawElement( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
110
111 return [ $markers, $classes ];
112 }
113
120 public function getTagDescription( string $tag, MessageLocalizer $localizer ): string {
121 $msg = $this->tagShortDescriptionMessage( $tag, $localizer );
122 $link = $this->tagHelpLink( $tag, $localizer );
123 if ( !$msg->isDisabled() && $link ) {
124 $label = $msg->parse();
125 // Avoid invalid HTML caused by link wrapping if the label already contains a link
126 if ( !str_contains( $label, '<a ' ) ) {
127 return Html::rawElement( 'a', [ 'href' => $link ], $label );
128 }
129 }
130 return !$msg->isDisabled() ? $msg->parse() : '';
131 }
132
136 private function tagShortDescriptionMessage( string $tag, MessageLocalizer $messageLocalizer ): Message {
137 $msg = $messageLocalizer->msg( "tag-$tag" );
138 if ( !$msg->exists() ) {
139 // No such message
140 // Pass through ->msg(), even though it seems redundant, to avoid requesting
141 // the user's language from session-less entry points (T227233)
142 return $messageLocalizer->msg( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) );
143 }
144
145 return $msg;
146 }
147
151 private function tagHelpLink( string $tag, MessageLocalizer $context ): ?string {
152 $msg = $context->msg( "tag-$tag-helppage" )->inContentLanguage();
153 if ( !$msg->isDisabled() ) {
154 return Skin::makeInternalOrExternalUrl( $msg->text() ) ?: null;
155 }
156 return null;
157 }
158
177 public function buildTagFilter(
178 string $selected,
179 string $format,
180 IContextSource $context,
181 bool $activeOnly = true,
182 bool $useAllTags = true
183 ): ?array {
184 if (
185 !$this->options->get( MainConfigNames::UseTagFilter ) ||
186 !count( $this->changeTagsStore->listDefinedTags() )
187 ) {
188 return null;
189 }
190
191 $tags = $this->getChangeTagList(
192 $context,
193 $context->getAuthority(),
194 $activeOnly,
195 $useAllTags,
196 true
197 );
198
199 $autocomplete = [];
200 foreach ( $tags as $tagInfo ) {
201 $autocomplete[ $tagInfo['label'] ] = $tagInfo['name'];
202 }
203
204 $data = [];
205 $data[0] = Html::rawElement(
206 'label',
207 [ 'for' => 'tagfilter' ],
208 $context->msg( 'tag-filter' )->parse()
209 );
210
211 if ( $format === 'ooui' ) {
212 $options = Html::listDropdownOptionsOoui( $autocomplete );
213
214 $data[1] = new ComboBoxInputWidget( [
215 'id' => 'tagfilter',
216 'name' => 'tagfilter',
217 'value' => $selected,
218 'classes' => 'mw-tagfilter-input',
219 'options' => $options,
220 ] );
221 } else {
222 $optionsHtml = '';
223 foreach ( $autocomplete as $label => $name ) {
224 $optionsHtml .= Html::element( 'option', [ 'value' => $name ], $label );
225 }
226 $datalistHtml = Html::rawElement( 'datalist', [ 'id' => 'tagfilter-datalist' ], $optionsHtml );
227
228 $data[1] = Html::input(
229 'tagfilter',
230 $selected,
231 'text',
232 [
233 'class' => [ 'mw-tagfilter-input', 'cdx-text-input__input' => $format === 'codex' ],
234 'size' => 20,
235 'id' => 'tagfilter',
236 'list' => 'tagfilter-datalist',
237 ]
238 );
239 if ( $format === 'codex' ) {
240 $data[1] = Html::rawElement( 'div', [ 'class' => 'cdx-text-input' ], $data[1] );
241 }
242 $data[1] .= $datalistHtml;
243 }
244
245 return $data;
246 }
247
278 public function getChangeTagListSummary(
279 LocalizationContext $localizationContext,
280 Authority $authority,
281 bool $activeOnly = true,
282 bool $useAllTags = true
283 ): array {
284 if ( $useAllTags ) {
285 $tagKeys = $this->changeTagsStore->listDefinedTags();
286 $cacheKey = 'tags-list-summary';
287 } else {
288 $tagKeys = $this->changeTagsStore->getCoreDefinedTags();
289 $cacheKey = 'core-software-tags-summary';
290 }
291
292 // if $tagHitCounts exists, check against it later to determine whether or not to omit tags
293 $tagHitCounts = null;
294 if ( $activeOnly ) {
295 $tagHitCounts = $this->changeTagsStore->tagUsageStatistics();
296 } else {
297 // The full set of tags should use a different cache key than the subset
298 $cacheKey .= '-all';
299 }
300
301 $summary = $this->cache->getWithSetCallback(
302 $this->cache->makeKey( $cacheKey, strtolower( $localizationContext->getLanguageCode()->toBcp47Code() ) ),
303 WANObjectCache::TTL_DAY,
304 function () use ( $localizationContext, $tagKeys, $tagHitCounts ) {
305 $result = [];
306 foreach ( $tagKeys as $tagName ) {
307 // Only list tags that are still actively defined
308 if ( $tagHitCounts !== null ) {
309 // Only list tags with more than 0 hits
310 $hits = $tagHitCounts[$tagName] ?? 0;
311 if ( $hits <= 0 ) {
312 continue;
313 }
314 }
315
316 $labelMsg = $this->tagShortDescriptionMessage( $tagName, $localizationContext );
317 $helpLink = $this->tagHelpLink( $tagName, $localizationContext );
318 $descriptionMsg = $localizationContext->msg( "tag-$tagName-description" );
319 // Don't cache the message object, use the correct MessageLocalizer to parse later.
320 $result[] = [
321 'name' => $tagName,
322 'labelMsg' => !$labelMsg->isDisabled(),
323 'label' => !$labelMsg->isDisabled() ? $labelMsg->plain() : $tagName,
324 'descriptionMsg' => !$descriptionMsg->isDisabled(),
325 'description' => !$descriptionMsg->isDisabled() ? $descriptionMsg->plain() : '',
326 'helpLink' => $helpLink,
327 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
328 ];
329 }
330 return $result;
331 }
332 );
333
334 // Filter out tags that the user cannot see before returning this (the cache assumes the user can see all tags
335 // to avoid splitting it by user)
336 $viewable = array_fill_keys(
337 $this->changeTagsStore->filterViewableTags( array_column( $summary, 'name' ), $authority ),
338 true
339 );
340 return array_values( array_filter(
341 $summary,
342 static fn ( array $tagInfo ) => isset( $viewable[ $tagInfo['name'] ] )
343 ) );
344 }
345
364 public function getChangeTagList(
365 LocalizationContext $localizationContext,
366 Authority $authority,
367 bool $activeOnly = true,
368 bool $useAllTags = true,
369 bool $labelsOnly = false
370 ): array {
371 $tags = $this->getChangeTagListSummary( $localizationContext, $authority, $activeOnly, $useAllTags );
372
373 $language = $this->languageFactory->getLanguage( $localizationContext->getLanguageCode() );
374 foreach ( $tags as &$tagInfo ) {
375 if ( $tagInfo['labelMsg'] ) {
376 // Optimization: Skip the parsing if the label contains only plain text (T344352)
377 if ( wfEscapeWikiText( $tagInfo['label'] ) !== $tagInfo['label'] ) {
378 // Use localizer with the correct page title to parse plain message from the cache.
379 $labelMsg = new RawMessage( $tagInfo['label'] );
380 $tagInfo['label'] = Sanitizer::stripAllTags( $localizationContext->msg( $labelMsg )->parse() );
381 }
382 } else {
383 $tagInfo['label'] = $localizationContext->msg( 'tag-hidden', $tagInfo['name'] )->text();
384 }
385 // Optimization: Skip parsing the descriptions if not needed by the caller (T344352)
386 if ( $labelsOnly ) {
387 unset( $tagInfo['description'] );
388 } elseif ( $tagInfo['descriptionMsg'] ) {
389 // Optimization: Skip the parsing if the description contains only plain text (T344352)
390 if ( wfEscapeWikiText( $tagInfo['description'] ) !== $tagInfo['description'] ) {
391 $descriptionMsg = new RawMessage( $tagInfo['description'] );
392 $tagInfo['description'] = Sanitizer::stripAllTags(
393 $localizationContext->msg( $descriptionMsg )->parse()
394 );
395 }
396 $tagInfo['description'] = $language->truncateForVisual( $tagInfo['description'],
397 self::TAG_DESC_CHARACTER_LIMIT );
398 }
399 unset( $tagInfo['labelMsg'] );
400 unset( $tagInfo['descriptionMsg'] );
401 }
402
403 // Instead of sorting by hit count (disabled for now), sort by display name
404 usort( $tags, static function ( $a, $b ) {
405 return strcasecmp( $a['label'], $b['label'] );
406 } );
407 return $tags;
408 }
409}
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
Formats change tags for display in HTML and use filter dropdown menus.
getChangeTagList(LocalizationContext $localizationContext, Authority $authority, bool $activeOnly=true, bool $useAllTags=true, bool $labelsOnly=false)
Get information about change tags for tag filter dropdown menus.
__construct(private readonly ServiceOptions $options, private readonly ChangeTagsStore $changeTagsStore, private readonly WANObjectCache $cache, private readonly LanguageFactory $languageFactory,)
formatTagsAsSummaryList(?string $tags, MessageLocalizer $localizer, Authority $authority)
Formats the provided tags into HTML for display to a user.
getChangeTagListSummary(LocalizationContext $localizationContext, Authority $authority, bool $activeOnly=true, bool $useAllTags=true)
Get information about change tags, without parsing messages, for tag filter dropdown menus.
getTagDescription(string $tag, MessageLocalizer $localizer)
Get a (short) description for a tag.
buildTagFilter(string $selected, string $format, IContextSource $context, bool $activeOnly=true, bool $useAllTags=true)
Build a text box to select a change tag.
Read-write access to the change_tags table.
A class for passing options to services.
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Variant of the Message class.
A class containing constants representing the names of configuration variables.
const UseTagFilter
Name constant for the UseTagFilter setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:34
The base class for all skins.
Definition Skin.php:54
Multi-datacenter aware caching interface.
Interface for objects which can provide a MediaWiki context on request.
Interface supporting message localization in MediaWiki.
getLanguageCode()
Returns the target language for UI localization.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
msg( $key,... $params)