MediaWiki REL1_39
SpecialTags.php
Go to the documentation of this file.
1<?php
25
31class SpecialTags extends SpecialPage {
32
37
42
47
48 public function __construct() {
49 parent::__construct( 'Tags' );
50 }
51
52 public function execute( $par ) {
53 $this->setHeaders();
54 $this->outputHeader();
55 $this->addHelpLink( 'Manual:Tags' );
56
57 $request = $this->getRequest();
58 switch ( $par ) {
59 case 'delete':
60 $this->showDeleteTagForm( $request->getVal( 'tag' ) );
61 break;
62 case 'activate':
63 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), true );
64 break;
65 case 'deactivate':
66 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), false );
67 break;
68 case 'create':
69 // fall through, thanks to HTMLForm's logic
70 default:
71 $this->showTagList();
72 break;
73 }
74 }
75
76 private function showTagList() {
77 $out = $this->getOutput();
78 $out->setPageTitle( $this->msg( 'tags-title' ) );
79 $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' );
80
81 $authority = $this->getAuthority();
82 $userCanManage = $authority->isAllowed( 'managechangetags' );
83 $userCanDelete = $authority->isAllowed( 'deletechangetags' );
84 $userCanEditInterface = $authority->isAllowed( 'editinterface' );
85
86 // Show form to create a tag
87 if ( $userCanManage ) {
88 $fields = [
89 'Tag' => [
90 'type' => 'text',
91 'label' => $this->msg( 'tags-create-tag-name' )->plain(),
92 'required' => true,
93 ],
94 'Reason' => [
95 'type' => 'text',
96 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
97 'label' => $this->msg( 'tags-create-reason' )->plain(),
98 'size' => 50,
99 ],
100 'IgnoreWarnings' => [
101 'type' => 'hidden',
102 ],
103 ];
104
105 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
106 ->setAction( $this->getPageTitle( 'create' )->getLocalURL() )
107 ->setWrapperLegendMsg( 'tags-create-heading' )
108 ->setHeaderText( $this->msg( 'tags-create-explanation' )->parseAsBlock() )
109 ->setSubmitCallback( [ $this, 'processCreateTagForm' ] )
110 ->setSubmitTextMsg( 'tags-create-submit' )
111 ->show();
112
113 // If processCreateTagForm generated a redirect, there's no point
114 // continuing with this, as the user is just going to end up getting sent
115 // somewhere else. Additionally, if we keep going here, we end up
116 // populating the memcache of tag data (see ChangeTags::listDefinedTags)
117 // with out-of-date data from the replica DB, because the replica DB hasn't caught
118 // up to the fact that a new tag has been created as part of an implicit,
119 // as yet uncommitted transaction on primary DB.
120 if ( $out->getRedirect() !== '' ) {
121 return;
122 }
123 }
124
125 // Used to get hitcounts for #doTagRow()
126 $tagStats = ChangeTags::tagUsageStatistics();
127
128 // Used in #doTagRow()
129 $this->explicitlyDefinedTags = array_fill_keys(
131 $this->softwareDefinedTags = array_fill_keys(
133
134 // List all defined tags, even if they were never applied
135 $definedTags = array_keys( $this->explicitlyDefinedTags + $this->softwareDefinedTags );
136
137 // Show header only if there exists at least one tag
138 if ( !$tagStats && !$definedTags ) {
139 return;
140 }
141
142 // Write the headers
143 $thead = Xml::tags( 'tr', null, Xml::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) .
144 Xml::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) .
145 Xml::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) .
146 Xml::tags( 'th', null, $this->msg( 'tags-source-header' )->parse() ) .
147 Xml::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) .
148 Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) .
149 ( ( $userCanManage || $userCanDelete ) ?
150 Xml::tags( 'th', [ 'class' => 'unsortable' ],
151 $this->msg( 'tags-actions-header' )->parse() ) :
152 '' )
153 );
154
155 $tbody = '';
156 // Used in #doTagRow()
157 $this->softwareActivatedTags = array_fill_keys(
159
160 // Insert tags that have been applied at least once
161 foreach ( $tagStats as $tag => $hitcount ) {
162 $tbody .= $this->doTagRow( $tag, $hitcount, $userCanManage,
163 $userCanDelete, $userCanEditInterface );
164 }
165 // Insert tags defined somewhere but never applied
166 foreach ( $definedTags as $tag ) {
167 if ( !isset( $tagStats[$tag] ) ) {
168 $tbody .= $this->doTagRow( $tag, 0, $userCanManage, $userCanDelete, $userCanEditInterface );
169 }
170 }
171
172 $out->addModuleStyles( [
173 'jquery.tablesorter.styles',
174 'mediawiki.pager.styles'
175 ] );
176 $out->addModules( 'jquery.tablesorter' );
177 $out->addHTML( Xml::tags(
178 'table',
179 [ 'class' => 'mw-datatable sortable mw-tags-table' ],
180 Xml::tags( 'thead', null, $thead ) .
181 Xml::tags( 'tbody', null, $tbody )
182 ) );
183 }
184
185 private function doTagRow(
186 $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks
187 ) {
188 $newRow = '';
189 $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
190
191 $linkRenderer = $this->getLinkRenderer();
192 $disp = ChangeTags::tagDescription( $tag, $this->getContext() );
193 if ( $disp === false ) {
194 $disp = Xml::element( 'em', null, $this->msg( 'tags-hidden' )->text() );
195 }
196 if ( $showEditLinks ) {
197 $disp .= ' ';
198 $editLink = $linkRenderer->makeLink(
199 $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
200 $this->msg( 'tags-edit' )->text(),
201 [],
202 [ 'action' => 'edit' ]
203 );
204 $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
205 }
206 $newRow .= Xml::tags( 'td', null, $disp );
207
208 $msg = $this->msg( "tag-$tag-description" );
209 $desc = !$msg->exists() ? '' : $msg->parse();
210 if ( $showEditLinks ) {
211 $desc .= ' ';
212 $editDescLink = $linkRenderer->makeLink(
213 $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
214 $this->msg( 'tags-edit' )->text(),
215 [],
216 [ 'action' => 'edit' ]
217 );
218 $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
219 }
220 $newRow .= Xml::tags( 'td', null, $desc );
221
222 $sourceMsgs = [];
223 $isSoftware = isset( $this->softwareDefinedTags[$tag] );
224 $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
225 if ( $isSoftware ) {
226 // TODO: Rename this message
227 $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
228 }
229 if ( $isExplicit ) {
230 $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
231 }
232 if ( !$sourceMsgs ) {
233 $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
234 }
235 $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
236
237 $isActive = $isExplicit || isset( $this->softwareActivatedTags[$tag] );
238 $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
239 $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
240
241 $hitcountLabelMsg = $this->msg( 'tags-hitcount' )->numParams( $hitcount );
242 if ( $this->getConfig()->get( MainConfigNames::UseTagFilter ) ) {
243 $hitcountLabel = $linkRenderer->makeLink(
244 SpecialPage::getTitleFor( 'Recentchanges' ),
245 $hitcountLabelMsg->text(),
246 [],
247 [ 'tagfilter' => $tag ]
248 );
249 } else {
250 $hitcountLabel = $hitcountLabelMsg->escaped();
251 }
252
253 // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
254 $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
255
256 $actionLinks = [];
257
258 if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
259 $actionLinks[] = $linkRenderer->makeKnownLink(
260 $this->getPageTitle( 'delete' ),
261 $this->msg( 'tags-delete' )->text(),
262 [],
263 [ 'tag' => $tag ] );
264 }
265
266 if ( $showManageActions ) { // we've already checked that the user had the requisite userright
267 if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
268 $actionLinks[] = $linkRenderer->makeKnownLink(
269 $this->getPageTitle( 'activate' ),
270 $this->msg( 'tags-activate' )->text(),
271 [],
272 [ 'tag' => $tag ] );
273 }
274
275 if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
276 $actionLinks[] = $linkRenderer->makeKnownLink(
277 $this->getPageTitle( 'deactivate' ),
278 $this->msg( 'tags-deactivate' )->text(),
279 [],
280 [ 'tag' => $tag ] );
281 }
282 }
283
284 if ( $showDeleteActions || $showManageActions ) {
285 $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
286 }
287
288 return Xml::tags( 'tr', null, $newRow ) . "\n";
289 }
290
291 public function processCreateTagForm( array $data, HTMLForm $form ) {
292 $context = $form->getContext();
293 $out = $context->getOutput();
294
295 $tag = trim( strval( $data['Tag'] ) );
296 $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
297 $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
298 $context->getAuthority(), $ignoreWarnings );
299
300 if ( $status->isGood() ) {
301 $out->redirect( $this->getPageTitle()->getLocalURL() );
302 return true;
303 } elseif ( $status->isOK() ) {
304 // We have some warnings, so we adjust the form for confirmation.
305 // This would override the existing field and its default value.
306 $form->addFields( [
307 'IgnoreWarnings' => [
308 'type' => 'hidden',
309 'default' => '1',
310 ],
311 ] );
312
313 $headerText = $this->msg( 'tags-create-warnings-above', $tag,
314 count( $status->getWarningsArray() ) )->parseAsBlock() .
315 $out->parseAsInterface( $status->getWikiText() ) .
316 $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
317
318 $form->setHeaderText( $headerText )
319 ->setSubmitTextMsg( 'htmlform-yes' );
320
321 $out->addBacklinkSubtitle( $this->getPageTitle() );
322 return false;
323 } else {
324 $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() );
325 return false;
326 }
327 }
328
329 protected function showDeleteTagForm( $tag ) {
330 $authority = $this->getAuthority();
331 if ( !$authority->isAllowed( 'deletechangetags' ) ) {
332 throw new PermissionsError( 'deletechangetags' );
333 }
334
335 $out = $this->getOutput();
336 $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
337 $out->addBacklinkSubtitle( $this->getPageTitle() );
338
339 // is the tag actually able to be deleted?
340 $canDeleteResult = ChangeTags::canDeleteTag( $tag, $authority );
341 if ( !$canDeleteResult->isGood() ) {
342 $out->wrapWikiTextAsInterface( 'error', $canDeleteResult->getWikiText() );
343 if ( !$canDeleteResult->isOK() ) {
344 return;
345 }
346 }
347
348 $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
349 $tagUsage = ChangeTags::tagUsageStatistics();
350 if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
351 $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
352 $tagUsage[$tag] )->parseAsBlock();
353 }
354 $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
355
356 // see if the tag is in use
357 $this->softwareActivatedTags = array_fill_keys(
359 if ( isset( $this->softwareActivatedTags[$tag] ) ) {
360 $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
361 }
362
363 $fields = [];
364 $fields['Reason'] = [
365 'type' => 'text',
366 'label' => $this->msg( 'tags-delete-reason' )->plain(),
367 'size' => 50,
368 ];
369 $fields['HiddenTag'] = [
370 'type' => 'hidden',
371 'name' => 'tag',
372 'default' => $tag,
373 'required' => true,
374 ];
375
376 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
377 ->setAction( $this->getPageTitle( 'delete' )->getLocalURL() )
378 ->setSubmitCallback( function ( $data, $form ) {
379 return $this->processTagForm( $data, $form, 'delete' );
380 } )
381 ->setSubmitTextMsg( 'tags-delete-submit' )
382 ->setSubmitDestructive()
383 ->addPreText( $preText )
384 ->show();
385 }
386
387 protected function showActivateDeactivateForm( $tag, $activate ) {
388 $actionStr = $activate ? 'activate' : 'deactivate';
389
390 $authority = $this->getAuthority();
391 if ( !$authority->isAllowed( 'managechangetags' ) ) {
392 throw new PermissionsError( 'managechangetags' );
393 }
394
395 $out = $this->getOutput();
396 // tags-activate-title, tags-deactivate-title
397 $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
398 $out->addBacklinkSubtitle( $this->getPageTitle() );
399
400 // is it possible to do this?
401 if ( $activate ) {
402 $result = ChangeTags::canActivateTag( $tag, $authority );
403 } else {
404 $result = ChangeTags::canDeactivateTag( $tag, $authority );
405 }
406 if ( !$result->isGood() ) {
407 $out->wrapWikiTextAsInterface( 'error', $result->getWikiText() );
408 if ( !$result->isOK() ) {
409 return;
410 }
411 }
412
413 // tags-activate-question, tags-deactivate-question
414 $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
415
416 $fields = [];
417 // tags-activate-reason, tags-deactivate-reason
418 $fields['Reason'] = [
419 'type' => 'text',
420 'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
421 'size' => 50,
422 ];
423 $fields['HiddenTag'] = [
424 'type' => 'hidden',
425 'name' => 'tag',
426 'default' => $tag,
427 'required' => true,
428 ];
429
430 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
431 ->setAction( $this->getPageTitle( $actionStr )->getLocalURL() )
432 ->setSubmitCallback( function ( $data, $form ) use ( $actionStr ) {
433 return $this->processTagForm( $data, $form, $actionStr );
434 } )
435 // tags-activate-submit, tags-deactivate-submit
436 ->setSubmitTextMsg( "tags-$actionStr-submit" )
437 ->addPreText( $preText )
438 ->show();
439 }
440
447 public function processTagForm( array $data, HTMLForm $form, string $action ) {
448 $context = $form->getContext();
449 $out = $context->getOutput();
450
451 $tag = $data['HiddenTag'];
452 // activateTagWithChecks, deactivateTagWithChecks, deleteTagWithChecks
453 $status = call_user_func( [ ChangeTags::class, "{$action}TagWithChecks" ],
454 $tag, $data['Reason'], $context->getUser(), true );
455
456 if ( $status->isGood() ) {
457 $out->redirect( $this->getPageTitle()->getLocalURL() );
458 return true;
459 } elseif ( $status->isOK() && $action === 'delete' ) {
460 // deletion succeeded, but hooks raised a warning
461 $out->addWikiTextAsInterface( $this->msg( 'tags-delete-warnings-after-delete', $tag,
462 count( $status->getWarningsArray() ) )->text() . "\n" .
463 $status->getWikitext() );
464 $out->addReturnTo( $this->getPageTitle() );
465 return true;
466 } else {
467 $out->wrapWikiTextAsInterface( 'error', $status->getWikitext() );
468 return false;
469 }
470 }
471
477 public function getSubpagesForPrefixSearch() {
478 // The subpages does not have an own form, so not listing it at the moment
479 return [
480 // 'delete',
481 // 'activate',
482 // 'deactivate',
483 // 'create',
484 ];
485 }
486
487 protected function getGroupName() {
488 return 'changes';
489 }
490}
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
static canDeactivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to deactivate this tag?
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
static canActivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to activate this tag?
static canDeleteTag( $tag, Authority $performer=null, int $flags=0)
Is it OK to allow the user to delete this tag?
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
static createTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
getContext()
Get the base IContextSource object.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:150
setHeaderText( $msg, $section=null)
Set header text, inside the form.
Definition HTMLForm.php:951
addFields( $descriptor)
Add fields to the form.
Definition HTMLForm.php:400
A class containing constants representing the names of configuration variables.
Show an error when a user tries to do something they do not have the necessary permissions for.
Parent class for all special pages.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getAuthority()
Shortcut to get the Authority executing this instance.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getLanguage()
Shortcut to get user's language.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
A special page that lists tags for edits.
array $softwareActivatedTags
List of software activated tags.
showActivateDeactivateForm( $tag, $activate)
execute( $par)
Default execute method Checks user permissions.
processTagForm(array $data, HTMLForm $form, string $action)
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
array $softwareDefinedTags
List of software defined tags.
array $explicitlyDefinedTags
List of explicitly defined tags.
showDeleteTagForm( $tag)
processCreateTagForm(array $data, HTMLForm $form)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition Xml.php:134
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition Xml.php:43