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