MediaWiki REL1_35
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 $user = $this->getUser();
82 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
83 $userCanManage = $permissionManager->userHasRight( $user, 'managechangetags' );
84 $userCanDelete = $permissionManager->userHasRight( $user, 'deletechangetags' );
85 $userCanEditInterface = $permissionManager->userHasRight( $user, 'editinterface' );
86
87 // Show form to create a tag
88 if ( $userCanManage ) {
89 $fields = [
90 'Tag' => [
91 'type' => 'text',
92 'label' => $this->msg( 'tags-create-tag-name' )->plain(),
93 'required' => true,
94 ],
95 'Reason' => [
96 'type' => 'text',
97 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
98 'label' => $this->msg( 'tags-create-reason' )->plain(),
99 'size' => 50,
100 ],
101 'IgnoreWarnings' => [
102 'type' => 'hidden',
103 ],
104 ];
105
106 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
107 $form->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
108 $form->setWrapperLegendMsg( 'tags-create-heading' );
109 $form->setHeaderText( $this->msg( 'tags-create-explanation' )->parseAsBlock() );
110 $form->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
111 $form->setSubmitTextMsg( 'tags-create-submit' );
112 $form->show();
113
114 // If processCreateTagForm generated a redirect, there's no point
115 // continuing with this, as the user is just going to end up getting sent
116 // somewhere else. Additionally, if we keep going here, we end up
117 // populating the memcache of tag data (see ChangeTags::listDefinedTags)
118 // with out-of-date data from the replica DB, because the replica DB hasn't caught
119 // up to the fact that a new tag has been created as part of an implicit,
120 // as yet uncommitted transaction on master.
121 if ( $out->getRedirect() !== '' ) {
122 return;
123 }
124 }
125
126 // Used to get hitcounts for #doTagRow()
127 $tagStats = ChangeTags::tagUsageStatistics();
128
129 // Used in #doTagRow()
130 $this->explicitlyDefinedTags = array_fill_keys(
132 $this->softwareDefinedTags = array_fill_keys(
134
135 // List all defined tags, even if they were never applied
136 $definedTags = array_keys( $this->explicitlyDefinedTags + $this->softwareDefinedTags );
137
138 // Show header only if there exists atleast one tag
139 if ( !$tagStats && !$definedTags ) {
140 return;
141 }
142
143 // Write the headers
144 $thead = Xml::tags( 'tr', null, Xml::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) .
145 Xml::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) .
146 Xml::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) .
147 Xml::tags( 'th', null, $this->msg( 'tags-source-header' )->parse() ) .
148 Xml::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) .
149 Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) .
150 ( ( $userCanManage || $userCanDelete ) ?
151 Xml::tags( 'th', [ 'class' => 'unsortable' ],
152 $this->msg( 'tags-actions-header' )->parse() ) :
153 '' )
154 );
155
156 $tbody = '';
157 // Used in #doTagRow()
158 $this->softwareActivatedTags = array_fill_keys(
160
161 // Insert tags that have been applied at least once
162 foreach ( $tagStats as $tag => $hitcount ) {
163 $tbody .= $this->doTagRow( $tag, $hitcount, $userCanManage,
164 $userCanDelete, $userCanEditInterface );
165 }
166 // Insert tags defined somewhere but never applied
167 foreach ( $definedTags as $tag ) {
168 if ( !isset( $tagStats[$tag] ) ) {
169 $tbody .= $this->doTagRow( $tag, 0, $userCanManage, $userCanDelete, $userCanEditInterface );
170 }
171 }
172
173 $out->addModuleStyles( 'jquery.tablesorter.styles' );
174 $out->addModules( 'jquery.tablesorter' );
175 $out->addHTML( Xml::tags(
176 'table',
177 [ 'class' => 'mw-datatable sortable mw-tags-table' ],
178 Xml::tags( 'thead', null, $thead ) .
179 Xml::tags( 'tbody', null, $tbody )
180 ) );
181 }
182
183 private function doTagRow(
184 $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks
185 ) {
186 $newRow = '';
187 $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
188
190 $disp = ChangeTags::tagDescription( $tag, $this->getContext() );
191 if ( $showEditLinks ) {
192 $disp .= ' ';
193 $editLink = $linkRenderer->makeLink(
194 $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
195 $this->msg( 'tags-edit' )->text(),
196 [],
197 [ 'action' => 'edit' ]
198 );
199 $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
200 }
201 $newRow .= Xml::tags( 'td', null, $disp );
202
203 $msg = $this->msg( "tag-$tag-description" );
204 $desc = !$msg->exists() ? '' : $msg->parse();
205 if ( $showEditLinks ) {
206 $desc .= ' ';
207 $editDescLink = $linkRenderer->makeLink(
208 $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
209 $this->msg( 'tags-edit' )->text(),
210 [],
211 [ 'action' => 'edit' ]
212 );
213 $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
214 }
215 $newRow .= Xml::tags( 'td', null, $desc );
216
217 $sourceMsgs = [];
218 $isSoftware = isset( $this->softwareDefinedTags[$tag] );
219 $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
220 if ( $isSoftware ) {
221 // TODO: Rename this message
222 $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
223 }
224 if ( $isExplicit ) {
225 $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
226 }
227 if ( !$sourceMsgs ) {
228 $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
229 }
230 $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
231
232 $isActive = $isExplicit || isset( $this->softwareActivatedTags[$tag] );
233 $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
234 $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
235
236 $hitcountLabelMsg = $this->msg( 'tags-hitcount' )->numParams( $hitcount );
237 if ( $this->getConfig()->get( 'UseTagFilter' ) ) {
238 $hitcountLabel = $linkRenderer->makeLink(
239 SpecialPage::getTitleFor( 'Recentchanges' ),
240 $hitcountLabelMsg->text(),
241 [],
242 [ 'tagfilter' => $tag ]
243 );
244 } else {
245 $hitcountLabel = $hitcountLabelMsg->escaped();
246 }
247
248 // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
249 $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
250
251 $actionLinks = [];
252
253 if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
254 $actionLinks[] = $linkRenderer->makeKnownLink(
255 $this->getPageTitle( 'delete' ),
256 $this->msg( 'tags-delete' )->text(),
257 [],
258 [ 'tag' => $tag ] );
259 }
260
261 if ( $showManageActions ) { // we've already checked that the user had the requisite userright
262 if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
263 $actionLinks[] = $linkRenderer->makeKnownLink(
264 $this->getPageTitle( 'activate' ),
265 $this->msg( 'tags-activate' )->text(),
266 [],
267 [ 'tag' => $tag ] );
268 }
269
270 if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
271 $actionLinks[] = $linkRenderer->makeKnownLink(
272 $this->getPageTitle( 'deactivate' ),
273 $this->msg( 'tags-deactivate' )->text(),
274 [],
275 [ 'tag' => $tag ] );
276 }
277 }
278
279 if ( $showDeleteActions || $showManageActions ) {
280 $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
281 }
282
283 return Xml::tags( 'tr', null, $newRow ) . "\n";
284 }
285
286 public function processCreateTagForm( array $data, HTMLForm $form ) {
287 $context = $form->getContext();
288 $out = $context->getOutput();
289
290 $tag = trim( strval( $data['Tag'] ) );
291 $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
292 $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
293 $context->getUser(), $ignoreWarnings );
294
295 if ( $status->isGood() ) {
296 $out->redirect( $this->getPageTitle()->getLocalURL() );
297 return true;
298 } elseif ( $status->isOK() ) {
299 // we have some warnings, so we show a confirmation form
300 $fields = [
301 'Tag' => [
302 'type' => 'hidden',
303 'default' => $data['Tag'],
304 ],
305 'Reason' => [
306 'type' => 'hidden',
307 'default' => $data['Reason'],
308 ],
309 'IgnoreWarnings' => [
310 'type' => 'hidden',
311 'default' => '1',
312 ],
313 ];
314
315 // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
316 // we get into an infinite loop!
317 $context->getRequest()->unsetVal( 'wpEditToken' );
318
319 $headerText = $this->msg( 'tags-create-warnings-above', $tag,
320 count( $status->getWarningsArray() ) )->parseAsBlock() .
321 $out->parseAsInterface( $status->getWikiText() ) .
322 $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
323
324 $subform = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
325 $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
326 $subform->setWrapperLegendMsg( 'tags-create-heading' );
327 $subform->setHeaderText( $headerText );
328 $subform->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
329 $subform->setSubmitTextMsg( 'htmlform-yes' );
330 $subform->show();
331
332 $out->addBacklinkSubtitle( $this->getPageTitle() );
333 return true;
334 } else {
335 $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() );
336 return false;
337 }
338 }
339
340 protected function showDeleteTagForm( $tag ) {
341 $user = $this->getUser();
342 if ( !MediaWikiServices::getInstance()
344 ->userHasRight( $user, 'deletechangetags' ) ) {
345 throw new PermissionsError( 'deletechangetags' );
346 }
347
348 $out = $this->getOutput();
349 $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
350 $out->addBacklinkSubtitle( $this->getPageTitle() );
351
352 // is the tag actually able to be deleted?
353 $canDeleteResult = ChangeTags::canDeleteTag( $tag, $user );
354 if ( !$canDeleteResult->isGood() ) {
355 $out->wrapWikiTextAsInterface( 'error', $canDeleteResult->getWikiText() );
356 if ( !$canDeleteResult->isOK() ) {
357 return;
358 }
359 }
360
361 $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
362 $tagUsage = ChangeTags::tagUsageStatistics();
363 if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
364 $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
365 $tagUsage[$tag] )->parseAsBlock();
366 }
367 $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
368
369 // see if the tag is in use
370 $this->softwareActivatedTags = array_fill_keys(
372 if ( isset( $this->softwareActivatedTags[$tag] ) ) {
373 $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
374 }
375
376 $fields = [];
377 $fields['Reason'] = [
378 'type' => 'text',
379 'label' => $this->msg( 'tags-delete-reason' )->plain(),
380 'size' => 50,
381 ];
382 $fields['HiddenTag'] = [
383 'type' => 'hidden',
384 'name' => 'tag',
385 'default' => $tag,
386 'required' => true,
387 ];
388
389 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
390 $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
391 // @phan-suppress-next-line PhanUndeclaredProperty
392 $form->tagAction = 'delete'; // custom property on HTMLForm object
393 $form->setSubmitCallback( [ $this, 'processTagForm' ] );
394 $form->setSubmitTextMsg( 'tags-delete-submit' );
395 $form->setSubmitDestructive(); // nasty!
396 $form->addPreText( $preText );
397 $form->show();
398 }
399
400 protected function showActivateDeactivateForm( $tag, $activate ) {
401 $actionStr = $activate ? 'activate' : 'deactivate';
402
403 $user = $this->getUser();
404 if ( !MediaWikiServices::getInstance()
406 ->userHasRight( $user, 'managechangetags' ) ) {
407 throw new PermissionsError( 'managechangetags' );
408 }
409
410 $out = $this->getOutput();
411 // tags-activate-title, tags-deactivate-title
412 $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
413 $out->addBacklinkSubtitle( $this->getPageTitle() );
414
415 // is it possible to do this?
416 $func = $activate ? 'canActivateTag' : 'canDeactivateTag';
417 $result = ChangeTags::$func( $tag, $user );
418 if ( !$result->isGood() ) {
419 $out->wrapWikiTextAsInterface( 'error', $result->getWikiText() );
420 if ( !$result->isOK() ) {
421 return;
422 }
423 }
424
425 // tags-activate-question, tags-deactivate-question
426 $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
427
428 $fields = [];
429 // tags-activate-reason, tags-deactivate-reason
430 $fields['Reason'] = [
431 'type' => 'text',
432 'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
433 'size' => 50,
434 ];
435 $fields['HiddenTag'] = [
436 'type' => 'hidden',
437 'name' => 'tag',
438 'default' => $tag,
439 'required' => true,
440 ];
441
442 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
443 $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
444 // @phan-suppress-next-line PhanUndeclaredProperty
445 $form->tagAction = $actionStr;
446 $form->setSubmitCallback( [ $this, 'processTagForm' ] );
447 // tags-activate-submit, tags-deactivate-submit
448 $form->setSubmitTextMsg( "tags-$actionStr-submit" );
449 $form->addPreText( $preText );
450 $form->show();
451 }
452
459 public function processTagForm( array $data, HTMLForm $form ) {
460 $context = $form->getContext();
461 $out = $context->getOutput();
462
463 $tag = $data['HiddenTag'];
464 $status = call_user_func( [ ChangeTags::class, "{$form->tagAction}TagWithChecks" ],
465 $tag, $data['Reason'], $context->getUser(), true );
466
467 if ( $status->isGood() ) {
468 $out->redirect( $this->getPageTitle()->getLocalURL() );
469 return true;
470 } elseif ( $status->isOK() && $form->tagAction === 'delete' ) {
471 // deletion succeeded, but hooks raised a warning
472 $out->addWikiTextAsInterface( $this->msg( 'tags-delete-warnings-after-delete', $tag,
473 count( $status->getWarningsArray() ) )->text() . "\n" .
474 $status->getWikitext() );
475 $out->addReturnTo( $this->getPageTitle() );
476 return true;
477 } else {
478 $out->wrapWikiTextAsInterface( 'error', $status->getWikitext() );
479 return false;
480 }
481 }
482
488 public function getSubpagesForPrefixSearch() {
489 // The subpages does not have an own form, so not listing it at the moment
490 return [
491 // 'delete',
492 // 'activate',
493 // 'deactivate',
494 // 'create',
495 ];
496 }
497
498 protected function getGroupName() {
499 return 'changes';
500 }
501}
getPermissionManager()
static createTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
static canDeleteTag( $tag, User $user=null, int $flags=0)
Is it OK to allow the user to delete this tag?
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
static canActivateTag( $tag, User $user=null)
Is it OK to allow the user to activate this tag?
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
static canDeactivateTag( $tag, User $user=null)
Is it OK to allow the user to deactivate this tag?
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:135
MediaWikiServices is the service locator for the application scope of MediaWiki.
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.
getUser()
Shortcut to get the User executing 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.
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.
MediaWiki Linker LinkRenderer null $linkRenderer
A special page that lists tags for edits.
processTagForm(array $data, HTMLForm $form)
array $softwareActivatedTags
List of software activated tags.
doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks)
showActivateDeactivateForm( $tag, $activate)
execute( $par)
Default execute method Checks user permissions.
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...