Merge "Enable users to watch category membership changes #2"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 20 Oct 2015 21:41:16 +0000 (21:41 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 20 Oct 2015 21:41:16 +0000 (21:41 +0000)
1  2 
includes/DefaultSettings.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/changes/ChangesList.php
includes/changes/EnhancedChangesList.php
includes/changes/RecentChange.php
includes/page/WikiPage.php
includes/specialpage/ChangesListSpecialPage.php
languages/i18n/en.json
languages/i18n/qqq.json

@@@ -720,14 -720,6 +720,14 @@@ $wgCopyUploadAsyncTimeout = false
   */
  $wgMaxUploadSize = 1024 * 1024 * 100; # 100MB
  
 +/**
 + * Minimum upload chunk size, in bytes. When using chunked upload, non-final
 + * chunks smaller than this will be rejected. May be reduced based on the
 + * 'upload_max_filesize' or 'post_max_size' PHP settings.
 + * @since 1.26
 + */
 +$wgMinUploadChunkSize = 1024; # 1KB
 +
  /**
   * Point the upload navigation link to an external URL
   * Useful if you want to use a shared repository by default
@@@ -2619,6 -2611,11 +2619,6 @@@ $wgSquidServers = array()
   */
  $wgSquidServersNoPurge = array();
  
 -/**
 - * Maximum number of titles to purge in any one client operation
 - */
 -$wgMaxSquidPurgeTitles = 400;
 -
  /**
   * Whether to use a Host header in purge requests sent to the proxy servers
   * configured in $wgSquidServers. Set this to false to support Squid
@@@ -4542,6 -4539,7 +4542,7 @@@ $wgDefaultUserOptions = array
        'gender' => 'unknown',
        'hideminor' => 0,
        'hidepatrolled' => 0,
+       'hidecategorization' => 1,
        'imagesize' => 2,
        'math' => 1,
        'minordefault' => 0,
        'watchlisthideminor' => 0,
        'watchlisthideown' => 0,
        'watchlisthidepatrolled' => 0,
+       'watchlisthidecategorization' => 1,
        'watchmoves' => 0,
        'watchrollback' => 0,
        'wllimit' => 250,
@@@ -5217,12 -5216,6 +5219,12 @@@ $wgRateLimits = array
                'ip' => null, // for each anon and recent account
                'subnet' => null, // ... within a /24 subnet in IPv4 or /64 in IPv6
        ),
 +      'upload' => array(
 +              'user' => null,
 +              'newbie' => null,
 +              'ip' => null,
 +              'subnet' => null,
 +      ),
        'move' => array(
                'user' => null,
                'newbie' => null,
@@@ -6178,6 -6171,12 +6180,12 @@@ $wgRCEngines = array
        'udp' => 'UDPRCFeedEngine',
  );
  
+ /**
+  * Treat category membership changes as a RecentChange
+  * @since 1.27
+  */
+ $wgRCWatchCategoryMembership = false;
  /**
   * Use RC Patrolling to check for vandalism
   */
        "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.",
        "apihelp-query+siteinfo-paramvalue-prop-usergroups": "Returns user groups and the associated permissions.",
        "apihelp-query+siteinfo-paramvalue-prop-libraries": "Returns libraries installed on the wiki.",
        "apihelp-query+siteinfo-paramvalue-prop-extensions": "Returns extensions installed on the wiki.",
 -      "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "Returns list of file extensions allowed to be uploaded.",
 +      "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "Returns list of file extensions (file types) allowed to be uploaded.",
        "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Returns wiki rights (license) information if available.",
        "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Returns information on available restriction (protection) types.",
        "apihelp-query+siteinfo-paramvalue-prop-languages": "Returns a list of languages MediaWiki supports (optionally localised by using <var>$1inlanguagecode</var>).",
        "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.",
        "apihelp-php-description": "Output data in serialized PHP format.",
        "apihelp-php-param-formatversion": "Output formatting:\n;1:Backwards-compatible format (XML-style booleans, <samp>*</samp> keys for content nodes, etc.).\n;2:Experimental modern format. Details may change!\n;latest:Use the latest format (currently <kbd>2</kbd>), may change without warning.",
        "apihelp-phpfm-description": "Output data in serialized PHP format (pretty-print in HTML).",
 -      "apihelp-rawfm-description": "Output data with the debugging elements in JSON format (pretty-print in HTML).",
 +      "apihelp-rawfm-description": "Output data, including debugging elements, in JSON format (pretty-print in HTML).",
        "apihelp-txt-description": "Output data in PHP's <code>print_r()</code> format.",
        "apihelp-txtfm-description": "Output data in PHP's <code>print_r()</code> format (pretty-print in HTML).",
        "apihelp-xml-description": "Output data in XML format.",
        "api-format-prettyprint-header": "This is the HTML representation of the $1 format. HTML is good for debugging, but is unsuitable for application use.\n\nSpecify the <var>format</var> parameter to change the output format. To see the non-HTML representation of the $1 format, set <kbd>format=$2</kbd>.\n\nSee the [[mw:API|complete documentation]], or the [[Special:ApiHelp/main|API help]] for more information.",
        "api-format-prettyprint-header-only-html": "This is an HTML representation intended for debugging, and is unsuitable for application use.\n\nSee the [[mw:API|complete documentation]], or the [[Special:ApiHelp/main|API help]] for more information.",
  
 -      "api-orm-param-props": "Fields to query.",
 -      "api-orm-param-limit": "Max amount of rows to return.",
 -
        "api-pageset-param-titles": "A list of titles to work on.",
        "api-pageset-param-pageids": "A list of page IDs to work on.",
        "api-pageset-param-revids": "A list of revision IDs to work on.",
        "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}}",
        "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}}",
        "api-format-title": "{{technical}}\nPage title when API output is pretty-printed in HTML.",
        "api-format-prettyprint-header": "{{technical}} Displayed as a header when API output is pretty-printed in HTML.\n\nParameters:\n* $1 - Format name\n* $2 - Non-pretty-printing module name",
        "api-format-prettyprint-header-only-html": "{{technical}} Displayed as a header when API output is pretty-printed in HTML, but there is no non-html module.\n\nParameters:\n* $1 - Format name",
 -      "api-orm-param-props": "{{doc-apihelp-param|orm|props|description=the \"props\" parameter in subclasses of ApiQueryORM}}",
 -      "api-orm-param-limit": "{{doc-apihelp-param|orm|limit|description=the \"limit\" parameter in subclasses of ApiQueryORM}}",
        "api-pageset-param-titles": "{{doc-apihelp-param|pageset|titles|description=the \"titles\" parameter in pageset-using modules}}",
        "api-pageset-param-pageids": "{{doc-apihelp-param|pageset|pageids|description=the \"pageids\" parameter in pageset-using modules}}",
        "api-pageset-param-revids": "{{doc-apihelp-param|pageset|revids|description=the \"revids\" parameter in pageset-using modules}}",
@@@ -76,21 -76,6 +76,21 @@@ class ChangesList extends ContextSourc
                }
        }
  
 +      /**
 +       * Format a line
 +       *
 +       * @since 1.27
 +       *
 +       * @param RecentChange $rc Passed by reference
 +       * @param bool $watched (default false)
 +       * @param int $linenumber (default null)
 +       *
 +       * @return string|bool
 +       */
 +      public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
 +              throw new RuntimeException( 'recentChangesLine should be implemented' );
 +      }
 +
        /**
         * Sets the list to use a "<li class='watchlist-(namespace)-(page)'>" tag
         * @param bool $value
         */
        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'];
                                $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> ';
  
                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;
+       }
  }
@@@ -83,16 -83,15 +83,16 @@@ class EnhancedChangesList extends Chang
        /**
         * Format a line for enhanced recentchange (aka with javascript and block of lines).
         *
 -       * @param RecentChange $baseRC
 +       * @param RecentChange $rc
         * @param bool $watched
 +       * @param int $linenumber (default null)
         *
         * @return string
         */
 -      public function recentChangesLine( &$baseRC, $watched = false ) {
 +      public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
  
                $date = $this->getLanguage()->userDate(
 -                      $baseRC->mAttribs['rc_timestamp'],
 +                      $rc->mAttribs['rc_timestamp'],
                        $this->getUser()
                );
  
                        $this->lastdate = $date;
                }
  
 -              $cacheEntry = $this->cacheEntryFactory->newFromRecentChange( $baseRC, $watched );
 +              $cacheEntry = $this->cacheEntryFactory->newFromRecentChange( $rc, $watched );
                $this->addCacheEntry( $cacheEntry );
  
                return $ret;
  
                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;
                /** @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 ) {
                }
  
                # 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'];
                }
  
                # 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> ';
  
  
                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 );
                }
  
                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
@@@ -324,15 -324,21 +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']
+                                       );
+                               }
                        }
                }
  
                // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
                $right = $auto ? 'autopatrol' : 'patrol';
                $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
 -              if ( !Hooks::run( 'MarkPatrolled', array( $this->getAttribute( 'rc_id' ), &$user, false ) ) ) {
 +              if ( !Hooks::run( 'MarkPatrolled',
 +                                      array( $this->getAttribute( 'rc_id' ), &$user, false, $auto ) )
 +              ) {
                        $errors[] = array( 'hookaborted' );
                }
                // Users without the 'autopatrol' right can't patrol their
                $this->reallyMarkPatrolled();
                // Log this patrol event
                PatrolLog::record( $this, $auto, $user );
 -              Hooks::run( 'MarkPatrolledComplete', array( $this->getAttribute( 'rc_id' ), &$user, false ) );
 +              Hooks::run(
 +                                      'MarkPatrolledComplete',
 +                                      array( $this->getAttribute( 'rc_id' ), &$user, false, $auto )
 +              );
  
                return array();
        }
                return $unserializedParams;
        }
  }
 +
@@@ -1163,7 -1163,7 +1163,7 @@@ class WikiPage implements Page, IDBAcce
                        $title->invalidateCache();
                        if ( $wgUseSquid ) {
                                // Send purge now that page_touched update was committed above
 -                              $update = SquidUpdate::newSimplePurge( $title );
 +                              $update = new SquidUpdate( $title->getSquidURLs() );
                                $update->doUpdate();
                        }
                } );
                        $updates = $content->getSecondaryDataUpdates(
                                $this->getTitle(), null, $recursive, $editInfo->output );
                        foreach ( $updates as $update ) {
+                               if ( $update instanceof LinksUpdate ) {
+                                       $update->setRevision( $revision );
+                               }
                                DeferredUpdates::addUpdate( $update );
                        }
                }
  
                $user = is_null( $user ) ? $wgUser : $user;
                if ( !Hooks::run( 'ArticleDelete',
 -                      array( &$this, &$user, &$reason, &$error, &$status )
 +                      array( &$this, &$user, &$reason, &$error, &$status, $suppress )
                ) ) {
                        if ( $status->isOK() ) {
                                // Hook aborted but didn't set a fatal status
                                }
  
                                if ( count( $added ) ) {
 -                                      $insertRows = array();
 -                                      foreach ( $added as $cat ) {
 -                                              $insertRows[] = array(
 -                                                      'cat_title'   => $cat,
 -                                                      'cat_pages'   => 1,
 -                                                      'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
 -                                                      'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
 -                                              );
 -                                      }
 -                                      $dbw->upsert(
 +                                      $existingAdded = $dbw->selectFieldValues(
                                                'category',
 -                                              $insertRows,
 -                                              array( 'cat_title' ),
 -                                              $addFields,
 -                                              $method
 +                                              'cat_title',
 +                                              array( 'cat_title' => $added ),
 +                                              __METHOD__
                                        );
 +
 +                                      // For category rows that already exist, do a plain
 +                                      // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
 +                                      // to avoid creating gaps in the cat_id sequence.
 +                                      if ( count( $existingAdded ) ) {
 +                                              $dbw->update(
 +                                                      'category',
 +                                                      $addFields,
 +                                                      array( 'cat_title' => $existingAdded ),
 +                                                      __METHOD__
 +                                              );
 +                                      }
 +
 +                                      $missingAdded = array_diff( $added, $existingAdded );
 +                                      if ( count( $missingAdded ) ) {
 +                                              $insertRows = array();
 +                                              foreach ( $missingAdded as $cat ) {
 +                                                      $insertRows[] = array(
 +                                                              'cat_title'   => $cat,
 +                                                              'cat_pages'   => 1,
 +                                                              'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
 +                                                              'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
 +                                                      );
 +                                              }
 +                                              $dbw->upsert(
 +                                                      'category',
 +                                                      $insertRows,
 +                                                      array( 'cat_title' ),
 +                                                      $addFields,
 +                                                      $method
 +                                              );
 +                                      }
                                }
  
                                if ( count( $deleted ) ) {
                if ( $this->getLinksTimestamp() < $this->getTouched() ) {
                        $params['isOpportunistic'] = true;
                        $params['rootJobTimestamp'] = $parserOutput->getCacheTime();
 -
 -                      JobQueueGroup::singleton()->lazyPush( EnqueueJob::newFromLocalJobs(
 -                              new JobSpecification( 'refreshLinks', $params,
 -                                      array( 'removeDuplicates' => true ), $this->mTitle )
 -                      ) );
 +                      JobQueueGroup::singleton()->lazyPush( new RefreshLinksJob( $this->mTitle, $params ) );
                }
        }
  
         *
         * @param Content|null $content Optional Content object for determining the
         *   necessary updates.
 -       * @return array An array of DataUpdates objects
 +       * @return DataUpdate[]
         */
        public function getDeletionUpdates( Content $content = null ) {
                if ( !$content ) {
@@@ -136,6 -136,7 +136,7 @@@ abstract class ChangesListSpecialPage e
         * @return FormOptions
         */
        public function getDefaultOptions() {
+               $config = $this->getConfig();
                $opts = new FormOptions();
  
                $opts->add( 'hideminor', 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 );
                $opts->add( 'associated', false );
                                $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
                        }
                }
+               if ( $opts['hidecategorization'] === true ) {
+                       $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE );
+               }
  
                // Namespace filtering
                if ( $opts['namespace'] !== '' ) {
         *
         * @param FormOptions $opts
         */
 -      function setTopText( FormOptions $opts ) {
 +      public function setTopText( FormOptions $opts ) {
                // nothing by default
        }
  
         *
         * @param FormOptions $opts
         */
 -      function setBottomText( FormOptions $opts ) {
 +      public function setBottomText( FormOptions $opts ) {
                // nothing by default
        }
  
         * @param FormOptions $opts
         * @return array
         */
 -      function getExtraOptions( $opts ) {
 +      public function getExtraOptions( $opts ) {
                return array();
        }
  
diff --combined languages/i18n/en.json
@@@ -7,6 -7,7 +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 +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",
        "prefs-help-recentchangescount": "This includes recent changes, page histories, and logs.",
        "prefs-help-watchlist-token2": "This is the secret key to the web feed of your watchlist.\nAnyone who knows it will be able to read your watchlist, so do not share it.\nIf you need to, [[Special:ResetTokens|you can reset it]].",
        "savedprefs": "Your preferences have been saved.",
 +      "savedrights": "The user rights of {{GENDER:$1|$1}} have been saved.",
        "timezonelegend": "Time zone:",
        "localtime": "Local time:",
        "timezoneuseserverdefault": "Use wiki default ($1)",
        "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",
        "upload-dialog-button-done": "Done",
        "upload-dialog-button-save": "Save",
        "upload-dialog-button-upload": "Upload",
 -      "upload-process-error": "An error occurred",
 -      "upload-process-warning": "A warning occurred",
        "upload-form-label-select-file": "Select file",
        "upload-form-label-infoform-title": "Details",
        "upload-form-label-infoform-name": "Name",
        "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 --combined languages/i18n/qqq.json
                        "Robin0van0der0vliet",
                        "TTO",
                        "J. 'mach' wust",
 -                      "Ciencia Al Poder"
 +                      "Ciencia Al Poder",
 +                      "Aursani"
                ]
        },
        "sidebar": "{{notranslate}}",
        "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}}",
        "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}}",
        "sig_tip": "This is the text that appears when you hover the mouse over the second key from the right on the edit toolbar.\n{{Identical|Signature with timestamp}}",
        "hr_tip": "This is the text that appears when you hover the mouse over the first button on the right on the edit toolbar.",
        "summary": "The Summary text beside the edit summary field\n\nSee also:\n* {{msg-mw|Subject}}\nSee also:\n* {{msg-mw|Accesskey-summary}}\n* {{msg-mw|Tooltip-summary}}\n{{Identical|Summary}}",
 -      "subject": "Used as label for input box in the EditPage page.\n\nSee also:\n* {{msg-mw|Summary}}",
 +      "subject": "Used as label for input box in the EditPage page.\n\nSee also:\n* {{msg-mw|Summary}}\n{{Identical|Subject}}",
        "minoredit": "Text above Save page button in editor\n\nSee also:\n* {{msg-mw|Minoredit}}\n* {{msg-mw|Accesskey-minoredit}}\n* {{msg-mw|Tooltip-minoredit}}",
        "watchthis": "Text of checkbox above {{msg-mw|Showpreview}} button in editor.\n\nSee also:\n* {{msg-mw|Watchthis}}\n* {{msg-mw|Accesskey-watch}}\n* {{msg-mw|Tooltip-watch}}\n{{Identical|Watch this page}}",
        "savearticle": "Text on the Save page button. See also {{msg-mw|showpreview}} and {{msg-mw|showdiff}} for the other buttons.\n\nSee also:\n* {{msg-mw|Savearticle}}\n* {{msg-mw|Accesskey-save}}\n* {{msg-mw|Tooltip-save}}\n{{Identical|Save page}}",
        "prefs-help-recentchangescount": "Used in [[Special:Preferences]], tab \"Recent changes\".",
        "prefs-help-watchlist-token2": "Used in [[Special:Preferences]], tab Watchlist. (Formerly in {{msg-mw|prefs-help-watchlist-token}}.)",
        "savedprefs": "This message appears after saving changes to your user preferences.",
 +      "savedrights": "This message appears after saving the user rights on [[Special:UserRights]].\n* $1 - The user name of the user which rights was saved.",
        "timezonelegend": "{{Identical|Time zone}}",
        "localtime": "Used as label in [[Special:Preferences#mw-prefsection-datetime|preferences]].",
        "timezoneuseserverdefault": "[[Special:Preferences]] > Date and time > Time zone\n\nThis option lets your time zone setting use the one that is used on the wiki (often UTC).\n\nParameters:\n* $1 - timezone name, or timezone offset (in \"%+03d:%02d\" format)",
        "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]], ...",
        "upload-dialog-button-done": "Button to close the dialog once upload is complete\n{{Identical|Done}}",
        "upload-dialog-button-save": "Button to save the file\n{{Identical|Save}}",
        "upload-dialog-button-upload": "Button to initiate upload\n{{Identical|Upload}}",
 -      "upload-process-error": "Error message from upload",
 -      "upload-process-warning": "Warning message from upload",
        "upload-form-label-select-file": "Label for the select file widget\n{{Identical|Select file}}",
        "upload-form-label-infoform-title": "Title for the information form\n{{Identical|Detail}}",
        "upload-form-label-infoform-name": "Label for the file name input\n{{Identical|Name}}",
        "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}}]",
        "specialpages-group-media": "{{doc-special-group|like=[[Special:FilePath]], [[Special:MIMESearch]] and [[Special:Upload]]}}",
        "specialpages-group-users": "{{doc-special-group|like=[[Special:ActiveUsers]], [[Special:Contributions]] and [[Special:ListGroupRights]]}}",
        "specialpages-group-highuse": "{{doc-special-group|like=[[Special:MostCategories]], [[Special:MostLinked]] and [[Special:MostRevisions]]}}",
 -      "specialpages-group-pages": "{{doc-special-group|like=[[Special:AllPages]], [[Special:PrefixIndex]], [[Special:Categories]],\n[[Special:Disambiguations]], etc}}",
 +      "specialpages-group-pages": "{{doc-special-group|like=[[Special:AllPages]], [[Special:PrefixIndex]], [[Special:Categories]], etc}}",
        "specialpages-group-pagetools": "{{doc-special-group|like=[[Special:MovePage]], [[Special:Undelete]], [[Special:WhatLinksHere]], [[Special:Export]] etc}}",
        "specialpages-group-wiki": "{{doc-special-group|like=[[Special:Version]], [[Special:Statistics]], [[Special:LockDB]], etc}}",
        "specialpages-group-redirects": "{{doc-special-group|that=redirect to another location|like=[[Special:Randompage]], [[Special:Mypage]], [[Special:Mytalk]], etc}}",