'ApiRsd' => __DIR__ . '/includes/api/ApiRsd.php',
'ApiSetNotificationTimestamp' => __DIR__ . '/includes/api/ApiSetNotificationTimestamp.php',
'ApiStashEdit' => __DIR__ . '/includes/api/ApiStashEdit.php',
+ 'ApiTag' => __DIR__ . '/includes/api/ApiTag.php',
'ApiTokens' => __DIR__ . '/includes/api/ApiTokens.php',
'ApiUnblock' => __DIR__ . '/includes/api/ApiUnblock.php',
'ApiUndelete' => __DIR__ . '/includes/api/ApiUndelete.php',
'TableCleanupTest' => __DIR__ . '/maintenance/cleanupTable.inc',
'TableDiffFormatter' => __DIR__ . '/includes/diff/TableDiffFormatter.php',
'TablePager' => __DIR__ . '/includes/pager/TablePager.php',
+ 'TagLogFormatter' => __DIR__ . '/includes/logging/TagLogFormatter.php',
'TempFSFile' => __DIR__ . '/includes/filebackend/TempFSFile.php',
'TempFileRepo' => __DIR__ . '/includes/filerepo/FileRepo.php',
'TemplateParser' => __DIR__ . '/includes/TemplateParser.php',
*
* @throws MWException
* @return bool False if no changes are made, otherwise true
- *
- * @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
) {
- if ( !is_array( $tags ) ) {
- $tags = array( $tags );
- }
+ $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params );
+ return (bool)$result[0];
+ }
- $tags = array_filter( $tags ); // Make sure we're submitting all tags...
+ /**
+ * Add and remove tags to/from a change given its rc_id, rev_id and/or log_id,
+ * without verifying that the tags exist or are valid. If a tag is present in
+ * both $tagsToAdd and $tagsToRemove, it will be removed.
+ *
+ * This function should only be used by extensions to manipulate tags they
+ * have registered using the ListDefinedTags hook. When dealing with user
+ * input, call updateTagsWithChecks() instead.
+ *
+ * @param string|array|null $tagsToAdd Tags to add to the change
+ * @param string|array|null $tagsToRemove Tags to remove from the change
+ * @param int|null &$rc_id The rc_id of the change to add the tags to.
+ * Pass a variable whose value is null if the rc_id is not relevant or unknown.
+ * @param int|null &$rev_id The rev_id of the change to add the tags to.
+ * Pass a variable whose value is null if the rev_id is not relevant or unknown.
+ * @param int|null &$log_id The log_id of the change to add the tags to.
+ * Pass a variable whose value is null if the log_id is not relevant or unknown.
+ * @param string $params Params to put in the ct_params field of table
+ * 'change_tag' when adding tags
+ *
+ * @throws MWException When $rc_id, $rev_id and $log_id are all null
+ * @return array Index 0 is an array of tags actually added, index 1 is an
+ * array of tags actually removed, index 2 is an array of tags present on the
+ * revision or log entry before any changes were made
+ *
+ * @since 1.25
+ */
+ public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
+ &$rev_id = null, &$log_id = null, $params = null ) {
+
+ $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
+ $tagsToRemove = array_filter( (array)$tagsToRemove );
if ( !$rc_id && !$rev_id && !$log_id ) {
throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
- 'specified when adding a tag to a change!' );
+ 'specified when adding or removing a tag from a change!' );
}
$dbw = wfGetDB( DB_MASTER );
);
}
+ // update the tag_summary row
+ $prevTags = array();
+ if ( !self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id,
+ $log_id, $prevTags ) ) {
+
+ // nothing to do
+ return array( array(), array(), $prevTags );
+ }
+
+ // insert a row into change_tag for each new tag
+ if ( count( $tagsToAdd ) ) {
+ $tagsRows = array();
+ foreach ( $tagsToAdd as $tag ) {
+ // Filter so we don't insert NULLs as zero accidentally.
+ // Keep in mind that $rc_id === null means "I don't care/know about the
+ // rc_id, just delete $tag on this revision/log entry". It doesn't
+ // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
+ $tagsRows[] = array_filter(
+ array(
+ 'ct_tag' => $tag,
+ 'ct_rc_id' => $rc_id,
+ 'ct_log_id' => $log_id,
+ 'ct_rev_id' => $rev_id,
+ 'ct_params' => $params
+ )
+ );
+ }
+
+ $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array( 'IGNORE' ) );
+ }
+
+ // delete from change_tag
+ if ( count( $tagsToRemove ) ) {
+ foreach ( $tagsToRemove as $tag ) {
+ $conds = array_filter(
+ array(
+ 'ct_tag' => $tag,
+ 'ct_rc_id' => $rc_id,
+ 'ct_log_id' => $log_id,
+ 'ct_rev_id' => $rev_id
+ )
+ );
+ $dbw->delete( 'change_tag', $conds, __METHOD__ );
+ }
+ }
+
+ self::purgeTagUsageCache();
+ return array( $tagsToAdd, $tagsToRemove, $prevTags );
+ }
+
+ /**
+ * Adds or removes a given set of tags to/from the relevant row of the
+ * tag_summary table. Modifies the tagsToAdd and tagsToRemove arrays to
+ * reflect the tags that were actually added and/or removed.
+ *
+ * @param array &$tagsToAdd
+ * @param array &$tagsToRemove If a tag is present in both $tagsToAdd and
+ * $tagsToRemove, it will be removed
+ * @param int|null $rc_id Null if not known or not applicable
+ * @param int|null $rev_id Null if not known or not applicable
+ * @param int|null $log_id Null if not known or not applicable
+ * @param array &$prevTags Optionally outputs a list of the tags that were
+ * in the tag_summary row to begin with
+ * @return bool True if any modifications were made, otherwise false
+ * @since 1.25
+ */
+ protected static function updateTagSummaryRow( &$tagsToAdd, &$tagsToRemove,
+ $rc_id, $rev_id, $log_id, &$prevTags = array() ) {
+
+ $dbw = wfGetDB( DB_MASTER );
+
$tsConds = array_filter( array(
'ts_rc_id' => $rc_id,
'ts_rev_id' => $rev_id,
- 'ts_log_id' => $log_id )
- );
+ 'ts_log_id' => $log_id
+ ) );
+
+ // Can't both add and remove a tag at the same time...
+ $tagsToAdd = array_diff( $tagsToAdd, $tagsToRemove );
// Update the summary row.
// $prevTags can be out of date on slaves, especially when addTags is called consecutively,
$prevTags = $dbw->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ );
$prevTags = $prevTags ? $prevTags : '';
$prevTags = array_filter( explode( ',', $prevTags ) );
- $newTags = array_unique( array_merge( $prevTags, $tags ) );
+
+ // add tags
+ $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
+ $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
+
+ // remove tags
+ $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
+ $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
+
sort( $prevTags );
sort( $newTags );
-
if ( $prevTags == $newTags ) {
// No change.
return false;
}
- $dbw->replace(
- 'tag_summary',
- array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ),
- array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ),
- __METHOD__
- );
-
- // Insert the tags rows.
- $tagsRows = array();
- foreach ( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally.
- $tagsRows[] = array_filter(
- array(
- 'ct_tag' => $tag,
- 'ct_rc_id' => $rc_id,
- 'ct_log_id' => $log_id,
- 'ct_rev_id' => $rev_id,
- 'ct_params' => $params
- )
+ if ( !$newTags ) {
+ // no tags left, so delete the row altogether
+ $dbw->delete( 'tag_summary', $tsConds, __METHOD__ );
+ } else {
+ $dbw->replace( 'tag_summary',
+ array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ),
+ array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ),
+ __METHOD__
);
}
- $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array( 'IGNORE' ) );
-
- self::purgeTagUsageCache();
return true;
}
+ /**
+ * Helper function to generate a fatal status with a 'not-allowed' type error.
+ *
+ * @param string $msgOne Message key to use in the case of one tag
+ * @param string $msgMulti Message key to use in the case of more than one tag
+ * @param array $tags Restricted tags (passed as $1 into the message, count of
+ * $tags passed as $2)
+ * @return Status
+ * @since 1.25
+ */
+ protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
+ $lang = RequestContext::getMain()->getLanguage();
+ $count = count( $tags );
+ return Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
+ $lang->commaList( $tags ), $count );
+ }
+
+ /**
+ * Is it OK to allow the user to apply all the specified tags at the same time
+ * as they edit/make the change?
+ *
+ * @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)
+ * @return Status
+ * @since 1.25
+ */
+ public static function canAddTagsAccompanyingChange( array $tags,
+ User $user = null ) {
+
+ if ( !is_null( $user ) && !$user->isAllowed( 'applychangetags' ) ) {
+ return Status::newFatal( 'tags-apply-no-permission' );
+ }
+
+ // to be applied, a tag has to be explicitly defined
+ // @todo Allow extensions to define tags that can be applied by users...
+ $allowedTags = self::listExplicitlyDefinedTags();
+ $disallowedTags = array_diff( $tags, $allowedTags );
+ if ( $disallowedTags ) {
+ return self::restrictedTagError( 'tags-apply-not-allowed-one',
+ 'tags-apply-not-allowed-multi', $disallowedTags );
+ }
+
+ return Status::newGood();
+ }
+
+ /**
+ * Adds tags to a given change, checking whether it is allowed first, but
+ * without adding a log entry. Useful for cases where the tag is being added
+ * along with the action that generated the change (e.g. tagging an edit as
+ * it is being made).
+ *
+ * Extensions should not use this function, unless directly handling a user
+ * request to add a particular tag. Normally, extensions should call
+ * ChangeTags::updateTags() instead.
+ *
+ * @param array $tags Tags to apply
+ * @param int|null $rc_id The rc_id of the change to add the tags to
+ * @param int|null $rev_id The rev_id of the change to add the tags to
+ * @param int|null $log_id The log_id of the change to add the tags to
+ * @param string $params Params to put in the ct_params field of table
+ * 'change_tag' when adding tags
+ * @param User $user Who to give credit for the action
+ * @return Status
+ * @since 1.25
+ */
+ public static function addTagsAccompanyingChangeWithChecks( array $tags,
+ $rc_id, $rev_id, $log_id, $params, User $user ) {
+
+ // are we allowed to do this?
+ $result = self::canAddTagsAccompanyingChange( $tags, $user );
+ if ( !$result->isOK() ) {
+ $result->value = null;
+ return $result;
+ }
+
+ // do it!
+ self::addTags( $tagsToAdd, $rc_id, $rev_id, $log_id, $params );
+
+ return Status::newGood( true );
+ }
+
+ /**
+ * Is it OK to allow the user to adds and remove the given tags tags to/from a
+ * change?
+ *
+ * @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)
+ * @return Status
+ * @since 1.25
+ */
+ public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove,
+ User $user = null ) {
+
+ if ( !is_null( $user ) && !$user->isAllowed( 'changetags' ) ) {
+ return Status::newFatal( 'tags-update-no-permission' );
+ }
+
+ // to be added, a tag has to be explicitly defined
+ // @todo Allow extensions to define tags that can be applied by users...
+ $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
+ $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
+ if ( $diff ) {
+ return self::restrictedTagError( 'tags-update-add-not-allowed-one',
+ 'tags-update-add-not-allowed-multi', $diff );
+ }
+
+ // to be removed, a tag has to be either explicitly defined or not defined
+ // at all
+ $definedTags = self::listDefinedTags();
+ $diff = array_diff( $tagsToRemove, $explicitlyDefinedTags );
+ if ( $diff ) {
+ $intersect = array_intersect( $diff, $definedTags );
+ if ( $intersect ) {
+ return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
+ 'tags-update-remove-not-allowed-multi', $intersect );
+ }
+ }
+
+ return Status::newGood();
+ }
+
+ /**
+ * 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
+ * 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.
+ *
+ * @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
+ * @param int|null $rev_id The rev_id of the change to add the tags to
+ * @param int|null $log_id The log_id of the change to add the tags to
+ * @param string $params Params to put in the ct_params field of table
+ * 'change_tag' when adding tags
+ * @param string $reason Comment for the log
+ * @param User $user Who to give credit for the action
+ * @return Status If successful, the value of this Status object will be an
+ * object (stdClass) with the following fields:
+ * - logId: the ID of the added log entry, or null if no log entry was added
+ * (i.e. no operation was performed)
+ * - addedTags: an array containing the tags that were actually added
+ * - removedTags: an array containing the tags that were actually removed
+ * @since 1.25
+ */
+ public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
+ $rc_id, $rev_id, $log_id, $params, $reason, User $user ) {
+
+ if ( is_null( $tagsToAdd ) ) {
+ $tagsToAdd = array();
+ }
+ if ( is_null( $tagsToRemove ) ) {
+ $tagsToRemove = array();
+ }
+ if ( !$tagsToAdd && !$tagsToRemove ) {
+ // no-op, don't bother
+ return Status::newGood( (object)array(
+ 'logId' => null,
+ 'addedTags' => array(),
+ 'removedTags' => array(),
+ ) );
+ }
+
+ // are we allowed to do this?
+ $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $user );
+ if ( !$result->isOK() ) {
+ $result->value = null;
+ return $result;
+ }
+
+ // basic rate limiting
+ if ( $user->pingLimiter( 'changetag' ) ) {
+ return Status::newFatal( 'actionthrottledtext' );
+ }
+
+ // do it!
+ list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
+ $tagsToRemove, $rc_id, $rev_id, $log_id, $params );
+ if ( !$tagsAdded && !$tagsRemoved ) {
+ // no-op, don't log it
+ return Status::newGood( (object)array(
+ 'logId' => null,
+ 'addedTags' => array(),
+ 'removedTags' => array(),
+ ) );
+ }
+
+ // log it
+ $logEntry = new ManualLogEntry( 'tag', 'update' );
+ $logEntry->setPerformer( $user );
+ $logEntry->setComment( $reason );
+
+ // find the appropriate target page
+ if ( $rev_id ) {
+ $rev = Revision::newFromId( $rev_id );
+ if ( $rev ) {
+ $title = $rev->getTitle();
+ $logEntry->setTarget( $rev->getTitle() );
+ }
+ } elseif ( $log_id ) {
+ // This function is from revision deletion logic and has nothing to do with
+ // change tags, but it appears to be the only other place in core where we
+ // perform logged actions on log items.
+ $logEntry->setTarget( RevDelLogList::suggestTarget( 0, array( $log_id ) ) );
+ }
+
+ if ( !$logEntry->getTarget() ) {
+ // target is required, so we have to set something
+ $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
+ }
+
+ $logParams = array(
+ '4::revid' => $rev_id,
+ '5::logid' => $log_id,
+ '6:list:tagsAdded' => $tagsAdded,
+ '7:number:tagsAddedCount' => count( $tagsAdded ),
+ '8:list:tagsRemoved' => $tagsRemoved,
+ '9:number:tagsRemovedCount' => count( $tagsRemoved ),
+ 'initialTags' => $initialTags,
+ );
+ $logEntry->setParameters( $logParams );
+ $logEntry->setRelations( array( 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ) );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $logId = $logEntry->insert( $dbw );
+ // Only send this to UDP, not RC, similar to patrol events
+ $logEntry->publish( $logId, 'udp' );
+
+ return Status::newGood( (object)array(
+ 'logId' => $logId,
+ 'addedTags' => $tagsAdded,
+ 'removedTags' => $tagsRemoved,
+ ) );
+ }
+
/**
* Applies all tags-related changes to a query.
* Handles selecting tags, and filtering.
* it was deleted.
* @since 1.25
*/
- protected static function logTagAction( $action, $tag, $reason, User $user,
- $tagCount = null ) {
+ protected static function logTagManagementAction( $action, $tag, $reason,
+ User $user, $tagCount = null ) {
$dbw = wfGetDB( DB_MASTER );
self::defineTag( $tag );
// log it
- $logId = self::logTagAction( 'activate', $tag, $reason, $user );
+ $logId = self::logTagManagementAction( 'activate', $tag, $reason, $user );
return Status::newGood( $logId );
}
self::undefineTag( $tag );
// log it
- $logId = self::logTagAction( 'deactivate', $tag, $reason, $user );
+ $logId = self::logTagManagementAction( 'deactivate', $tag, $reason, $user );
return Status::newGood( $logId );
}
self::defineTag( $tag );
// log it
- $logId = self::logTagAction( 'create', $tag, $reason, $user );
+ $logId = self::logTagManagementAction( 'create', $tag, $reason, $user );
return Status::newGood( $logId );
}
array( 'ct_tag' => $tag ),
__METHOD__ );
foreach ( $result as $row ) {
- if ( $row->ct_rev_id ) {
- $field = 'ts_rev_id';
- $fieldValue = $row->ct_rev_id;
- } elseif ( $row->ct_log_id ) {
- $field = 'ts_log_id';
- $fieldValue = $row->ct_log_id;
- } elseif ( $row->ct_rc_id ) {
- $field = 'ts_rc_id';
- $fieldValue = $row->ct_rc_id;
- } else {
- // don't know what's up; just skip it
- continue;
- }
-
// remove the tag from the relevant row of tag_summary
- $tsResult = $dbw->selectField( 'tag_summary',
- 'ts_tags',
- array( $field => $fieldValue ),
- __METHOD__ );
- $tsValues = explode( ',', $tsResult );
- $tsValues = array_values( array_diff( $tsValues, array( $tag ) ) );
- if ( !$tsValues ) {
- // no tags left, so delete the row altogether
- $dbw->delete( 'tag_summary',
- array( $field => $fieldValue ),
- __METHOD__ );
- } else {
- $dbw->update( 'tag_summary',
- array( 'ts_tags' => implode( ',', $tsValues ) ),
- array( $field => $fieldValue ),
- __METHOD__ );
- }
+ $tagsToAdd = array();
+ $tagsToRemove = array( $tag );
+ self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $row->ct_rc_id,
+ $row->ct_rev_id, $row->ct_log_id );
}
// delete from change_tag
}
// log it
- $logId = self::logTagAction( 'delete', $tag, $reason, $user, $tagUsage[$tag] );
+ $logId = self::logTagManagementAction( 'delete', $tag, $reason, $user, $tagUsage[$tag] );
$deleteResult->value = $logId;
return $deleteResult;
}
$wgGroupPermissions['user']['minoredit'] = true;
$wgGroupPermissions['user']['purge'] = true; // can use ?action=purge without clicking "ok"
$wgGroupPermissions['user']['sendemail'] = true;
+$wgGroupPermissions['user']['applychangetags'] = true;
+$wgGroupPermissions['user']['changetags'] = true;
// Implicit group for accounts that pass $wgAutoConfirmAge
$wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true;
'newbie' => null,
'ip' => null,
'subnet' => null,
- )
+ ),
+ 'changetag' => array( // adding or removing change tags
+ 'user' => null,
+ 'newbie' => null,
+ ),
);
/**
'patrol',
'merge',
'suppress',
+ 'tag',
'managetags',
);
* for the link text.
*/
$wgFilterLogTypes = array(
- 'patrol' => true
+ 'patrol' => true,
+ 'tag' => true,
);
/**
'upload/overwrite' => 'LogFormatter',
'upload/revert' => 'LogFormatter',
'merge/merge' => 'MergeLogFormatter',
+ 'tag/update' => 'TagLogFormatter',
'managetags/create' => 'LogFormatter',
'managetags/delete' => 'LogFormatter',
'managetags/activate' => 'LogFormatter',
*/
const AS_SELF_REDIRECT = 236;
+ /**
+ * Status: an error relating to change tagging. Look at the message key for
+ * more details
+ */
+ const AS_CHANGE_TAG_ERROR = 237;
+
/**
* Status: can't parse content
*/
/** @var null|string */
public $contentFormat = null;
+ /** @var null|array */
+ public $changeTags = null;
+
# Placeholders for text injection by hooks (must be HTML)
# extensions should take care to _append_ to the present value
$this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
$this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
+
+ $changeTags = $request->getVal( 'wpChangeTags' );
+ if ( is_null( $changeTags ) || $changeTags === '' ) {
+ $this->changeTags = array();
+ } else {
+ $this->changeTags = array_filter( array_map( 'trim', explode( ',',
+ $changeTags ) ) );
+ }
} else {
# Not a posted form? Start with nothing.
wfDebug( __METHOD__ . ": Not a posted form.\n" );
return $status;
}
+ if ( $this->changeTags ) {
+ $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
+ $this->changeTags, $wgUser );
+ if ( !$changeTagsStatus->isOK() ) {
+ $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
+ return $changeTagsStatus;
+ }
+ }
+
if ( wfReadOnly() ) {
$status->fatal( 'readonlytext' );
$status->value = self::AS_READ_ONLY_PAGE;
$wgUser->pingLimiter( 'linkpurge' );
}
$result['redirect'] = $content->isRedirect();
+
$this->updateWatchlist();
+
+ if ( $this->changeTags && isset( $doEditStatus->value['revision'] ) ) {
+ // If a revision was created, apply any change tags that were requested
+ ChangeTags::addTags(
+ $this->changeTags,
+ isset( $doEditStatus->value['rc'] ) ? $doEditStatus->value['rc']->mAttribs['rc_id'] : null,
+ $doEditStatus->value['revision']->getId()
+ );
+ }
+
return $status;
}
*/
protected static $mCoreRights = array(
'apihighlimits',
+ 'applychangetags',
'autoconfirmed',
'autopatrol',
'bigdelete',
'blockemail',
'bot',
'browsearchive',
+ 'changetags',
'createaccount',
'createpage',
'createtalk',
'code' => 'nosuchrcid',
'info' => "There is no change with rcid \"\$1\""
),
+ 'nosuchlogid' => array(
+ 'code' => 'nosuchlogid',
+ 'info' => "There is no log entry with ID \"\$1\""
+ ),
'protect-invalidaction' => array(
'code' => 'protect-invalidaction',
'info' => "Invalid protection type \"\$1\""
$requestArray['wpWatchthis'] = '';
}
+ // Apply change tags
+ if ( count( $params['tags'] ) ) {
+ if ( $user->isAllowed( 'applychangetags' ) ) {
+ $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
+ } else {
+ $this->dieUsage( 'You don\'t have permission to set change tags.', 'taggingnotallowed' );
+ }
+ }
+
// Pass through anything else we might have been given, to support extensions
// This is kind of a hack but it's the best we can do to make extensions work
$requestArray += $this->getRequest()->getValues();
case EditPage::AS_TEXTBOX_EMPTY:
$this->dieUsageMsg( 'emptynewsection' );
+ case EditPage::AS_CHANGE_TAG_ERROR:
+ $this->dieStatus( $status );
+
case EditPage::AS_SUCCESS_NEW_ARTICLE:
$r['new'] = '';
// fall-through
),
'text' => null,
'summary' => null,
+ 'tags' => array(
+ ApiBase::PARAM_TYPE => ChangeTags::listExplicitlyDefinedTags(),
+ ApiBase::PARAM_ISMULTI => true,
+ ),
'minor' => false,
'notminor' => false,
'bot' => false,
'imagerotate' => 'ApiImageRotate',
'revisiondelete' => 'ApiRevisionDelete',
'managetags' => 'ApiManageTags',
+ 'tag' => 'ApiTag',
);
/**
--- /dev/null
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup API
+ * @since 1.25
+ */
+class ApiTag extends ApiBase {
+
+ protected function getAvailableTags() {
+ return ChangeTags::listExplicitlyDefinedTags();
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ // make sure the user is allowed
+ if ( !$this->getUser()->isAllowed( 'changetags' ) ) {
+ $this->dieUsage( "You don't have permission to add or remove change tags from individual edits",
+ 'permissiondenied' );
+ }
+
+ // validate and process each revid, rcid and logid
+ $this->requireAtLeastOneParameter( $params, 'revid', 'rcid', 'logid' );
+ $result = $this->getResult();
+ $ret = array();
+ if ( $params['revid'] ) {
+ foreach ( $params['revid'] as $id ) {
+ $ret[] = $this->processIndividual( 'revid', $params, $id, $result );
+ }
+ }
+ if ( $params['rcid'] ) {
+ foreach ( $params['rcid'] as $id ) {
+ $ret[] = $this->processIndividual( 'rcid', $params, $id, $result );
+ }
+ }
+ if ( $params['logid'] ) {
+ foreach ( $params['logid'] as $id ) {
+ $ret[] = $this->processIndividual( 'logid', $params, $id, $result );
+ }
+ }
+
+ $result->setIndexedTagName( $ret, 'result' );
+ $result->addValue( null, $this->getModuleName(), $ret );
+ }
+
+ protected static function validateLogId( $logid ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->selectField( 'logging', 'log_id', array( 'log_id' => $logid ),
+ __METHOD__ );
+ return (bool)$result;
+ }
+
+ protected function processIndividual( $type, $params, $id, &$result ) {
+ $idResult = array( $type => $id );
+
+ // validate the ID
+ $valid = false;
+ switch ( $type ) {
+ case 'rcid':
+ $valid = RecentChange::newFromId( $id );
+ break;
+ case 'revid':
+ $valid = Revision::newFromId( $id );
+ break;
+ case 'logid':
+ $valid = self::validateLogId( $id );
+ break;
+ }
+
+ if ( !$valid ) {
+ $idResult['status'] = 'error';
+ $idResult += $this->parseMsg( array( "nosuch$type", $id ) );
+ return $idResult;
+ }
+
+ $status = ChangeTags::updateTagsWithChecks( $params['add'],
+ $params['remove'],
+ ( $type === 'rcid' ? $id : null ),
+ ( $type === 'revid' ? $id : null ),
+ ( $type === 'logid' ? $id : null ),
+ null,
+ $params['reason'],
+ $this->getUser() );
+
+ if ( !$status->isOK() ) {
+ if ( $status->hasWarning( 'actionthrottledtext' ) ) {
+ $idResult['status'] = 'skipped';
+ } else {
+ $idResult['status'] = 'failure';
+ $ret['errors'] = $result->convertStatusToArray( $status, 'error' );
+ }
+ } else {
+ $idResult['status'] = 'success';
+ if ( is_null( $status->value->logId ) ) {
+ $idResult['noop'] = '';
+ } else {
+ $idResult['actionlogid'] = $status->value->logId;
+ $idResult['added'] = $status->value->addedTags;
+ $result->setIndexedTagName( $idResult['added'], 't' );
+ $idResult['removed'] = $status->value->removedTags;
+ $result->setIndexedTagName( $idResult['removed'], 't' );
+ }
+ }
+ return $idResult;
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'rcid' => array(
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_ISMULTI => true,
+ ),
+ 'revid' => array(
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_ISMULTI => true,
+ ),
+ 'logid' => array(
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_ISMULTI => true,
+ ),
+ 'add' => array(
+ ApiBase::PARAM_TYPE => $this->getAvailableTags(),
+ ApiBase::PARAM_ISMULTI => true,
+ ),
+ 'remove' => array(
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => true,
+ ),
+ 'reason' => array(
+ ApiBase::PARAM_DFLT => '',
+ ),
+ );
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ protected function getExamplesMessages() {
+ return array(
+ 'action=tag&revid=123&add=vandalism&token=123ABC'
+ => 'apihelp-tag-example-rev',
+ 'action=tag&logid=123&remove=spam&reason=Wrongly+applied&token=123ABC'
+ => 'apihelp-tag-example-log',
+ );
+ }
+
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Tag';
+ }
+}
"apihelp-edit-param-sectiontitle": "The title for a new section.",
"apihelp-edit-param-text": "Page content.",
"apihelp-edit-param-summary": "Edit summary. Also section title when $1section=new and $1sectiontitle is not set.",
+ "apihelp-edit-param-tags": "Change tags to apply to the revision.",
"apihelp-edit-param-minor": "Minor edit.",
"apihelp-edit-param-notminor": "Non-minor edit.",
"apihelp-edit-param-bot": "Mark this edit as bot.",
"apihelp-setnotificationtimestamp-example-pagetimestamp": "Set the notification timestamp for <kbd>Main page</kbd> so all edits since 1 January 2012 are unviewed.",
"apihelp-setnotificationtimestamp-example-allpages": "Reset the notification status for pages in the <kbd>{{ns:user}}</kbd> namespace.",
+ "apihelp-tag-description": "Add or remove change tags from individual revisions or log entries.",
+ "apihelp-tag-param-rcid": "One or more recent changes IDs from which to add or remove the tag.",
+ "apihelp-tag-param-revid": "One or more revision IDs from which to add or remove the tag.",
+ "apihelp-tag-param-logid": "One or more log entry IDs from which to add or remove the tag.",
+ "apihelp-tag-param-add": "Tags to add. Only manually defined tags can be added.",
+ "apihelp-tag-param-remove": "Tags to remove. Only tags that are either manually defined or completely undefined can be removed.",
+ "apihelp-tag-param-reason": "Reason for the change.",
+ "apihelp-tag-example-rev": "Add the <kbd>vandalism</kbd> tag from revision ID 123 without specifying a reason",
+ "apihelp-tag-example-log": "Remove the <kbd>spam</kbd> tag from log entry ID 123 with the reason <kbd>Wrongly applied</kbd>",
+
"apihelp-tokens-description": "Get tokens for data-modifying actions.\n\nThis module is deprecated in favor of [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
"apihelp-tokens-param-type": "Types of token to request.",
"apihelp-tokens-example-edit": "Retrieve an edit token (the default).",
"apihelp-edit-param-sectiontitle": "{{doc-apihelp-param|edit|sectiontitle}}",
"apihelp-edit-param-text": "{{doc-apihelp-param|edit|text}}",
"apihelp-edit-param-summary": "{{doc-apihelp-param|edit|summary}}",
+ "apihelp-edit-param-tags": "{{doc-apihelp-param|edit|tags}}",
"apihelp-edit-param-minor": "{{doc-apihelp-param|edit|minor}}\n{{Identical|Minor edit}}",
"apihelp-edit-param-notminor": "{{doc-apihelp-param|edit|notminor}}",
"apihelp-edit-param-bot": "{{doc-apihelp-param|edit|bot}}",
"apihelp-setnotificationtimestamp-example-page": "{{doc-apihelp-example|setnotificationtimestamp}}",
"apihelp-setnotificationtimestamp-example-pagetimestamp": "{{doc-apihelp-example|setnotificationtimestamp}}",
"apihelp-setnotificationtimestamp-example-allpages": "{{doc-apihelp-example|setnotificationtimestamp}}",
+ "apihelp-tag-description": "{{doc-apihelp-description|tag}}",
+ "apihelp-tag-param-rcid": "{{doc-apihelp-param|tag|rcid}}",
+ "apihelp-tag-param-revid": "{{doc-apihelp-param|tag|revid}}",
+ "apihelp-tag-param-logid": "{{doc-apihelp-param|tag|logid}}",
+ "apihelp-tag-param-add": "{{doc-apihelp-param|tag|add}}",
+ "apihelp-tag-param-remove": "{{doc-apihelp-param|tag|remove}}",
+ "apihelp-tag-param-reason": "{{doc-apihelp-param|tag|reason}}",
+ "apihelp-tag-example-rev": "{{doc-apihelp-example|tag}}",
+ "apihelp-tag-example-log": "{{doc-apihelp-example|tag}}",
"apihelp-tokens-description": "{{doc-apihelp-description|tokens}}",
"apihelp-tokens-param-type": "{{doc-apihelp-param|tokens|type}}",
"apihelp-tokens-example-edit": "{{doc-apihelp-example|tokens}}",
* * title-link: The value is a page title,
* returns link to this page
* * number: Format value as number
- * @param string $value The parameter value that should
- * be formated
+ * * list: Format value as a comma-separated list
+ * @param mixed $value The parameter value that should be formatted
* @return string|array Formated value
* @since 1.21
*/
case 'raw':
$value = Message::rawParam( $value );
break;
+ case 'list':
+ $value = $this->context->getLanguage()->commaList( $value );
+ break;
case 'msg':
$value = $this->msg( $value )->text();
break;
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * This class formats tag log entries.
+ *
+ * Parameters (one-based indexes):
+ * 4::revid
+ * 5::logid
+ * 6:list:tagsAdded
+ * 7:number:tagsAddedCount
+ * 8:list:tagsRemoved
+ * 9:number:tagsRemovedCount
+ *
+ * @since 1.25
+ */
+class TagLogFormatter extends LogFormatter {
+ protected function getMessageKey() {
+ $key = parent::getMessageKey();
+ $params = $this->getMessageParameters();
+
+ $add = ( isset( $params[6] ) && isset( $params[6]['num'] ) && $params[6]['num'] );
+ $remove = ( isset( $params[8] ) && isset( $params[8]['num'] ) && $params[8]['num'] );
+ $key .= ( $remove ? ( $add ? '' : '-remove' ) : '-add' );
+
+ if ( isset( $params[4] ) && $params[4] ) {
+ $key .= '-logentry';
+ } else {
+ $key .= '-revision';
+ }
+
+ return $key;
+ }
+}
"right-sendemail": "Send email to other users",
"right-passwordreset": "View password reset emails",
"right-managechangetags": "Create and delete [[Special:Tags|tags]] from the database",
+ "right-applychangetags": "Apply [[Special:Tags|tags]] along with one's changes",
+ "right-changetags": "Add and remove arbitrary [[Special:Tags|tags]] on individual revisions and log entries",
"newuserlogpage": "User creation log",
"newuserlogpagetext": "This is a log of user creations.",
"rightslog": "User rights log",
"action-editmyprivateinfo": "edit your private information",
"action-editcontentmodel": "edit the content model of a page",
"action-managechangetags": "create and delete tags from the database",
+ "action-applychangetags": "apply tags along with your changes",
+ "action-changetags": "add and remove arbitrary tags on individual revisions and log entries",
"nchanges": "$1 {{PLURAL:$1|change|changes}}",
"enhancedrc-since-last-visit": "$1 {{PLURAL:$1|since last visit}}",
"enhancedrc-history": "history",
"patrol-log-page": "Patrol log",
"patrol-log-header": "This is a log of patrolled revisions.",
"log-show-hide-patrol": "$1 patrol log",
+ "log-show-hide-tag": "$1 tag log",
"deletedrevision": "Deleted old revision $1",
"filedeleteerror-short": "Error deleting file: $1",
"filedeleteerror-long": "Errors were encountered while deleting the file:\n\n$1",
"tags-deactivate-reason": "Reason:",
"tags-deactivate-not-allowed": "It is not possible to deactivate the tag \"$1\".",
"tags-deactivate-submit": "Deactivate",
+ "tags-apply-no-permission": "You do not have permission to apply change tags along with your changes.",
+ "tags-apply-not-allowed-one": "The tag \"$1\" is not allowed to be manually applied.",
+ "tags-apply-not-allowed-multi": "The following {{PLURAL:$2|tag is|tags are}} not allowed to be manually applied: $1",
+ "tags-update-no-permission": "You do not have permission to add or remove change tags from individual revisions or log entries.",
+ "tags-update-add-not-allowed-one": "The tag \"$1\" is not allowed to be manually added.",
+ "tags-update-add-not-allowed-multi": "The following {{PLURAL:$2|tag is|tags are}} not allowed to be manually added: $1",
+ "tags-update-remove-not-allowed-one": "The tag \"$1\" is not allowed to be removed.",
+ "tags-update-remove-not-allowed-multi": "The following {{PLURAL:$2|tag is|tags are}} not allowed to be manually removed: $1",
"comparepages": "Compare pages",
"comparepages-summary": "",
"compare-page1": "Page 1",
"logentry-managetags-delete": "$1 {{GENDER:$2|deleted}} the tag \"$4\" (removed from $5 {{PLURAL:$5|revision or log entry|revisions and/or log entries}})",
"logentry-managetags-activate": "$1 {{GENDER:$2|activated}} the tag \"$4\" for use by users and bots",
"logentry-managetags-deactivate": "$1 {{GENDER:$2|deactivated}} the tag \"$4\" for use by users and bots",
+ "log-name-tag": "Tag log",
+ "log-description-tag": "This page shows when users have added or removed [[Special:Tags|tags]] from individual revisions or log entries. The log does not list tagging actions when they occur as part of an edit, deletion, or similar action.",
+ "logentry-tag-update-add-revision": "$1 {{GENDER:$2|added}} the {{PLURAL:$7|tag|tags}} $6 to revision $4 of page $3",
+ "logentry-tag-update-add-logentry": "$1 {{GENDER:$2|added}} the {{PLURAL:$7|tag|tags}} $6 to log entry $5 of page $3",
+ "logentry-tag-update-remove-revision": "$1 {{GENDER:$2|removed}} the {{PLURAL:$9|tag|tags}} $8 from revision $4 of page $3",
+ "logentry-tag-update-remove-logentry": "$1 {{GENDER:$2|removed}} the {{PLURAL:$9|tag|tags}} $8 from log entry $5 of page $3",
+ "logentry-tag-update-revision": "$1 {{GENDER:$2|updated}} tags on revision $4 of page $3 ({{PLURAL:$7|added}} $6; {{PLURAL:$9|removed}} $8)",
+ "logentry-tag-update-logentry": "$1 {{GENDER:$2|updated}} tags on log entry $5 of page $3 ({{PLURAL:$7|added}} $6; {{PLURAL:$9|removed}} $8)",
"rightsnone": "(none)",
"revdelete-logentry": "changed revision visibility of \"[[$1]]\"",
"logdelete-logentry": "changed event visibility of \"[[$1]]\"",
"right-sendemail": "{{doc-right|sendemail}}",
"right-passwordreset": "{{doc-right|passwordreset}}",
"right-managechangetags": "{{doc-right|managechangetags}}",
+ "right-applychangetags": "{{doc-right|applychangetags}}",
+ "right-changetags": "{{doc-right|changetags}}",
"newuserlogpage": "{{doc-logpage}}\n\nPart of the \"Newuserlog\" extension. It is both the title of [[Special:Log/newusers]] and the link you can see in [[Special:RecentChanges]].",
"newuserlogpagetext": "Part of the \"Newuserlog\" extension. It is the description you can see on [[Special:Log/newusers]].",
"rightslog": "{{doc-logpage}}\n\nIn [[Special:Log]]",
"action-editmyprivateinfo": "{{doc-action|editmyprivateinfo}}",
"action-editcontentmodel": "{{doc-action|editcontentmodel}}",
"action-managechangetags": "{{doc-action|managechangetags}}",
+ "action-applychangetags": "{{doc-action|applychangetags}}",
+ "action-changetags": "{{doc-action|changetags}}",
"nchanges": "Appears on enhanced watchlist and recent changes when page has more than one change on given date, linking to a diff of the changes.\n\nParameters:\n* $1 - the number of changes on that day (2 or more)\nThree messages are shown side-by-side: ({{msg-mw|Nchanges}} | {{msg-mw|Enhancedrc-since-last-visit}} | {{msg-mw|Enhancedrc-history}}).",
"enhancedrc-since-last-visit": "Appears on enhanced watchlist and recent changes when page has more than one change on given date and at least one that the user hasn't seen yet, linking to a diff of the unviewed changes.\n\nParameters:\n* $1 - the number of unviewed changes (1 or more)\nThree messages are shown side-by-side: ({{msg-mw|nchanges}} | {{msg-mw|enhancedrc-since-last-visit}} | {{msg-mw|enhancedrc-history}}).",
"enhancedrc-history": "Appears on enhanced watchlist and recent changes when page has more than one change on given date, linking to its history.\n\nThis is the same as {{msg-mw|hist}}, but not abbreviated.\n\nThree messages are shown side-by-side: ({{msg-mw|nchanges}} | {{msg-mw|enhancedrc-since-last-visit}} | {{msg-mw|enhancedrc-history}}).\n{{Identical|History}}",
"patrol-log-page": "{{doc-logpage}}",
"patrol-log-header": "Text that appears above the log entries on the [[Special:log|patrol log]].",
"log-show-hide-patrol": "Used in [[Special:Log]]. Parameters:\n* $1 - link text; one of {{msg-mw|Show}} or {{msg-mw|Hide}}\n{{Related|Log-show-hide}}",
+ "log-show-hide-tag": "Used in [[Special:Log]]. Parameters:\n* $1 - link text; one of {{msg-mw|Show}} or {{msg-mw|Hide}}\n{{Related|Log-show-hide}}",
"deletedrevision": "Used as log comment. Parameters:\n* $1 - archive name of old image",
"filedeleteerror-short": "Used as error message. Parameters:\n* $1 – There are two uses: 1) filename or 2) more specific error message like {{msg-mw|Backend-fail-internal}}.\nSee also:\n* {{msg-mw|Filedeleteerror-long}}",
"filedeleteerror-long": "Used as error message. Parameters:\n* $1 - ...\nSee also:\n* {{msg-mw|Filedeleteerror-short}}",
"tags-deactivate-reason": "{{Identical|Reason}}",
"tags-deactivate-not-allowed": "Error message on [[Special:Tags]]",
"tags-deactivate-submit": "The label of the form \"submit\" button when the user is about to deactivate a tag.\n{{Identical|Deactivate}}",
+ "tags-apply-no-permission": "Error message seen via the API when a user lacks the permission to apply change tags.",
+ "tags-apply-not-allowed-one": "Error message seen via the API when a user tries to apply a single tag that is not properly defined. This message is only ever used in the case of 1 tag.\n\nParameters:\n* $1 - tag name",
+ "tags-apply-not-allowed-multi": "Error message seen via the API when a user tries to apply more than one tag that is not properly defined.\n\nParameters:\n* $1 - comma-separated list of tag names\n* $2 - number of tags",
+ "tags-update-no-permission": "Error message seen via the API when a user lacks the permission to add or remove change tags after the fact.",
+ "tags-update-add-not-allowed-one": "Error message seen via the API when a user tries to add a single tag that is not properly defined. This message is only ever used in the case of 1 tag.\n\nParameters:\n* $1 - tag name",
+ "tags-update-add-not-allowed-multi": "Error message seen via the API when a user tries to add more than one tag that is not properly defined.\n\nParameters:\n* $1 - comma-separated list of tag names\n* $2 - number of tags",
+ "tags-update-remove-not-allowed-one": "Error message seen via the API when a user tries to remove a single tag that is not properly defined. This message is only ever used in the case of 1 tag.\n\nParameters:\n* $1 - tag name",
+ "tags-update-remove-not-allowed-multi": "Error message seen via the API when a user tries to remove more than one tag that is not properly defined.\n\nParameters:\n* $1 - comma-separated list of tag names\n* $2 - number of tags",
"comparepages": "The title of [[Special:ComparePages]]",
"comparepages-summary": "{{doc-specialpagesummary|comparepages}}",
"compare-page1": "Label for the field of the 1st page in the comparison for [[Special:ComparePages]]\n{{Identical|Page}}",
"logentry-upload-upload": "{{Logentry|[[Special:Log/upload]]}}",
"logentry-upload-overwrite": "{{Logentry|[[Special:Log/upload]]}}",
"logentry-upload-revert": "{{Logentry|[[Special:Log/upload]]}}",
- "log-name-managetags": "The title of a log which contains entries related to the management of change tags. \"Tag\" here refers to the same thing as {{msg-mw|tags-tag}}.",
+ "log-name-managetags": "The title of a log which contains entries related to the management of change tags. This includes creation and deletion of the tags themselves. \"Tag\" here refers to the same thing as {{msg-mw|tags-tag}}.",
"log-description-managetags": "The description of the tag management log. \"Tag\" here refers to the same thing as {{msg-mw|tags-tag}}.",
"logentry-managetags-create": "{{Logentry|[[Special:Log/managetags]]}}\n*$4 - tag name",
"logentry-managetags-delete": "{{Logentry|[[Special:Log/managetags]]}}\n*$4 - tag name\n* $5 - number of revisions + log entries that were tagged with the tag",
"logentry-managetags-activate": "{{Logentry|[[Special:Log/managetags]]}}\n*$4 - tag name",
"logentry-managetags-deactivate": "{{Logentry|[[Special:Log/managetags]]}}\n*$4 - tag name",
+ "log-name-tag": "The title of a log which contains entries related to applying and removing change tags from individual revisions or log entries. \"Tag\" here refers to the same thing as {{msg-mw|tags-tag}}.",
+ "log-description-tag": "The description of the tag log. \"Tag\" here refers to the same thing as {{msg-mw|tags-tag}}.",
+ "logentry-tag-update-add-revision": "{{Logentry|[[Special:Log/tag]]}}\n*$4 - revision ID\n* $6 - list of tags that were added, separated by {{msg-mw|Comma-separator}}\n* $7 - number of added tags",
+ "logentry-tag-update-add-logentry": "{{Logentry|[[Special:Log/tag]]}}\n*$5 - log entry ID\n* $6 - list of tags that were added, separated by {{msg-mw|Comma-separator}}\n* $7 - number of added tags",
+ "logentry-tag-update-remove-revision": "{{Logentry|[[Special:Log/tag]]}}\n*$4 - revision ID\n* $8 - list of tags that were removed, separated by {{msg-mw|Comma-separator}}\n* $9 - number of removed tags",
+ "logentry-tag-update-remove-logentry": "{{Logentry|[[Special:Log/tag]]}}\n*$5 - log entry ID\n* $8 - list of tags that were removed, separated by {{msg-mw|Comma-separator}}\n* $9 - number of removed tags",
+ "logentry-tag-update-revision": "{{Logentry|[[Special:Log/tag]]}}\n*$4 - revision ID\n* $6 - list of tags that were added, separated by {{msg-mw|Comma-separator}}\n* $7 - number of added tags\n* $8 - list of tags that were removed, separated by {{msg-mw|Comma-separator}}\n* $9 - number of removed tags",
+ "logentry-tag-update-logentry": "{{Logentry|[[Special:Log/tag]]}}\n*$5 - log entry ID\n* $6 - list of tags that were added, separated by {{msg-mw|Comma-separator}}\n* $7 - number of added tags\n* $8 - list of tags that were removed, separated by {{msg-mw|Comma-separator}}\n* $9 - number of removed tags",
"rightsnone": "Default rights for registered users.\n\n{{Identical|None}}",
"revdelete-logentry": "{{RevisionDelete}}\nThis is the message for the log entry in [[Special:Log/delete]] when changing visibility restrictions for page revisions.\n\nFollowed by the message {{msg-mw|revdelete-log-message}} in brackets.\n\nPreceded by the name of the user doing this task.\n\nParameters:\n* $1 - the page name\nSee also:\n* {{msg-mw|Logdelete-logentry}}",
"logdelete-logentry": "{{RevisionDelete}}\nThis is the message for the log entry in [[Special:Log/delete]] when changing visibility restrictions for log events.\n\nFollowed by the message {{msg-mw|logdelete-log-message}} in brackets.\n\nPreceded by the name of the user who did this task.\n\nParameters:\n* $1 - the log name in brackets\nSee also:\n* {{msg-mw|Revdelete-logentry}}",