TTO has uploaded a new change for review.
https://gerrit.wikimedia.org/r/181958
Change subject: Allow sysops to delete change tags
......................................................................
Allow sysops to delete change tags
Currently, if a tag is misspelt (vandlaism) or no longer wanted (HHVM,
eventually VisualEditor), the wiki is stuck with it forever. This change
allows users with the "deletechangetags" right to delete change tags from
the database, including removing them from all revisions to which they
are applied.
Obviously this is a powerful thing to be able to do, but I view change
tags as a "light" kind of interface, useful for revision patrolling and
spam/vandalism fighting but not something that necessarily needs to hang
around forever. It's not a big deal for this kind of data to be thrown
away without being archived anywhere.
For justification, see bug T58237, where this functionality would have
been nice to have, and Special:Tags on enwiki, where you will see random
old tags like "michael jackson" and "Removal of de: interwiki link".
Change-Id: I77f476c8d0f32c80f720aa2c5e66869c81faa282
---
M includes/ChangeTags.php
M includes/DefaultSettings.php
M includes/specials/SpecialTags.php
M languages/i18n/en.json
M languages/i18n/qqq.json
5 files changed, 263 insertions(+), 1 deletion(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core
refs/changes/58/181958/1
diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php
index 9ee2460..ef41570 100644
--- a/includes/ChangeTags.php
+++ b/includes/ChangeTags.php
@@ -238,6 +238,77 @@
}
/**
+ * Permanently removes all traces of a tag from the DB. Good for
removing
+ * misspelt or temporary tags.
+ *
+ * @param string $tag Tag to remove
+ * @return bool False if no changes are made, otherwise true
+ */
+ public static function deleteTagEverywhere( $tag ) {
+ // delete from valid_tag
+ $dbw = wfGetDB( DB_MASTER );
+ $result = $dbw->delete( 'valid_tag', array( 'vt_tag' => $tag ),
__METHOD__ );
+ if ( $result !== true ) {
+ $dbw->rollback( __METHOD__ );
+ return $result;
+ }
+
+ // find out which revisions use this tag, so we can delete from
tag_summary
+ $result = $dbw->select( 'change_tag',
+ array( 'ct_rc_id', 'ct_log_id', 'ct_rev_id', 'ct_tag' ),
+ array( 'ct_tag' => $tag ),
+ __METHOD__ );
+ foreach ( $result as $row ) {
+ if ( $row->ct_rev_id ) {
+ $field = 'ts_rev_id';
+ $fieldValue = $row->ct_rev_id;
+ } elseif ( $row->ct_log_id ) {
+ $field = 'ts_log_id';
+ $fieldValue = $row->ct_log_id;
+ } elseif ( $row->ct_rc_id ) {
+ $field = 'ts_rc_id';
+ $fieldValue = $row->ct_rc_id;
+ } else {
+ // don't know what's up; just skip it
+ continue;
+ }
+
+ // remove the tag from the relevant row of tag_summary
+ $tsResult = $dbw->selectField( 'tag_summary',
+ 'ts_tags',
+ array( $field => $fieldValue ),
+ __METHOD__ );
+ $tsValues = explode( ',', $tsResult );
+ $tsValues = array_values( array_diff( $tsValues, array(
$tag ) ) );
+ if ( !$tsValues ) {
+ // no tags left, so delete the row altogether
+ $result = $dbw->delete( 'tag_summary',
+ array( $field => $fieldValue ),
+ __METHOD__ );
+ } else {
+ $result = $dbw->update( 'tag_summary',
+ array( 'ts_tags' => implode( ',',
$tsValues ) ),
+ array( $field => $fieldValue ),
+ __METHOD__ );
+ }
+
+ if ( $result !== true ) {
+ $dbw->rollback( __METHOD__ );
+ return false;
+ }
+ }
+
+ // delete from change_tag
+ $result = $dbw->delete( 'change_tag', array( 'ct_tag' => $tag
), __METHOD__ );
+ if ( $result !== true ) {
+ $dbw->rollback( __METHOD__ );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
* Build a text box to select a change tag
*
* @param string $selected Tag to select by default
@@ -330,6 +401,39 @@
}
/**
+ * Checks whether the gievn tag is defined (listed in the 'valid_tag'
table).
+ *
+ * Tries memcached first.
+ *
+ * @param string $tag
+ */
+ public static function isTagDefined( $tag ) {
+ // Caching...
+ global $wgMemc;
+ $key = wfMemcKey( 'valid-tags' );
+ $tags = $wgMemc->get( $key );
+ if ( $tags ) {
+ return in_array( $tag, $tags );
+ }
+
+ // Some DB stuff
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'valid_tag', 'vt_tag', array( 'vt_tag' =>
$tag ), __METHOD__ );
+ if ( $res ) {
+ return true;
+ }
+
+ // If not in DB, ask hooks
+ Hooks::run( 'ListDefinedTags', array( &$emptyTags ) );
+
+ $emptyTags = array_filter( array_unique( $emptyTags ) );
+
+ // Short-term caching.
+ $wgMemc->set( $key, $emptyTags, 300 );
+ return $emptyTags;
+ }
+
+ /**
* Returns a map of any tags used on the wiki to number of edits
* tagged with them, ordered descending by the hitcount.
*
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 4261c68..492a12b 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -4600,6 +4600,7 @@
#$wgGroupPermissions['sysop']['pagelang'] = true;
#$wgGroupPermissions['sysop']['upload_by_url'] = true;
$wgGroupPermissions['sysop']['mergehistory'] = true;
+$wgGroupPermissions['sysop']['deletechangetags'] = true;
// Permission to change users' group assignments
$wgGroupPermissions['bureaucrat']['userrights'] = true;
diff --git a/includes/specials/SpecialTags.php
b/includes/specials/SpecialTags.php
index b762728..0095f2b 100644
--- a/includes/specials/SpecialTags.php
+++ b/includes/specials/SpecialTags.php
@@ -40,16 +40,39 @@
$this->setHeaders();
$this->outputHeader();
+ // Are we being asked to delete a tag?
+ $request = $this->getRequest();
+ switch ( $request->getVal( 'action' ) ) {
+ case 'delete':
+ $this->deleteTag( $request->getVal( 'tag' ) );
+ break;
+ default:
+ $this->doTagList();
+ break;
+ }
+ }
+
+ function doTagList() {
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'tags-title' ) );
$out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>",
'tags-intro' );
+
+ $user = $this->getUser();
+ // Whether to show the "Actions" column
+ // If any actions added in the future require other user
rights, add those
+ // rights here
+ $showActions = $user->isAllowedAny( 'deletechangetags' );
// Write the headers
$html = Xml::tags( 'tr', null, Xml::tags( 'th', null,
$this->msg( 'tags-tag' )->parse() ) .
Xml::tags( 'th', null, $this->msg(
'tags-display-header' )->parse() ) .
Xml::tags( 'th', null, $this->msg(
'tags-description-header' )->parse() ) .
Xml::tags( 'th', null, $this->msg( 'tags-active-header'
)->parse() ) .
- Xml::tags( 'th', null, $this->msg(
'tags-hitcount-header' )->parse() )
+ Xml::tags( 'th', null, $this->msg(
'tags-hitcount-header' )->parse() ) .
+ ( $showActions ?
+ Xml::tags( 'th', array( 'class' => 'unsortable'
),
+ $this->msg( 'tags-actions-header'
)->parse() ) :
+ '' )
);
// Used in #doTagRow()
@@ -109,9 +132,117 @@
// add raw $hitcount for sorting, because tags-hitcount
contains numbers and letters
$newRow .= Xml::tags( 'td', array( 'data-sort-value' =>
$hitcount ), $hitcountLink );
+ // actions
+ $actionLinks = array();
+ if ( $user->isAllowed( 'deletechangetags' ) ) {
+ $actionLinks[] = Linker::linkKnown(
$this->getPageTitle(),
+ $this->msg( 'tags-delete' )->escaped(),
+ array(),
+ array( 'action' => 'delete', 'tag' => $tag ) );
+ }
+ if ( count( $actionLinks ) ) {
+ $newRow .= Xml::tags( 'td', null,
$this->getLanguage()->pipeList( $actionLinks ) );
+ }
+
return Xml::tags( 'tr', null, $newRow ) . "\n";
}
+ protected function deleteTag( $tag ) {
+ $user = $this->getUser();
+ if ( !$user->isAllowed( 'deletechangetags' ) ) {
+ throw new PermissionsError( 'deletechangetags' );
+ }
+
+ $out = $this->getOutput();
+ $out->preventClickjacking();
+ $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
+
+ // is the tag actually able to be deleted?
+ $tagUsage = ChangeTags::tagUsageStatistics();
+ if ( !isset( $tagUsage[$tag] ) ) {
+ $out->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
+ array( 'tags-delete-not-found', $tag ) );
+ return;
+ } else {
+ $preText = $this->msg(
'tags-delete-explanation-initial', $tag )->parseAsBlock();
+ if ( $tagUsage[$tag] > 0 ) {
+ $preText .= $this->msg(
'tags-delete-explanation-in-use', $tag,
+ $tagUsage[$tag] )->parseAsBlock();
+ }
+ $preText .= $this->msg(
'tags-delete-explanation-warning', $tag )->parseAsBlock();
+ }
+ // see if the tag is in use
+ $definedTags = ChangeTags::listDefinedTags();
+ if ( in_array( $tag, $definedTags ) ) {
+ $preText .= $this->msg(
'tags-delete-explanation-active', $tag )->parseAsBlock();
+ }
+
+ $fields = array();
+ if ( $user->isAllowedAll( 'editinterface', 'delete' ) ) {
+ $fields['DeleteMessages'] = array(
+ 'type' => 'check',
+ 'label-message' => 'tags-delete-messages',
+ 'default' => false,
+ );
+ }
+ $fields['HiddenAction'] = array(
+ 'type' => 'hidden',
+ 'name' => 'action',
+ 'default' => 'delete',
+ );
+ $fields['HiddenTag'] = array(
+ 'type' => 'hidden',
+ 'name' => 'tag',
+ 'default' => $tag,
+ );
+
+ $form = new HTMLForm( $fields, $this->getContext() );
+ $form->setSubmitCallback( array( __CLASS__, 'processDeleteTag'
) );
+ $form->setSubmitTextMsg( 'tags-delete-submit' );
+ $form->addPreText( $preText );
+ $form->show();
+
+ // if $form->show() didn't send us off somewhere else, let's
set our
+ // breadcrumb link
+ $out->addBacklinkSubtitle( $this->getPageTitle() );
+ }
+
+ public static function processDeleteTag( array $data, HTMLForm $form ) {
+ $tag = $data['HiddenTag'];
+ if ( !$tag ) {
+ // nothing to do...
+ $sp = new SpecialTags;
+ $sp->doTagList();
+ return true;
+ }
+
+ $context = $form->getContext();
+ $out = $context->getOutput();
+
+ // does the tag exist?
+ $tagUsage = ChangeTags::tagUsageStatistics();
+ if ( !isset( $tagUsage[$tag] ) ) {
+ $out->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
+ array( 'tags-delete-not-found', $tag ) );
+ return false;
+ }
+
+ // do it!
+ if ( ChangeTags::deleteTagEverywhere( $tag ) ) {
+ $out->wrapWikiMsg( "<div
class=\"success\">\n$1\n</div>",
+ 'tags-delete-success', $data['HiddenTag'] );
+ $sp = new SpecialTags;
+ $sp->doTagList();
+ return true;
+ } else {
+ // some kind of error
+ $out->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
+ 'tags-delete-error', $data['HiddenTag'] );
+ // redisplay the form, in case the error was transient
+ return false;
+ }
+ }
+
protected function getGroupName() {
return 'changes';
}
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index b0751e3..e20621d 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -1141,6 +1141,7 @@
"right-override-export-depth": "Export pages including linked pages up
to a depth of 5",
"right-sendemail": "Send email to other users",
"right-passwordreset": "View password reset emails",
+ "right-deletechangetags": "Remove [[Special:Tags|tags]] from the
database",
"newuserlogpage": "User creation log",
"newuserlogpagetext": "This is a log of user creations.",
"rightslog": "User rights log",
@@ -1187,6 +1188,7 @@
"action-viewmyprivateinfo": "view your private information",
"action-editmyprivateinfo": "edit your private information",
"action-editcontentmodel": "edit the content model of a page",
+ "action-deletechangetags": "remove tags from the database",
"nchanges": "$1 {{PLURAL:$1|change|changes}}",
"enhancedrc-since-last-visit": "$1 {{PLURAL:$1|since last visit}}",
"enhancedrc-history": "history",
@@ -3355,10 +3357,21 @@
"tags-description-header": "Full description of meaning",
"tags-active-header": "Active?",
"tags-hitcount-header": "Tagged changes",
+ "tags-actions-header": "Actions",
"tags-active-yes": "Yes",
"tags-active-no": "No",
"tags-edit": "edit",
+ "tags-delete": "delete",
"tags-hitcount": "$1 {{PLURAL:$1|change|changes}}",
+ "tags-delete-title": "Delete tag",
+ "tags-delete-explanation-initial": "You are about to delete the tag
\"$1\" from the database.",
+ "tags-delete-explanation-in-use": "It will be removed from
{{PLURAL:$2|$2 revision or log entry|all $2 revisions and/or log entries}} to
which it is currently applied.",
+ "tags-delete-explanation-warning": "This action is '''irreversible'''
and '''cannot be undone''', not even by database administrators. Be certain
this is the tag you mean to delete.",
+ "tags-delete-explanation-active": "'''The tag \"$1\" is still active,
and will continue to be applied in the future.''' To stop this from happening,
go to the place(s) where the tag is set to be applied, and disable it there.",
+ "tags-delete-submit": "Irreversibly delete this tag",
+ "tags-delete-not-found": "The tag \"$1\" does not exist.",
+ "tags-delete-success": "The tag \"$1\" was successfully deleted.",
+ "tags-delete-error": "Failed to delete the tag \"$1\".",
"comparepages": "Compare pages",
"comparepages-summary": "",
"compare-page1": "Page 1",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index 04a6f9d..347804c 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -1305,6 +1305,7 @@
"right-override-export-depth": "{{doc-right|override-export-depth}}",
"right-sendemail": "{{doc-right|sendemail}}",
"right-passwordreset": "{{doc-right|passwordreset}}",
+ "right-deletechangetags": "{{doc-right|deletechangetags}}",
"newuserlogpage": "{{doc-logpage}}\n\nPart of the \"Newuserlog\"
extension. It is both the title of [[Special:Log/newusers]] and the link you
can see in [[Special:RecentChanges]].",
"newuserlogpagetext": "Part of the \"Newuserlog\" extension. It is the
description you can see on [[Special:Log/newusers]].",
"rightslog": "{{doc-logpage}}\n\nIn [[Special:Log]]",
@@ -1351,6 +1352,7 @@
"action-viewmyprivateinfo": "{{doc-action|viewmyprivateinfo}}",
"action-editmyprivateinfo": "{{doc-action|editmyprivateinfo}}",
"action-editcontentmodel": "{{doc-action|editcontentmodel}}",
+ "right-deletechangetags": "{{doc-action|deletechangetags}}",
"nchanges": "Appears on enhanced watchlist and recent changes when page
has more than one change on given date, linking to a diff of the
changes.\n\nParameters:\n* $1 - the number of changes on that day (2 or
more)\nThree messages are shown side-by-side: ({{msg-mw|Nchanges}} |
{{msg-mw|Enhancedrc-since-last-visit}} | {{msg-mw|Enhancedrc-history}}).",
"enhancedrc-since-last-visit": "Appears on enhanced watchlist and
recent changes when page has more than one change on given date and at least
one that the user hasn't seen yet, linking to a diff of the unviewed
changes.\n\nParameters:\n* $1 - the number of unviewed changes (1 or
more)\nThree messages are shown side-by-side: ({{msg-mw|nchanges}} |
{{msg-mw|enhancedrc-since-last-visit}} | {{msg-mw|enhancedrc-history}}).",
"enhancedrc-history": "Appears on enhanced watchlist and recent changes
when page has more than one change on given date, linking to its
history.\n\nThis is the same as {{msg-mw|hist}}, but not abbreviated.\n\nThree
messages are shown side-by-side: ({{msg-mw|nchanges}} |
{{msg-mw|enhancedrc-since-last-visit}} |
{{msg-mw|enhancedrc-history}}).\n{{Identical|History}}",
@@ -3519,10 +3521,21 @@
"tags-description-header": "Caption of a column in [[Special:Tags]].
For more information on tags see [[mw:Manual:Tags|MediaWiki]].",
"tags-active-header": "Caption of a column in [[Special:Tags]]. Values
are \"Yes\" or \"No\" to indicate if a tag that was ever used is current still
registered.\n\nSee example: [[mw:Special:Tags]].\n\nFor more information on
tags see [[mw:Manual:Tags|MediaWiki]].\n{{Identical|Active}}",
"tags-hitcount-header": "Caption of a column in [[Special:Tags]]. For
more information on tags see [[mw:Manual:Tags|MediaWiki]].",
+ "tags-actions-header": "Caption of a column in [[Special:Tags]]. The
column contains action links like \"delete\". For more information on tags see
[[mw:Manual:Tags|MediaWiki]].",
"tags-active-yes": "Table cell contents if given tag is
\"active\".\n\nSee also:\n* {{msg-mw|Tags-active-no}}\n{{Identical|Yes}}",
"tags-active-no": "Table cell contents if given tag is not
\"active\".\n\nSee also:\n* {{msg-mw|Tags-active-yes}}\n{{Identical|No}}",
"tags-edit": "Used on [[Special:Tags]]. Verb. Used as display text on a
link to create/edit a description.\n{{Identical|Edit}}",
+ "tags-delete": "Used on [[Special:Tags]]. Verb. Used as display text on
a link to delete a tag.\n{{Identical|Delete}}",
"tags-hitcount": "Shown in the \"{{msg-mw|Tags-hitcount-header}}\"
column in [[Special:Tags]]. For more information on tags see
[[mw:Manual:Tags|MediaWiki]].\n\nParameters:\n* $1 - the number of changes
marked with the tag",
+ "tags-delete-title": "The title of a page used to delete tags. For more
information on tags see [[mw:Manual:Tags|MediaWiki]].",
+ "tags-delete-explanation-initial": "The first paragraph of an
explanation to tell users what they are about to do.\n\nParameters:\n* $1 - the
code name of the tag that is about to be deleted",
+ "tags-delete-explanation-in-use": "The second paragraph (not always
shown) of an explanation to tell users what they are about to
do.\n\nParameters:\n* $1 - the code name of the tag that is about to be
deleted\n*$2 - the number of places the tag is used. The value is the sum of
(revisions + log entries) where the tag is used.",
+ "tags-delete-explanation-warning": "The third paragraph of an
explanation to tell users what they are about to do.\n\nParameters:\n* $1 - the
code name of the tag that is about to be deleted",
+ "tags-delete-explanation-active": "The fourth paragraph (not always
shown) of an explanation to tell users what they are about to
do.\n\nParameters:\n* $1 - the code name of the tag that is about to be
deleted\n*$2 - the number of places the tag is used. The value is the sum of
(rev",
+ "tags-delete-submit": "The label of the form \"submit\" button when the
user is about to delete a tag. The word \"irreversibly\" is used to emphasise
that the action destroys some data and is impossible to undo, even by server
administrators.",
+ "tags-delete-not-found": "Error message on [[Special:Tags]]",
+ "tags-delete-success": "Success message on [[Special:Tags]]",
+ "tags-delete-error": "Error message on [[Special:Tags]]",
"comparepages": "The title of [[Special:ComparePages]]",
"comparepages-summary": "{{doc-specialpagesummary|comparepages}}",
"compare-page1": "Label for the field of the 1st page in the comparison
for [[Special:ComparePages]]\n{{Identical|Page}}",
--
To view, visit https://gerrit.wikimedia.org/r/181958
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I77f476c8d0f32c80f720aa2c5e66869c81faa282
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: TTO <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits