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

Reply via email to