TTO has uploaded a new change for review.
https://gerrit.wikimedia.org/r/182563
Change subject: [WIP] Creation, activation and improved management of change
tags
......................................................................
[WIP] Creation, activation and improved management of change tags
This allows users with the `managechangetags` right to create tags for
use by wiki users. (Currently there is no way for editors to apply tags
to their edits; that's to come in a later patch.)
Tags are now grouped into "sources", and this info is displayed in an
extra column on Special:Tags (and accessible via the API).
There are some possible sources:
* extension (Defined by an extension)
* manual (Applied manually by users and bots)
* none (No longer in use)
Tag activation is now a separate notion from definition, at least for
extension-defined tags. (The two concepts are still the same for user-
defined tags.)
YET TO INCLUDE:
* qqq
* complete API strings
* fix API QueryTags. Current implementation is bugged out (doesn't include
extension-defined tags).
Bug: T20670
Change-Id: If07aa53bdee69e35e514cd36c79aed06895a8bd2
---
M docs/hooks.txt
M includes/ChangeTags.php
M includes/DefaultSettings.php
A includes/api/ApiActivateTag.php
A includes/api/ApiCreateTag.php
A includes/api/ApiDeactivateTag.php
M includes/api/ApiMain.php
M includes/api/i18n/en.json
M includes/specials/SpecialTags.php
M languages/i18n/en.json
10 files changed, 745 insertions(+), 33 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core
refs/changes/63/182563/1
diff --git a/docs/hooks.txt b/docs/hooks.txt
index f5a375c..2f1da58 100644
--- a/docs/hooks.txt
+++ b/docs/hooks.txt
@@ -892,6 +892,13 @@
&$join_conds: join conditions for the tables
$opts: FormOptions for this request
+'ChangeTagCanCreate': Tell whether a change tag should be able to be created
+from the UI (Special:Tags) or via the API. You could use this hook if you want
+to reserve a specific "namespace" of tags, or something similar.
+$tag: name of the tag
+&$status: Status object. Add your errors, which will be relayed to the user.
+ If you set an error, the user will be unable to create the tag.
+
'ChangeTagAfterDelete': Called after a change tag has been deleted (that is,
removed from all revisions and log entries to which it was applied). This gives
extensions a chance to take it off their books.
@@ -909,6 +916,10 @@
$tag: name of the tag
&$status: Status object. See above.
+'ChangeTagsListActive': Allows you to nominate which of the tags your extension
+uses are in active use.
+&$tags: list of all active tags. Append to this array.
+
'LoginUserMigrated': Called during login to allow extensions the opportunity to
inform a user that their username doesn't exist for a specific reason, instead
of letting the login form give the generic error message that the account does
diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php
index ebfa0b5..73feb4b 100644
--- a/includes/ChangeTags.php
+++ b/includes/ChangeTags.php
@@ -244,6 +244,234 @@
}
/**
+ * Defines a tag in the valid_tag table.
+ * Extensions should NOT use this function; they can use the
ListDefinedTags
+ * hook instead.
+ *
+ * @param string $tag Tag to create
+ */
+ public static function defineTag( $tag ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace( 'valid_tag',
+ array( 'vt_tag' ),
+ array( 'vt_tag' => $tag ),
+ __METHOD__ );
+
+ // clear the memcache of defined tags
+ self::purgeTagCache();
+ }
+
+ /**
+ * Removes a tag from the valid_tag table. The tag may remain in use by
+ * extensions, and may still show up as 'defined' if an extension is
setting
+ * it from the ListDefinedTags hook.
+ *
+ * @param string $tag Tag to remove
+ */
+ public static function undefineTag( $tag ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete( 'valid_tag', array( 'vt_tag' => $tag ),
__METHOD__ );
+
+ // clear the memcache of defined tags
+ self::purgeTagCache();
+ }
+
+ /**
+ * Writes a tag action into the tagmanagement log.
+ *
+ * @param string $action
+ * @param string $tag
+ * @param string $reason
+ * @param User $user Who to attribute the action to
+ * @param int $tagCount For deletion only, how many usages the tag had
before
+ * it was deleted.
+ */
+ protected static function logTagAction( $action, $tag, $reason, User
$user,
+ $tagCount = null ) {
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $logEntry = new ManualLogEntry( 'tagmanagement', $action );
+ $logEntry->setPerformer( $user );
+ // target page is not relevant, but it has to be set, so we
just put in
+ // the title of Special:Tags
+ $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
+ $logEntry->setComment( $reason );
+
+ $params = array( '4:tag' => $tag );
+ if ( !is_null( $tagCount ) ) {
+ $params['5:count'] = $tagCount;
+ }
+ $logEntry->setParameters( $params );
+ $logEntry->setRelations( array( 'Tag' => $tag ) );
+
+ return $logEntry->insert( $dbw );
+ }
+
+ /**
+ * Internal function to perform a logged tag management operation.
+ *
+ * @param string $action
+ * @param string $permissionFunc Name of can___Tag function
+ * @param string $actionFunc Name of function that does the action
itself
+ * @param string $tag
+ * @param string $reason
+ * @param User $user Who to give credit for the action
+ * @param bool $ignoreWarnings Can be used for API interaction, default
false
+ * @return Status If successful, the Status contains the ID of the
added log
+ * entry as its value
+ */
+ protected static function internalTagWithChecks( $action,
$permissionFunc,
+ $actionFunc, $tag, $reason, User $user, $ignoreWarnings ) {
+ }
+
+ /**
+ * Is it OK to allow the user to activate this tag?
+ *
+ * @param string $tag Tag that you are interested in activating
+ * @return Status
+ */
+ public static function canActivateTag( $tag ) {
+ // only undefined tags can be activated
+ $definedTags = self::listDefinedTags();
+ if ( in_array( $tag, $definedTags ) ) {
+ return Status::newFatal( 'tags-activate-not-allowed' );
+ }
+ return Status::newGood();
+ }
+
+ /**
+ * Activates a tag, checking whether it is allowed first, and adding a
log
+ * entry afterwards.
+ *
+ * Includes a call to ChangeTag::canActivateTag(), so your code doesn't
need
+ * to do that.
+ *
+ * @param string $tag
+ * @param string $reason
+ * @param User $user Who to give credit for the action
+ * @param bool $ignoreWarnings Can be used for API interaction, default
false
+ * @return Status If successful, the Status contains the ID of the
added log
+ * entry as its value
+ */
+ public static function activateTagWithChecks( $tag, $reason, User $user,
+ $ignoreWarnings = false ) {
+
+ // are we allowed to do this?
+ $result = self::canActivateTag( $tag );
+ if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
+ return $result;
+ }
+
+ // do it!
+ self::defineTag( $tag );
+
+ // log it
+ $logId = self::logTagAction( 'activate', $tag, $reason, $user );
+ return Status::newGood( $logId );
+ }
+
+ /**
+ * Is it OK to allow the user to deactivate this tag?
+ *
+ * @param string $tag Tag that you are interested in deactivating
+ * @return Status
+ */
+ public static function canDeactivateTag( $tag ) {
+ // only explicitly-defined tags can be deactivated
+ $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
+ if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
+ return Status::newFatal( 'tags-deactivate-not-allowed'
);
+ }
+ return Status::newGood();
+ }
+
+ /**
+ * Deactivates a tag, checking whether it is allowed first, and adding
a log
+ * entry afterwards.
+ *
+ * Includes a call to ChangeTag::canDeactivateTag(), so your code
doesn't need
+ * to do that.
+ *
+ * @param string $tag
+ * @param string $reason
+ * @param User $user Who to give credit for the action
+ * @param bool $ignoreWarnings Can be used for API interaction, default
false
+ * @return Status If successful, the Status contains the ID of the
added log
+ * entry as its value
+ */
+ public static function deactivateTagWithChecks( $tag, $reason, User
$user,
+ $ignoreWarnings = false ) {
+
+ // are we allowed to do this?
+ $result = self::canDeactivateTag( $tag );
+ if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
+ return $result;
+ }
+
+ // do it!
+ self::undefineTag( $tag );
+
+ // log it
+ $logId = self::logTagAction( 'deactivate', $tag, $reason, $user
);
+ return Status::newGood( $logId );
+ }
+
+ /**
+ * Is it OK to allow the user to create this tag?
+ *
+ * @param string $tag Tag that you are interested in creating
+ * @return Status
+ */
+ public static function canCreateTag( $tag ) {
+ // tags cannot contain commas
+ if ( strpos( $tag, ',' ) !== false ) {
+ return Status::newFatal( 'tags-create-no-commas' );
+ }
+
+ // does the tag already exist?
+ $tagUsage = self::tagUsageStatistics();
+ if ( isset( $tagUsage[$tag] ) ) {
+ return Status::newFatal( 'tags-create-already-exists',
$tag );
+ }
+
+ // check with hooks
+ $canCreateResult = Status::newGood();
+ Hooks::run( 'ChangeTagCanCreate', array( $tag,
&$canCreateResult ) );
+ return $canCreateResult;
+ }
+
+ /**
+ * Creates a tag by adding a row to the `valid_tag` table.
+ *
+ * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't
need to
+ * do that.
+ *
+ * @param string $tag
+ * @param string $reason
+ * @param User $user Who to give credit for the action
+ * @param bool $ignoreWarnings Can be used for API interaction, default
false
+ * @return Status If successful, the Status contains the ID of the
added log
+ * entry as its value
+ */
+ public static function createTagWithChecks( $tag, $reason, User $user,
+ $ignoreWarnings = false ) {
+
+ // are we allowed to do this?
+ $result = self::canCreateTag( $tag );
+ if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
+ return $result;
+ }
+
+ // do it!
+ self::defineTag( $tag );
+
+ // log it
+ $logId = self::logTagAction( 'create', $tag, $reason, $user );
+ return Status::newGood( $logId );
+ }
+
+ /**
* Permanently removes all traces of a tag from the DB. Good for
removing
* misspelt or temporary tags.
*
@@ -259,7 +487,7 @@
$dbw->begin( __METHOD__ );
// delete from valid_tag
- $dbw->delete( 'valid_tag', array( 'vt_tag' => $tag ),
__METHOD__ );
+ self::undefineTag( $tag );
// find out which revisions use this tag, so we can delete from
tag_summary
$result = $dbw->select( 'change_tag',
@@ -363,9 +591,9 @@
$ignoreWarnings = false ) {
// are we allowed to do this?
- $canDeleteResult = self::canDeleteTag( $tag );
- if ( $ignoreWarnings ? !$canDeleteResult->isOK() :
!$canDeleteResult->isGood() ) {
- return $canDeleteResult;
+ $result = self::canDeleteTag( $tag );
+ if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
+ return $result;
}
// store the tag usage statistics
@@ -378,20 +606,7 @@
}
// log it
- $dbw = wfGetDB( DB_MASTER );
- $logEntry = new ManualLogEntry( 'tagmanagement', 'delete' );
- $logEntry->setPerformer( $user );
- // target page is not relevant, but it has to be set, so we
just put in
- // the title of Special:Tags
- $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
- $logEntry->setComment( $reason );
- $logEntry->setParameters( array(
- '4:tag' => $tag,
- '5:count' => $tagUsage[$tag],
- ) );
- $logEntry->setRelations( array( 'Tag' => $tag ) );
- $logId = $logEntry->insert( $dbw );
-
+ $logId = self::logTagAction( 'delete', $tag, $reason, $user,
$tagUsage[$tag] );
return Status::newGood( $logId );
}
@@ -449,6 +664,29 @@
);
return $html;
+ }
+
+ /**
+ * Lists those tags which extensions report as being "active".
+ *
+ * @return array
+ */
+ public static function listExtensionActivatedTags() {
+ // Caching...
+ global $wgMemc;
+ $key = wfMemcKey( 'active-tags' );
+ $tags = $wgMemc->get( $key );
+ if ( $tags ) {
+ return $tags;
+ }
+
+ // ask extensions which tags they consider active
+ $extensionActive = array();
+ Hooks::run( 'ChangeTagsListActive', array( &$extensionActive )
);
+
+ // Short-term caching.
+ $wgMemc->set( $key, $extensionActive, 300 );
+ return $extensionActive;
}
/**
@@ -530,6 +768,7 @@
*/
public static function purgeTagCache() {
global $wgMemc;
+ $wgMemc->delete( wfMemcKey( 'active-tags' ) );
$wgMemc->delete( wfMemcKey( 'valid-tags-db' ) );
$wgMemc->delete( wfMemcKey( 'valid-tags-hook' ) );
$wgMemc->delete( wfMemcKey( 'change-tag-statistics' ) );
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 66a93e7..2b261b3 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -6675,7 +6675,10 @@
'upload/overwrite' => 'LogFormatter',
'upload/revert' => 'LogFormatter',
'merge/merge' => 'MergeLogFormatter',
+ 'tagmanagement/create' => 'TagManagementLogFormatter',
'tagmanagement/delete' => 'TagManagementLogFormatter',
+ 'tagmanagement/activate' => 'TagManagementLogFormatter',
+ 'tagmanagement/deactivate' => 'TagManagementLogFormatter',
);
/**
diff --git a/includes/api/ApiActivateTag.php b/includes/api/ApiActivateTag.php
new file mode 100644
index 0000000..1e04481
--- /dev/null
+++ b/includes/api/ApiActivateTag.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup API
+ * @since 1.25
+ */
+class ApiActivateTag extends ApiBase {
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ // make sure the user is allowed
+ if ( !$this->getUser()->isAllowed( 'managechangetags' ) ) {
+ $this->dieUsage( "You don't have permission to manage
change tags", 'permissiondenied' );
+ }
+
+ $status = ChangeTags::activateTagWithChecks( $params['tag'],
$params['reason'],
+ $this->getUser(), $params['ignorewarnings'] );
+
+ if ( !$status->isGood() ) {
+ $this->dieStatus( $status );
+ }
+
+ $result = $this->getResult();
+ $result->addValue( null, $this->getModuleName(), array(
+ 'tag' => $params['tag'],
+ 'success' => '',
+ 'logid' => $status->getValue(),
+ ) );
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'tag' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ),
+ 'reason' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ),
+ 'ignorewarnings' => array(
+ ApiBase::PARAM_TYPE => 'boolean',
+ ApiBase::PARAM_DFLT => false,
+ ),
+ );
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ protected function getExamplesMessages() {
+ return array(
+
'action=activatetag&tag=spam&reason=For+use+in+edit+patrolling&token=123ABC'
+ => 'apihelp-createtag-example-simple',
+ );
+ }
+
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Tag_management';
+ }
+}
diff --git a/includes/api/ApiCreateTag.php b/includes/api/ApiCreateTag.php
new file mode 100644
index 0000000..075755a
--- /dev/null
+++ b/includes/api/ApiCreateTag.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup API
+ * @since 1.25
+ */
+class ApiCreateTag extends ApiBase {
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ // make sure the user is allowed
+ if ( !$this->getUser()->isAllowed( 'managechangetags' ) ) {
+ $this->dieUsage( "You don't have permission to manage
change tags", 'permissiondenied' );
+ }
+
+ $status = ChangeTags::createTagWithChecks( $params['tag'],
$params['reason'],
+ $this->getUser(), $params['ignorewarnings'] );
+
+ if ( !$status->isGood() ) {
+ $this->dieStatus( $status );
+ }
+
+ $result = $this->getResult();
+ $result->addValue( null, $this->getModuleName(), array(
+ 'tag' => $params['tag'],
+ 'success' => '',
+ 'logid' => $status->getValue(),
+ ) );
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'tag' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ),
+ 'reason' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ),
+ 'ignorewarnings' => array(
+ ApiBase::PARAM_TYPE => 'boolean',
+ ApiBase::PARAM_DFLT => false,
+ ),
+ );
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ protected function getExamplesMessages() {
+ return array(
+
'action=createtag&tag=spam&reason=For+use+in+edit+patrolling&token=123ABC'
+ => 'apihelp-createtag-example-simple',
+ );
+ }
+
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Tag_management';
+ }
+}
diff --git a/includes/api/ApiDeactivateTag.php
b/includes/api/ApiDeactivateTag.php
new file mode 100644
index 0000000..cbb9ce3
--- /dev/null
+++ b/includes/api/ApiDeactivateTag.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup API
+ * @since 1.25
+ */
+class ApiDeactivateTag extends ApiBase {
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ // make sure the user is allowed
+ if ( !$this->getUser()->isAllowed( 'managechangetags' ) ) {
+ $this->dieUsage( "You don't have permission to manage
change tags", 'permissiondenied' );
+ }
+
+ $status = ChangeTags::deactivateTagWithChecks( $params['tag'],
$params['reason'],
+ $this->getUser(), $params['ignorewarnings'] );
+
+ if ( !$status->isGood() ) {
+ $this->dieStatus( $status );
+ }
+
+ $result = $this->getResult();
+ $result->addValue( null, $this->getModuleName(), array(
+ 'tag' => $params['tag'],
+ 'success' => '',
+ 'logid' => $status->getValue(),
+ ) );
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'tag' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ),
+ 'reason' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ),
+ 'ignorewarnings' => array(
+ ApiBase::PARAM_TYPE => 'boolean',
+ ApiBase::PARAM_DFLT => false,
+ ),
+ );
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ protected function getExamplesMessages() {
+ return array(
+
'action=deactivatetag&tag=spam&reason=For+use+in+edit+patrolling&token=123ABC'
+ => 'apihelp-createtag-example-simple',
+ );
+ }
+
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Tag_management';
+ }
+}
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index a251029..8ceee99 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -87,7 +87,10 @@
'options' => 'ApiOptions',
'imagerotate' => 'ApiImageRotate',
'revisiondelete' => 'ApiRevisionDelete',
+ 'createtag' => 'ApiCreateTag',
'deletetag' => 'ApiDeleteTag',
+ 'activatetag' => 'ApiActivateTag',
+ 'deactivatetag' => 'ApiDeactivateTag',
);
/**
diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json
index 9764b1d..4b67a26 100644
--- a/includes/api/i18n/en.json
+++ b/includes/api/i18n/en.json
@@ -58,6 +58,12 @@
"apihelp-createaccount-example-pass": "Create user \"testuser\" with
password \"test123\"",
"apihelp-createaccount-example-mail": "Create user \"testmailuser\" and
email a randomly-generated password",
+ "apihelp-createtag-description": "Create a new change tag for manual
use.",
+ "apihelp-createtag-param-tag": "Tag to create.",
+ "apihelp-createtag-param-reason": "An optional reason for creating the
tag.",
+ "apihelp-createtag-param-ignorewarnings": "Whether to ignore any
warnings that are issued during the creation process.",
+ "apihelp-createtag-example-simple": "Create a tag named <kbd>spam</kbd>
with the reason <kbd>For use in edit patrolling</kbd>",
+
"apihelp-delete-description": "Delete a page.",
"apihelp-delete-param-title": "Title of the page you want to delete.
Cannot be used together with $1pageid.",
"apihelp-delete-param-pageid": "Page ID of the page you want to delete.
Cannot be used together with $1title.",
diff --git a/includes/specials/SpecialTags.php
b/includes/specials/SpecialTags.php
index eafe8ca..a84046c 100644
--- a/includes/specials/SpecialTags.php
+++ b/includes/specials/SpecialTags.php
@@ -31,6 +31,10 @@
* @var array List of defined tags
*/
public $definedTags;
+ /**
+ * @var array List of active tags
+ */
+ public $activeTags;
function __construct() {
parent::__construct( 'Tags' );
@@ -40,12 +44,19 @@
$this->setHeaders();
$this->outputHeader();
- // Are we being asked to delete a tag?
$request = $this->getRequest();
switch ( $par ) {
case 'delete':
$this->showDeleteTagForm( $request->getVal(
'tag' ) );
break;
+ case 'activate':
+ $this->showActivateDeactivateForm(
$request->getVal( 'tag' ), true );
+ break;
+ case 'deactivate':
+ $this->showActivateDeactivateForm(
$request->getVal( 'tag' ), false );
+ break;
+ case 'create':
+ // fall through, thanks to HTMLForm's logic
default:
$this->showTagList();
break;
@@ -58,7 +69,32 @@
$out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>",
'tags-intro' );
$user = $this->getUser();
- // Whether to show the "Actions" column
+
+ // Show form to create a tag
+ if ( $user->isAllowed( 'managechangetags' ) ) {
+ $fields = array(
+ 'Tag' => array(
+ 'type' => 'text',
+ 'label' => $this->msg(
'tags-create-tag-name' )->plain(),
+ 'required' => true,
+ ),
+ 'Reason' => array(
+ 'type' => 'text',
+ 'label' => $this->msg(
'tags-create-reason' )->plain(),
+ 'size' => 50,
+ ),
+ );
+
+ $form = new HTMLForm( $fields, $this->getContext() );
+ $form->setAction( $this->getPageTitle( 'create'
)->getLocalURL() );
+ $form->setWrapperLegendMsg( 'tags-create-heading' );
+ $form->setHeaderText( $this->msg(
'tags-create-explanation' )->plain() );
+ $form->setSubmitCallback( array( $this,
'processCreateTagForm' ) );
+ $form->setSubmitTextMsg( 'tags-create-submit' );
+ $form->show();
+ }
+
+ // Whether to show the "Actions" column in the tag list
// If any actions added in the future require other user
rights, add those
// rights here
$showActions = $user->isAllowed( 'managechangetags' );
@@ -67,6 +103,7 @@
$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-source-header'
)->parse() ) .
Xml::tags( 'th', null, $this->msg( 'tags-active-header'
)->parse() ) .
Xml::tags( 'th', null, $this->msg(
'tags-hitcount-header' )->parse() ) .
( $showActions ?
@@ -76,7 +113,12 @@
);
// Used in #doTagRow()
- $this->definedTags = array_fill_keys(
ChangeTags::listDefinedTags(), true );
+ $this->explicitlyDefinedTags = array_fill_keys(
+ ChangeTags::listExplicitlyDefinedTags(), true );
+ $this->extensionDefinedTags = array_fill_keys(
+ ChangeTags::listExtensionDefinedTags(), true );
+ $this->extensionActivatedTags = array_fill_keys(
+ ChangeTags::listExtensionActivatedTags(), true );
foreach ( ChangeTags::tagUsageStatistics() as $tag => $hitcount
) {
$html .= $this->doTagRow( $tag, $hitcount, $showActions
);
@@ -84,7 +126,7 @@
$out->addHTML( Xml::tags(
'table',
- array( 'class' => 'wikitable sortable mw-tags-table' ),
+ array( 'class' => 'mw-datatable sortable mw-tags-table'
),
$html
) );
}
@@ -92,7 +134,7 @@
function doTagRow( $tag, $hitcount, $showActions ) {
$user = $this->getUser();
$newRow = '';
- $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null,
$tag ) );
+ $newRow .= Xml::tags( 'td', null, Xml::element( 'kbd', null,
$tag ) );
$disp = ChangeTags::tagDescription( $tag );
if ( $user->isAllowed( 'editinterface' ) ) {
@@ -117,9 +159,23 @@
}
$newRow .= Xml::tags( 'td', null, $desc );
- $active = isset( $this->definedTags[$tag] ) ? 'tags-active-yes'
: 'tags-active-no';
- $active = $this->msg( $active )->escaped();
- $newRow .= Xml::tags( 'td', null, $active );
+ $sourceMsgs = array();
+ $isExtension = isset( $this->extensionDefinedTags[$tag] );
+ $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
+ if ( $isExtension ) {
+ $sourceMsgs[] = $this->msg( 'tags-source-extension'
)->escaped();
+ }
+ if ( $isExplicit ) {
+ $sourceMsgs[] = $this->msg( 'tags-source-manual'
)->escaped();
+ }
+ if ( !$sourceMsgs ) {
+ $sourceMsgs[] = $this->msg( 'tags-source-none'
)->escaped();
+ }
+ $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br'
), $sourceMsgs ) );
+
+ $isActive = $isExplicit || isset(
$this->extensionActivatedTags[$tag] );
+ $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no'
);
+ $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg
)->escaped() );
$hitcountLabel = $this->msg( 'tags-hitcount' )->numParams(
$hitcount )->escaped();
$hitcountLink = Linker::link(
@@ -135,9 +191,26 @@
// actions
$actionLinks = array();
if ( $showActions ) {
- if ( ChangeTags::canDeleteTag( $tag )->isGood() ) {
+ // delete
+ if ( ChangeTags::canDeleteTag( $tag )->isOK() ) {
$actionLinks[] = Linker::linkKnown(
$this->getPageTitle( 'delete' ),
$this->msg( 'tags-delete' )->escaped(),
+ array(),
+ array( 'tag' => $tag ) );
+ }
+
+ // activate
+ if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
+ $actionLinks[] = Linker::linkKnown(
$this->getPageTitle( 'activate' ),
+ $this->msg( 'tags-activate'
)->escaped(),
+ array(),
+ array( 'tag' => $tag ) );
+ }
+
+ // deactivate
+ if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
+ $actionLinks[] = Linker::linkKnown(
$this->getPageTitle( 'deactivate' ),
+ $this->msg( 'tags-deactivate'
)->escaped(),
array(),
array( 'tag' => $tag ) );
}
@@ -148,6 +221,23 @@
return Xml::tags( 'tr', null, $newRow ) . "\n";
}
+ public function processCreateTagForm( array $data, HTMLForm $form ) {
+ $context = $form->getContext();
+ $out = $context->getOutput();
+
+ $tag = trim( strval( $data['Tag'] ) );
+
+ $status = ChangeTags::createTagWithChecks( $tag,
$data['Reason'],
+ $context->getUser() );
+ if ( $status->isGood() ) {
+ $out->redirect( $this->getPageTitle()->getLocalURL() );
+ return true;
+ } else {
+ $out->addWikiText( "<div class=\"error\">\n" .
$status->getWikitext() .
+ "\n</div>" );
+ return false;
+ }
+ }
protected function showDeleteTagForm( $tag ) {
$user = $this->getUser();
@@ -178,8 +268,13 @@
$preText .= $this->msg( 'tags-delete-explanation-warning', $tag
)->parseAsBlock();
// see if the tag is in use
- $definedTags = ChangeTags::listDefinedTags();
- if ( in_array( $tag, $definedTags ) ) {
+ $this->explicitlyDefinedTags = array_fill_keys(
+ ChangeTags::listExplicitlyDefinedTags(), true );
+ $this->extensionActivatedTags = array_fill_keys(
+ ChangeTags::listExtensionActivatedTags(), true );
+ if ( isset( $this->explicitlyDefinedTags[$tag] ) ||
+ isset( $this->extensionActivatedTags[$tag] )
+ ) {
$preText .= $this->msg(
'tags-delete-explanation-active', $tag )->parseAsBlock();
}
@@ -198,7 +293,9 @@
$form = new HTMLForm( $fields, $this->getContext() );
$form->setAction( $this->getPageTitle( 'delete'
)->getLocalURL() );
- $form->setSubmitCallback( array( $this, 'processDeleteTagForm'
) );
+ $form->tagAction = 'delete'; // custom property on HTMLForm
object
+ $form->setSubmitCallback( array( $this, 'processTagForm' ) );
+ $form->setSubmitDestructive(); // nasty!
$form->setSubmitTextMsg( 'tags-delete-submit' );
$form->addPreText( $preText );
$form->show();
@@ -208,12 +305,71 @@
$out->addBacklinkSubtitle( $this->getPageTitle() );
}
- public function processDeleteTagForm( array $data, HTMLForm $form ) {
+ protected function showActivateDeactivateForm( $tag, $activate ) {
+ $actionStr = $activate ? 'activate' : 'deactivate';
+
+ $user = $this->getUser();
+ if ( !$user->isAllowed( 'managechangetags' ) ) {
+ throw new PermissionsError( 'managechangetags' );
+ }
+
+ $out = $this->getOutput();
+ $out->preventClickjacking();
+ $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
+
+ // is it possible to do this?
+ $this->explicitlyDefinedTags = array_fill_keys(
+ ChangeTags::listExplicitlyDefinedTags(), true );
+ $this->extensionActivatedTags = array_fill_keys(
+ ChangeTags::listExtensionActivatedTags(), true );
+ if ( $activate ? isset( $this->explicitlyDefinedTags[$tag] ) :
+ isset( $this->extensionActivatedTags[$tag] ) ) {
+ $out->wrapWikiMsg( "<div class=\"error\">\n$1" .
$canCreateResult->getWikiText() .
+ "\n</div>" );
+ if ( !$canCreateResult->isOK() ) {
+ return;
+ }
+ }
+
+ $preText = $this->msg( "tags-$actionStr-question", $tag
)->parseAsBlock();
+
+ $tagUsage = ChangeTags::tagUsageStatistics();
+ if ( !$activate && !$tagUsage[$tag] ) {
+ $preText .= $this->msg(
'tags-deactivate-question-delete', $tag )->parseAsBlock();
+ }
+
+ $fields = array();
+ $fields['Reason'] = array(
+ 'type' => 'text',
+ 'label' => $this->msg( "tags-$actionStr-reason"
)->plain(),
+ 'size' => 50,
+ );
+ $fields['HiddenTag'] = array(
+ 'type' => 'hidden',
+ 'name' => 'tag',
+ 'default' => $tag,
+ 'required' => true,
+ );
+
+ $form = new HTMLForm( $fields, $this->getContext() );
+ $form->setAction( $this->getPageTitle( $actionStr
)->getLocalURL() );
+ $form->tagAction = $actionStr;
+ $form->setSubmitCallback( array( $this, 'processTagForm' ) );
+ $form->setSubmitTextMsg( "tags-$actionStr-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 function processTagForm( array $data, HTMLForm $form ) {
$context = $form->getContext();
$out = $context->getOutput();
- $status = ChangeTags::deleteTagWithChecks( $data['HiddenTag'],
$data['Reason'],
- $context->getUser() );
+ $status = call_user_func( array( 'ChangeTags',
"{$form->tagAction}TagWithChecks" ),
+ $data['HiddenTag'], $data['Reason'],
$context->getUser() );
if ( $status->isGood() ) {
$out->redirect( $this->getPageTitle()->getLocalURL() );
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index 080867d..4e2af33 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -3355,14 +3355,27 @@
"tags-tag": "Tag name",
"tags-display-header": "Appearance on change lists",
"tags-description-header": "Full description of meaning",
+ "tags-source-header": "Source",
"tags-active-header": "Active?",
"tags-hitcount-header": "Tagged changes",
"tags-actions-header": "Actions",
"tags-active-yes": "Yes",
"tags-active-no": "No",
+ "tags-source-extension": "Defined by an extension",
+ "tags-source-manual": "Applied manually by users and bots",
+ "tags-source-none": "No longer in use",
"tags-edit": "edit",
"tags-delete": "delete",
+ "tags-activate": "activate",
+ "tags-deactivate": "deactivate",
"tags-hitcount": "$1 {{PLURAL:$1|change|changes}}",
+ "tags-create-heading": "Create a new tag",
+ "tags-create-explanation": "By default, newly created tags will be made
available for use by users and bots.",
+ "tags-create-tag-name": "Tag name:",
+ "tags-create-reason": "Reason:",
+ "tags-create-submit": "Create",
+ "tags-create-no-commas": "Tag names must not contain commas
(<code>,</code>).",
+ "tags-create-already-exists": "The tag \"$1\" already exists.",
"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.",
@@ -3373,6 +3386,17 @@
"tags-delete-not-allowed": "Tags defined by an extension cannot be
deleted unless the extension specifically allows it.",
"tags-delete-not-found": "The tag \"$1\" does not exist.",
"tags-delete-too-many-uses": "The tag \"$1\" is applied to more than $2
{{PLURAL:$2|revision|revisions}}, which means it cannot be deleted.",
+ "tags-activate-title": "Activate tag",
+ "tags-activate-question": "You are about to activate the tag \"$1\".",
+ "tags-activate-reason": "Reason:",
+ "tags-activate-not-allowed": "It is not possible to activate the tag
\"$1\".",
+ "tags-activate-submit": "Activate",
+ "tags-deactivate-title": "Deactivate tag",
+ "tags-deactivate-question": "You are about to deactivate the tag
\"$1\".",
+ "tags-deactivate-question-delete": "Because the tag \"$1\" is not
currently in use, deactivating it will have the same effect as deleting it.",
+ "tags-deactivate-reason": "Reason:",
+ "tags-deactivate-not-allowed": "It is not possible to deactivate the
tag \"$1\".",
+ "tags-deactivate-submit": "Deactivate",
"comparepages": "Compare pages",
"comparepages-summary": "",
"compare-page1": "Page 1",
@@ -3447,7 +3471,10 @@
"logentry-upload-revert": "$1 {{GENDER:$2|uploaded}} $3",
"log-name-tagmanagement": "Tag management log",
"log-description-tagmanagement": "This page lists management tasks
related to [[Special:Tags|tags]]. The log contains only actions carried out
manually by an administrator; tags may be created or deleted by the wiki
software without an entry being recorded in this log.",
+ "logentry-tagmanagement-create": "$1 {{GENDER:$2|created}} the tag
\"$4\"",
"logentry-tagmanagement-delete": "$1 {{GENDER:$2|deleted}} the tag
\"$4\" (removed from $5 {{PLURAL:$5|revision or log entry|revisions and/or log
entries}})",
+ "logentry-tagmanagement-activate": "$1 {{GENDER:$2|activated}} the tag
\"$4\" for use by users and bots",
+ "logentry-tagmanagement-deactivate": "$1 {{GENDER:$2|deactivated}} the
tag \"$4\" for use by users and bots",
"rightsnone": "(none)",
"revdelete-logentry": "changed revision visibility of \"[[$1]]\"",
"logdelete-logentry": "changed event visibility of \"[[$1]]\"",
--
To view, visit https://gerrit.wikimedia.org/r/182563
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: If07aa53bdee69e35e514cd36c79aed06895a8bd2
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: TTO <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: TTO <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits