521f0cee16714dd3228ac2dfb44da59e33fdefe1
3 * Implements Special:Tags
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
21 * @ingroup SpecialPage
25 * A special page that lists tags for edits
27 * @ingroup SpecialPage
29 class SpecialTags
extends SpecialPage
{
32 * @var array List of explicitly defined tags
34 protected $explicitlyDefinedTags;
37 * @var array List of extension defined tags
39 protected $extensionDefinedTags;
42 * @var array List of extension activated tags
44 protected $extensionActivatedTags;
46 function __construct() {
47 parent
::__construct( 'Tags' );
50 function execute( $par ) {
52 $this->outputHeader();
54 $request = $this->getRequest();
57 $this->showDeleteTagForm( $request->getVal( 'tag' ) );
60 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), true );
63 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), false );
66 // fall through, thanks to HTMLForm's logic
73 function showTagList() {
74 $out = $this->getOutput();
75 $out->setPageTitle( $this->msg( 'tags-title' ) );
76 $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' );
78 $user = $this->getUser();
79 $userCanManage = $user->isAllowed( 'managechangetags' );
80 $userCanDelete = $user->isAllowed( 'deletechangetags' );
81 $userCanEditInterface = $user->isAllowed( 'editinterface' );
83 // Show form to create a tag
84 if ( $userCanManage ) {
88 'label' => $this->msg( 'tags-create-tag-name' )->plain(),
93 'label' => $this->msg( 'tags-create-reason' )->plain(),
101 $form = new HTMLForm( $fields, $this->getContext() );
102 $form->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
103 $form->setWrapperLegendMsg( 'tags-create-heading' );
104 $form->setHeaderText( $this->msg( 'tags-create-explanation' )->parseAsBlock() );
105 $form->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
106 $form->setSubmitTextMsg( 'tags-create-submit' );
109 // If processCreateTagForm generated a redirect, there's no point
110 // continuing with this, as the user is just going to end up getting sent
111 // somewhere else. Additionally, if we keep going here, we end up
112 // populating the memcache of tag data (see ChangeTags::listDefinedTags)
113 // with out-of-date data from the slave, because the slave hasn't caught
114 // up to the fact that a new tag has been created as part of an implicit,
115 // as yet uncommitted transaction on master.
116 if ( $out->getRedirect() !== '' ) {
121 // Used to get hitcounts for #doTagRow()
122 $tagStats = ChangeTags
::tagUsageStatistics();
124 // Used in #doTagRow()
125 $this->explicitlyDefinedTags
= array_fill_keys(
126 ChangeTags
::listExplicitlyDefinedTags(), true );
127 $this->extensionDefinedTags
= array_fill_keys(
128 ChangeTags
::listExtensionDefinedTags(), true );
130 // List all defined tags, even if they were never applied
131 $definedTags = array_keys( array_merge(
132 $this->explicitlyDefinedTags
, $this->extensionDefinedTags
) );
134 // Show header only if there exists atleast one tag
135 if ( !$tagStats && !$definedTags ) {
140 $html = Xml
::tags( 'tr', null, Xml
::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) .
141 Xml
::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) .
142 Xml
::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) .
143 Xml
::tags( 'th', null, $this->msg( 'tags-source-header' )->parse() ) .
144 Xml
::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) .
145 Xml
::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) .
147 Xml
::tags( 'th', [ 'class' => 'unsortable' ],
148 $this->msg( 'tags-actions-header' )->parse() ) :
152 // Used in #doTagRow()
153 $this->extensionActivatedTags
= array_fill_keys(
154 ChangeTags
::listExtensionActivatedTags(), true );
156 // Insert tags that have been applied at least once
157 foreach ( $tagStats as $tag => $hitcount ) {
158 $html .= $this->doTagRow( $tag, $hitcount, $userCanManage,
159 $userCanDelete, $userCanEditInterface );
161 // Insert tags defined somewhere but never applied
162 foreach ( $definedTags as $tag ) {
163 if ( !isset( $tagStats[$tag] ) ) {
164 $html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanDelete, $userCanEditInterface );
168 $out->addHTML( Xml
::tags(
170 [ 'class' => 'mw-datatable sortable mw-tags-table' ],
175 function doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks ) {
177 $newRow .= Xml
::tags( 'td', null, Xml
::element( 'code', null, $tag ) );
179 $disp = ChangeTags
::tagDescription( $tag );
180 if ( $showEditLinks ) {
182 $editLink = Linker
::link(
183 $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
184 $this->msg( 'tags-edit' )->escaped()
186 $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
188 $newRow .= Xml
::tags( 'td', null, $disp );
190 $msg = $this->msg( "tag-$tag-description" );
191 $desc = !$msg->exists() ?
'' : $msg->parse();
192 if ( $showEditLinks ) {
194 $editDescLink = Linker
::link(
195 $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
196 $this->msg( 'tags-edit' )->escaped()
198 $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
200 $newRow .= Xml
::tags( 'td', null, $desc );
203 $isExtension = isset( $this->extensionDefinedTags
[$tag] );
204 $isExplicit = isset( $this->explicitlyDefinedTags
[$tag] );
205 if ( $isExtension ) {
206 $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
209 $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
211 if ( !$sourceMsgs ) {
212 $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
214 $newRow .= Xml
::tags( 'td', null, implode( Xml
::element( 'br' ), $sourceMsgs ) );
216 $isActive = $isExplicit ||
isset( $this->extensionActivatedTags
[$tag] );
217 $activeMsg = ( $isActive ?
'tags-active-yes' : 'tags-active-no' );
218 $newRow .= Xml
::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
220 $hitcountLabel = $this->msg( 'tags-hitcount' )->numParams( $hitcount )->escaped();
221 if ( $this->getConfig()->get( 'UseTagFilter' ) ) {
222 $hitcountLabel = Linker
::link(
223 SpecialPage
::getTitleFor( 'Recentchanges' ),
226 [ 'tagfilter' => $tag ]
230 // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
231 $newRow .= Xml
::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
237 if ( $showDeleteActions && ChangeTags
::canDeleteTag( $tag )->isOK() ) {
238 $actionLinks[] = Linker
::linkKnown( $this->getPageTitle( 'delete' ),
239 $this->msg( 'tags-delete' )->escaped(),
244 if ( $showManageActions ) { // we've already checked that the user had the requisite userright
247 if ( ChangeTags
::canActivateTag( $tag )->isOK() ) {
248 $actionLinks[] = Linker
::linkKnown( $this->getPageTitle( 'activate' ),
249 $this->msg( 'tags-activate' )->escaped(),
255 if ( ChangeTags
::canDeactivateTag( $tag )->isOK() ) {
256 $actionLinks[] = Linker
::linkKnown( $this->getPageTitle( 'deactivate' ),
257 $this->msg( 'tags-deactivate' )->escaped(),
262 $newRow .= Xml
::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
265 return Xml
::tags( 'tr', null, $newRow ) . "\n";
268 public function processCreateTagForm( array $data, HTMLForm
$form ) {
269 $context = $form->getContext();
270 $out = $context->getOutput();
272 $tag = trim( strval( $data['Tag'] ) );
273 $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
274 $status = ChangeTags
::createTagWithChecks( $tag, $data['Reason'],
275 $context->getUser(), $ignoreWarnings );
277 if ( $status->isGood() ) {
278 $out->redirect( $this->getPageTitle()->getLocalURL() );
280 } elseif ( $status->isOK() ) {
281 // we have some warnings, so we show a confirmation form
285 'default' => $data['Tag'],
289 'default' => $data['Reason'],
291 'IgnoreWarnings' => [
297 // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
298 // we get into an infinite loop!
299 $context->getRequest()->unsetVal( 'wpEditToken' );
301 $headerText = $this->msg( 'tags-create-warnings-above', $tag,
302 count( $status->getWarningsArray() ) )->parseAsBlock() .
303 $out->parse( $status->getWikiText() ) .
304 $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
306 $subform = new HTMLForm( $fields, $this->getContext() );
307 $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
308 $subform->setWrapperLegendMsg( 'tags-create-heading' );
309 $subform->setHeaderText( $headerText );
310 $subform->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
311 $subform->setSubmitTextMsg( 'htmlform-yes' );
314 $out->addBacklinkSubtitle( $this->getPageTitle() );
317 $out->addWikiText( "<div class=\"error\">\n" . $status->getWikiText() .
323 protected function showDeleteTagForm( $tag ) {
324 $user = $this->getUser();
325 if ( !$user->isAllowed( 'deletechangetags' ) ) {
326 throw new PermissionsError( 'deletechangetags' );
329 $out = $this->getOutput();
330 $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
331 $out->addBacklinkSubtitle( $this->getPageTitle() );
333 // is the tag actually able to be deleted?
334 $canDeleteResult = ChangeTags
::canDeleteTag( $tag, $user );
335 if ( !$canDeleteResult->isGood() ) {
336 $out->addWikiText( "<div class=\"error\">\n" . $canDeleteResult->getWikiText() .
338 if ( !$canDeleteResult->isOK() ) {
343 $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
344 $tagUsage = ChangeTags
::tagUsageStatistics();
345 if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
346 $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
347 $tagUsage[$tag] )->parseAsBlock();
349 $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
351 // see if the tag is in use
352 $this->extensionActivatedTags
= array_fill_keys(
353 ChangeTags
::listExtensionActivatedTags(), true );
354 if ( isset( $this->extensionActivatedTags
[$tag] ) ) {
355 $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
359 $fields['Reason'] = [
361 'label' => $this->msg( 'tags-delete-reason' )->plain(),
364 $fields['HiddenTag'] = [
371 $form = new HTMLForm( $fields, $this->getContext() );
372 $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
373 $form->tagAction
= 'delete'; // custom property on HTMLForm object
374 $form->setSubmitCallback( [ $this, 'processTagForm' ] );
375 $form->setSubmitTextMsg( 'tags-delete-submit' );
376 $form->setSubmitDestructive(); // nasty!
377 $form->addPreText( $preText );
381 protected function showActivateDeactivateForm( $tag, $activate ) {
382 $actionStr = $activate ?
'activate' : 'deactivate';
384 $user = $this->getUser();
385 if ( !$user->isAllowed( 'managechangetags' ) ) {
386 throw new PermissionsError( 'managechangetags' );
389 $out = $this->getOutput();
390 // tags-activate-title, tags-deactivate-title
391 $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
392 $out->addBacklinkSubtitle( $this->getPageTitle() );
394 // is it possible to do this?
395 $func = $activate ?
'canActivateTag' : 'canDeactivateTag';
396 $result = ChangeTags
::$func( $tag, $user );
397 if ( !$result->isGood() ) {
398 $out->addWikiText( "<div class=\"error\">\n" . $result->getWikiText() .
400 if ( !$result->isOK() ) {
405 // tags-activate-question, tags-deactivate-question
406 $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
409 // tags-activate-reason, tags-deactivate-reason
410 $fields['Reason'] = [
412 'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
415 $fields['HiddenTag'] = [
422 $form = new HTMLForm( $fields, $this->getContext() );
423 $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
424 $form->tagAction
= $actionStr;
425 $form->setSubmitCallback( [ $this, 'processTagForm' ] );
426 // tags-activate-submit, tags-deactivate-submit
427 $form->setSubmitTextMsg( "tags-$actionStr-submit" );
428 $form->addPreText( $preText );
432 public function processTagForm( array $data, HTMLForm
$form ) {
433 $context = $form->getContext();
434 $out = $context->getOutput();
436 $tag = $data['HiddenTag'];
437 $status = call_user_func( [ 'ChangeTags', "{$form->tagAction}TagWithChecks" ],
438 $tag, $data['Reason'], $context->getUser(), true );
440 if ( $status->isGood() ) {
441 $out->redirect( $this->getPageTitle()->getLocalURL() );
443 } elseif ( $status->isOK() && $form->tagAction
=== 'delete' ) {
444 // deletion succeeded, but hooks raised a warning
445 $out->addWikiText( $this->msg( 'tags-delete-warnings-after-delete', $tag,
446 count( $status->getWarningsArray() ) )->text() . "\n" .
447 $status->getWikitext() );
448 $out->addReturnTo( $this->getPageTitle() );
451 $out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() .
458 * Return an array of subpages that this special page will accept.
460 * @return string[] subpages
462 public function getSubpagesForPrefixSearch() {
463 // The subpages does not have an own form, so not listing it at the moment
472 protected function getGroupName() {