Cenarium has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/201905

Change subject: Use single hook to register tags and performance improvements
......................................................................

Use single hook to register tags and performance improvements

This introduces a single hook for extensions to register tags.
All of the tag params, including but not limited to 'active'
status, are provided in this single hook. New params are added :
source to indicate the extension's name (or a more specific project),
an array of params for use in a message specific to the source (for
AbuseFilter, this would be the abuse filters using the tag), and a
boolean 'problem' needed by T91425 and T89553.
The hooks ListDefinedTags, ChangeTagListActive and ChangeTagCanDelete
are merged in this hook and can be deprecated. It should improve
performance since the extensions will need to hit their database
only once.

Caching is overhauled so that we can have a cached list of tags
applied at least once for T27909. Up to date hitcounts are cached
separately for each tag, and incremented when applied. This way,
we don't have to purge the whole cache for every tags each time
one tag gets added. Stored and registered tags with their params
are cached separately. Extensions should purge the cache of
registered tags if the newly defined tags need to appear quickly
(such is the case for AbuseFilter - the current 5 min wait is
noticeable).

Performance improvements are made to Special:Tags, in particular use
of in-class cache. To this end, a function that makes a "change tag
object" is created. Finally, this adds a class of core tags, as
needed by T73236 and T90310 (but doesn't implement them).

Bug: T91535
Change-Id: I4f4b097d660ada77f5cf7b4231925009b27127ea
---
M docs/hooks.txt
M includes/ChangeTags.php
M includes/DefaultSettings.php
M includes/specials/SpecialTags.php
M languages/i18n/en.json
M languages/i18n/qqq.json
6 files changed, 719 insertions(+), 275 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/05/201905/1

diff --git a/docs/hooks.txt b/docs/hooks.txt
index 877b7ed..271c9ad 100644
--- a/docs/hooks.txt
+++ b/docs/hooks.txt
@@ -945,20 +945,34 @@
   using `$status->warning()`. Errors and warnings will be relayed to the user.
   If you set an error, the user will be unable to create the tag.
 
-'ChangeTagCanDelete': Tell whether a change tag should be able to be
-deleted from the UI (Special:Tags) or via the API. The default is that tags
-defined using the ListDefinedTags hook are not allowed to be deleted unless
-specifically allowed. If you wish to allow deletion of the tag, set
-`$status = Status::newGood()` to allow deletion, and then `return false` from
-the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry
-out custom deletion actions.
-$tag: name of the tag
-$user: user initiating the action
-&$status: Status object. See above.
+'ChangeTagCanDelete': Deprecated, use 'ChangeTagsRegister' instead
 
-'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.
+'ChangeTagsListActive': Deprecated, use 'ChangeTagsRegister' instead
+
+'ChangeTagsRegister': The hook to use to register a change tag along with
+all the relevant information, such as its active status, source (extension
+name or more specific), whether it is deletable, params to use in the
+source-specific message, etc.
+Available params :
+'active' (bool): Whether the tag is in active use by the extension.
+'problem' (bool): Whether the tag is meant to indicate a potential problem 
with the
+change (e.g. vandalism, syntax error, etc) - these can be highlighted in the
+user interface or used for specific purposes by extensions.
+'source' (string): Name of the extension, or a more specific project, issuing 
the tag.
+'msgParams' (array): An array of params for use by the tag description message 
specific
+to the source given, it is placed below the custom description message. You 
need
+to provide the message in your extension. The message key is given by
+'tags-description-extension-<<source>>' where <<source>> is the source you have
+provided in the corresponding param.
+'canDelete' (bool): Tell whether a change tag should be able to be deleted 
from the UI
+(Special:Tags) or via the API. The default is that tags registered with this 
hook are
+not allowed to be deleted unless specifically allowed. If you wish to allow 
deletion
+of the tag, set this param to true. This does not undefine the tag on its own, 
it only
+removes applied instances. Ensure you consume the 'ChangeTagAfterDelete' hook 
to carry
+out custom deletion actions (for example offering the opportunity for the 
admin to also
+undefine the tag).
+&$tags: the list of tags, append to this array using the tag name as key and
+an array of tag params as value
 
 'LoginUserMigrated': Called during login to allow extensions the opportunity to
 inform a user that their username doesn't exist for a specific reason, instead
@@ -1769,8 +1783,7 @@
 'LinksUpdateConstructed': At the end of LinksUpdate() is construction.
 &$linksUpdate: the LinksUpdate object
 
-'ListDefinedTags': When trying to find all defined tags.
-&$tags: The list of tags.
+'ListDefinedTags': Deprecated. Use 'ChangeTagsRegister' instead
 
 'LoadExtensionSchemaUpdates': Called during database installation and updates.
 &updater: A DatabaseUpdater subclass
diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php
index d597d6d..5c5d73e 100644
--- a/includes/ChangeTags.php
+++ b/includes/ChangeTags.php
@@ -29,6 +29,133 @@
        const MAX_DELETE_USES = 5000;
 
        /**
+        * Function making a change tag object from a tag name based on some 
context
+        * Make sure you provide all the relevant data for the checks you need
+        *
+        * @param string $tag tag's name
+        * @param array $tagStats tag usage statistics mapping each tag to its 
hitcount
+        * @param array $storedTags tags stored in the change_tag table of the 
database
+        * @param array $coreTags tags defined in core
+        * @param array $registeredTags tags defined by extensions
+        * in the last three lists, each tag is mapped to an array of params
+        * @return change tag object
+        */
+       public static function getChangeTagObject( $tag, $tagStats, $storedTags,
+               $coreTags, $registeredTags ) {
+
+               $ctObj = (object) array( 'name' => $tag );
+
+               $ctObj->isApplied = isset( $tagStats[$tag] );
+               $ctObj->isStored = isset( $storedTags[$tag] );
+               $ctObj->coreDefined = isset( $coreTags[$tag] );
+               $ctObj->extensionDefined = isset( $registeredTags[$tag] );
+
+               $ctObj->exists = $ctObj->isApplied || $ctObj->extensionDefined 
||
+                       $ctObj->isStored || $ctObj->coreDefined;
+
+               // shortcut
+               if ( !$ctObj->exists ) {
+                       return $ctObj;
+               }
+
+               // a change tag is user defined if it is stored in the 
valid_tag table and
+               // neither defined in core nor by an extension
+               $ctObj->userDefined = $ctObj->isStored && 
!$ctObj->extensionDefined &&
+                       !$ctObj->coreDefined;
+
+               // some info based on tag stats
+               $ctObj->hitcount = $ctObj->isApplied ? $tagStats[$tag] : 0;
+               $ctObj->isBig = ( $ctObj->hitcount > self::MAX_DELETE_USES );
+
+               // active status and problem status
+               if ( $ctObj->extensionDefined ) {
+                       // is the tag in active use by the extension ?
+                       $ctObj->isActive = isset( 
$registeredTags[$tag]['active'] ) &&
+                               $registeredTags[$tag]['active'];
+                       // is the tag meant to indicate a potential problem ? 
(for T91425, T89553)
+                       $ctObj->isProblem = isset( 
$registeredTags[$tag]['problem'] ) &&
+                               $registeredTags[$tag]['problem'];
+
+               } elseif ( $ctObj->coreDefined ) {
+                       // is automatic tagging enabled for this core tag ?
+                       $ctObj->isActive = $coreTags[$tag]['active'];
+                       // is the tag meant to indicate a potential problem ? 
(for T91425, T89553)
+                       $ctObj->isProblem = $coreTags[$tag]['problem'];
+
+               } elseif ( $ctObj->userDefined ) {
+                       // is the tag allowed to be applied by users and bots ?
+                       $ctObj->isActive = $storedTags[$tag]['active'];
+                       // is the tag meant to indicate a potential problem ? 
(for T91425, T89553)
+                       $ctObj->isProblem = $storedTags[$tag]['problem'];
+
+               } else {
+                       // for undefined tags
+                       $ctObj->isActive = false;
+                       $ctObj->isProblem = false;
+               }
+
+               // an inactive tag can be activated provided it is not 
extension or core defined
+               $ctObj->canActivate = !$ctObj->isActive && 
!$ctObj->extensionDefined &&
+                       !$ctObj->coreDefined;
+               # no permission check, make it separately
+               # no existence check, make it first if not from SpecialTags
+
+               // an active tag can be deactivated provided it is stored in 
valid_tag
+               // and it has been applied previously when user defined
+               $ctObj->canDeactivate = $ctObj->isActive && $ctObj->isStored &&
+                       ( !$ctObj->userDefined || $ctObj->isApplied );
+               # no permission or existence check
+
+               // a tag can be deleted if it has not too many uses, is not 
core defined, and
+               // it is not extension defined or it is explicitly allowed
+               $ctObj->canDelete = !$ctObj->isBig && !ctObj->coreDefined &&
+                       ( !$ctObj->extensionDefined || 
$ctObj->extensionDeletable );
+               # no permission or existence check
+
+               if ( !$ctObj->extensionDefined ) {
+               return $ctObj;
+               }
+
+               // extra params for extension defined tags
+               // which extension, or more specific project, defines the tag ?
+               $ctObj->extensionSource = isset( $tagParams['source'] ) ?
+                       (string) $tagParams['source'] : '';
+               // params to pass in the source-specific message
+               $ctObj->extensionMsgParams = isset( $tagParams['msgParams'] ) ?
+                       (array) $tagParams['msgParams'] : array();
+               // should admins be allowed to delete applied instances of the 
tag ?
+               // this does not undefine the tag, unless the extension does it 
by means of
+               // the ChangeTagAfterDelete hook
+               $ctObj->extensionCanDelete = isset( 
$registeredTags[$tag]['canDelete'] ) &&
+                       $registeredTags[$tag]['canDelete'];
+
+               return $ctObj;
+       }
+
+       /**
+        * Shortcut for a change tag object where context is completely rebuilt
+        * and provided in full
+        *
+        * @param string $tag tag's name
+        * @return change tag object
+        */
+       public static function rebuildChangeTagObject ( $tag ) {
+
+               // some of the caches might be outdated due to extensions not 
purging them
+               self::purgeTagUsageCache();
+               self::purgeStoredTagsCache();
+               self::purgeRegisteredTagsCache();
+
+               return getChangeTagObject(
+                       $tag,
+                       self::getTagUsageStatistics(),
+                       self::getStoredTags(),
+                       self::getCoreTags(),
+                       self::getRegisteredTags()
+               );
+       }
+
+       /**
         * Creates HTML for the given tags
         *
         * @param string $tags Comma-separated list of tags
@@ -99,8 +226,8 @@
         * @exception MWException When $rc_id, $rev_id and $log_id are all null
         */
        public static function addTags( $tags, $rc_id = null, $rev_id = null,
-               $log_id = null, $params = null
-       ) {
+               $log_id = null, $params = null ) {
+               global $wgMemc;
                if ( !is_array( $tags ) ) {
                        $tags = array( $tags );
                }
@@ -188,11 +315,12 @@
                                        'ct_params' => $params
                                )
                        );
+                       // Increment cache
+                       $wgMemc->incr( wfMemcKey( 'ChangeTags', 'tag-hitcount', 
"$tag" ) );
                }
 
                $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array( 
'IGNORE' ) );
 
-               self::purgeTagUsageCache();
                return true;
        }
 
@@ -211,7 +339,7 @@
         * @throws MWException When unable to determine appropriate JOIN 
condition for tagging
         */
        public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
-                                                                               
&$join_conds, &$options, $filter_tag = false ) {
+               &$join_conds, &$options, $filter_tag = false ) {
                global $wgRequest, $wgUseTagFilter;
 
                if ( $filter_tag === false ) {
@@ -254,16 +382,22 @@
         *        - if true, it returns an entire form around the selector.
         * @param Title $title Title object to send the form to.
         *        Used when, and only when $fullForm is true.
+        * @param bool $activeOnly specify whether only tags marked
+        * as active should be considered (e.g. for Special:ProblemChanges)
+        * @param bool $problemOnly specify whether only tags indicating
+        * a problem should be considered (e.g. for Special:ProblemChanges)
         * @return string|array
         *        - if $fullForm is false: Array with
         *        - if $fullForm is true: String, html fragment
         */
        public static function buildTagFilterSelector( $selected = '',
-               $fullForm = false, Title $title = null
-       ) {
+               $fullForm = false, Title $title = null,
+               $activeOnly = false, $problemOnly = false ) {
                global $wgUseTagFilter;
 
-               if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) {
+               // check config and if tags of the type requested have been 
applied at least once
+               // @todo use the list of tags to build a dropdown menu, an 
autocomplete form or some hybrid
+               if ( !$wgUseTagFilter || !count( self::getAppliedTags( 
$activeOnly, $problemOnly ) ) ) {
                        return $fullForm ? '' : array();
                }
 
@@ -304,7 +438,7 @@
        /**
         * Defines a tag in the valid_tag table, without checking that the tag 
name
         * is valid.
-        * Extensions should NOT use this function; they can use the 
ListDefinedTags
+        * Extensions should NOT use this function; they can use the 
ChangeTagsRegister
         * hook instead.
         *
         * @param string $tag Tag to create
@@ -316,25 +450,23 @@
                        array( 'vt_tag' ),
                        array( 'vt_tag' => $tag ),
                        __METHOD__ );
-
-               // clear the memcache of defined tags
-               self::purgeTagCacheAll();
+               self::purgeStoredTagsCache();
        }
 
        /**
         * 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.
+        * it from the ChangeTagsRegister hook.
         *
         * @param string $tag Tag to remove
         * @since 1.25
         */
        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::purgeTagCacheAll();
+               $dbw->delete( 'valid_tag',
+                       array( 'vt_tag' => $tag ),
+                       __METHOD__ );
+               self::purgeStoredTagsCache();
        }
 
        /**
@@ -375,29 +507,31 @@
        /**
         * Is it OK to allow the user to activate this tag?
         *
-        * @param string $tag Tag that you are interested in activating
+        * @param string|changeTag object $tag Tag name or tag object for the 
tag
+        * that you are interested in activating
         * @param User|null $user User whose permission you wish to check, or 
null if
         * you don't care (e.g. maintenance scripts)
         * @return Status
         * @since 1.25
         */
        public static function canActivateTag( $tag, User $user = null ) {
+
+               // permission check
                if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' 
) ) {
                        return Status::newFatal( 'tags-manage-no-permission' );
                }
-
-               // non-existing tags cannot be activated
-               $tagUsage = self::tagUsageStatistics();
-               if ( !isset( $tagUsage[$tag] ) ) {
-                       return Status::newFatal( 'tags-activate-not-found', 
$tag );
+               // rebuild change tag object if not already provided
+               if ( !is_object( $tag ) ) {
+                       $tag = self::rebuildChangeTagObject( $tag );
+               }
+               // non-existing tags cannot be managed
+               if ( !$tag->exists ) {
+                       return Status::newFatal( 'tags-manage-not-found', 
$tag->name );
                }
 
-               // defined tags cannot be activated (a defined tag is either 
extension-
-               // defined, in which case the extension chooses whether or not 
to active it;
-               // or user-defined, in which case it is considered active)
-               $definedTags = self::listDefinedTags();
-               if ( in_array( $tag, $definedTags ) ) {
-                       return Status::newFatal( 'tags-activate-not-allowed', 
$tag );
+               // check specific to activate action
+               if ( !$tag->canActivate ) {
+                       return Status::newFatal( 'tags-activate-not-allowed', 
$tag->name );
                }
 
                return Status::newGood();
@@ -439,21 +573,31 @@
        /**
         * Is it OK to allow the user to deactivate this tag?
         *
-        * @param string $tag Tag that you are interested in deactivating
+        * @param string|changeTag object $changeTag Tag name or tag object for 
the tag
+        * that you are interested in deactivating
         * @param User|null $user User whose permission you wish to check, or 
null if
         * you don't care (e.g. maintenance scripts)
         * @return Status
         * @since 1.25
         */
        public static function canDeactivateTag( $tag, User $user = null ) {
+
+               // permission check
                if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' 
) ) {
                        return Status::newFatal( 'tags-manage-no-permission' );
                }
+               // rebuild change tag object if not already provided
+               if ( !is_object( $tag ) ) {
+                       $tag = self::rebuildChangeTagObject( $tag );
+               }
+               // non-existing tags cannot be managed
+               if ( !$tag->exists ) {
+                       return Status::newFatal( 'tags-manage-not-found', 
$tag->name );
+               }
 
-               // only explicitly-defined tags can be deactivated
-               $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
-               if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
-                       return Status::newFatal( 'tags-deactivate-not-allowed', 
$tag );
+               // check specific to deactivate action
+               if ( !$tag->canDeactivate ) {                           
+                       return Status::newFatal( 'tags-deactivate-not-allowed', 
$tag->name );
                }
                return Status::newGood();
        }
@@ -522,15 +666,17 @@
                        return Status::newFatal( 
'tags-create-invalid-title-chars' );
                }
 
+               // rebuild change tag object
+               $tag = self::rebuildChangeTagObject( $tag );
+
                // does the tag already exist?
-               $tagUsage = self::tagUsageStatistics();
-               if ( isset( $tagUsage[$tag] ) ) {
-                       return Status::newFatal( 'tags-create-already-exists', 
$tag );
+               if ( $tag->exists ) {
+                       return Status::newFatal( 'tags-create-already-exists', 
$tag->name );
                }
 
                // check with hooks
                $canCreateResult = Status::newGood();
-               Hooks::run( 'ChangeTagCanCreate', array( $tag, $user, 
&$canCreateResult ) );
+               Hooks::run( 'ChangeTagCanCreate', array( $tag->name, $user, 
&$canCreateResult ) );
                return $canCreateResult;
        }
 
@@ -560,6 +706,7 @@
 
                // do it!
                self::defineTag( $tag );
+               self::purgeTagUsageCache( $tag );
 
                // log it
                $logId = self::logTagAction( 'create', $tag, $reason, $user );
@@ -583,7 +730,11 @@
                $dbw->begin( __METHOD__ );
 
                // delete from valid_tag
-               self::undefineTag( $tag );
+               // we don't call self::undefineTag since the purging of caches 
should occur
+               // at the same time after all operations have been performed
+               $dbw->delete( 'valid_tag',
+                       array( 'vt_tag' => $tag ),
+                       __METHOD__ );
 
                // find out which revisions use this tag, so we can delete from 
tag_summary
                $result = $dbw->select( 'change_tag',
@@ -639,8 +790,10 @@
                        $status->ok = true;
                }
 
-               // clear the memcache of defined tags
-               self::purgeTagCacheAll();
+               // Clearing tag caches
+               self::purgeTagUsageCache( $tag );
+               self::purgeStoredTagsCache();
+               self::purgeRegisteredTagsCache();
 
                return $status;
        }
@@ -648,39 +801,43 @@
        /**
         * Is it OK to allow the user to delete this tag?
         *
-        * @param string $tag Tag that you are interested in deleting
+        * @param string|changeTag object $tag Tag that you are interested in 
deleting
         * @param User|null $user User whose permission you wish to check, or 
null if
         * you don't care (e.g. maintenance scripts)
         * @return Status
         * @since 1.25
         */
        public static function canDeleteTag( $tag, User $user = null ) {
-               $tagUsage = self::tagUsageStatistics();
 
+               // permission check
                if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' 
) ) {
                        return Status::newFatal( 'tags-manage-no-permission' );
                }
-
-               if ( !isset( $tagUsage[$tag] ) ) {
-                       return Status::newFatal( 'tags-delete-not-found', $tag 
);
+               // rebuild change tag object if not already provided
+               if ( !is_object( $tag ) ) {
+                       $tag = self::rebuildChangeTagObject( $tag );
+               }
+               // non-existing tags cannot be managed
+               if ( !$tag->exists ) {
+                       return Status::newFatal( 'tags-manage-not-found', 
$tag->name );
                }
 
-               if ( $tagUsage[$tag] > self::MAX_DELETE_USES ) {
-                       return Status::newFatal( 'tags-delete-too-many-uses', 
$tag, self::MAX_DELETE_USES );
+               // tags with too many uses cannot be deleted
+               if ( $tag->isBig ) {
+                       return Status::newFatal( 'tags-delete-too-many-uses', 
$tag->name, self::MAX_DELETE_USES );
                }
 
-               $extensionDefined = self::listExtensionDefinedTags();
-               if ( in_array( $tag, $extensionDefined ) ) {
+               if ( $tag->extensionDefined && !$tag->extensionDeletable ) {
                        // extension-defined tags can't be deleted unless the 
extension
                        // specifically allows it
-                       $status = Status::newFatal( 'tags-delete-not-allowed' );
+                       return Status::newFatal( 'tags-delete-not-allowed' );
+               } elseif ( $tag->coreDefined ) {
+                       // core defined tags can't be deleted
+                       return Status::newFatal( 'tags-delete-core' );
                } else {
-                       // user-defined tags are deletable unless otherwise 
specified
-                       $status = Status::newGood();
+                       // user-defined tags, extension defined tags when 
allowed, or undefined tags can be deleted
+                       return Status::newGood();
                }
-
-               Hooks::run( 'ChangeTagCanDelete', array( $tag, $user, &$status 
) );
-               return $status;
        }
 
        /**
@@ -701,15 +858,15 @@
        public static function deleteTagWithChecks( $tag, $reason, User $user,
                $ignoreWarnings = false ) {
 
+               // make a change tag object here since we'll need hitcount
+               $changeTag = self::rebuildChangeTagObject( $tag );
+
                // are we allowed to do this?
-               $result = self::canDeleteTag( $tag, $user );
+               $result = self::canDeleteTag( $changeTag, $user );
                if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
                        $result->value = null;
                        return $result;
                }
-
-               // store the tag usage statistics
-               $tagUsage = self::tagUsageStatistics();
 
                // do it!
                $deleteResult = self::deleteTagEverywhere( $tag );
@@ -718,172 +875,370 @@
                }
 
                // log it
-               $logId = self::logTagAction( 'delete', $tag, $reason, $user, 
$tagUsage[$tag] );
+               $logId = self::logTagAction( 'delete', $tag, $reason, $user, 
$changeTag->hitcount );
                $deleteResult->value = $logId;
                return $deleteResult;
        }
 
        /**
-        * Lists those tags which extensions report as being "active".
-        *
-        * @return array
-        * @since 1.25
-        */
-       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;
-       }
-
-       /**
-        * Basically lists defined tags which count even if they aren't applied 
to anything.
-        * It returns a union of the results of listExplicitlyDefinedTags() and
-        * listExtensionDefinedTags().
-        *
-        * @return string[] Array of strings: tags
-        */
-       public static function listDefinedTags() {
-               $tags1 = self::listExplicitlyDefinedTags();
-               $tags2 = self::listExtensionDefinedTags();
-               return array_values( array_unique( array_merge( $tags1, $tags2 
) ) );
-       }
-
-       /**
-        * Lists tags explicitly defined in the `valid_tag` table of the 
database.
+        * Gets tags stored in the `valid_tag` table of the database.
         * Tags in table 'change_tag' which are not in table 'valid_tag' are not
         * included.
+        * The keys are the tag names and the values are arrays of params (none 
for now).
         *
         * Tries memcached first.
+        *
+        * @return Array of strings: tags => array of params
+        * @since 1.25
+        */
+       public static function getStoredTags() {
+               global $wgMemc;
+               // Attempting to retrieve from cache...
+               $key = wfMemcKey( 'ChangeTags', 'valid-tags-db' );
+               $storedTags = $wgMemc->get( $key );
+
+               // If not in cache, db query
+               if ( $storedTags == false ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $res = $dbr->select( 'valid_tag', array( 'vt_tag'),
+                               array(), __METHOD__ );
+
+                       // Filling array mapping tags to their params
+                       $storedTags = array();
+                       foreach ( $res as $row ) {
+                               $storedTags[$row->vt_tag] = array(
+                                       // tags stored in the valid_tag table 
are assumed to be active
+                                       // @todo: store this in a new field of 
valid_tag so that tags
+                                       // previously used by extensions cannot 
be activated if not
+                                       // deleted beforehand, and modify 
actions accordingly
+                                       'active' => true,
+                                       // and they are not assumed to indicate 
a problem
+                                       // @todo: store this setting in a new 
field in the valid_tag
+                                       // table, and provide an action for 
sysops to change it
+                                       'problem' => false
+                               );
+                       }
+
+                       // removing nulls inserted as keys
+                       unset( $storedTags[''] );
+
+                       // Caching for a long time (a week), since
+                       // operations on valid_tag clear this cache
+                       $wgMemc->set( $key, $storedTags, 60*60*24*7 );  
+               }
+
+               return $storedTags;
+       }
+
+       /**
+        * Gets tags registered by extensions using the ChangeTagsRegister hook.
+        * Extensions need only define those tags they deem to be in active use,
+        * or that have been actively used in the past and are likely to be
+        * actively used again in the future.
+        * You need to pass the tag names as keys and an array of params as 
values.
+        * All params are optional and include:
+        * (bool) 'active' whether the tag is in active use (false by default)
+        * (bool) 'problem' whether the tag is meant to indicate a problem 
(false by default)
+        * (string) 'source' name of the extension or more specific project 
issuing the tag
+        * (array) 'msgParams' array of params for the tag description specific 
to the source
+        * (bool) 'canDelete' whether the tag can be deleted by admins (false 
by default)
+        *
+        * Tries memcached first.
+        *
+        * @return Array of strings: tags => arrays of params
+        * @since 1.25
+        */
+       public static function getRegisteredTags() {
+               global $wgMemc;
+               // Attempting to retrieve from cache...
+               $key = wfMemcKey( 'ChangeTags', 'valid-tags-hook' );
+               $registeredTags = $wgMemc->get( $key );
+
+               // If not in cache, ask extensions
+               if ( $registeredTags == false ) {
+                       // hack for ListDefinedTags hook until deprecated
+                       $extensionDefined = array();
+                       Hooks::run( 'ListDefinedTags', array( 
&$extensionDefined ) );
+                       // Filling with param 'active' set to false
+                       $extensionDefined = array_fill_keys( $extensionDefined,
+                               array() );
+
+                       // hack for ChangeTagsListActive hook until deprecated
+                       $extensionActive = array();
+                       Hooks::run( 'ChangeTagsListActive', array( 
&$extensionActive ) );
+                       // Filling with arrays with param 'active' set to true
+                       $extensionActive = array_fill_keys( $extensionActive,
+                               array( 'active' => true ) );
+
+                       // Merging, with ChangeTagsListActive overriding 
ListDefinedTags
+                       $registeredTags = array_merge( $extensionDefined, 
$extensionActive );
+                       // Applying the new hook, tags as keys and array of 
params as values
+                       Hooks::run( 'ChangeTagsRegister', array( 
&$registeredTags ) );
+
+                       // removing nulls inserted as keys
+                       unset( $registeredTags[''] );
+
+                       // Caching for a very short time (5 minutes), since 
extensions may
+                       // register or unregister tags without clearing the 
cache (which
+                       // should be done with the purgeRegisteredTagsCache 
function).
+                       // @todo AbuseFilter should do it (the current 5 
minutes wait is
+                       // clearly noticeable), maybe also a few other 
extensions that need to.
+                       // When done, we'll be able to increase the cache 
duration if needed.
+                       $wgMemc->set( $key, $registeredTags, 60*5 );
+               }
+               return $registeredTags;
+       }
+
+       /**
+        * Nothing for now
+        */
+       public static function getCoreTags() {
+               return array();
+       }
+
+       /**
+        * Gets an array mapping tags applied at least once to their hitcount
+        *
+        * Tags defined somewhere but not applied are not included.
+        *
+        * @param bool $upToDateHitcounts whether to fetch the most up to date 
hitcounts
+        * or only hitcounts of an older cache (delay of up to 24 hours)
+        */
+       public static function buildTagUsageStatistics( $upToDateHitcounts = 
true ) {
+               global $wgMemc, $wgTagUsageCacheDuration;
+
+               // Try to retrieve cached array mapping tags to their hitcount
+               $commonKey = wfMemcKey( 'ChangeTags', 'tag-usage-stats' );
+               $changeTags = $wgMemc->get( $commonKey );
+
+               // If the common cache exists and we want up to date hitcounts, 
get them
+               if ( $changeTags != false && $upToDateHitcounts ) {
+                       foreach ( $changeTags as $tag => $hitcount ) {
+                               $hitcountKey = wfMemcKey( 'ChangeTags', 
'tag-hitcount', "$tag" );
+                               // getting most up to date hitcount from 
specific hitcount cache
+                               $hitcount = $wgMemc->get( $hitcountKey );
+                               if ( $hitcount != false ) {
+                                       // Mapping tags to their hitcount
+                                       $changeTags[$tag] = $hitcount;
+                               } else {
+                                       // Cache attempt failed
+                                       // (normally, this should not happen)
+                                       // So do DB query
+                                       $changeTags = false;
+                                       break;
+                               }
+                       }
+               }
+
+               // Database query and cache rebuild
+               if ( $changeTags == false ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $res = $dbr->select(
+                               'change_tag',
+                               array( 'ct_tag', 'hitcount' => 'count(*)' ),
+                               array(),
+                               __METHOD__,
+                               array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 
'hitcount DESC' )
+                       );
+
+                       $changeTags = array();
+                       foreach ( $res as $row ) {
+                               $tag = $row->ct_tag;
+                               $hitcount = $row->hitcount;
+                               // Mapping tags to their hitcount
+                               $changeTags[$tag] = $hitcount;
+                               // Caching for a week (gets incremented when 
applied, decremented when unapplied)
+                               $hitcountKey = wfMemcKey( 'ChangeTags', 
'tag-hitcount', "$tag" );
+                               $wgMemc->set( $hitcountKey, $hitcount, 
60*60*24*7 );
+                       }
+
+                       // removing nulls inserted as keys
+                       unset( $changeTags[''] );
+                       // Caching for a moderate duration (24 hours by default)
+                       $wgMemc->set( $commonKey, $changeTags, 
$wgTagUsageCacheDuration );
+               }
+
+               // returning list of tags, or map of tags to their hitcount
+               return $changeTags;
+       }
+
+       /**
+        * Returns a map of any tags used on the wiki to number of edits
+        * tagged with them, ordered descending by the hitcount as of the
+        * latest caching. The ordering may on rare occasions be incorrect
+        * since hitcounts are updated when tags are applied.
+        * Does not include tags defined somewhere but not applied
+        *
+        * @since 1.25
+        */
+       public static function tagUsageStatistics() {
+               return self::buildTagUsageStatistics( true );
+       }
+
+       /**
+        * Lists all defined tags with their params.
+        *
+        * @return string[] Array of strings: tags
+        * @since 1.25
+        */
+       public static function getDefinedTags() {
+               return array_merge( self::getStoredTags(),
+                       self::getCoreTags(), self::getRegisteredTags() );
+       }
+
+       /**
+        * Returns an array of tags applied at least once,
+        * mapped to their cached hitcount.
+        * A delay of up to 24 hours (by default) may occur
+        * before a newly applied tag gets listed.
+        *
+        * Faster than self::tagUsageStatistics() (which returns up to date 
hitcounts)
+        *
+        * @param string $tag: tag
+        * @param bool $activeOnly: whether to return only active tags
+        * @param bool $problemOnly: whether to return only problem tags
+        * @since 1.25
+        */
+       public static function getAppliedTags( $activeOnly, $problemOnly ) {
+               $appliedTags = self::buildTagUsageStatistics( false );
+
+               // shortcut
+               if ( !$activeOnly && !$problemOnly ) {
+                       return array_keys( $appliedTags );
+               }
+
+               $definedTags = self::getDefinedTags();
+               // filtering out tags when requested
+               foreach ( $definedTags as $tag => &$tagParams ) {
+                       if ( ( $problemOnly && !$tagParams['problem'] ) ||
+                               ( $activeOnly && !$tagParams['active'] ) ) {
+                               unset( $appliedTags[$tag] );
+                       }
+               }
+               return $appliedTags;
+       }
+
+       /**
+        * Invalidates the cache of tags stored in the valid_tag table.
+        * Use case 1) alone : updating tag params
+        * Use case 2) in combination with purgeTagUsageCache :
+        * defining (incl. creating) or undefining (incl. deleting) tags
+        *
+        * @since 1.25
+        */
+       public static function purgeStoredTagsCache() {
+               global $wgMemc;
+               $wgMemc->delete( wfMemcKey( 'ChangeTags', 'valid-tags-db' ) );
+       }
+
+       /**
+        * Invalidates the cache of tags registered by extensions.
+        * Use case 1) alone : updating tag params
+        * Use case 2) in combination with purgeTagUsageCache :
+        * defining (incl. creating) or undefining (incl. deleting) tags
+        *
+        * @since 1.25
+        */
+       public static function purgeRegisteredTagsCache() {
+               global $wgMemc;
+               $wgMemc->delete( wfMemcKey( 'ChangeTags', 'valid-tags-hook' ) );
+       }
+
+       /**
+        * Invalidates caches related to tag usage stats
+        * Use case : defining (incl. creating) or undefining (incl. deleting) 
a tag
+        * This should not be used when applying the tag, the hitcount cache 
should
+        * be incremented instead. (And when the tag gets removed, decremented.)
+        *
+        * @param string $tag: (optional) tag to clear the hitcount cache of
+        * @since 1.25
+        */
+       public static function purgeTagUsageCache( $tag = false ) {
+               global $wgMemc;
+               // delete tag usage cache
+               $wgMemc->delete( wfMemcKey( 'ChangeTags', 'tag-usage-stats' ) );
+               // delete cached list of never applied tags
+               $wgMemc->delete( wfMemcKey( 'ChangeTags', 'never-applied-tags' 
) );
+               if ( $tag ) {
+                       // delete hitcount cache
+                       $wgMemc->delete( wfMemcKey( 'ChangeTags', 
'tag-hitcount', "$tag" ) );
+               }
+       }
+
+       /**
+        *
+        *
+        * Deprecated functions follow
+        *
+        *
+        */
+
+       /**
+        * Lists tags from the valid_tag table as values.
+        * Provided for backward compatibility.
+        * Should be deprecated, used anywhere ?
         *
         * @return string[] Array of strings: tags
         * @since 1.25
         */
        public static function listExplicitlyDefinedTags() {
-               // Caching...
-               global $wgMemc;
-               $key = wfMemcKey( 'valid-tags-db' );
-               $tags = $wgMemc->get( $key );
-               if ( $tags ) {
-                       return $tags;
-               }
-
-               $emptyTags = array();
-
-               // Some DB stuff
-               $dbr = wfGetDB( DB_SLAVE );
-               $res = $dbr->select( 'valid_tag', 'vt_tag', array(), __METHOD__ 
);
-               foreach ( $res as $row ) {
-                       $emptyTags[] = $row->vt_tag;
-               }
-
-               $emptyTags = array_filter( array_unique( $emptyTags ) );
-
-               // Short-term caching.
-               $wgMemc->set( $key, $emptyTags, 300 );
-               return $emptyTags;
+               return array_keys( self::getStoredTags() );
        }
-
+       
        /**
-        * Lists tags defined by extensions using the ListDefinedTags hook.
-        * Extensions need only define those tags they deem to be in active use.
-        *
-        * Tries memcached first.
+        * Lists tags defined by extensions as values.
+        * Provided for backward compatibility.
+        * Should be deprecated, used anywhere ?
         *
         * @return string[] Array of strings: tags
         * @since 1.25
         */
        public static function listExtensionDefinedTags() {
-               // Caching...
-               global $wgMemc;
-               $key = wfMemcKey( 'valid-tags-hook' );
-               $tags = $wgMemc->get( $key );
-               if ( $tags ) {
-                       return $tags;
-               }
-
-               $emptyTags = array();
-               Hooks::run( 'ListDefinedTags', array( &$emptyTags ) );
-               $emptyTags = array_filter( array_unique( $emptyTags ) );
-
-               // Short-term caching.
-               $wgMemc->set( $key, $emptyTags, 300 );
-               return $emptyTags;
+               return array_keys( self::getRegisteredTags() );
        }
 
        /**
-        * Invalidates the short-term cache of defined tags used by the
-        * list*DefinedTags functions, as well as the tag statistics cache.
+        * Lists all defined tags as values.
+        * Provided for backward compatibility.
+        * Should be deprecated, used anywhere ?
+        *
+        * @return string[] Array of strings: tags
+        * @since 1.25
+        */
+       public static function listDefinedTags() {
+               return array_keys( self::getDefinedTags() );
+       }
+
+       /**
+        * Lists all active tags as values.
+        * Provided for backward compatibility.
+        * Should be deprecated, used anywhere ?
+        *
+        * @return string[] Array of strings: tags
+        * @since 1.25
+        */
+       public static function listExtensionActivatedTags() {
+               $tags = self::getRegisteredTags();
+               // sorting out inactive tags
+               foreach ( $tags as $tag => &$tagParams ) {
+                       if ( !$tagParams['active'] ) {
+                               unset( $tags[$tag] );
+                       }
+               }
+               return array_keys( $tags );
+       }
+
+       /**
+        * Invalidates all tags-related caches.
+        * Provided for backward compatibility.
+        * Should be deprecated, used anywhere ?
+        *
         * @since 1.25
         */
        public static function purgeTagCacheAll() {
                global $wgMemc;
-               $wgMemc->delete( wfMemcKey( 'active-tags' ) );
-               $wgMemc->delete( wfMemcKey( 'valid-tags-db' ) );
-               $wgMemc->delete( wfMemcKey( 'valid-tags-hook' ) );
+               self::purgeStoredTagsCache();
+               self::purgeRegisteredTagsCache();
                self::purgeTagUsageCache();
-       }
-
-       /**
-        * Invalidates the tag statistics cache only.
-        * @since 1.25
-        */
-       public static function purgeTagUsageCache() {
-               global $wgMemc;
-               $wgMemc->delete( wfMemcKey( 'change-tag-statistics' ) );
-       }
-
-       /**
-        * Returns a map of any tags used on the wiki to number of edits
-        * tagged with them, ordered descending by the hitcount.
-        *
-        * Keeps a short-term cache in memory, so calling this multiple times 
in the
-        * same request should be fine.
-        *
-        * @return array Array of string => int
-        */
-       public static function tagUsageStatistics() {
-               // Caching...
-               global $wgMemc;
-               $key = wfMemcKey( 'change-tag-statistics' );
-               $stats = $wgMemc->get( $key );
-               if ( $stats ) {
-                       return $stats;
-               }
-
-               $out = array();
-
-               $dbr = wfGetDB( DB_SLAVE );
-               $res = $dbr->select(
-                       'change_tag',
-                       array( 'ct_tag', 'hitcount' => 'count(*)' ),
-                       array(),
-                       __METHOD__,
-                       array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount 
DESC' )
-               );
-
-               foreach ( $res as $row ) {
-                       $out[$row->ct_tag] = $row->hitcount;
-               }
-               foreach ( self::listDefinedTags() as $tag ) {
-                       if ( !isset( $out[$tag] ) ) {
-                               $out[$tag] = 0;
-                       }
-               }
-
-               // Cache for a very short time
-               $wgMemc->set( $key, $out, 300 );
-               return $out;
        }
 }
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index a5d6605..a07cf14 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -6001,11 +6001,18 @@
 
 /**
  * Allow filtering by change tag in recentchanges, history, etc
- * Has no effect if no tags are defined in valid_tag.
+ * Has no effect if no tag is applied to edits
+ * Update can be delayed up to $wgTagUsageCacheDuration
  */
 $wgUseTagFilter = true;
 
 /**
+ * Expiry to use for caching of tag usage statistics
+ * 24 hours by default
+ */
+$wgTagUsageCacheDuration = 60*60*24;
+
+/**
  * If set to an integer, pages that are watched by this many users or more
  * will not require the unwatchedpages permission to view the number of
  * watchers.
diff --git a/includes/specials/SpecialTags.php 
b/includes/specials/SpecialTags.php
index 0b8147e..1ba1346 100644
--- a/includes/specials/SpecialTags.php
+++ b/includes/specials/SpecialTags.php
@@ -27,14 +27,11 @@
  * @ingroup SpecialPage
  */
 class SpecialTags extends SpecialPage {
-       /**
-        * @var array List of defined tags
-        */
-       public $definedTags;
-       /**
-        * @var array List of active tags
-        */
-       public $activeTags;
+
+       protected $tagStats = null;
+       protected $storedTags = null;
+       protected $coreTags = null;
+       protected $registeredTags = null;
 
        function __construct() {
                parent::__construct( 'Tags' );
@@ -69,9 +66,10 @@
                $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 
'tags-intro' );
 
                $user = $this->getUser();
+               $userCanManage = $user->isAllowed( 'managechangetags' );
 
                // Show form to create a tag
-               if ( $user->isAllowed( 'managechangetags' ) ) {
+               if ( $userCanManage ) {
                        $fields = array(
                                'Tag' => array(
                                        'type' => 'text',
@@ -111,20 +109,29 @@
                // 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' );
+               $showActions = $userCanManage;
 
-               // Write the headers
-               $tagUsageStatistics = ChangeTags::tagUsageStatistics();
+               // Whether to show the edit interface links
+               $showEditLinks = $user->isAllowed( 'editinterface' );
 
-               // Show header only if there exists atleast one tag
-               if ( !$tagUsageStatistics ) {
+               // Used in #doTagRow()
+               $this->tagStats = ChangeTags::tagUsageStatistics();
+               $this->storedTags = ChangeTags::getStoredTags();
+               $this->coreTags = ChangeTags::getCoreTags();
+               $this->registeredTags = ChangeTags::getRegisteredTags();
+
+               // Show header only if there exists at least one tag
+               if ( !$this->tagStats && !$this->coreTags && 
!$this->registeredTags && !$this->storedTags ) {
                        return;
                }
+
+               // 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-source-header' 
)->parse() ) .
                        Xml::tags( 'th', null, $this->msg( 'tags-active-header' 
)->parse() ) .
+                       Xml::tags( 'th', null, $this->msg( 
'tags-problem-header' )->parse() ) .
                        Xml::tags( 'th', null, $this->msg( 
'tags-hitcount-header' )->parse() ) .
                        ( $showActions ?
                                Xml::tags( 'th', array( 'class' => 'unsortable' 
),
@@ -132,17 +139,13 @@
                                '' )
                );
 
-               // Used in #doTagRow()
-               $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 ( $tagUsageStatistics as $tag => $hitcount ) {
-                       $html .= $this->doTagRow( $tag, $hitcount, $showActions 
);
+               // Append tag rows for tags applied at least once (based on 
change_tag table)
+               foreach ( array_keys( $this->tagStats ) as $tag ) {
+                       $html .= $this->doTagRow( $tag, $showActions, 
$showEditLinks );
                }
+
+               // Append tag rows for tags that have "never" been applied but 
are defined somewhere
+               $html .= $this->appendNeverAppliedDefinedTags();
 
                $out->addHTML( Xml::tags(
                        'table',
@@ -151,13 +154,17 @@
                ) );
        }
 
-       function doTagRow( $tag, $hitcount, $showActions ) {
-               $user = $this->getUser();
+       function doTagRow( $tag, $showActions, $showEditLinks ) {
+
+               // building change tag object
+               $changeTag = ChangeTags::getChangeTagObject( $tag, 
$this->tagStats,
+                       $this->storedTags, $this->coreTags, 
$this->registeredTags );
+
                $newRow = '';
                $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, 
$tag ) );
 
                $disp = ChangeTags::tagDescription( $tag );
-               if ( $user->isAllowed( 'editinterface' ) ) {
+               if ( $showEditLinks ) {
                        $disp .= ' ';
                        $editLink = Linker::link(
                                Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ),
@@ -169,7 +176,18 @@
 
                $msg = $this->msg( "tag-$tag-description" );
                $desc = !$msg->exists() ? '' : $msg->parse();
-               if ( $user->isAllowed( 'editinterface' ) ) {
+               if ( $changeTag->isRegistered && $changeTag->extensionSource != 
'' ) {
+                       // Adding a description specific to the extension 
source with params from hook
+                       $msgKey = 'tags-description-extension-' . 
$changeTag->extensionSource;
+                       $extMsg = $this->msg( $msgKey );
+                       if ( $extMsg->exists() ) {
+                               if ( $msg->exists() ) {
+                                       $desc .= Xml::element( 'br' );
+                               }
+                               $desc .= $extMsg->params( 
$changeTag->extensionMsgParams )->parse();
+                       }
+               }
+               if ( $showEditLinks ) {
                        $desc .= ' ';
                        $editDescLink = Linker::link(
                                Title::makeTitle( NS_MEDIAWIKI, 
"Tag-$tag-description" ),
@@ -180,24 +198,28 @@
                $newRow .= Xml::tags( 'td', null, $desc );
 
                $sourceMsgs = array();
-               $isExtension = isset( $this->extensionDefinedTags[$tag] );
-               $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
-               if ( $isExtension ) {
-                       $sourceMsgs[] = $this->msg( 'tags-source-extension' 
)->escaped();
-               }
-               if ( $isExplicit ) {
+               if ( $changeTag->userDefined ) {
                        $sourceMsgs[] = $this->msg( 'tags-source-manual' 
)->escaped();
+               }
+               if ( $changeTag->coreDefined ) {
+                       $sourceMsgs[] = $this->msg( 'tags-source-core' 
)->escaped();
+               }
+               if ( $changeTag->extensionDefined ) {
+                       $sourceMsgs[] = $this->msg( 'tags-source-extension' 
)->params(
+                               $changeTag->extensionSource )->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' 
);
+               $activeMsg = $changeTag->isActive ? 'tags-active-yes' : 
'tags-active-no';
                $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg 
)->escaped() );
 
-               $hitcountLabel = $this->msg( 'tags-hitcount' )->numParams( 
$hitcount )->escaped();
+               $problemMsg = $changeTag->isProblem ? 'tags-problem-yes' : 
'tags-problem-no';
+               $newRow .= Xml::tags( 'td', null, $this->msg( $problemMsg 
)->escaped() );
+
+               $hitcountLabel = $this->msg( 'tags-hitcount' )->numParams( 
$changeTag->hitcount )->escaped();
                $hitcountLink = Linker::link(
                        SpecialPage::getTitleFor( 'Recentchanges' ),
                        $hitcountLabel,
@@ -205,22 +227,15 @@
                        array( 'tagfilter' => $tag )
                );
 
-               // add raw $hitcount for sorting, because tags-hitcount 
contains numbers and letters
-               $newRow .= Xml::tags( 'td', array( 'data-sort-value' => 
$hitcount ), $hitcountLink );
+               // add raw hitcount for sorting, because tags-hitcount contains 
numbers and letters
+               $newRow .= Xml::tags( 'td', array( 'data-sort-value' => 
$changeTag->hitcount ), $hitcountLink );
 
                // actions
-               $actionLinks = array();
                if ( $showActions ) {
-                       // delete
-                       if ( ChangeTags::canDeleteTag( $tag, $user )->isOK() ) {
-                               $actionLinks[] = Linker::linkKnown( 
$this->getPageTitle( 'delete' ),
-                                       $this->msg( 'tags-delete' )->escaped(),
-                                       array(),
-                                       array( 'tag' => $tag ) );
-                       }
+                       $actionLinks = array();
 
                        // activate
-                       if ( ChangeTags::canActivateTag( $tag, $user )->isOK() 
) {
+                       if ( $changeTag->canActivate ) {
                                $actionLinks[] = Linker::linkKnown( 
$this->getPageTitle( 'activate' ),
                                        $this->msg( 'tags-activate' 
)->escaped(),
                                        array(),
@@ -228,9 +243,17 @@
                        }
 
                        // deactivate
-                       if ( ChangeTags::canDeactivateTag( $tag, $user 
)->isOK() ) {
+                       if ( $changeTag->canDeactivate ) {
                                $actionLinks[] = Linker::linkKnown( 
$this->getPageTitle( 'deactivate' ),
                                        $this->msg( 'tags-deactivate' 
)->escaped(),
+                                       array(),
+                                       array( 'tag' => $tag ) );
+                       }
+
+                       // delete
+                       if ( $changeTag->canDelete ) {
+                               $actionLinks[] = Linker::linkKnown( 
$this->getPageTitle( 'delete' ),
+                                       $this->msg( 'tags-delete' )->escaped(),
                                        array(),
                                        array( 'tag' => $tag ) );
                        }
@@ -239,6 +262,43 @@
                }
 
                return Xml::tags( 'tr', null, $newRow ) . "\n";
+       }
+
+       function appendNeverAppliedDefinedTags() {
+               global $wgMemc, $wgTagUsageCacheDuration;
+               $html = '';
+               // Retrieve "never"-applied tags, whether defined by extensions 
or stored
+               // in valid_tag table. (They might have been applied since 
caching occurred.)
+               $neverKey = wfMemcKey( 'ChangeTags', 'never-applied-tags' );
+               $neverAppliedTags = $wgMemc->get( $neverKey );
+
+               if ( $neverAppliedTags != false ) {
+                       // The cache exists, so use it but make sure the tag 
hasn't been applied since then
+                       foreach ( $neverAppliedTags as $tag ) {
+                               if ( !isset( $this->tagStats[$tag] ) ) {
+                                       // Append tag row to html
+                                       $html .= $this->doTagRow( $tag, 
$showActions, $showEditLinks );
+                               }
+                       }
+               } else {
+                       // The cache doesn't exist, so we need to retrieve all 
defined tags
+                       $neverAppliedTags = array();
+                       foreach ( array_keys( ChangeTags::getDefinedTags() ) as 
$tag ) {
+                               // We only need tags not already applied.
+                               if ( !isset( $this->tagStats[$tag] ) ) {
+                                       // Append tag row to html
+                                       $html .= $this->doTagRow( $tag, 
$showActions, $showEditLinks );
+                                       // For later caching
+                                       $neverAppliedTags[] = $tag;
+                                       // Caching hitcount of 0 for a week 
(gets incremented when applied)
+                                       $hitcountKey = wfMemcKey( 'ChangeTags', 
'tag-hitcount', "$tag" );
+                                       $wgMemc->set( $hitcountKey, 0, 
60*60*24*7 );
+                               }
+                       }
+                       // Caching for a moderate duration (24 hours by default)
+                       $wgMemc->set( $neverKey, $neverAppliedTags, 
$wgTagUsageCacheDuration );
+               }
+               return $html;
        }
 
        public function processCreateTagForm( array $data, HTMLForm $form ) {
@@ -306,8 +366,10 @@
                $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
                $out->addBacklinkSubtitle( $this->getPageTitle() );
 
+               $changeTag = ChangeTags::rebuildChangeTagObject( $tag );
+
                // is the tag actually able to be deleted?
-               $canDeleteResult = ChangeTags::canDeleteTag( $tag, $user );
+               $canDeleteResult = ChangeTags::canDeleteTag( $changeTag, $user 
);
                if ( !$canDeleteResult->isGood() ) {
                        $out->addWikiText( "<div class=\"error\">\n" . 
$canDeleteResult->getWikiText() .
                                "\n</div>" );
@@ -317,17 +379,16 @@
                }
 
                $preText = $this->msg( 'tags-delete-explanation-initial', $tag 
)->parseAsBlock();
-               $tagUsage = ChangeTags::tagUsageStatistics();
-               if ( $tagUsage[$tag] > 0 ) {
+
+               // see if the tag has been previously applied
+               if ( $changeTag->isApplied ) {
                        $preText .= $this->msg( 
'tags-delete-explanation-in-use', $tag,
-                               $tagUsage[$tag] )->parseAsBlock();
+                               $changeTag->hitcount )->parseAsBlock();
                }
                $preText .= $this->msg( 'tags-delete-explanation-warning', $tag 
)->parseAsBlock();
 
-               // see if the tag is in use
-               $this->extensionActivatedTags = array_fill_keys(
-                       ChangeTags::listExtensionActivatedTags(), true );
-               if ( isset( $this->extensionActivatedTags[$tag] ) ) {
+               // see if the tag is registered as active by an extension
+               if ( $changeTag->extensionDefined && $changeTag->isActive ) {
                        $preText .= $this->msg( 
'tags-delete-explanation-active', $tag )->parseAsBlock();
                }
 
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index 16a1e6a..44d1312 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -3372,11 +3372,15 @@
        "tags-description-header": "Full description of meaning",
        "tags-source-header": "Source",
        "tags-active-header": "Active?",
+       "tags-problem-header": "Problem?",
        "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-problem-yes": "Yes",
+       "tags-problem-no": "No",
+       "tags-source-core": "Applied automatically in core",
+       "tags-source-extension": "Applied automatically by an extension",
        "tags-source-manual": "Applied manually by users and bots",
        "tags-source-none": "No longer in use",
        "tags-edit": "edit",
@@ -3385,6 +3389,7 @@
        "tags-deactivate": "deactivate",
        "tags-hitcount": "$1 {{PLURAL:$1|change|changes}}",
        "tags-manage-no-permission": "You do not have permission to manage 
change tags.",
+       "tags-manage-not-found": "The tag \"$1\" does not exist.",
        "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:",
@@ -3403,15 +3408,14 @@
        "tags-delete-explanation-active": "<strong>The tag \"$1\" is still 
active, and will continue to be applied in the future.</strong> To stop this 
from happening, go to the place(s) where the tag is set to be applied, and 
disable it there.",
        "tags-delete-reason": "Reason:",
        "tags-delete-submit": "Irreversibly delete this tag",
+       "tags-delete-core": "Tags defined in core cannot be deleted.",
        "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-delete-warnings-after-delete": "The tag \"$1\" was deleted 
successfully, but the following {{PLURAL:$2|warning was|warnings were}} 
encountered:",
        "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-not-found": "The tag \"$1\" does not exist.",
        "tags-activate-submit": "Activate",
        "tags-deactivate-title": "Deactivate tag",
        "tags-deactivate-question": "You are about to deactivate the tag 
\"$1\".",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index 0992b49..5dfebe2 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -3538,10 +3538,14 @@
        "tags-description-header": "Caption of a column in [[Special:Tags]]. 
For more information on tags see [[mw:Manual:Tags|MediaWiki]].",
        "tags-source-header": "Caption of a column in [[Special:Tags]]. For 
more information on tags see 
[[mw:Manual:Tags|MediaWiki]].\n{{Identical|Source}}",
        "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-problem-header": "Caption of a column in [[Special:Tags]]. Values 
are \"Yes\" or \"No\" to indicate if a tag is meant to indicate a potential 
problem with the change (possible vandalism, syntax errors, etc).\n\nSee 
example: [[mw:Special:Tags]].\n\nFor more information on tags see 
[[mw:Manual:Tags|MediaWiki]].",
        "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]].\n{{Identical|Action}}",
        "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-problem-yes": "Table cell contents if given tag is meant to 
indicate a potential \"problem\" with the change.\n\nSee also:\n* 
{{msg-mw|Tags-active-no}}\n{{Identical|Yes}}",
+       "tags-problem-no": "Table cell contents if given tag is not meant to 
indicate a potential \"problem\" with the change.\n\nSee also:\n* 
{{msg-mw|Tags-active-yes}}\n{{Identical|No}}",
+       "tags-source-core": "Table cell contents if given tag is applied 
automatically by the [[mw:Manual:Core|core]] software.\n\nSee also:\n* 
{{msg-mw|Tags-source-manual}}\n* {{msg-mw|Tags-source-none}}",
        "tags-source-extension": "Table cell contents if given tag can be 
applied automatically by a software [[mw:Manual:Extensions|extension]].\n\nSee 
also:\n* {{msg-mw|Tags-source-manual}}\n* {{msg-mw|Tags-source-none}}",
        "tags-source-manual": "Table cell contents if given tag can be applied 
by users or bots.\n\nSee also:\n* {{msg-mw|Tags-source-extension}}\n* 
{{msg-mw|Tags-source-none}}",
        "tags-source-none": "Table cell contents if given tag is no longer in 
use. (It was applied in the past, but it is currently not applied.)\n\nSee 
also:\n* {{msg-mw|Tags-source-extension}}\n* {{msg-mw|Tags-source-manual}}",
@@ -3551,6 +3555,7 @@
        "tags-deactivate": "Used on [[Special:Tags]]. Verb. Used as display 
text on a link to deactivate 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-manage-no-permission": "Error message on [[Special:Tags]]",
+       "tags-manage-not-found": "Error message on [[Special:Tags]]",
        "tags-create-heading": "The title of a fieldset, beneath which lies a 
form used to create a tag. For more information on tags see 
[[mw:Manual:Tags|MediaWiki]].",
        "tags-create-explanation": "The first paragraph of an explanation to 
tell users what they are about to do.",
        "tags-create-tag-name": "Form field label for the name of the tag to be 
created.",
@@ -3569,15 +3574,14 @@
        "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-reason": "{{Identical|Reason}}",
        "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-core": "Error message on [[Special:Tags]]",
        "tags-delete-not-allowed": "Error message on [[Special:Tags]]",
-       "tags-delete-not-found": "Error message on [[Special:Tags]]",
        "tags-delete-too-many-uses": "Error message on [[Special:Tags]]",
        "tags-delete-warnings-after-delete": "Warning shown after deleting a 
tag.\n\nParameters:\n* $1 - the code name of the tag that was deleted\n* $2 - 
the number of warnings",
        "tags-activate-title": "The title of a page used to activate a tag. For 
more information on tags see [[mw:Manual:Tags|MediaWiki]].",
        "tags-activate-question": "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 activated",
        "tags-activate-reason": "{{Identical|Reason}}",
        "tags-activate-not-allowed": "Error message on [[Special:Tags]]",
-       "tags-activate-not-found": "Error message on [[Special:Tags]]",
        "tags-activate-submit": "The label of the form \"submit\" button when 
the user is about to activate a tag.\n{{Identical|Activate}}",
        "tags-deactivate-title": "The title of a page used to deactivate a tag. 
For more information on tags see [[mw:Manual:Tags|MediaWiki]].",
        "tags-deactivate-question": "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 deactivated",

-- 
To view, visit https://gerrit.wikimedia.org/r/201905
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I4f4b097d660ada77f5cf7b4231925009b27127ea
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Cenarium <cenarium.sy...@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to