MediaWiki REL1_34
SpecialTags.php
Go to the documentation of this file.
1<?php
25
31class SpecialTags extends SpecialPage {
32
37
42
47
48 function __construct() {
49 parent::__construct( 'Tags' );
50 }
51
52 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 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 'label' => $this->msg( 'tags-create-reason' )->plain(),
98 'size' => 50,
99 ],
100 'IgnoreWarnings' => [
101 'type' => 'hidden',
102 ],
103 ];
104
105 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
106 $form->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
107 $form->setWrapperLegendMsg( 'tags-create-heading' );
108 $form->setHeaderText( $this->msg( 'tags-create-explanation' )->parseAsBlock() );
109 $form->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
110 $form->setSubmitTextMsg( 'tags-create-submit' );
111 $form->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 master.
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 atleast 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( 'jquery.tablesorter.styles' );
173 $out->addModules( 'jquery.tablesorter' );
174 $out->addHTML( Xml::tags(
175 'table',
176 [ 'class' => 'mw-datatable sortable mw-tags-table' ],
177 Xml::tags( 'thead', null, $thead ) .
178 Xml::tags( 'tbody', null, $tbody )
179 ) );
180 }
181
182 function doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks ) {
183 $newRow = '';
184 $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
185
187 $disp = ChangeTags::tagDescription( $tag, $this->getContext() );
188 if ( $showEditLinks ) {
189 $disp .= ' ';
190 $editLink = $linkRenderer->makeLink(
191 $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
192 $this->msg( 'tags-edit' )->text()
193 );
194 $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
195 }
196 $newRow .= Xml::tags( 'td', null, $disp );
197
198 $msg = $this->msg( "tag-$tag-description" );
199 $desc = !$msg->exists() ? '' : $msg->parse();
200 if ( $showEditLinks ) {
201 $desc .= ' ';
202 $editDescLink = $linkRenderer->makeLink(
203 $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
204 $this->msg( 'tags-edit' )->text()
205 );
206 $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
207 }
208 $newRow .= Xml::tags( 'td', null, $desc );
209
210 $sourceMsgs = [];
211 $isSoftware = isset( $this->softwareDefinedTags[$tag] );
212 $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
213 if ( $isSoftware ) {
214 // TODO: Rename this message
215 $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
216 }
217 if ( $isExplicit ) {
218 $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
219 }
220 if ( !$sourceMsgs ) {
221 $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
222 }
223 $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
224
225 $isActive = $isExplicit || isset( $this->softwareActivatedTags[$tag] );
226 $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
227 $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
228
229 $hitcountLabelMsg = $this->msg( 'tags-hitcount' )->numParams( $hitcount );
230 if ( $this->getConfig()->get( 'UseTagFilter' ) ) {
231 $hitcountLabel = $linkRenderer->makeLink(
232 SpecialPage::getTitleFor( 'Recentchanges' ),
233 $hitcountLabelMsg->text(),
234 [],
235 [ 'tagfilter' => $tag ]
236 );
237 } else {
238 $hitcountLabel = $hitcountLabelMsg->escaped();
239 }
240
241 // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
242 $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
243
244 $actionLinks = [];
245
246 if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
247 $actionLinks[] = $linkRenderer->makeKnownLink(
248 $this->getPageTitle( 'delete' ),
249 $this->msg( 'tags-delete' )->text(),
250 [],
251 [ 'tag' => $tag ] );
252 }
253
254 if ( $showManageActions ) { // we've already checked that the user had the requisite userright
255 if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
256 $actionLinks[] = $linkRenderer->makeKnownLink(
257 $this->getPageTitle( 'activate' ),
258 $this->msg( 'tags-activate' )->text(),
259 [],
260 [ 'tag' => $tag ] );
261 }
262
263 if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
264 $actionLinks[] = $linkRenderer->makeKnownLink(
265 $this->getPageTitle( 'deactivate' ),
266 $this->msg( 'tags-deactivate' )->text(),
267 [],
268 [ 'tag' => $tag ] );
269 }
270 }
271
272 if ( $showDeleteActions || $showManageActions ) {
273 $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
274 }
275
276 return Xml::tags( 'tr', null, $newRow ) . "\n";
277 }
278
279 public function processCreateTagForm( array $data, HTMLForm $form ) {
280 $context = $form->getContext();
281 $out = $context->getOutput();
282
283 $tag = trim( strval( $data['Tag'] ) );
284 $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
285 $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
286 $context->getUser(), $ignoreWarnings );
287
288 if ( $status->isGood() ) {
289 $out->redirect( $this->getPageTitle()->getLocalURL() );
290 return true;
291 } elseif ( $status->isOK() ) {
292 // we have some warnings, so we show a confirmation form
293 $fields = [
294 'Tag' => [
295 'type' => 'hidden',
296 'default' => $data['Tag'],
297 ],
298 'Reason' => [
299 'type' => 'hidden',
300 'default' => $data['Reason'],
301 ],
302 'IgnoreWarnings' => [
303 'type' => 'hidden',
304 'default' => '1',
305 ],
306 ];
307
308 // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
309 // we get into an infinite loop!
310 $context->getRequest()->unsetVal( 'wpEditToken' );
311
312 $headerText = $this->msg( 'tags-create-warnings-above', $tag,
313 count( $status->getWarningsArray() ) )->parseAsBlock() .
314 $out->parseAsInterface( $status->getWikiText() ) .
315 $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
316
317 $subform = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
318 $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
319 $subform->setWrapperLegendMsg( 'tags-create-heading' );
320 $subform->setHeaderText( $headerText );
321 $subform->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
322 $subform->setSubmitTextMsg( 'htmlform-yes' );
323 $subform->show();
324
325 $out->addBacklinkSubtitle( $this->getPageTitle() );
326 return true;
327 } else {
328 $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() );
329 return false;
330 }
331 }
332
333 protected function showDeleteTagForm( $tag ) {
334 $user = $this->getUser();
335 if ( !MediaWikiServices::getInstance()
337 ->userHasRight( $user, 'deletechangetags' ) ) {
338 throw new PermissionsError( 'deletechangetags' );
339 }
340
341 $out = $this->getOutput();
342 $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
343 $out->addBacklinkSubtitle( $this->getPageTitle() );
344
345 // is the tag actually able to be deleted?
346 $canDeleteResult = ChangeTags::canDeleteTag( $tag, $user );
347 if ( !$canDeleteResult->isGood() ) {
348 $out->wrapWikiTextAsInterface( 'error', $canDeleteResult->getWikiText() );
349 if ( !$canDeleteResult->isOK() ) {
350 return;
351 }
352 }
353
354 $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
355 $tagUsage = ChangeTags::tagUsageStatistics();
356 if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
357 $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
358 $tagUsage[$tag] )->parseAsBlock();
359 }
360 $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
361
362 // see if the tag is in use
363 $this->softwareActivatedTags = array_fill_keys(
365 if ( isset( $this->softwareActivatedTags[$tag] ) ) {
366 $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
367 }
368
369 $fields = [];
370 $fields['Reason'] = [
371 'type' => 'text',
372 'label' => $this->msg( 'tags-delete-reason' )->plain(),
373 'size' => 50,
374 ];
375 $fields['HiddenTag'] = [
376 'type' => 'hidden',
377 'name' => 'tag',
378 'default' => $tag,
379 'required' => true,
380 ];
381
382 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
383 $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
384 // @phan-suppress-next-line PhanUndeclaredProperty
385 $form->tagAction = 'delete'; // custom property on HTMLForm object
386 $form->setSubmitCallback( [ $this, 'processTagForm' ] );
387 $form->setSubmitTextMsg( 'tags-delete-submit' );
388 $form->setSubmitDestructive(); // nasty!
389 $form->addPreText( $preText );
390 $form->show();
391 }
392
393 protected function showActivateDeactivateForm( $tag, $activate ) {
394 $actionStr = $activate ? 'activate' : 'deactivate';
395
396 $user = $this->getUser();
397 if ( !MediaWikiServices::getInstance()
399 ->userHasRight( $user, 'managechangetags' ) ) {
400 throw new PermissionsError( 'managechangetags' );
401 }
402
403 $out = $this->getOutput();
404 // tags-activate-title, tags-deactivate-title
405 $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
406 $out->addBacklinkSubtitle( $this->getPageTitle() );
407
408 // is it possible to do this?
409 $func = $activate ? 'canActivateTag' : 'canDeactivateTag';
410 $result = ChangeTags::$func( $tag, $user );
411 if ( !$result->isGood() ) {
412 $out->wrapWikiTextAsInterface( 'error', $result->getWikiText() );
413 if ( !$result->isOK() ) {
414 return;
415 }
416 }
417
418 // tags-activate-question, tags-deactivate-question
419 $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
420
421 $fields = [];
422 // tags-activate-reason, tags-deactivate-reason
423 $fields['Reason'] = [
424 'type' => 'text',
425 'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
426 'size' => 50,
427 ];
428 $fields['HiddenTag'] = [
429 'type' => 'hidden',
430 'name' => 'tag',
431 'default' => $tag,
432 'required' => true,
433 ];
434
435 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
436 $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
437 // @phan-suppress-next-line PhanUndeclaredProperty
438 $form->tagAction = $actionStr;
439 $form->setSubmitCallback( [ $this, 'processTagForm' ] );
440 // tags-activate-submit, tags-deactivate-submit
441 $form->setSubmitTextMsg( "tags-$actionStr-submit" );
442 $form->addPreText( $preText );
443 $form->show();
444 }
445
452 public function processTagForm( array $data, HTMLForm $form ) {
453 $context = $form->getContext();
454 $out = $context->getOutput();
455
456 $tag = $data['HiddenTag'];
457 $status = call_user_func( [ ChangeTags::class, "{$form->tagAction}TagWithChecks" ],
458 $tag, $data['Reason'], $context->getUser(), true );
459
460 if ( $status->isGood() ) {
461 $out->redirect( $this->getPageTitle()->getLocalURL() );
462 return true;
463 } elseif ( $status->isOK() && $form->tagAction === 'delete' ) {
464 // deletion succeeded, but hooks raised a warning
465 $out->addWikiTextAsInterface( $this->msg( 'tags-delete-warnings-after-delete', $tag,
466 count( $status->getWarningsArray() ) )->text() . "\n" .
467 $status->getWikitext() );
468 $out->addReturnTo( $this->getPageTitle() );
469 return true;
470 } else {
471 $out->wrapWikiTextAsInterface( 'error', $status->getWikitext() );
472 return false;
473 }
474 }
475
481 public function getSubpagesForPrefixSearch() {
482 // The subpages does not have an own form, so not listing it at the moment
483 return [
484 // 'delete',
485 // 'activate',
486 // 'deactivate',
487 // 'create',
488 ];
489 }
490
491 protected function getGroupName() {
492 return 'changes';
493 }
494}
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 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 canDeleteTag( $tag, User $user=null)
Is it OK to allow the user to delete 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:131
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...
$context
Definition load.php:45