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
{
31 * @var array List of defined tags
35 * @var array List of active tags
39 function __construct() {
40 parent
::__construct( 'Tags' );
43 function execute( $par ) {
45 $this->outputHeader();
47 $request = $this->getRequest();
50 $this->showDeleteTagForm( $request->getVal( 'tag' ) );
53 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), true );
56 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), false );
59 // fall through, thanks to HTMLForm's logic
66 function showTagList() {
67 $out = $this->getOutput();
68 $out->setPageTitle( $this->msg( 'tags-title' ) );
69 $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' );
71 $user = $this->getUser();
73 // Show form to create a tag
74 if ( $user->isAllowed( 'managechangetags' ) ) {
78 'label' => $this->msg( 'tags-create-tag-name' )->plain(),
83 'label' => $this->msg( 'tags-create-reason' )->plain(),
86 'IgnoreWarnings' => array(
91 $form = new HTMLForm( $fields, $this->getContext() );
92 $form->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
93 $form->setWrapperLegendMsg( 'tags-create-heading' );
94 $form->setHeaderText( $this->msg( 'tags-create-explanation' )->plain() );
95 $form->setSubmitCallback( array( $this, 'processCreateTagForm' ) );
96 $form->setSubmitTextMsg( 'tags-create-submit' );
99 // If processCreateTagForm generated a redirect, there's no point
100 // continuing with this, as the user is just going to end up getting sent
101 // somewhere else. Additionally, if we keep going here, we end up
102 // populating the memcache of tag data (see ChangeTags::listDefinedTags)
103 // with out-of-date data from the slave, because the slave hasn't caught
104 // up to the fact that a new tag has been created as part of an implicit,
105 // as yet uncommitted transaction on master.
106 if ( $out->getRedirect() !== '' ) {
111 // Whether to show the "Actions" column in the tag list
112 // If any actions added in the future require other user rights, add those
114 $showActions = $user->isAllowed( 'managechangetags' );
117 $html = Xml
::tags( 'tr', null, Xml
::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) .
118 Xml
::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) .
119 Xml
::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) .
120 Xml
::tags( 'th', null, $this->msg( 'tags-source-header' )->parse() ) .
121 Xml
::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) .
122 Xml
::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) .
124 Xml
::tags( 'th', array( 'class' => 'unsortable' ),
125 $this->msg( 'tags-actions-header' )->parse() ) :
129 // Used in #doTagRow()
130 $this->explicitlyDefinedTags
= array_fill_keys(
131 ChangeTags
::listExplicitlyDefinedTags(), true );
132 $this->extensionDefinedTags
= array_fill_keys(
133 ChangeTags
::listExtensionDefinedTags(), true );
134 $this->extensionActivatedTags
= array_fill_keys(
135 ChangeTags
::listExtensionActivatedTags(), true );
137 foreach ( ChangeTags
::tagUsageStatistics() as $tag => $hitcount ) {
138 $html .= $this->doTagRow( $tag, $hitcount, $showActions );
141 $out->addHTML( Xml
::tags(
143 array( 'class' => 'mw-datatable sortable mw-tags-table' ),
148 function doTagRow( $tag, $hitcount, $showActions ) {
149 $user = $this->getUser();
151 $newRow .= Xml
::tags( 'td', null, Xml
::element( 'code', null, $tag ) );
153 $disp = ChangeTags
::tagDescription( $tag );
154 if ( $user->isAllowed( 'editinterface' ) ) {
156 $editLink = Linker
::link(
157 Title
::makeTitle( NS_MEDIAWIKI
, "Tag-$tag" ),
158 $this->msg( 'tags-edit' )->escaped()
160 $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
162 $newRow .= Xml
::tags( 'td', null, $disp );
164 $msg = $this->msg( "tag-$tag-description" );
165 $desc = !$msg->exists() ?
'' : $msg->parse();
166 if ( $user->isAllowed( 'editinterface' ) ) {
168 $editDescLink = Linker
::link(
169 Title
::makeTitle( NS_MEDIAWIKI
, "Tag-$tag-description" ),
170 $this->msg( 'tags-edit' )->escaped()
172 $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
174 $newRow .= Xml
::tags( 'td', null, $desc );
176 $sourceMsgs = array();
177 $isExtension = isset( $this->extensionDefinedTags
[$tag] );
178 $isExplicit = isset( $this->explicitlyDefinedTags
[$tag] );
179 if ( $isExtension ) {
180 $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
183 $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
185 if ( !$sourceMsgs ) {
186 $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
188 $newRow .= Xml
::tags( 'td', null, implode( Xml
::element( 'br' ), $sourceMsgs ) );
190 $isActive = $isExplicit ||
isset( $this->extensionActivatedTags
[$tag] );
191 $activeMsg = ( $isActive ?
'tags-active-yes' : 'tags-active-no' );
192 $newRow .= Xml
::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
194 $hitcountLabel = $this->msg( 'tags-hitcount' )->numParams( $hitcount )->escaped();
195 $hitcountLink = Linker
::link(
196 SpecialPage
::getTitleFor( 'Recentchanges' ),
199 array( 'tagfilter' => $tag )
202 // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
203 $newRow .= Xml
::tags( 'td', array( 'data-sort-value' => $hitcount ), $hitcountLink );
206 $actionLinks = array();
207 if ( $showActions ) {
209 if ( ChangeTags
::canDeleteTag( $tag, $user )->isOK() ) {
210 $actionLinks[] = Linker
::linkKnown( $this->getPageTitle( 'delete' ),
211 $this->msg( 'tags-delete' )->escaped(),
213 array( 'tag' => $tag ) );
217 if ( ChangeTags
::canActivateTag( $tag, $user )->isOK() ) {
218 $actionLinks[] = Linker
::linkKnown( $this->getPageTitle( 'activate' ),
219 $this->msg( 'tags-activate' )->escaped(),
221 array( 'tag' => $tag ) );
225 if ( ChangeTags
::canDeactivateTag( $tag, $user )->isOK() ) {
226 $actionLinks[] = Linker
::linkKnown( $this->getPageTitle( 'deactivate' ),
227 $this->msg( 'tags-deactivate' )->escaped(),
229 array( 'tag' => $tag ) );
232 $newRow .= Xml
::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
235 return Xml
::tags( 'tr', null, $newRow ) . "\n";
238 public function processCreateTagForm( array $data, HTMLForm
$form ) {
239 $context = $form->getContext();
240 $out = $context->getOutput();
242 $tag = trim( strval( $data['Tag'] ) );
243 $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
244 $status = ChangeTags
::createTagWithChecks( $tag, $data['Reason'],
245 $context->getUser(), $ignoreWarnings );
247 if ( $status->isGood() ) {
248 $out->redirect( $this->getPageTitle()->getLocalURL() );
250 } elseif ( $status->isOK() ) {
251 // we have some warnings, so we show a confirmation form
255 'default' => $data['Tag'],
259 'default' => $data['Reason'],
261 'IgnoreWarnings' => array(
267 // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
268 // we get into an infinite loop!
269 $context->getRequest()->unsetVal( 'wpEditToken' );
271 $headerText = $this->msg( 'tags-create-warnings-above', $tag,
272 count( $status->getWarningsArray() ) )->parseAsBlock() .
273 $out->parse( $status->getWikitext() ) .
274 $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
276 $subform = new HTMLForm( $fields, $this->getContext() );
277 $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
278 $subform->setWrapperLegendMsg( 'tags-create-heading' );
279 $subform->setHeaderText( $headerText );
280 $subform->setSubmitCallback( array( $this, 'processCreateTagForm' ) );
281 $subform->setSubmitTextMsg( 'htmlform-yes' );
284 $out->addBacklinkSubtitle( $this->getPageTitle() );
287 $out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() .
293 protected function showDeleteTagForm( $tag ) {
294 $user = $this->getUser();
295 if ( !$user->isAllowed( 'managechangetags' ) ) {
296 throw new PermissionsError( 'managechangetags' );
299 $out = $this->getOutput();
300 $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
301 $out->addBacklinkSubtitle( $this->getPageTitle() );
303 // is the tag actually able to be deleted?
304 $canDeleteResult = ChangeTags
::canDeleteTag( $tag, $user );
305 if ( !$canDeleteResult->isGood() ) {
306 $out->addWikiText( "<div class=\"error\">\n" . $canDeleteResult->getWikiText() .
308 if ( !$canDeleteResult->isOK() ) {
313 $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
314 $tagUsage = ChangeTags
::tagUsageStatistics();
315 if ( $tagUsage[$tag] > 0 ) {
316 $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
317 $tagUsage[$tag] )->parseAsBlock();
319 $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
321 // see if the tag is in use
322 $this->extensionActivatedTags
= array_fill_keys(
323 ChangeTags
::listExtensionActivatedTags(), true );
324 if ( isset( $this->extensionActivatedTags
[$tag] ) ) {
325 $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
329 $fields['Reason'] = array(
331 'label' => $this->msg( 'tags-delete-reason' )->plain(),
334 $fields['HiddenTag'] = array(
341 $form = new HTMLForm( $fields, $this->getContext() );
342 $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
343 $form->tagAction
= 'delete'; // custom property on HTMLForm object
344 $form->setSubmitCallback( array( $this, 'processTagForm' ) );
345 $form->setSubmitTextMsg( 'tags-delete-submit' );
346 $form->setSubmitDestructive(); // nasty!
347 $form->addPreText( $preText );
351 protected function showActivateDeactivateForm( $tag, $activate ) {
352 $actionStr = $activate ?
'activate' : 'deactivate';
354 $user = $this->getUser();
355 if ( !$user->isAllowed( 'managechangetags' ) ) {
356 throw new PermissionsError( 'managechangetags' );
359 $out = $this->getOutput();
360 // tags-activate-title, tags-deactivate-title
361 $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
362 $out->addBacklinkSubtitle( $this->getPageTitle() );
364 // is it possible to do this?
365 $func = $activate ?
'canActivateTag' : 'canDeactivateTag';
366 $result = ChangeTags
::$func( $tag, $user );
367 if ( !$result->isGood() ) {
368 $out->wrapWikiMsg( "<div class=\"error\">\n$1" . $result->getWikiText() .
370 if ( !$result->isOK() ) {
375 // tags-activate-question, tags-deactivate-question
376 $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
379 // tags-activate-reason, tags-deactivate-reason
380 $fields['Reason'] = array(
382 'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
385 $fields['HiddenTag'] = array(
392 $form = new HTMLForm( $fields, $this->getContext() );
393 $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
394 $form->tagAction
= $actionStr;
395 $form->setSubmitCallback( array( $this, 'processTagForm' ) );
396 // tags-activate-submit, tags-deactivate-submit
397 $form->setSubmitTextMsg( "tags-$actionStr-submit" );
398 $form->addPreText( $preText );
402 public function processTagForm( array $data, HTMLForm
$form ) {
403 $context = $form->getContext();
404 $out = $context->getOutput();
406 $tag = $data['HiddenTag'];
407 $status = call_user_func( array( 'ChangeTags', "{$form->tagAction}TagWithChecks" ),
408 $tag, $data['Reason'], $context->getUser(), true );
410 if ( $status->isGood() ) {
411 $out->redirect( $this->getPageTitle()->getLocalURL() );
413 } elseif ( $status->isOK() && $form->tagAction
=== 'delete' ) {
414 // deletion succeeded, but hooks raised a warning
415 $out->addWikiText( $this->msg( 'tags-delete-warnings-after-delete', $tag,
416 count( $status->getWarningsArray() ) )->text() . "\n" .
417 $status->getWikitext() );
418 $out->addReturnTo( $this->getPageTitle() );
421 $out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() .
427 protected function getGroupName() {