From d40cd42b9f960c6878349bc889c2d20862ecf1b7 Mon Sep 17 00:00:00 2001 From: addshore Date: Mon, 24 Aug 2015 17:40:06 +0000 Subject: [PATCH] Enable users to watch category membership changes #2 This is part of a chain that reverts: e412ff5ecc900991cce4f99b7a069f625a5694b3. NOTE: - The feature is disabled by default - User settings default to hiding changes - T109707 Touching a file on wikisource adds and removes it from a category... Even when page has no changes.... WTF? See linked issue, marked as stalled with a possible way forward for this patch. @see https://gerrit.wikimedia.org/r/#/c/235467/ Changes since version 1: - T109604 - Page names in comment are no longer url encoded / have _'s - T109638 & T110338 - Reserved username now used when we can't determine a username for the change (we could perhaps set the user and id to be blank in the RC table, but who knows what this might do) - T109688 - History links are now disabled in RC.... (could be fine for the introduction and worked on more in the future) - Categorization changes are now always patrolled - Touching on T109672 in this change emails will never be sent regarding categorization changes. (this can of course be changed in a followup) - Added $wgRCWatchCategoryMembership defaulting to true for enabling / disabling the feature - T109700 - for cases when no revision was retrieved for a category change set the bot flag to true. This means all changes caused by parser functions & Lua will be marked as bot, as will changes that cant find their revision due to slave lag.. Bug: T9148 Bug: T109604 Bug: T109638 Bug: T109688 Bug: T109700 Bug: T110338 Bug: T110340 Change-Id: I51c2c1254de862f24a26ef9dbbf027c6c83e9063 --- includes/DefaultSettings.php | 8 ++ includes/Preferences.php | 16 ++++ includes/api/ApiFeedRecentChanges.php | 1 + includes/api/ApiQueryRecentChanges.php | 9 +- includes/api/ApiQueryWatchlist.php | 10 +-- includes/api/i18n/en.json | 8 +- includes/api/i18n/qqq.json | 6 ++ includes/changes/CategoryMembershipChange.php | 5 +- includes/changes/ChangesList.php | 48 +++++++--- includes/changes/EnhancedChangesList.php | 48 +++++++--- includes/changes/OldChangesList.php | 3 +- includes/changes/RCCacheEntryFactory.php | 9 ++ includes/changes/RecentChange.php | 24 +++-- includes/deferred/LinksUpdate.php | 53 +++++++++++ includes/jobqueue/jobs/RefreshLinksJob.php | 7 ++ includes/page/WikiPage.php | 3 + .../specialpage/ChangesListSpecialPage.php | 8 ++ includes/specials/SpecialRecentchanges.php | 20 ++++- includes/specials/SpecialWatchlist.php | 9 ++ languages/i18n/en.json | 6 ++ languages/i18n/qqq.json | 6 ++ .../changes/EnhancedChangesListTest.php | 43 +++++++++ .../changes/TestRecentChangesHelper.php | 30 +++++++ .../includes/deferred/LinksUpdateTest.php | 87 ++++++++++++++++++- .../specials/SpecialRecentchangesTest.php | 17 +++- 25 files changed, 427 insertions(+), 57 deletions(-) diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 9eff602bf4..7b1b4e23f1 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -4539,6 +4539,7 @@ $wgDefaultUserOptions = array( 'gender' => 'unknown', 'hideminor' => 0, 'hidepatrolled' => 0, + 'hidecategorization' => 1, 'imagesize' => 2, 'math' => 1, 'minordefault' => 0, @@ -4570,6 +4571,7 @@ $wgDefaultUserOptions = array( 'watchlisthideminor' => 0, 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, + 'watchlisthidecategorization' => 1, 'watchmoves' => 0, 'watchrollback' => 0, 'wllimit' => 250, @@ -6169,6 +6171,12 @@ $wgRCEngines = array( 'udp' => 'UDPRCFeedEngine', ); +/** + * Treat category membership changes as a RecentChange + * @since 1.27 + */ +$wgRCWatchCategoryMembership = false; + /** * Use RC Patrolling to check for vandalism */ diff --git a/includes/Preferences.php b/includes/Preferences.php index b3ee207f46..0f8dcc3251 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -892,6 +892,14 @@ class Preferences { 'section' => 'rc/advancedrc', ); + if ( $config->get( 'RCWatchCategoryMembership' ) ) { + $defaultPreferences['hidecategorization'] = array( + 'type' => 'toggle', + 'label-message' => 'tog-hidecategorization', + 'section' => 'rc/advancedrc', + ); + } + if ( $user->useRCPatrol() ) { $defaultPreferences['hidepatrolled'] = array( 'type' => 'toggle', @@ -999,6 +1007,14 @@ class Preferences { 'label-message' => 'tog-watchlisthideliu', ); + if ( $config->get( 'RCWatchCategoryMembership' ) ) { + $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 ed0a2a7a05..0a11f4b7bb 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -678,14 +678,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..75fc33e2ab 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -483,14 +483,10 @@ 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_HELP_MSG_PER_VALUE => array(), + 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 90d7fa7b9d..c363ae8891 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -163,6 +163,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.", @@ -1211,7 +1212,12 @@ "apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "Adds timestamp of when the user was last notified about the edit.", "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:", + "apihelp-query+watchlist-paramvalue-type-edit": "Regular page edits.", + "apihelp-query+watchlist-paramvalue-type-external": "External changes.", + "apihelp-query+watchlist-paramvalue-type-new": "Page creations.", + "apihelp-query+watchlist-paramvalue-type-log": "Log entries.", + "apihelp-query+watchlist-paramvalue-type-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 9710e16367..ed0eb59380 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -159,6 +159,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}}", @@ -1131,6 +1132,11 @@ "apihelp-query+watchlist-paramvalue-prop-loginfo": "{{doc-apihelp-paramvalue|query+watchlist|prop|loginfo}}", "apihelp-query+watchlist-param-show": "{{doc-apihelp-param|query+watchlist|show}}", "apihelp-query+watchlist-param-type": "{{doc-apihelp-param|query+watchlist|type}}", + "apihelp-query+watchlist-paramvalue-type-edit": "{{doc-apihelp-paramvalue|query+watchlist|type|edit}}", + "apihelp-query+watchlist-paramvalue-type-external": "{{doc-apihelp-paramvalue|query+watchlist|type|external}}", + "apihelp-query+watchlist-paramvalue-type-new": "{{doc-apihelp-paramvalue|query+watchlist|type|new}}", + "apihelp-query+watchlist-paramvalue-type-log": "{{doc-apihelp-paramvalue|query+watchlist|type|log}}", + "apihelp-query+watchlist-paramvalue-type-categorize": "{{doc-apihelp-paramvalue|query+watchlist|type|categorize}}", "apihelp-query+watchlist-param-owner": "{{doc-apihelp-param|query+watchlist|owner}}", "apihelp-query+watchlist-param-token": "{{doc-apihelp-param|query+watchlist|token}}", "apihelp-query+watchlist-example-simple": "{{doc-apihelp-example|query+watchlist}}", diff --git a/includes/changes/CategoryMembershipChange.php b/includes/changes/CategoryMembershipChange.php index 9e73ebeacc..b4086f9be6 100644 --- a/includes/changes/CategoryMembershipChange.php +++ b/includes/changes/CategoryMembershipChange.php @@ -47,7 +47,8 @@ class CategoryMembershipChange { /** * @var int - * Number of pages this WikiPage is embedded by; set by CategoryMembershipChange::setRecursive() + * Number of pages this WikiPage is embedded by + * Set by CategoryMembershipChange::checkTemplateLinks() */ private $numTemplateLinks = 0; @@ -239,7 +240,7 @@ class CategoryMembershipChange { * @param int $type may be CategoryMembershipChange::CATEGORY_ADDITION * or CategoryMembershipChange::CATEGORY_REMOVAL * @param array $params - * - prefixedUrl: result of Title::->getPrefixedURL() + * - prefixedText: result of Title::->getPrefixedText() * * @return string */ diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index fdc9944fdc..3e0958a08c 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -305,7 +305,11 @@ class ChangesList extends ContextSource { */ public function insertDiffHist( &$s, &$rc, $unpatrolled ) { # Diff link - if ( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { + if ( + $rc->mAttribs['rc_type'] == RC_NEW || + $rc->mAttribs['rc_type'] == RC_LOG || + $rc->mAttribs['rc_type'] == RC_CATEGORIZE + ) { $diffLink = $this->message['diff']; } elseif ( !self::userCan( $rc, Revision::DELETED_TEXT, $this->getUser() ) ) { $diffLink = $this->message['diff']; @@ -323,17 +327,22 @@ class ChangesList extends ContextSource { $query ); } - $diffhist = $diffLink . $this->message['pipe-separator']; - # History link - $diffhist .= Linker::linkKnown( - $rc->getTitle(), - $this->message['hist'], - array(), - array( - 'curid' => $rc->mAttribs['rc_cur_id'], - 'action' => 'history' - ) - ); + if ( $rc->mAttribs['rc_type'] == RC_CATEGORIZE ) { + $diffhist = $diffLink . $this->message['pipe-separator'] . $this->message['hist']; + } else { + $diffhist = $diffLink . $this->message['pipe-separator']; + # History link + $diffhist .= Linker::linkKnown( + $rc->getTitle(), + $this->message['hist'], + array(), + array( + 'curid' => $rc->mAttribs['rc_cur_id'], + 'action' => 'history' + ) + ); + } + // @todo FIXME: Hard coded ". .". Is there a message for this? Should there be? $s .= $this->msg( 'parentheses' )->rawParams( $diffhist )->escaped() . ' . . '; @@ -630,4 +639,19 @@ 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 + * + * @since 1.27 + * + * @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 1dcb7aef48..547630131e 100644 --- a/includes/changes/EnhancedChangesList.php +++ b/includes/changes/EnhancedChangesList.php @@ -405,6 +405,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; @@ -497,7 +499,7 @@ class EnhancedChangesList extends ChangesList { /** @var $block0 RecentChange */ $block0 = $block[0]; $last = $block[count( $block ) - 1]; - if ( !$allLogs ) { + if ( !$allLogs && $rcObj->mAttribs['rc_type'] != RC_CATEGORIZE ) { if ( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) { $links['total-changes'] = $nchanges[$n]; } elseif ( $isnew ) { @@ -529,7 +531,7 @@ class EnhancedChangesList extends ChangesList { } # History - if ( $allLogs ) { + if ( $allLogs || $rcObj->mAttribs['rc_type'] == RC_CATEGORIZE ) { // don't show history link for logs } elseif ( $namehidden || !$block0->getTitle()->exists() ) { $links['history'] = $this->message['enhancedrc-history']; @@ -605,15 +607,9 @@ class EnhancedChangesList extends ChangesList { } # Diff and hist links - if ( $type != RC_LOG ) { + if ( $type == RC_LOG && $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'] = ' . . '; @@ -628,10 +624,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 ( $type == RC_CATEGORIZE ) { + $data['historyLink'] = $this->getDiffHistLinks( $rcObj, $query ); + } $data['rollback'] = $this->getRollback( $rcObj ); } @@ -672,6 +673,33 @@ class EnhancedChangesList extends ChangesList { return $line; } + /** + * Returns value to be used in 'historyLink' element of $data param in + * EnhancedChangesListModifyBlockLineData hook. + * + * @since 1.27 + * + * @param RCCacheEntry $rc + * @param array $query array of key/value pairs to append as a query string + * @return string HTML + */ + public function getDiffHistLinks( RCCacheEntry $rc, array $query ) { + $pageTitle = $rc->getTitle(); + if ( $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE ) { + // For categorizations we must swap the category title with the page title! + $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; + } + /** * If enhanced RC is in use, this function takes the previously cached * RC lines, arranges them, and outputs the HTML diff --git a/includes/changes/OldChangesList.php b/includes/changes/OldChangesList.php index 4ce564d1e4..31b355d4cf 100644 --- a/includes/changes/OldChangesList.php +++ b/includes/changes/OldChangesList.php @@ -87,7 +87,6 @@ class OldChangesList extends ChangesList { // Regular entries } else { $unpatrolled = $this->showAsUnpatrolled( $rc ); - $this->insertDiffHist( $html, $rc, $unpatrolled ); # M, N, b and ! (minor, new, bot and unpatrolled) $html .= $this->recentChangesFlags( @@ -113,6 +112,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..f31125d384 100644 --- a/includes/changes/RCCacheEntryFactory.php +++ b/includes/changes/RCCacheEntryFactory.php @@ -209,6 +209,15 @@ class RCCacheEntryFactory { $diffLink = $diffMessage; } elseif ( in_array( $cacheEntry->mAttribs['rc_type'], $logTypes ) ) { $diffLink = $diffMessage; + } elseif ( $cacheEntry->getAttribute( 'rc_type' ) == RC_CATEGORIZE ) { + $rcCurId = $cacheEntry->getAttribute( 'rc_cur_id' ); + $pageTitle = Title::newFromID( $rcCurId ); + if ( $pageTitle === null ) { + wfDebugLog( 'RCCacheEntryFactory', 'Could not get Title for rc_cur_id: ' . $rcCurId ); + return $diffMessage; + } + $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 07d1487626..606a652a50 100644 --- a/includes/changes/RecentChange.php +++ b/includes/changes/RecentChange.php @@ -324,15 +324,21 @@ class RecentChange { $editor = $this->getPerformer(); $title = $this->getTitle(); - if ( Hooks::run( 'AbortEmailNotification', array( $editor, $title, $this ) ) ) { - # @todo FIXME: This would be better as an extension hook - $enotif = new EmailNotification(); - $enotif->notifyOnPageChange( $editor, $title, - $this->mAttribs['rc_timestamp'], - $this->mAttribs['rc_comment'], - $this->mAttribs['rc_minor'], - $this->mAttribs['rc_last_oldid'], - $this->mExtra['pageStatus'] ); + // Never send an RC notification email about categorization changes + if ( $this->mAttribs['rc_type'] != RC_CATEGORIZE ) { + if ( Hooks::run( 'AbortEmailNotification', array( $editor, $title, $this ) ) ) { + # @todo FIXME: This would be better as an extension hook + $enotif = new EmailNotification(); + $enotif->notifyOnPageChange( + $editor, + $title, + $this->mAttribs['rc_timestamp'], + $this->mAttribs['rc_comment'], + $this->mAttribs['rc_minor'], + $this->mAttribs['rc_last_oldid'], + $this->mExtra['pageStatus'] + ); + } } } diff --git a/includes/deferred/LinksUpdate.php b/includes/deferred/LinksUpdate.php index d99687026a..d1386c6e75 100644 --- a/includes/deferred/LinksUpdate.php +++ b/includes/deferred/LinksUpdate.php @@ -61,6 +61,12 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { /** @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,6 +153,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { } protected function doIncrementalUpdate() { + global $wgRCWatchCategoryMembership; # Page links $existing = $this->getExistingLinks(); @@ -199,6 +206,14 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { $this->invalidateCategories( $categoryUpdates ); $this->updateCategoryCounts( $categoryInserts, $categoryDeletes ); + # Category membership changes + if ( + $wgRCWatchCategoryMembership && + !$this->mTriggeredRecursive && ( $categoryInserts || $categoryDeletes ) + ) { + $this->triggerCategoryChanges( $categoryInserts, $categoryDeletes ); + } + # Page properties $existing = $this->getExistingProperties(); @@ -222,6 +237,24 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { } + private function triggerCategoryChanges( $categoryInserts, $categoryDeletes ) { + $catMembChange = new CategoryMembershipChange( $this->mTitle, $this->mRevision ); + + if ( $this->mRecursive ) { + $catMembChange->checkTemplateLinks(); + } + + foreach ( $categoryInserts as $categoryName => $value ) { + $categoryTitle = Title::newFromText( $categoryName, NS_CATEGORY ); + $catMembChange->triggerCategoryAddedNotification( $categoryTitle ); + } + + foreach ( $categoryDeletes as $categoryName => $value ) { + $categoryTitle = Title::newFromText( $categoryName, NS_CATEGORY ); + $catMembChange->triggerCategoryRemovedNotification( $categoryTitle ); + } + } + /** * Queue recursive jobs for this page * @@ -863,6 +896,26 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { return $this->mImages; } + /** + * Set this object as being triggered by a recursive LinksUpdate + * + * @since 1.27 + */ + public function setTriggeredRecursive() { + $this->mTriggeredRecursive = true; + } + + /** + * Set the revision corresponding to this LinksUpdate + * + * @since 1.27 + * + * @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 4ba1d4c828..7093e14e98 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( @@ -197,6 +198,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 d656bad195..39e1ba7204 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -2182,6 +2182,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..92b4ac63dd 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -136,6 +136,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { * @return FormOptions */ public function getDefaultOptions() { + $config = $this->getConfig(); $opts = new FormOptions(); $opts->add( 'hideminor', false ); @@ -145,6 +146,10 @@ abstract class ChangesListSpecialPage extends SpecialPage { $opts->add( 'hidepatrolled', false ); $opts->add( 'hidemyself', false ); + if ( $config->get( 'RCWatchCategoryMembership' ) ) { + $opts->add( 'hidecategorization', false ); + } + $opts->add( 'namespace', '', FormOptions::INTNULL ); $opts->add( 'invert', false ); $opts->add( 'associated', false ); @@ -249,6 +254,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..da84a9ee42 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -72,6 +72,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { public function getDefaultOptions() { $opts = parent::getDefaultOptions(); $user = $this->getUser(); + $config = $this->getConfig(); $opts->add( 'days', $user->getIntOption( 'rcdays' ) ); $opts->add( 'limit', $user->getIntOption( 'rclimit' ) ); @@ -84,6 +85,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $opts->add( 'hidepatrolled', $user->getBoolOption( 'hidepatrolled' ) ); $opts->add( 'hidemyself', false ); + if ( $config->get( 'RCWatchCategoryMembership' ) ) { + $opts->add( 'hidecategorization', $user->getBoolOption( 'hidecategorization' ) ); + } + $opts->add( 'categories', '' ); $opts->add( 'categories_any', false ); $opts->add( 'tagfilter', '' ); @@ -138,6 +143,9 @@ class SpecialRecentChanges extends ChangesListSpecialPage { if ( 'hidemyself' === $bit ) { $opts['hidemyself'] = true; } + if ( 'hidecategorization' === $bit ) { + $opts['hidecategorization'] = true; + } if ( is_numeric( $bit ) ) { $opts['limit'] = $bit; @@ -677,6 +685,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $lang = $this->getLanguage(); $user = $this->getUser(); + $config = $this->getConfig(); if ( $options['from'] ) { $note .= $this->msg( 'rcnotefrom' ) ->numParams( $options['limit'] ) @@ -690,12 +699,12 @@ class SpecialRecentChanges extends ChangesListSpecialPage { } # Sort data for display and make sure it's unique after we've added user data. - $linkLimits = $this->getConfig()->get( 'RCLinkLimits' ); + $linkLimits = $config->get( 'RCLinkLimits' ); $linkLimits[] = $options['limit']; sort( $linkLimits ); $linkLimits = array_unique( $linkLimits ); - $linkDays = $this->getConfig()->get( 'RCLinkDays' ); + $linkDays = $config->get( 'RCLinkDays' ); $linkDays[] = $options['days']; sort( $linkDays ); $linkDays = array_unique( $linkDays ); @@ -726,6 +735,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage { 'hidemyself' => 'rcshowhidemine' ); + if ( $config->get( 'RCWatchCategoryMembership' ) ) { + $filters['hidecategorization'] = 'rcshowhidecategorization'; + } + $showhide = array( 'show', 'hide' ); foreach ( $this->getCustomFilters() as $key => $params ) { @@ -741,7 +754,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..962e0c30e2 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -111,6 +111,10 @@ class SpecialWatchlist extends ChangesListSpecialPage { $opts->add( 'hidepatrolled', $user->getBoolOption( 'watchlisthidepatrolled' ) ); $opts->add( 'hidemyself', $user->getBoolOption( 'watchlisthideown' ) ); + if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) { + $opts->add( 'hidecategorization', $user->getBoolOption( 'watchlisthidecategorization' ) ); + } + $opts->add( 'extended', $user->getBoolOption( 'extendwatchlist' ) ); return $opts; @@ -425,6 +429,11 @@ class SpecialWatchlist extends ChangesListSpecialPage { 'hidemyself' => 'rcshowhidemine', 'hidepatrolled' => 'rcshowhidepatr' ); + + if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) { + $filters['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 b9301ac634..b1f129e38f 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", @@ -1263,6 +1265,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", @@ -2613,6 +2618,7 @@ "spam_blanking": "All revisions contained links to $1, blanking", "spam_deleting": "All revisions contained links to $1, deleting", "simpleantispam-label": "Anti-spam check.\nDo not fill this in!", + "autochange-username": "MediaWiki automatic change", "pageinfo-header": "-", "pageinfo-title": "Information for \"$1\"", "pageinfo-not-current": "Sorry, it's impossible to provide this information for old revisions.", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 6c3609a60b..31fe83a127 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -180,6 +180,7 @@ "tog-hideminor": "[[Special:Preferences]], tab 'Recent changes'. Offers user to hide minor edits in recent changes or not. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", "tog-hidepatrolled": "Option in Recent changes tab of [[Special:Preferences]] (if [[mw:Manual:$wgUseRCPatrol|$wgUseRCPatrol]] is enabled). {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", "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. Appears next to messages such as {{msg-mw|tog-hideminor}}.", "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}}", @@ -209,6 +210,7 @@ "tog-watchlisthideliu": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", "tog-watchlisthideanons": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", "tog-watchlisthidepatrolled": "Option in Watchlist tab of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", + "tog-watchlisthidecategorization": "Option in Watchlist tab of [[Special:Preferences]]. Offers user to hide/show categorization of pages. Appears next to checkboxes with labels such as {{msg-mw|tog-watchlisthideminor}}.", "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}}", @@ -1436,6 +1438,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]], ...", @@ -2786,6 +2791,7 @@ "spam_blanking": "Edit summary for spam cleanup script.\n\nUsed when a page is blanked (made to have no content, but still exist) because the script could not find an appropriate revision to set the page to.\n\nParameters:\n* $1 - a spammed domain name", "spam_deleting": "Edit summary for spam cleanup script.\n\nUsed when a page is deleted because all revisions contained a particular link.\n\nParameters:\n* $1 - a spammed domain name", "simpleantispam-label": "Used as label for the input box in \"Edit\" page.\n\nThe label and the input box are always hidden.", + "autochange-username": "Used as bot / unknown username.", "pageinfo-header": "{{ignored}}Custom text for the top of the info page (action=info).", "pageinfo-title": "Page title for action=info. Parameters:\n* $1 is the page name", "pageinfo-not-current": "Error message displayed when information for an old revision is requested. Example: [{{fullurl:Project:News|oldid=4266597&action=info}}]", 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 fe5bdd2674..10d4c6ec0b 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 bbd196d3d2..25ee5ecaba 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', ) ); } @@ -39,6 +40,13 @@ class LinksUpdateTest extends MediaWikiTestCase { 'iw_wikiid' => 'linksupdatetest', ) ); + $this->setMwGlobals( 'wgRCWatchCategoryMembership', true ); + } + + public function addDBData() { + $this->insertPage( 'Testing' ); + $this->insertPage( 'Some_other_page' ); + $this->insertPage( 'Template:TestingTemplate' ); } protected function makeTitleAndParserOutput( $name, $id ) { @@ -133,6 +141,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 +326,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 + ); + } } diff --git a/tests/phpunit/includes/specials/SpecialRecentchangesTest.php b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php index 0a6336f1de..384b0006c9 100644 --- a/tests/phpunit/includes/specials/SpecialRecentchangesTest.php +++ b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php @@ -11,6 +11,11 @@ */ class SpecialRecentchangesTest extends MediaWikiTestCase { + protected function setUp() { + parent::setUp(); + $this->setMwGlobals( 'wgRCWatchCategoryMembership', true ); + } + /** * @var SpecialRecentChanges */ @@ -50,7 +55,8 @@ class SpecialRecentchangesTest extends MediaWikiTestCase { $this->assertConditions( array( # expected 'rc_bot' => 0, - 0 => "rc_namespace = '0'", + 0 => "rc_type != '6'", + 1 => "rc_namespace = '0'", ), array( 'namespace' => NS_MAIN, @@ -63,7 +69,8 @@ class SpecialRecentchangesTest extends MediaWikiTestCase { $this->assertConditions( array( # expected 'rc_bot' => 0, - 0 => sprintf( "rc_namespace != '%s'", NS_MAIN ), + 0 => "rc_type != '6'", + 1 => sprintf( "rc_namespace != '%s'", NS_MAIN ), ), array( 'namespace' => NS_MAIN, @@ -81,7 +88,8 @@ class SpecialRecentchangesTest extends MediaWikiTestCase { $this->assertConditions( array( # expected 'rc_bot' => 0, - 0 => sprintf( "(rc_namespace = '%s' OR rc_namespace = '%s')", $ns1, $ns2 ), + 0 => "rc_type != '6'", + 1 => sprintf( "(rc_namespace = '%s' OR rc_namespace = '%s')", $ns1, $ns2 ), ), array( 'namespace' => $ns1, @@ -99,7 +107,8 @@ class SpecialRecentchangesTest extends MediaWikiTestCase { $this->assertConditions( array( # expected 'rc_bot' => 0, - 0 => sprintf( "(rc_namespace != '%s' AND rc_namespace != '%s')", $ns1, $ns2 ), + 0 => "rc_type != '6'", + 1 => sprintf( "(rc_namespace != '%s' AND rc_namespace != '%s')", $ns1, $ns2 ), ), array( 'namespace' => $ns1, -- 2.20.1