Merge "parser: Validate $length in padleft/padright parser functions"
[lhc/web/wiklou.git] / includes / changetags / ChangeTags.php
index 7e4dd00..d019f41 100644 (file)
@@ -32,6 +32,9 @@ class ChangeTags {
         */
        const MAX_DELETE_USES = 5000;
 
+       /**
+        * A list of tags defined and used by MediaWiki itself.
+        */
        private static $definedSoftwareTags = [
                'mw-contentmodelchange',
                'mw-new-redirect',
@@ -181,6 +184,28 @@ class ChangeTags {
                return $msg;
        }
 
+       /**
+        * Get truncated message for the tag's long description.
+        *
+        * @param string $tag Tag name.
+        * @param int $length Maximum length of truncated message, including ellipsis.
+        * @param IContextSource $context
+        *
+        * @return string Truncated long tag description.
+        */
+       public static function truncateTagDescription( $tag, $length, IContextSource $context ) {
+               $originalDesc = self::tagLongDescriptionMessage( $tag, $context );
+               // If there is no tag description, return empty string
+               if ( !$originalDesc ) {
+                       return '';
+               }
+
+               $taglessDesc = Sanitizer::stripAllTags( $originalDesc->parse() );
+               $escapedDesc = Sanitizer::escapeHtmlAllowEntities( $taglessDesc );
+
+               return $context->getLanguage()->truncateForVisual( $escapedDesc, $length );
+       }
+
        /**
         * Add tags to a change given its rc_id, rev_id and/or log_id
         *
@@ -236,6 +261,8 @@ class ChangeTags {
                &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
                User $user = null
        ) {
+               global $wgChangeTagsSchemaMigrationStage;
+
                $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
                $tagsToRemove = array_filter( (array)$tagsToRemove );
 
@@ -317,6 +344,35 @@ class ChangeTags {
 
                // insert a row into change_tag for each new tag
                if ( count( $tagsToAdd ) ) {
+                       $changeTagMapping = [];
+                       if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) {
+                               $tagDefRows = [];
+                               foreach ( $tagsToAdd as $tag ) {
+                                       $tagDefRows[] = [
+                                               'ctd_name' => $tag,
+                                               'ctd_user_defined' => 0,
+                                               'ctd_count' => 1
+                                       ];
+                               }
+
+                               $dbw->upsert(
+                                       'change_tag_def',
+                                       $tagDefRows,
+                                       [ 'ctd_name' ],
+                                       [ 'ctd_count = ctd_count + 1' ],
+                                       __METHOD__
+                               );
+
+                               $res = $dbw->select(
+                                       'change_tag_def',
+                                       [ 'ctd_name', 'ctd_id' ],
+                                       [ 'ctd_name' => $tagsToAdd ]
+                               );
+                               foreach ( $res as $row ) {
+                                       $changeTagMapping[$row->ctd_name] = $row->ctd_id;
+                               }
+                       }
+
                        $tagsRows = [];
                        foreach ( $tagsToAdd as $tag ) {
                                // Filter so we don't insert NULLs as zero accidentally.
@@ -329,9 +385,11 @@ class ChangeTags {
                                                'ct_rc_id' => $rc_id,
                                                'ct_log_id' => $log_id,
                                                'ct_rev_id' => $rev_id,
-                                               'ct_params' => $params
+                                               'ct_params' => $params,
+                                               'ct_tag_id' => $changeTagMapping[$tag] ?? null,
                                        ]
                                );
+
                        }
 
                        $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
@@ -349,6 +407,20 @@ class ChangeTags {
                                        ]
                                );
                                $dbw->delete( 'change_tag', $conds, __METHOD__ );
+                               if ( $dbw->affectedRows() && $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) {
+                                       $dbw->update(
+                                               'change_tag_def',
+                                               [ 'ctd_count = ctd_count - 1' ],
+                                               [ 'ctd_name' => $tag ],
+                                               __METHOD__
+                                       );
+
+                                       $dbw->delete(
+                                               'change_tag_def',
+                                               [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
+                                               __METHOD__
+                                       );
+                               }
                        }
                }
 
@@ -394,7 +466,7 @@ class ChangeTags {
                // $prevTags can be out of date on replica DBs, especially when addTags is called consecutively,
                // causing loss of tags added recently in tag_summary table.
                $prevTags = $dbw->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ );
-               $prevTags = $prevTags ? $prevTags : '';
+               $prevTags = $prevTags ?: '';
                $prevTags = array_filter( explode( ',', $prevTags ) );
 
                // add tags
@@ -452,9 +524,12 @@ class ChangeTags {
         * Is it OK to allow the user to apply all the specified tags at the same time
         * as they edit/make the change?
         *
+        * Extensions should not use this function, unless directly handling a user
+        * request to add a tag to a revision or log entry that the user is making.
+        *
         * @param array $tags Tags that you are interested in applying
-        * @param User|null $user User whose permission you wish to check, or null if
-        * you don't care (e.g. maintenance scripts)
+        * @param User|null $user User whose permission you wish to check, or null to
+        * check for a generic non-blocked user with the relevant rights
         * @return Status
         * @since 1.25
         */
@@ -519,10 +594,13 @@ class ChangeTags {
         * Is it OK to allow the user to adds and remove the given tags tags to/from a
         * change?
         *
+        * Extensions should not use this function, unless directly handling a user
+        * request to add or remove tags from an existing revision or log entry.
+        *
         * @param array $tagsToAdd Tags that you are interested in adding
         * @param array $tagsToRemove Tags that you are interested in removing
-        * @param User|null $user User whose permission you wish to check, or null if
-        * you don't care (e.g. maintenance scripts)
+        * @param User|null $user User whose permission you wish to check, or null to
+        * check for a generic non-blocked user with the relevant rights
         * @return Status
         * @since 1.25
         */
@@ -567,11 +645,15 @@ class ChangeTags {
         * Adds and/or removes tags to/from a given change, checking whether it is
         * allowed first, and adding a log entry afterwards.
         *
-        * Includes a call to ChangeTag::canUpdateTags(), so your code doesn't need
+        * Includes a call to ChangeTags::canUpdateTags(), so your code doesn't need
         * to do that. However, it doesn't check whether the *_id parameters are a
         * valid combination. That is up to you to enforce. See ApiTag::execute() for
         * an example.
         *
+        * Extensions should generally avoid this function. Call
+        * ChangeTags::updateTags() instead, unless directly handling a user request
+        * to add or remove tags from an existing revision or log entry.
+        *
         * @param array|null $tagsToAdd If none, pass array() or null
         * @param array|null $tagsToRemove If none, pass array() or null
         * @param int|null $rc_id The rc_id of the change to add the tags to
@@ -699,7 +781,8 @@ class ChangeTags {
         * @throws MWException When unable to determine appropriate JOIN condition for tagging
         */
        public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
-                                                                               &$join_conds, &$options, $filter_tag = '' ) {
+               &$join_conds, &$options, $filter_tag = ''
+       ) {
                global $wgUseTagFilter;
 
                // Normalize to arrays
@@ -792,8 +875,8 @@ class ChangeTags {
        }
 
        /**
-        * Defines a tag in the valid_tag table, without checking that the tag name
-        * is valid.
+        * Defines a tag in the valid_tag table and/or update ctd_user_defined field in change_tag_def,
+        * without checking that the tag name is valid.
         * Extensions should NOT use this function; they can use the ListDefinedTags
         * hook instead.
         *
@@ -801,26 +884,63 @@ class ChangeTags {
         * @since 1.25
         */
        public static function defineTag( $tag ) {
+               global $wgChangeTagsSchemaMigrationStage;
+
                $dbw = wfGetDB( DB_MASTER );
-               $dbw->replace( 'valid_tag',
+               if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) {
+                       $tagDef = [
+                               'ctd_name' => $tag,
+                               'ctd_user_defined' => 1,
+                               'ctd_count' => 0
+                       ];
+                       $dbw->upsert(
+                               'change_tag_def',
+                               $tagDef,
+                               [ 'ctd_name' ],
+                               [ 'ctd_user_defined' => 1 ],
+                               __METHOD__
+                       );
+               }
+
+               $dbw->replace(
+                       'valid_tag',
                        [ 'vt_tag' ],
                        [ 'vt_tag' => $tag ],
-                       __METHOD__ );
+                       __METHOD__
+               );
 
                // clear the memcache of defined tags
                self::purgeTagCacheAll();
        }
 
        /**
-        * 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.
+        * Removes a tag from the valid_tag table and/or update ctd_user_defined field in change_tag_def.
+        * The tag may remain in use by extensions, and may still show up as 'defined'
+        * if an extension is setting it from the ListDefinedTags hook.
         *
         * @param string $tag Tag to remove
         * @since 1.25
         */
        public static function undefineTag( $tag ) {
+               global $wgChangeTagsSchemaMigrationStage;
+
                $dbw = wfGetDB( DB_MASTER );
+
+               if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) {
+                       $dbw->update(
+                               'change_tag_def',
+                               [ 'ctd_name' => $tag ],
+                               [ 'ctd_user_defined' => 0 ],
+                               __METHOD__
+                       );
+
+                       $dbw->delete(
+                               'change_tag_def',
+                               [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
+                               __METHOD__
+                       );
+               }
+
                $dbw->delete( 'valid_tag', [ 'vt_tag' => $tag ], __METHOD__ );
 
                // clear the memcache of defined tags
@@ -1035,6 +1155,9 @@ class ChangeTags {
        /**
         * Is it OK to allow the user to create this tag?
         *
+        * Extensions should NOT use this function. In most cases, a tag can be
+        * defined using the ListDefinedTags hook without any checking.
+        *
         * @param string $tag Tag that you are interested in creating
         * @param User|null $user User whose permission you wish to check, or null if
         * you don't care (e.g. maintenance scripts)
@@ -1069,6 +1192,10 @@ class ChangeTags {
 
        /**
         * Creates a tag by adding a row to the `valid_tag` table.
+        * and/or add it to `change_tag_def` table.
+        *
+        * Extensions should NOT use this function; they can use the ListDefinedTags
+        * hook instead.
         *
         * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to
         * do that.
@@ -1116,10 +1243,11 @@ class ChangeTags {
         * @since 1.25
         */
        public static function deleteTagEverywhere( $tag ) {
+               global $wgChangeTagsSchemaMigrationStage;
                $dbw = wfGetDB( DB_MASTER );
                $dbw->startAtomic( __METHOD__ );
 
-               // delete from valid_tag
+               // delete from valid_tag and/or set ctd_user_defined = 0
                self::undefineTag( $tag );
 
                // find out which revisions use this tag, so we can delete from tag_summary
@@ -1138,6 +1266,10 @@ class ChangeTags {
                // delete from change_tag
                $dbw->delete( 'change_tag', [ 'ct_tag' => $tag ], __METHOD__ );
 
+               if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) {
+                       $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
+               }
+
                $dbw->endAtomic( __METHOD__ );
 
                // give extensions a chance
@@ -1226,7 +1358,7 @@ class ChangeTags {
 
                // store the tag usage statistics
                $tagUsage = self::tagUsageStatistics();
-               $hitcount = isset( $tagUsage[$tag] ) ? $tagUsage[$tag] : 0;
+               $hitcount = $tagUsage[$tag] ?? 0;
 
                // do it!
                $deleteResult = self::deleteTagEverywhere( $tag );
@@ -1273,20 +1405,9 @@ class ChangeTags {
                );
        }
 
-       /**
-        * @see listSoftwareActivatedTags
-        * @deprecated since 1.28 call listSoftwareActivatedTags directly
-        * @return array
-        */
-       public static function listExtensionActivatedTags() {
-               wfDeprecated( __METHOD__, '1.28' );
-               return self::listSoftwareActivatedTags();
-       }
-
        /**
         * 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().
+        * It returns a union of the results of listExplicitlyDefinedTags()
         *
         * @return string[] Array of strings: tags
         */
@@ -1363,18 +1484,6 @@ class ChangeTags {
                );
        }
 
-       /**
-        * Call listSoftwareDefinedTags directly
-        *
-        * @see listSoftwareDefinedTags
-        * @deprecated since 1.28
-        * @return array
-        */
-       public static function listExtensionDefinedTags() {
-               wfDeprecated( __METHOD__, '1.28' );
-               return self::listSoftwareDefinedTags();
-       }
-
        /**
         * Invalidates the short-term cache of defined tags used by the
         * list*DefinedTags functions, as well as the tag statistics cache.