From f6879ea16edf008eb012bd4dbe133e2ba4b9338f Mon Sep 17 00:00:00 2001 From: Kai_WMDE Date: Sun, 17 May 2015 18:09:59 +0200 Subject: [PATCH] Enable users to watch category membership changes Bug: T9148 Change-Id: I5a89d8f19804b1120f4c755d834e2da6ca12ceae --- autoload.php | 1 + includes/DefaultSettings.php | 2 + includes/Defines.php | 1 + includes/Preferences.php | 12 + includes/api/ApiFeedRecentChanges.php | 1 + includes/api/ApiQueryRecentChanges.php | 9 +- includes/api/ApiQueryWatchlist.php | 9 +- includes/api/i18n/en.json | 3 +- includes/api/i18n/qqq.json | 1 + includes/changes/CategoryMembershipChange.php | 205 ++++++++++++++++++ includes/changes/ChangesList.php | 35 +++ includes/changes/EnhancedChangesList.php | 17 +- includes/changes/OldChangesList.php | 6 +- includes/changes/RCCacheEntryFactory.php | 4 + includes/changes/RecentChange.php | 117 +++++++--- includes/deferred/LinksUpdate.php | 43 +++- includes/jobqueue/jobs/RefreshLinksJob.php | 7 + includes/page/WikiPage.php | 3 + .../specialpage/ChangesListSpecialPage.php | 4 + includes/specials/SpecialRecentchanges.php | 10 +- includes/specials/SpecialWatchlist.php | 4 +- languages/i18n/en.json | 9 + languages/i18n/qqq.json | 9 + .../changes/EnhancedChangesListTest.php | 43 ++++ .../changes/TestRecentChangesHelper.php | 30 +++ .../includes/deferred/LinksUpdateTest.php | 86 +++++++- 26 files changed, 614 insertions(+), 57 deletions(-) create mode 100644 includes/changes/CategoryMembershipChange.php diff --git a/autoload.php b/autoload.php index 6444e3efa8..acb272f262 100644 --- a/autoload.php +++ b/autoload.php @@ -197,6 +197,7 @@ $wgAutoloadLocalClasses = array( 'CapsCleanup' => __DIR__ . '/maintenance/cleanupCaps.php', 'Category' => __DIR__ . '/includes/Category.php', 'CategoryFinder' => __DIR__ . '/includes/CategoryFinder.php', + 'CategoryMembershipChange' => __DIR__ . '/includes/changes/CategoryMembershipChange.php', 'CategoryPage' => __DIR__ . '/includes/page/CategoryPage.php', 'CategoryPager' => __DIR__ . '/includes/specials/SpecialCategories.php', 'CategoryViewer' => __DIR__ . '/includes/CategoryViewer.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 6050ba7bc6..12aa938717 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -4503,6 +4503,7 @@ $wgDefaultUserOptions = array( 'gender' => 'unknown', 'hideminor' => 0, 'hidepatrolled' => 0, + 'hidecategorization' => 0, 'imagesize' => 2, 'math' => 1, 'minordefault' => 0, @@ -4534,6 +4535,7 @@ $wgDefaultUserOptions = array( 'watchlisthideminor' => 0, 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, + 'watchlisthidecategorization' => 0, 'watchmoves' => 0, 'watchrollback' => 0, 'wllimit' => 250, diff --git a/includes/Defines.php b/includes/Defines.php index d55bbcf819..38f2d424da 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -170,6 +170,7 @@ define( 'RC_EDIT', 0 ); define( 'RC_NEW', 1 ); define( 'RC_LOG', 3 ); define( 'RC_EXTERNAL', 5 ); +define( 'RC_CATEGORIZE', 6 ); /**@}*/ /**@{ diff --git a/includes/Preferences.php b/includes/Preferences.php index 9497ee78aa..deea7572e3 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -888,6 +888,12 @@ class Preferences { 'section' => 'rc/advancedrc', ); + $defaultPreferences['hidecategorization'] = array( + 'type' => 'toggle', + 'label-message' => 'tog-hidecategorization', + 'section' => 'rc/advancedrc', + ); + if ( $user->useRCPatrol() ) { $defaultPreferences['hidepatrolled'] = array( 'type' => 'toggle', @@ -995,6 +1001,12 @@ class Preferences { 'label-message' => 'tog-watchlisthideliu', ); + $defaultPreferences['watchlisthidecategorization'] = array( + 'type' => 'toggle', + 'section' => 'watchlist/advancedwatchlist', + 'label-message' => 'tog-watchlisthidecategorization', + ); + if ( $user->useRCPatrol() ) { $defaultPreferences['watchlisthidepatrolled'] = array( 'type' => 'toggle', diff --git a/includes/api/ApiFeedRecentChanges.php b/includes/api/ApiFeedRecentChanges.php index d24112c7c8..5adde875f1 100644 --- a/includes/api/ApiFeedRecentChanges.php +++ b/includes/api/ApiFeedRecentChanges.php @@ -155,6 +155,7 @@ class ApiFeedRecentChanges extends ApiBase { 'hideliu' => false, 'hidepatrolled' => false, 'hidemyself' => false, + 'hidecategorization' => false, 'tagfilter' => array( ApiBase::PARAM_TYPE => 'string', diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 74bccc2e85..b6d2c4069a 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -667,14 +667,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 ), 'type' => array( - ApiBase::PARAM_DFLT => 'edit|new|log', + ApiBase::PARAM_DFLT => 'edit|new|log|categorize', ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => array( - 'edit', - 'external', - 'new', - 'log' - ) + ApiBase::PARAM_TYPE => RecentChange::getChangeTypes() ), 'toponly' => false, 'continue' => array( diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 648d25965d..9d35a55233 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -483,14 +483,9 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ) ), 'type' => array( - ApiBase::PARAM_DFLT => 'edit|new|log', + ApiBase::PARAM_DFLT => 'edit|new|log|categorize', ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => array( - 'edit', - 'external', - 'new', - 'log', - ) + ApiBase::PARAM_TYPE => RecentChange::getChangeTypes() ), 'owner' => array( ApiBase::PARAM_TYPE => 'user' diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 5a574d978c..d09718ecd4 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -162,6 +162,7 @@ "apihelp-feedrecentchanges-param-hideliu": "Hide changes made by registered users.", "apihelp-feedrecentchanges-param-hidepatrolled": "Hide patrolled changes.", "apihelp-feedrecentchanges-param-hidemyself": "Hide changes made by the current user.", + "apihelp-feedrecentchanges-param-hidecategorization": "Hide category membership changes.", "apihelp-feedrecentchanges-param-tagfilter": "Filter by tag.", "apihelp-feedrecentchanges-param-target": "Show only changes on pages linked from this page.", "apihelp-feedrecentchanges-param-showlinkedto": "Show changes on pages linked to the selected page instead.", @@ -1013,7 +1014,7 @@ "apihelp-query+watchlist-paramvalue-prop-loginfo": "Adds log information where appropriate.", "apihelp-query+watchlist-param-show": "Show only items that meet these criteria. For example, to see only minor edits done by logged-in users, set $1show=minor|!anon.", - "apihelp-query+watchlist-param-type": "Which types of changes to show:\n;edit:Regular page edits.\n;external:External changes.\n;new:Page creations.\n;log:Log entries.", + "apihelp-query+watchlist-param-type": "Which types of changes to show:\n;edit:Regular page edits.\n;external:External changes.\n;new:Page creations.\n;log:Log entries.\n;categorize:Category membership changes.", "apihelp-query+watchlist-param-owner": "Used along with $1token to access a different user's watchlist.", "apihelp-query+watchlist-param-token": "A security token (available in the user's [[Special:Preferences#mw-prefsection-watchlist|preferences]]) to allow access to another user's watchlist.", "apihelp-query+watchlist-example-simple": "List the top revision for recently changed pages on the current user's watchlist.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 95562d8f1d..e33378403f 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -157,6 +157,7 @@ "apihelp-feedrecentchanges-param-hideliu": "{{doc-apihelp-param|feedrecentchanges|hideliu}}", "apihelp-feedrecentchanges-param-hidepatrolled": "{{doc-apihelp-param|feedrecentchanges|hidepatrolled}}", "apihelp-feedrecentchanges-param-hidemyself": "{{doc-apihelp-param|feedrecentchanges|hidemyself}}", + "apihelp-feedrecentchanges-param-hidecategorization": "{{doc-apihelp-param|feedrecentchanges|hidecategorization}}", "apihelp-feedrecentchanges-param-tagfilter": "{{doc-apihelp-param|feedrecentchanges|tagfilter}}", "apihelp-feedrecentchanges-param-target": "{{doc-apihelp-param|feedrecentchanges|target}}", "apihelp-feedrecentchanges-param-showlinkedto": "{{doc-apihelp-param|feedrecentchanges|showlinkedto}}", diff --git a/includes/changes/CategoryMembershipChange.php b/includes/changes/CategoryMembershipChange.php new file mode 100644 index 0000000000..8bc4cf57e4 --- /dev/null +++ b/includes/changes/CategoryMembershipChange.php @@ -0,0 +1,205 @@ +pageTitle = $pageTitle; + $this->page = WikiPage::factory( $pageTitle ); + $this->timestamp = wfTimestampNow(); + + # if no revision is given, the change was probably triggered by parser functions + if ( $revision ) { + $this->revision = $revision; + $this->correspondingRC = $this->revision->getRecentChange(); + $this->user = $this->getRevisionUser(); + } else { + $this->user = User::newFromId( 0 ); + } + } + + /** + * Determines the number of template links for recursive link updates + */ + public function setRecursive() { + $this->numTemplateLinks = $this->pageTitle->getBacklinkCache()->getNumLinks( 'templatelinks' ); + } + + /** + * Create a recentchanges entry for category additions + * @param string $categoryName + */ + public function pageAddedToCategory( $categoryName ) { + $this->createRecentChangesEntry( $categoryName, self::CATEGORY_ADDITION ); + } + + /** + * Create a recentchanges entry for category removals + * @param string $categoryName + */ + public function pageRemovedFromCategory( $categoryName ) { + $this->createRecentChangesEntry( $categoryName, self::CATEGORY_REMOVAL ); + } + + /** + * Create a recentchanges entry using RecentChange::notifyCategorization() + * @param string $categoryName + * @param int $type + */ + private function createRecentChangesEntry( $categoryName, $type ) { + $categoryTitle = Title::newFromText( $categoryName, NS_CATEGORY ); + if ( !$categoryTitle ) { + return; + } + + $previousRevTimestamp = $this->getPreviousRevisionTimestamp(); + $unpatrolled = $this->revision ? $this->revision->isUnpatrolled() : 0; + + $lastRevId = 0; + $bot = 0; + $ip = ''; + if ( $this->correspondingRC ) { + $lastRevId = $this->correspondingRC->getAttribute( 'rc_last_oldid' ) ?: 0; + $bot = $this->correspondingRC->getAttribute( 'rc_bot' ) ?: 0; + $ip = $this->correspondingRC->getAttribute( 'rc_ip' ) ?: ''; + } + + RecentChange::notifyCategorization( + $this->timestamp, + $categoryTitle, + $this->user, + $this->getChangeMessage( $type, array( + 'prefixedUrl' => $this->page->getTitle()->getPrefixedURL(), + 'numTemplateLinks' => $this->numTemplateLinks + ) ), + $this->pageTitle, + $lastRevId, + $this->revision ? $this->revision->getId() : 0, + $previousRevTimestamp, + $bot, + $ip, + $unpatrolled ? 0 : 1, + $this->revision ? $this->revision->getVisibility() & Revision::SUPPRESSED_USER : 0 + ); + } + + /** + * Get the user who created the revision. may be an anonymous IP + * @return User + */ + private function getRevisionUser() { + $userId = $this->revision->getUser( Revision::RAW ); + if ( $userId === 0 ) { + return User::newFromName( $this->revision->getUserText( Revision::RAW ), false ); + } else { + return User::newFromId( $userId ); + } + } + + /** + * Returns the change message according to the type of category membership change + * + * The message keys created in this method may be one of: + * - recentchanges-page-added-to-category + * - recentchanges-page-added-to-category-bundled + * - recentchanges-page-removed-from-category + * - recentchanges-page-removed-from-category-bundled + * + * @param int $type may be CategoryMembershipChange::CATEGORY_ADDITION + * or CategoryMembershipChange::CATEGORY_REMOVAL + * @param array $params + * - prefixedUrl: result of Title::->getPrefixedURL() + * - numTemplateLinks + * @return string + */ + private function getChangeMessage( $type, array $params ) { + $msgKey = 'recentchanges-'; + + switch ( $type ) { + case self::CATEGORY_ADDITION: + $msgKey .= 'page-added-to-category'; + break; + case self::CATEGORY_REMOVAL: + $msgKey .= 'page-removed-from-category'; + break; + } + + if ( intval( $params['numTemplateLinks'] ) > 0 ) { + $msgKey .= '-bundled'; + } + + return wfMessage( $msgKey, $params )->inContentLanguage()->text(); + } + + /** + * Returns the timestamp of the page's previous revision or null if the latest revision + * does not refer to a parent revision + * @return null|string + */ + private function getPreviousRevisionTimestamp() { + $latestRev = Revision::newFromId( $this->pageTitle->getLatestRevID() ); + $previousRev = Revision::newFromId( $latestRev->getParentId() ); + + return $previousRev ? $previousRev->getTimestamp() : null; + } + +} diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index fdc9944fdc..249f53f0d7 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -456,6 +456,28 @@ class ChangesList extends ContextSource { } } + /** + * @param RCCacheEntry $rc + * @param array $query array of key/value pairs to append as a query string + * @return string + * @since 1.26 + */ + public function getDiffHistLinks( RCCacheEntry $rc, array $query ) { + $pageTitle = $rc->getTitle(); + if ( intval( $rc->getAttribute( 'rc_type' ) ) === RC_CATEGORIZE ) { + $pageTitle = Title::newFromID( $rc->getAttribute( 'rc_cur_id' ) ); + } + + $retVal = ' ' . $this->msg( 'parentheses' ) + ->rawParams( $rc->difflink . $this->message['pipe-separator'] . Linker::linkKnown( + $pageTitle, + $this->message['hist'], + array(), + $query + ) )->escaped(); + return $retVal; + } + /** * Check whether to enable recent changes patrol features * @@ -630,4 +652,17 @@ class ChangesList extends ContextSource { return false; } + + /** + * Determines whether a revision is linked to this change; this may not be the case + * when the categorization wasn't done by an edit but a conditional parser function + * + * @param RecentChange|RCCacheEntry $rcObj + * @return bool + */ + protected function isCategorizationWithoutRevision( $rcObj ) { + return intval( $rcObj->getAttribute( 'rc_type' ) ) === RC_CATEGORIZE + && intval( $rcObj->getAttribute( 'rc_this_oldid' ) ) === 0; + } + } diff --git a/includes/changes/EnhancedChangesList.php b/includes/changes/EnhancedChangesList.php index e5916bd7f2..a1e9d2843b 100644 --- a/includes/changes/EnhancedChangesList.php +++ b/includes/changes/EnhancedChangesList.php @@ -364,6 +364,8 @@ class EnhancedChangesList extends ChangesList { if ( $rcObj->mAttribs['rc_type'] == RC_LOG ) { $data['logEntry'] = $this->insertLogEntry( $rcObj ); + } elseif ( $this->isCategorizationWithoutRevision( $rcObj ) ) { + $data['comment'] = $this->insertComment( $rcObj ); } else { # User links $data['userLink'] = $rcObj->userlink; @@ -566,15 +568,9 @@ class EnhancedChangesList extends ChangesList { } # Diff and hist links - if ( $type != RC_LOG ) { + if ( intval( $type ) !== RC_LOG && intval( $type ) !== RC_CATEGORIZE ) { $query['action'] = 'history'; - $data['historyLink'] = ' ' . $this->msg( 'parentheses' ) - ->rawParams( $rcObj->difflink . $this->message['pipe-separator'] . Linker::linkKnown( - $rcObj->getTitle(), - $this->message['hist'], - array(), - $query - ) )->escaped(); + $data['historyLink'] = $this->getDiffHistLinks( $rcObj, $query ); } $data['separatorAfterLinks'] = ' . . '; @@ -589,10 +585,15 @@ class EnhancedChangesList extends ChangesList { if ( $type == RC_LOG ) { $data['logEntry'] = $this->insertLogEntry( $rcObj ); + } elseif ( $this->isCategorizationWithoutRevision( $rcObj ) ) { + $data['comment'] = $this->insertComment( $rcObj ); } else { $data['userLink'] = $rcObj->userlink; $data['userTalkLink'] = $rcObj->usertalklink; $data['comment'] = $this->insertComment( $rcObj ); + if ( intval( $type ) === RC_CATEGORIZE ) { + $data['historyLink'] = $this->getDiffHistLinks( $rcObj, $query ); + } $data['rollback'] = $this->getRollback( $rcObj ); } diff --git a/includes/changes/OldChangesList.php b/includes/changes/OldChangesList.php index 4ce564d1e4..1bd78ca69c 100644 --- a/includes/changes/OldChangesList.php +++ b/includes/changes/OldChangesList.php @@ -88,7 +88,9 @@ class OldChangesList extends ChangesList { } else { $unpatrolled = $this->showAsUnpatrolled( $rc ); - $this->insertDiffHist( $html, $rc, $unpatrolled ); + if ( !$this->isCategorizationWithoutRevision( $rc ) ) { + $this->insertDiffHist( $html, $rc, $unpatrolled ); + } # M, N, b and ! (minor, new, bot and unpatrolled) $html .= $this->recentChangesFlags( array( @@ -113,6 +115,8 @@ class OldChangesList extends ChangesList { if ( $rc->mAttribs['rc_type'] == RC_LOG ) { $html .= $this->insertLogEntry( $rc ); + } elseif ( $this->isCategorizationWithoutRevision( $rc ) ) { + $html .= $this->insertComment( $rc ); } else { # User tool links $this->insertUserRelatedLinks( $html, $rc ); diff --git a/includes/changes/RCCacheEntryFactory.php b/includes/changes/RCCacheEntryFactory.php index c3fe183e1f..e41f7358b6 100644 --- a/includes/changes/RCCacheEntryFactory.php +++ b/includes/changes/RCCacheEntryFactory.php @@ -209,6 +209,10 @@ class RCCacheEntryFactory { $diffLink = $diffMessage; } elseif ( in_array( $cacheEntry->mAttribs['rc_type'], $logTypes ) ) { $diffLink = $diffMessage; + } elseif ( intval( $cacheEntry->getAttribute( 'rc_type' ) ) === RC_CATEGORIZE ) { + $pageTitle = Title::newFromID( $cacheEntry->getAttribute( 'rc_cur_id' ) ); + $diffUrl = htmlspecialchars( $pageTitle->getLinkURL( $queryParams ) ); + $diffLink = "$diffMessage"; } else { $diffUrl = htmlspecialchars( $cacheEntry->getTitle()->getLinkURL( $queryParams ) ); $diffLink = "$diffMessage"; diff --git a/includes/changes/RecentChange.php b/includes/changes/RecentChange.php index 77bf5df09d..a73ed5b6a3 100644 --- a/includes/changes/RecentChange.php +++ b/includes/changes/RecentChange.php @@ -67,6 +67,7 @@ class RecentChange { const SRC_NEW = 'mw.new'; const SRC_LOG = 'mw.log'; const SRC_EXTERNAL = 'mw.external'; // obsolete + const SRC_CATEGORIZE = 'mw.categorize'; public $mAttribs = array(); public $mExtra = array(); @@ -89,6 +90,17 @@ class RecentChange { */ public $counter = -1; + /** + * @var array Array of change types + */ + private static $changeTypes = array( + 'edit' => RC_EDIT, + 'new' => RC_NEW, + 'log' => RC_LOG, + 'external' => RC_EXTERNAL, + 'categorize' => RC_CATEGORIZE, + ); + # Factory methods /** @@ -119,18 +131,10 @@ class RecentChange { return $retval; } - switch ( $type ) { - case 'edit': - return RC_EDIT; - case 'new': - return RC_NEW; - case 'log': - return RC_LOG; - case 'external': - return RC_EXTERNAL; - default: - throw new MWException( "Unknown type '$type'" ); + if ( !array_key_exists( $type, self::$changeTypes ) ) { + throw new MWException( "Unknown type '$type'" ); } + return self::$changeTypes[$type]; } /** @@ -140,24 +144,15 @@ class RecentChange { * @return string $type */ public static function parseFromRCType( $rcType ) { - switch ( $rcType ) { - case RC_EDIT: - $type = 'edit'; - break; - case RC_NEW: - $type = 'new'; - break; - case RC_LOG: - $type = 'log'; - break; - case RC_EXTERNAL: - $type = 'external'; - break; - default: - $type = "$rcType"; - } + return array_search( $rcType, self::$changeTypes, true ) ?: "$rcType"; + } - return $type; + /** + * Get an array of all change types + * @return array + */ + public static function getChangeTypes() { + return array_keys( self::$changeTypes ); } /** @@ -746,6 +741,72 @@ class RecentChange { return $rc; } + /** + * Makes an entry in the database corresponding to a categorization + * + * @param string $timestamp Timestamp of the recent change to occur + * @param Title $categoryTitle Title of the category a page is being added to or removed from + * @param User $user User object of the user that made the change + * @param string $comment Change summary + * @param Title $pageTitle Title of the page that is being added or removed + * @param int $oldRevId Parent revision ID of this change + * @param int $newRevId Revision ID of this change + * @param string $lastTimestamp Parent revision timestamp of this change + * @param bool $bot true, if the change was made by a bot + * @param string $ip IP address of the user, if the change was made anonymously + * @param int $patrol Indicates whether the change is patrolled/unpatrolled + * @param int $deleted Indicates whether the change has been deleted + * @param array|null $params Additional parameters as explained on + * https://www.mediawiki.org/wiki/Manual:Logging_table#log_params + * @return RecentChange + * @throws MWException + * @since 1.26 + */ + public static function notifyCategorization( $timestamp, Title $categoryTitle, User $user = null, + $comment, Title $pageTitle, $oldRevId, $newRevId, $lastTimestamp, $bot, $ip = '', + $patrol = 0, $deleted = 0, $params = null ) { + + $rc = new RecentChange; + $rc->mTitle = $categoryTitle; + $rc->mPerformer = $user; + $rc->mAttribs = array( + 'rc_timestamp' => $timestamp, + 'rc_namespace' => $categoryTitle->getNamespace(), + 'rc_title' => $categoryTitle->getDBkey(), + 'rc_type' => RC_CATEGORIZE, + 'rc_source' => self::SRC_CATEGORIZE, + 'rc_minor' => 0, + 'rc_cur_id' => $pageTitle->getArticleID(), + 'rc_user' => $user ? $user->getId() : 0, + 'rc_user_text' => $user ? $user->getName() : '', + 'rc_comment' => $comment, + 'rc_this_oldid' => $newRevId, + 'rc_last_oldid' => $oldRevId, + 'rc_bot' => $bot ? 1 : 0, + 'rc_ip' => self::checkIPAddress( $ip ), + 'rc_patrolled' => intval( $patrol ), + 'rc_new' => 0, # obsolete + 'rc_old_len' => 0, + 'rc_new_len' => 0, + 'rc_deleted' => $deleted, + 'rc_logid' => 0, + 'rc_log_type' => null, + 'rc_log_action' => '', + 'rc_params' => $params ? serialize( $params ) : '' + ); + + $rc->mExtra = array( + 'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(), + 'lastTimestamp' => $lastTimestamp, + 'oldSize' => 0, + 'newSize' => 0, + 'pageStatus' => 'changed' + ); + $rc->save(); + + return $rc; + } + /** * Initialises the members of this object from a mysql row object * diff --git a/includes/deferred/LinksUpdate.php b/includes/deferred/LinksUpdate.php index e9ec7ffa2b..36cc86566d 100644 --- a/includes/deferred/LinksUpdate.php +++ b/includes/deferred/LinksUpdate.php @@ -61,6 +61,12 @@ class LinksUpdate extends SqlDataUpdate { /** @var bool Whether to queue jobs for recursive updates */ public $mRecursive; + /** @var bool Whether this job was triggered by a recursive update job */ + private $mTriggeredRecursive; + + /** @var Revision Revision for which this update has been triggered */ + private $mRevision; + /** * @var null|array Added links if calculated. */ @@ -147,7 +153,6 @@ class LinksUpdate extends SqlDataUpdate { } protected function doIncrementalUpdate() { - # Page links $existing = $this->getExistingLinks(); $this->linkDeletions = $this->getLinkDeletions( $existing ); @@ -199,6 +204,27 @@ class LinksUpdate extends SqlDataUpdate { $this->invalidateCategories( $categoryUpdates ); $this->updateCategoryCounts( $categoryInserts, $categoryDeletes ); + # Category membership changes + if ( !$this->mTriggeredRecursive && ( $categoryInserts || $categoryDeletes ) ) { + try { + $catMembChange = new CategoryMembershipChange( $this->mTitle, $this->mRevision ); + + if ( $this->mRecursive ) { + $catMembChange->setRecursive(); + } + + foreach ( $categoryInserts as $categoryName => $value ) { + $catMembChange->pageAddedToCategory( $categoryName ); + } + + foreach ( $categoryDeletes as $categoryName => $value ) { + $catMembChange->pageRemovedFromCategory( $categoryName ); + } + } catch ( MWException $e ) { + MWExceptionHandler::logException( $e ); + } + } + # Page properties $existing = $this->getExistingProperties(); @@ -863,6 +889,21 @@ class LinksUpdate extends SqlDataUpdate { return $this->mImages; } + /** + * Set this object as being triggered by a recursive LinksUpdate + */ + public function setTriggeredRecursive() { + $this->mTriggeredRecursive = true; + } + + /** + * Set the revision corresponding to this LinksUpdate + * @param Revision $revision + */ + public function setRevision( Revision $revision ) { + $this->mRevision = $revision; + } + /** * Invalidate any necessary link lists related to page property changes * @param array $changed diff --git a/includes/jobqueue/jobs/RefreshLinksJob.php b/includes/jobqueue/jobs/RefreshLinksJob.php index dec944a80d..c66da4181f 100644 --- a/includes/jobqueue/jobs/RefreshLinksJob.php +++ b/includes/jobqueue/jobs/RefreshLinksJob.php @@ -83,6 +83,7 @@ class RefreshLinksJob extends Job { } else { $extraParams['masterPos'] = false; } + $extraParams['triggeredRecursive'] = true; // Convert this into no more than $wgUpdateRowsPerJob RefreshLinks per-title // jobs and possibly a recursive RefreshLinks job for the rest of the backlinks $jobs = BacklinkJobUtils::partitionBacklinkJob( @@ -196,6 +197,12 @@ class RefreshLinksJob extends Job { } $updates = $content->getSecondaryDataUpdates( $title, null, false, $parserOutput ); + foreach ( $updates as $key => $update ) { + if ( $update instanceof LinksUpdate && isset( $this->params['triggeredRecursive'] ) ) { + $update->setTriggeredRecursive(); + } + } + DataUpdate::runUpdates( $updates ); InfoAction::invalidateCache( $title ); diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index f7f25289b5..7c599e9b0e 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -2180,6 +2180,9 @@ class WikiPage implements Page, IDBAccessObject { $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, $recursive, $editInfo->output ); foreach ( $updates as $update ) { + if ( $update instanceof LinksUpdate ) { + $update->setRevision( $revision ); + } DeferredUpdates::addUpdate( $update ); } } diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index 23bd394ccb..0938316488 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -144,6 +144,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { $opts->add( 'hideliu', false ); $opts->add( 'hidepatrolled', false ); $opts->add( 'hidemyself', false ); + $opts->add( 'hidecategorization', false ); $opts->add( 'namespace', '', FormOptions::INTNULL ); $opts->add( 'invert', false ); @@ -249,6 +250,9 @@ abstract class ChangesListSpecialPage extends SpecialPage { $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() ); } } + if ( $opts['hidecategorization'] === true ) { + $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ); + } // Namespace filtering if ( $opts['namespace'] !== '' ) { diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index 96d512c4a5..4b2d2d4e35 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -83,6 +83,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $opts->add( 'hideliu', false ); $opts->add( 'hidepatrolled', $user->getBoolOption( 'hidepatrolled' ) ); $opts->add( 'hidemyself', false ); + $opts->add( 'hidecategorization', $user->getBoolOption( 'hidecategorization' ) ); $opts->add( 'categories', '' ); $opts->add( 'categories_any', false ); @@ -138,6 +139,9 @@ class SpecialRecentChanges extends ChangesListSpecialPage { if ( 'hidemyself' === $bit ) { $opts['hidemyself'] = true; } + if ( 'hidecategorization' === $bit ) { + $opts['hidecategorization'] = true; + } if ( is_numeric( $bit ) ) { $opts['limit'] = $bit; @@ -723,7 +727,8 @@ class SpecialRecentChanges extends ChangesListSpecialPage { 'hideanons' => 'rcshowhideanons', 'hideliu' => 'rcshowhideliu', 'hidepatrolled' => 'rcshowhidepatr', - 'hidemyself' => 'rcshowhidemine' + 'hidemyself' => 'rcshowhidemine', + 'hidecategorization' => 'rcshowhidecategorization' ); $showhide = array( 'show', 'hide' ); @@ -741,7 +746,8 @@ class SpecialRecentChanges extends ChangesListSpecialPage { // The following messages are used here: // rcshowhideminor-show, rcshowhideminor-hide, rcshowhidebots-show, rcshowhidebots-hide, // rcshowhideanons-show, rcshowhideanons-hide, rcshowhideliu-show, rcshowhideliu-hide, - // rcshowhidepatr-show, rcshowhidepatr-hide, rcshowhidemine-show, rcshowhidemine-hide. + // rcshowhidepatr-show, rcshowhidepatr-hide, rcshowhidemine-show, rcshowhidemine-hide, + // rcshowhidecategorization-show, rcshowhidecategorization-hide. $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] ); // Extensions can define additional filters, but don't need to define the corresponding // messages. If they don't exist, just fall back to 'show' and 'hide'. diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index 20f577603b..088a3fe276 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -110,6 +110,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { $opts->add( 'hideliu', $user->getBoolOption( 'watchlisthideliu' ) ); $opts->add( 'hidepatrolled', $user->getBoolOption( 'watchlisthidepatrolled' ) ); $opts->add( 'hidemyself', $user->getBoolOption( 'watchlisthideown' ) ); + $opts->add( 'hidecategorization', $user->getBoolOption( 'watchlisthidecategorization' ) ); $opts->add( 'extended', $user->getBoolOption( 'extendwatchlist' ) ); @@ -423,7 +424,8 @@ class SpecialWatchlist extends ChangesListSpecialPage { 'hideanons' => 'rcshowhideanons', 'hideliu' => 'rcshowhideliu', 'hidemyself' => 'rcshowhidemine', - 'hidepatrolled' => 'rcshowhidepatr' + 'hidepatrolled' => 'rcshowhidepatr', + 'hidecategorization' => 'rcshowhidecategorization', ); foreach ( $this->getCustomFilters() as $key => $params ) { $filters[$key] = $params['msg']; diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 613885f874..654c1791d2 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -7,6 +7,7 @@ "tog-hideminor": "Hide minor edits from recent changes", "tog-hidepatrolled": "Hide patrolled edits from recent changes", "tog-newpageshidepatrolled": "Hide patrolled pages from new page list", + "tog-hidecategorization": "Hide categorization of pages", "tog-extendwatchlist": "Expand watchlist to show all changes, not just the most recent", "tog-usenewrc": "Group changes by page in recent changes and watchlist", "tog-numberheadings": "Auto-number headings", @@ -36,6 +37,7 @@ "tog-watchlisthideliu": "Hide edits by logged in users from the watchlist", "tog-watchlisthideanons": "Hide edits by anonymous users from the watchlist", "tog-watchlisthidepatrolled": "Hide patrolled edits from the watchlist", + "tog-watchlisthidecategorization": "Hide categorization of pages", "tog-ccmeonemails": "Send me copies of emails I send to other users", "tog-diffonly": "Do not show page content below diffs", "tog-showhiddencats": "Show hidden categories", @@ -1256,6 +1258,9 @@ "rcshowhidemine": "$1 my edits", "rcshowhidemine-show": "Show", "rcshowhidemine-hide": "Hide", + "rcshowhidecategorization": "$1 page categorization", + "rcshowhidecategorization-show": "Show", + "rcshowhidecategorization-hide": "Hide", "rclinks": "Show last $1 changes in last $2 days
$3", "diff": "diff", "hist": "hist", @@ -1282,6 +1287,10 @@ "recentchangeslinked-summary": "This is a list of changes made recently to pages linked from a specified page (or to members of a specified category).\nPages on [[Special:Watchlist|your watchlist]] are bold.", "recentchangeslinked-page": "Page name:", "recentchangeslinked-to": "Show changes to pages linked to the given page instead", + "recentchanges-page-added-to-category": "[[:$1]] added to category", + "recentchanges-page-added-to-category-bundled": "[[:$1]] and {{PLURAL:$2|one page|$2 pages}} added to category", + "recentchanges-page-removed-from-category": "[[:$1]] removed from category", + "recentchanges-page-removed-from-category-bundled": "[[:$1]] and {{PLURAL:$2|one page|$2 pages}} removed from category", "upload": "Upload file", "uploadbtn": "Upload file", "reuploaddesc": "Cancel upload and return to the upload form", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 0867cf7d4d..99286b8d95 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -178,6 +178,7 @@ "tog-hideminor": "[[Special:Preferences]], tab 'Recent changes'. Offers user to hide minor edits in recent changes or not. {{Gender}}", "tog-hidepatrolled": "Option in Recent changes tab of [[Special:Preferences]] (if [[mw:Manual:$wgUseRCPatrol|$wgUseRCPatrol]] is enabled). {{Gender}}", "tog-newpageshidepatrolled": "Toggle in [[Special:Preferences]], section \"Recent changes\" (if [[mw:Manual:$wgUseRCPatrol|$wgUseRCPatrol]] is enabled). {{Gender}}", + "tog-hidecategorization": "Option in \"Recent changes\" tab of [[Special:Preferences]]. Offers user to hide/show categorization of pages.", "tog-extendwatchlist": "[[Special:Preferences]], tab 'Watchlist'. Offers user to show all applicable changes in watchlist (by default only the last change to a page on the watchlist is shown). {{Gender}}", "tog-usenewrc": "{{Gender}}\nUsed as label for the checkbox in [[Special:Preferences]], tab \"Recent changes\".\n\nOffers user to use alternative representation of [[Special:RecentChanges]] and watchlist.", "tog-numberheadings": "[[Special:Preferences]], tab 'Misc'. Offers numbered headings on content pages to user. {{Gender}}", @@ -207,6 +208,7 @@ "tog-watchlisthideliu": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}", "tog-watchlisthideanons": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}", "tog-watchlisthidepatrolled": "Option in Watchlist tab of [[Special:Preferences]]. {{Gender}}", + "tog-watchlisthidecategorization": "Option in Watchlist tab of [[Special:Preferences]]. Offers user to hide/show categorization of pages.", "tog-ccmeonemails": "Option in [[Special:Preferences]] > {{int:prefs-personal}} > {{int:email}}. {{Gender}}", "tog-diffonly": "Toggle option used in [[Special:Preferences]]. {{Gender}}", "tog-showhiddencats": "Toggle option used in [[Special:Preferences]]. {{Gender}}", @@ -1427,6 +1429,9 @@ "rcshowhidemine": "Option text in [[Special:RecentChanges]]. Parameters:\n* $1 - the \"show/hide\" command, with the text taken from either {{msg-mw|rcshowhidemine-show}} or {{msg-mw|rcshowhidemine-hide}}", "rcshowhidemine-show": "{{doc-actionlink}}\nOption text in [[Special:RecentChanges]] in conjunction with {{msg-mw|rcshowhidemine}}.\n\nSee also:\n* {{msg-mw|rcshowhidemine-hide}}\n{{Identical|show}}", "rcshowhidemine-hide": "{{doc-actionlink}}\nOption text in [[Special:RecentChanges]] in conjunction with {{msg-mw|rcshowhidemine}}.\n\nSee also:\n* {{msg-mw|rcshowhidemine-show}}\n{{Identical|hide}}", + "rcshowhidecategorization": "Option text in [[Special:RecentChanges]]. Parameters:\n* $1 - the \"show/hide\" command, with the text taken from either {{msg-mw|rcshowhidecategorization-show}} or {{msg-mw|rcshowhidecategorization-hide}}", + "rcshowhidecategorization-show": "{{doc-actionlink}}\nOption text in [[Special:RecentChanges]] in conjunction with {{msg-mw|rcshowhidecategorization}}.\n\nSee also:\n* {{msg-mw|rcshowhidecategorization-hide}}\n{{Identical|show}}", + "rcshowhidecategorization-hide": "{{doc-actionlink}}\nOption text in [[Special:RecentChanges]] in conjunction with {{msg-mw|rcshowhidecategorization}}.\n\nSee also:\n* {{msg-mw|rcshowhidecategorization-show}}\n{{Identical|hide}}", "rclinks": "Used on [[Special:RecentChanges]].\n* $1 - a list of different choices with number of pages to be shown.
 Example: \"''50{{int:pipe-separator}}100{{int:pipe-separator}}250{{int:pipe-separator}}500\".\n* $2 - a list of clickable links with a number of days for which recent changes are to be displayed.
 Example: \"''1{{int:pipe-separator}}3{{int:pipe-separator}}7{{int:pipe-separator}}14{{int:pipe-separator}}30''\".\n* $3 - a block of text that consists of other messages.
 Example: \"''Hide minor edits{{int:pipe-separator}}Show bots{{int:pipe-separator}}Hide anonymous users{{int:pipe-separator}}Hide logged-in users{{int:pipe-separator}}Hide patrolled edits{{int:pipe-separator}}Hide my edits''\"\nList elements are separated by {{msg-mw|Pipe-separator}} each. Each list element is, or contains, a link.", "diff": "Short form of \"differences\". Used on [[Special:RecentChanges]], [[Special:Watchlist]], ...\n{{Identical|Diff}}", "hist": "Short form of \"history\". Used on [[Special:RecentChanges]], [[Special:Watchlist]], ...", @@ -1453,6 +1458,10 @@ "recentchangeslinked-summary": "Summary of [[Special:RecentChangesLinked]].", "recentchangeslinked-page": "{{Identical|Page name}}", "recentchangeslinked-to": "Checkbox in [[Special:RecentChangesLinked]].", + "recentchanges-page-added-to-category": "Comment message for pages added to a category\n\nParameters:\n* $1 - name of the page being added", + "recentchanges-page-added-to-category-bundled": "Comment message for template embedded by other pages added to a category\n\nParameters:\n* $1 - name of the page being added\n* $2 - number of additional pages being affected", + "recentchanges-page-removed-from-category": "Comment message for pages removed from a category\n\nParameters:\n* $1 - name of the page being removed", + "recentchanges-page-removed-from-category-bundled": "Comment message for templates embedded by other pages removed from a category\n\nParameters:\n* $1 - name of the page being added\n* $2 - number of additional pages being affected", "upload": "Display name for link to [[Special:Upload]] for uploading files to the wiki.\n\nSee also:\n* {{msg-mw|Upload}}\n* {{msg-mw|Accesskey-t-upload}}\n* {{msg-mw|Tooltip-t-upload}}\n{{Identical|Upload file}}", "uploadbtn": "Button name in [[Special:Upload]].\n\nSee also:\n* {{msg-mw|Uploadbtn}}\n* {{msg-mw|Accesskey-upload}}\n* {{msg-mw|Tooltip-upload}}\n{{Identical|Upload file}}", "reuploaddesc": "Used as button text in the Upload form on [[Special:Upload]].\n\nSee also:\n* {{msg-mw|upload-tryagain|Submit button text}}\n* {{msg-mw|ignorewarning|button text}}", diff --git a/tests/phpunit/includes/changes/EnhancedChangesListTest.php b/tests/phpunit/includes/changes/EnhancedChangesListTest.php index a14a50d2cb..01e221f95a 100644 --- a/tests/phpunit/includes/changes/EnhancedChangesListTest.php +++ b/tests/phpunit/includes/changes/EnhancedChangesListTest.php @@ -74,6 +74,20 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { $this->assertEquals( '', $html ); } + public function testCategorizationLineFormatting() { + $html = $this->createCategorizationLine( + $this->getCategorizationChange( '20150629191735', 0, 0 ) + ); + $this->assertNotContains( '(diff | hist)', strip_tags( $html ) ); + } + + public function testCategorizationLineFormattingWithRevision() { + $html = $this->createCategorizationLine( + $this->getCategorizationChange( '20150629191735', 1025, 1024 ) + ); + $this->assertContains( '(diff | hist)', strip_tags( $html ) ); + } + /** * @todo more tests for actual formatting, this is more of a smoke test */ @@ -115,6 +129,24 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { return $recentChange; } + /** + * @return RecentChange + */ + private function getCategorizationChange( $timestamp, $thisId, $lastId ) { + $wikiPage = new WikiPage( Title::newFromText( 'Testpage' ) ); + $wikiPage->doEditContent( new WikitextContent( 'Some random text' ), 'page created' ); + + $wikiPage = new WikiPage( Title::newFromText( 'Category:Foo' ) ); + $wikiPage->doEditContent( new WikitextContent( 'Some random text' ), 'category page created' ); + + $user = $this->getTestUser(); + $recentChange = $this->testRecentChangesHelper->makeCategorizationRecentChange( + $user, 'Category:Foo', $wikiPage->getId(), $thisId, $lastId, $timestamp + ); + + return $recentChange; + } + /** * @return User */ @@ -128,4 +160,15 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { return $user; } + private function createCategorizationLine( $recentChange ) { + $enhancedChangesList = $this->newEnhancedChangesList(); + $cacheEntry = $this->testRecentChangesHelper->getCacheEntry( $recentChange ); + + $reflection = new \ReflectionClass( get_class( $enhancedChangesList ) ); + $method = $reflection->getMethod( 'recentChangesBlockLine' ); + $method->setAccessible( true ); + + return $method->invokeArgs( $enhancedChangesList, array( $cacheEntry ) ); + } + } diff --git a/tests/phpunit/includes/changes/TestRecentChangesHelper.php b/tests/phpunit/includes/changes/TestRecentChangesHelper.php index 2506087bfa..e59825a9f5 100644 --- a/tests/phpunit/includes/changes/TestRecentChangesHelper.php +++ b/tests/phpunit/includes/changes/TestRecentChangesHelper.php @@ -97,6 +97,36 @@ class TestRecentChangesHelper { return $change; } + public function getCacheEntry( $recentChange ) { + $rcCacheFactory = new RCCacheEntryFactory( + new RequestContext(), + array( 'diff' => 'diff', 'cur' => 'cur', 'last' => 'last' ) + ); + return $rcCacheFactory->newFromRecentChange( $recentChange, false ); + } + + public function makeCategorizationRecentChange( + User $user, $titleText, $curid, $thisid, $lastid, $timestamp + ) { + + $attribs = array_merge( + $this->getDefaultAttributes( $titleText, $timestamp ), + array( + 'rc_type' => RC_CATEGORIZE, + 'rc_user' => $user->getId(), + 'rc_user_text' => $user->getName(), + 'rc_this_oldid' => $thisid, + 'rc_last_oldid' => $lastid, + 'rc_cur_id' => $curid, + 'rc_comment' => '[[:Testpage]] added to category', + 'rc_old_len' => 0, + 'rc_new_len' => 0, + ) + ); + + return $this->makeRecentChange( $attribs, 0, 0 ); + } + private function getDefaultAttributes( $titleText, $timestamp ) { return array( 'rc_id' => 545, diff --git a/tests/phpunit/includes/deferred/LinksUpdateTest.php b/tests/phpunit/includes/deferred/LinksUpdateTest.php index 02f6b2ab2d..efbfe6f6a9 100644 --- a/tests/phpunit/includes/deferred/LinksUpdateTest.php +++ b/tests/phpunit/includes/deferred/LinksUpdateTest.php @@ -19,7 +19,8 @@ class LinksUpdateTest extends MediaWikiTestCase { 'externallinks', 'imagelinks', 'templatelinks', - 'iwlinks' + 'iwlinks', + 'recentchanges', ) ); } @@ -41,6 +42,12 @@ class LinksUpdateTest extends MediaWikiTestCase { ); } + public function addDBData() { + $this->insertPage( 'Testing' ); + $this->insertPage( 'Some_other_page' ); + $this->insertPage( 'Template:TestingTemplate' ); + } + protected function makeTitleAndParserOutput( $name, $id ) { $t = Title::newFromText( $name ); $t->mArticleID = $id; # XXX: this is fugly @@ -133,6 +140,61 @@ class LinksUpdateTest extends MediaWikiTestCase { ) ); } + public function testOnAddingAndRemovingCategory_recentChangesRowIsAdded() { + $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' ); + + $title = Title::newFromText( 'Testing' ); + $wikiPage = new WikiPage( $title ); + $wikiPage->doEditContent( new WikitextContent( '[[Category:Foo]]' ), 'added category' ); + + $this->assertRecentChangeByCategorization( + $title, + $wikiPage->getParserOutput( new ParserOptions() ), + Title::newFromText( 'Category:Foo' ), + array( array( 'Foo', '[[:Testing]] added to category' ) ) + ); + + $wikiPage->doEditContent( new WikitextContent( '[[Category:Bar]]' ), 'added category' ); + $this->assertRecentChangeByCategorization( + $title, + $wikiPage->getParserOutput( new ParserOptions() ), + Title::newFromText( 'Category:Foo' ), + array( + array( 'Foo', '[[:Testing]] added to category' ), + array( 'Foo', '[[:Testing]] removed from category' ), + ) + ); + + $this->assertRecentChangeByCategorization( + $title, + $wikiPage->getParserOutput( new ParserOptions() ), + Title::newFromText( 'Category:Bar' ), + array( + array( 'Bar', '[[:Testing]] added to category' ), + ) + ); + } + + public function testOnAddingAndRemovingCategoryToTemplates_embeddingPagesAreIgnored() { + $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' ); + + $templateTitle = Title::newFromText( 'Template:TestingTemplate' ); + $templatePage = new WikiPage( $templateTitle ); + + $wikiPage = new WikiPage( Title::newFromText( 'Testing' ) ); + $wikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' ); + $otherWikiPage = new WikiPage( Title::newFromText( 'Some_other_page' ) ); + $otherWikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' ); + $templatePage->doEditContent( new WikitextContent( '[[Category:Foo]]' ), 'added category' ); + + $this->assertRecentChangeByCategorization( + $templateTitle, + $templatePage->getParserOutput( new ParserOptions() ), + Title::newFromText( 'Foo' ), + array( array( 'Foo', '[[:Template:TestingTemplate]] and 2 pages added to category' ) ) + ); + } + /** * @covers ParserOutput::addInterwikiLink */ @@ -263,4 +325,26 @@ class LinksUpdateTest extends MediaWikiTestCase { $this->assertSelect( $table, $fields, $condition, $expectedRows ); return $update; } + + protected function assertRecentChangeByCategorization( + Title $pageTitle, ParserOutput $parserOutput, Title $categoryTitle, $expectedRows + ) { + $update = new LinksUpdate( $pageTitle, $parserOutput ); + $revision = Revision::newFromTitle( $pageTitle ); + $update->setRevision( $revision ); + $update->beginTransaction(); + $update->doUpdate(); + $update->commitTransaction(); + + $this->assertSelect( + 'recentchanges', + 'rc_title, rc_comment', + array( + 'rc_type' => RC_CATEGORIZE, + 'rc_namespace' => NS_CATEGORY, + 'rc_title' => $categoryTitle->getDBkey() + ), + $expectedRows + ); + } } -- 2.20.1