jenkins-bot has submitted this change and it was merged.

Change subject: Enable users to watch category membership changes #2
......................................................................


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
---
M includes/DefaultSettings.php
M includes/Preferences.php
M includes/api/ApiFeedRecentChanges.php
M includes/api/ApiQueryRecentChanges.php
M includes/api/ApiQueryWatchlist.php
M includes/api/i18n/en.json
M includes/api/i18n/qqq.json
M includes/changes/CategoryMembershipChange.php
M includes/changes/ChangesList.php
M includes/changes/EnhancedChangesList.php
M includes/changes/OldChangesList.php
M includes/changes/RCCacheEntryFactory.php
M includes/changes/RecentChange.php
M includes/deferred/LinksUpdate.php
M includes/jobqueue/jobs/RefreshLinksJob.php
M includes/page/WikiPage.php
M includes/specialpage/ChangesListSpecialPage.php
M includes/specials/SpecialRecentchanges.php
M includes/specials/SpecialWatchlist.php
M languages/i18n/en.json
M languages/i18n/qqq.json
M tests/phpunit/includes/changes/EnhancedChangesListTest.php
M tests/phpunit/includes/changes/TestRecentChangesHelper.php
M tests/phpunit/includes/deferred/LinksUpdateTest.php
M tests/phpunit/includes/specials/SpecialRecentchangesTest.php
25 files changed, 427 insertions(+), 57 deletions(-)

Approvals:
  Legoktm: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 9eff602..7b1b4e2 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -4539,6 +4539,7 @@
        'gender' => 'unknown',
        'hideminor' => 0,
        'hidepatrolled' => 0,
+       'hidecategorization' => 1,
        'imagesize' => 2,
        'math' => 1,
        'minordefault' => 0,
@@ -4570,6 +4571,7 @@
        'watchlisthideminor' => 0,
        'watchlisthideown' => 0,
        'watchlisthidepatrolled' => 0,
+       'watchlisthidecategorization' => 1,
        'watchmoves' => 0,
        'watchrollback' => 0,
        'wllimit' => 250,
@@ -6170,6 +6172,12 @@
 );
 
 /**
+ * Treat category membership changes as a RecentChange
+ * @since 1.27
+ */
+$wgRCWatchCategoryMembership = false;
+
+/**
  * Use RC Patrolling to check for vandalism
  */
 $wgUseRCPatrol = true;
diff --git a/includes/Preferences.php b/includes/Preferences.php
index b3ee207..0f8dcc3 100644
--- a/includes/Preferences.php
+++ b/includes/Preferences.php
@@ -892,6 +892,14 @@
                        '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 @@
                        '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 d24112c..5adde87 100644
--- a/includes/api/ApiFeedRecentChanges.php
+++ b/includes/api/ApiFeedRecentChanges.php
@@ -155,6 +155,7 @@
                        '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 ed0a2a7..0a11f4b 100644
--- a/includes/api/ApiQueryRecentChanges.php
+++ b/includes/api/ApiQueryRecentChanges.php
@@ -678,14 +678,9 @@
                                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 648d259..75fc33e 100644
--- a/includes/api/ApiQueryWatchlist.php
+++ b/includes/api/ApiQueryWatchlist.php
@@ -483,14 +483,10 @@
                                )
                        ),
                        '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 90d7fa7..c363ae8 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 9710e16..ed0eb59 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 9e73ebe..b4086f9 100644
--- a/includes/changes/CategoryMembershipChange.php
+++ b/includes/changes/CategoryMembershipChange.php
@@ -47,7 +47,8 @@
 
        /**
         * @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 @@
         * @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 fdc9944..3e0958a 100644
--- a/includes/changes/ChangesList.php
+++ b/includes/changes/ChangesList.php
@@ -305,7 +305,11 @@
         */
        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 @@
                                $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() .
                        ' <span class="mw-changeslist-separator">. .</span> ';
@@ -630,4 +639,19 @@
 
                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 1dcb7ae..5476301 100644
--- a/includes/changes/EnhancedChangesList.php
+++ b/includes/changes/EnhancedChangesList.php
@@ -405,6 +405,8 @@
 
                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 @@
                /** @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 @@
                }
 
                # 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 @@
                }
 
                # 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'] = ' <span 
class="mw-changeslist-separator">. .</span> ';
 
@@ -628,10 +624,15 @@
 
                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 );
                }
 
@@ -673,6 +674,33 @@
        }
 
        /**
+        * 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 4ce564d..31b355d 100644
--- a/includes/changes/OldChangesList.php
+++ b/includes/changes/OldChangesList.php
@@ -87,7 +87,6 @@
                // 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 @@
 
                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 c3fe183..f31125d 100644
--- a/includes/changes/RCCacheEntryFactory.php
+++ b/includes/changes/RCCacheEntryFactory.php
@@ -209,6 +209,15 @@
                        $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 = "<a href=\"$diffUrl\" 
tabindex=\"$counter\">$diffMessage</a>";
                } else {
                        $diffUrl = htmlspecialchars( 
$cacheEntry->getTitle()->getLinkURL( $queryParams ) );
                        $diffLink = "<a href=\"$diffUrl\" 
tabindex=\"$counter\">$diffMessage</a>";
diff --git a/includes/changes/RecentChange.php 
b/includes/changes/RecentChange.php
index 07d1487..606a652 100644
--- a/includes/changes/RecentChange.php
+++ b/includes/changes/RecentChange.php
@@ -324,15 +324,21 @@
                        $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 d996870..d1386c6 100644
--- a/includes/deferred/LinksUpdate.php
+++ b/includes/deferred/LinksUpdate.php
@@ -61,6 +61,12 @@
        /** @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 @@
        }
 
        protected function doIncrementalUpdate() {
+               global $wgRCWatchCategoryMembership;
 
                # Page links
                $existing = $this->getExistingLinks();
@@ -199,6 +206,14 @@
                $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();
 
@@ -220,6 +235,24 @@
                        $this->queueRecursiveJobs();
                }
 
+       }
+
+       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 );
+               }
        }
 
        /**
@@ -864,6 +897,26 @@
        }
 
        /**
+        * 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 4ba1d4c..7093e14 100644
--- a/includes/jobqueue/jobs/RefreshLinksJob.php
+++ b/includes/jobqueue/jobs/RefreshLinksJob.php
@@ -83,6 +83,7 @@
                        } 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 @@
                }
 
                $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 d656bad..39e1ba7 100644
--- a/includes/page/WikiPage.php
+++ b/includes/page/WikiPage.php
@@ -2182,6 +2182,9 @@
                        $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 23bd394..92b4ac6 100644
--- a/includes/specialpage/ChangesListSpecialPage.php
+++ b/includes/specialpage/ChangesListSpecialPage.php
@@ -136,6 +136,7 @@
         * @return FormOptions
         */
        public function getDefaultOptions() {
+               $config = $this->getConfig();
                $opts = new FormOptions();
 
                $opts->add( 'hideminor', false );
@@ -144,6 +145,10 @@
                $opts->add( 'hideliu', false );
                $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 );
@@ -249,6 +254,9 @@
                                $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 96d512c..da84a9e 100644
--- a/includes/specials/SpecialRecentchanges.php
+++ b/includes/specials/SpecialRecentchanges.php
@@ -72,6 +72,7 @@
        public function getDefaultOptions() {
                $opts = parent::getDefaultOptions();
                $user = $this->getUser();
+               $config = $this->getConfig();
 
                $opts->add( 'days', $user->getIntOption( 'rcdays' ) );
                $opts->add( 'limit', $user->getIntOption( 'rclimit' ) );
@@ -83,6 +84,10 @@
                $opts->add( 'hideliu', false );
                $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 );
@@ -137,6 +142,9 @@
                        }
                        if ( 'hidemyself' === $bit ) {
                                $opts['hidemyself'] = true;
+                       }
+                       if ( 'hidecategorization' === $bit ) {
+                               $opts['hidecategorization'] = true;
                        }
 
                        if ( is_numeric( $bit ) ) {
@@ -677,6 +685,7 @@
 
                $lang = $this->getLanguage();
                $user = $this->getUser();
+               $config = $this->getConfig();
                if ( $options['from'] ) {
                        $note .= $this->msg( 'rcnotefrom' )
                                ->numParams( $options['limit'] )
@@ -690,12 +699,12 @@
                }
 
                # 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 @@
                        'hidemyself' => 'rcshowhidemine'
                );
 
+               if ( $config->get( 'RCWatchCategoryMembership' ) ) {
+                       $filters['hidecategorization'] = 
'rcshowhidecategorization';
+               }
+
                $showhide = array( 'show', 'hide' );
 
                foreach ( $this->getCustomFilters() as $key => $params ) {
@@ -741,7 +754,8 @@
                        // 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 20f5776..962e0c3 100644
--- a/includes/specials/SpecialWatchlist.php
+++ b/includes/specials/SpecialWatchlist.php
@@ -111,6 +111,10 @@
                $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 @@
                        '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 b9301ac..b1f129e 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<br />$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 <strong>not</strong> 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 6c3609a..31fe83a 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.<br />&nbsp;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.<br />&nbsp;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.<br />&nbsp;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 a14a50d..01e221f 100644
--- a/tests/phpunit/includes/changes/EnhancedChangesListTest.php
+++ b/tests/phpunit/includes/changes/EnhancedChangesListTest.php
@@ -74,6 +74,20 @@
                $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
         */
@@ -116,6 +130,24 @@
        }
 
        /**
+        * @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
         */
        private function getTestUser() {
@@ -128,4 +160,15 @@
                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 fe5bdd2..10d4c6e 100644
--- a/tests/phpunit/includes/changes/TestRecentChangesHelper.php
+++ b/tests/phpunit/includes/changes/TestRecentChangesHelper.php
@@ -97,6 +97,36 @@
                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 bbd196d..25ee5ec 100644
--- a/tests/phpunit/includes/deferred/LinksUpdateTest.php
+++ b/tests/phpunit/includes/deferred/LinksUpdateTest.php
@@ -19,7 +19,8 @@
                                'externallinks',
                                'imagelinks',
                                'templatelinks',
-                               'iwlinks'
+                               'iwlinks',
+                               'recentchanges',
                        )
                );
        }
@@ -39,6 +40,13 @@
                                '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 ) {
@@ -131,6 +139,61 @@
                $this->assertLinksUpdate( $t, $po, 'categorylinks', 'cl_to, 
cl_sortkey', 'cl_from = 111', array(
                        array( 'Foo', "FOO\nTESTING" ),
                ) );
+       }
+
+       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' ) )
+               );
        }
 
        /**
@@ -263,4 +326,26 @@
                $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 0a6336f..384b000 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 @@
                $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 @@
                $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 @@
                $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 @@
                $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,

-- 
To view, visit https://gerrit.wikimedia.org/r/239065
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I51c2c1254de862f24a26ef9dbbf027c6c83e9063
Gerrit-PatchSet: 15
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Addshore <[email protected]>
Gerrit-Reviewer: Aaron Schulz <[email protected]>
Gerrit-Reviewer: Addshore <[email protected]>
Gerrit-Reviewer: Alex Monk <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: Brion VIBBER <[email protected]>
Gerrit-Reviewer: CSteipp <[email protected]>
Gerrit-Reviewer: Daniel Kinzler <[email protected]>
Gerrit-Reviewer: Kai Nissen (WMDE) <[email protected]>
Gerrit-Reviewer: Kaldari <[email protected]>
Gerrit-Reviewer: Kishanio <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Reedy <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: Tobias Gritschacher <[email protected]>
Gerrit-Reviewer: Victor Vasiliev <[email protected]>
Gerrit-Reviewer: WMDE-Fisch <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to