Merge "Restore the newFromId() approach in SpecialNewpages::feedItemDesc"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 17 Aug 2017 22:16:27 +0000 (22:16 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 17 Aug 2017 22:16:27 +0000 (22:16 +0000)
20 files changed:
includes/changes/EnhancedChangesList.php
includes/db/MWLBFactory.php
includes/jobqueue/jobs/RefreshLinksJob.php
includes/libs/rdbms/lbfactory/ILBFactory.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/rdbms/loadmonitor/ILoadMonitor.php
includes/libs/rdbms/loadmonitor/LoadMonitor.php
includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php
includes/libs/rdbms/loadmonitor/LoadMonitorNull.php
includes/skins/SkinTemplate.php
includes/templates/EnhancedChangesListGroup.mustache
languages/i18n/en.json
languages/i18n/qqq.json
resources/Resources.php
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js
resources/src/mediawiki/page/watch.js

index def6457..21a811e 100644 (file)
@@ -349,6 +349,7 @@ class EnhancedChangesList extends ChangesList {
                        'rev-deleted-event' => $revDeletedMsg,
                        'tableClasses' => $tableClasses,
                        'timestamp' => $block[0]->timestamp,
+                       'fullTimestamp' => $block[0]->getAttribute( 'rc_timestamp' ),
                        'users' => $usersList,
                ];
 
index 464a918..5196ac2 100644 (file)
@@ -149,7 +149,7 @@ abstract class MWLBFactory {
                }
                $cCache = ObjectCache::getLocalClusterInstance();
                if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) {
-                       $lbConf['memCache'] = $cCache;
+                       $lbConf['memStash'] = $cCache;
                }
                $wCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
index 02bb829..0c84a13 100644 (file)
@@ -279,6 +279,10 @@ class RefreshLinksJob extends Job {
 
                InfoAction::invalidateCache( $title );
 
+               // Commit any writes here in case this method is called in a loop.
+               // In that case, the scoped lock will fail to be acquired.
+               $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+
                return true;
        }
 
index 117df68..ff6635d 100644 (file)
@@ -44,7 +44,7 @@ interface ILBFactory {
         *  - localDomain: A DatabaseDomain or domain ID string.
         *  - readOnlyReason : Reason the master DB is read-only if so [optional]
         *  - srvCache : BagOStuff object for server cache [optional]
-        *  - memCache : BagOStuff object for cluster memory cache [optional]
+        *  - memStash : BagOStuff object for cross-datacenter memory storage [optional]
         *  - wanCache : WANObjectCache object [optional]
         *  - hostname : The name of the current server [optional]
         *  - cliMode: Whether the execution context is a CLI script. [optional]
index 919f103..c891fb6 100644 (file)
@@ -55,7 +55,7 @@ abstract class LBFactory implements ILBFactory {
        /** @var BagOStuff */
        protected $srvCache;
        /** @var BagOStuff */
-       protected $memCache;
+       protected $memStash;
        /** @var WANObjectCache */
        protected $wanCache;
 
@@ -93,7 +93,7 @@ abstract class LBFactory implements ILBFactory {
                }
 
                $this->srvCache = isset( $conf['srvCache'] ) ? $conf['srvCache'] : new EmptyBagOStuff();
-               $this->memCache = isset( $conf['memCache'] ) ? $conf['memCache'] : new EmptyBagOStuff();
+               $this->memStash = isset( $conf['memStash'] ) ? $conf['memStash'] : new EmptyBagOStuff();
                $this->wanCache = isset( $conf['wanCache'] )
                        ? $conf['wanCache']
                        : WANObjectCache::newEmpty();
@@ -435,7 +435,7 @@ abstract class LBFactory implements ILBFactory {
                }
 
                $this->chronProt = new ChronologyProtector(
-                       $this->memCache,
+                       $this->memStash,
                        [
                                'ip' => $this->requestInfo['IPAddress'],
                                'agent' => $this->requestInfo['UserAgent'],
index fc50961..c940392 100644 (file)
@@ -94,7 +94,6 @@ interface ILoadBalancer {
         *  - readOnlyReason : Reason the master DB is read-only if so [optional]
         *  - waitTimeout : Maximum time to wait for replicas for consistency [optional]
         *  - srvCache : BagOStuff object for server cache [optional]
-        *  - memCache : BagOStuff object for cluster memory cache [optional]
         *  - wanCache : WANObjectCache object [optional]
         *  - chronologyProtector: ChronologyProtector object [optional]
         *  - hostname : The name of the current server [optional]
index 72217da..1df2409 100644 (file)
@@ -62,8 +62,6 @@ class LoadBalancer implements ILoadBalancer {
        private $chronProt;
        /** @var BagOStuff */
        private $srvCache;
-       /** @var BagOStuff */
-       private $memCache;
        /** @var WANObjectCache */
        private $wanCache;
        /** @var object|string Class name or object With profileIn/profileOut methods */
@@ -187,11 +185,6 @@ class LoadBalancer implements ILoadBalancer {
                } else {
                        $this->srvCache = new EmptyBagOStuff();
                }
-               if ( isset( $params['memCache'] ) ) {
-                       $this->memCache = $params['memCache'];
-               } else {
-                       $this->memCache = new EmptyBagOStuff();
-               }
                if ( isset( $params['wanCache'] ) ) {
                        $this->wanCache = $params['wanCache'];
                } else {
@@ -244,7 +237,7 @@ class LoadBalancer implements ILoadBalancer {
                        }
 
                        $this->loadMonitor = new $class(
-                               $this, $this->srvCache, $this->memCache, $this->loadMonitorConfig );
+                               $this, $this->srvCache, $this->wanCache, $this->loadMonitorConfig );
                        $this->loadMonitor->setLogger( $this->replLogger );
                }
 
index 4f6701f..fba5e13 100644 (file)
@@ -25,6 +25,7 @@ namespace Wikimedia\Rdbms;
 
 use Psr\Log\LoggerAwareInterface;
 use BagOStuff;
+use WANObjectCache;
 
 /**
  * An interface for database load monitoring
@@ -37,11 +38,11 @@ interface ILoadMonitor extends LoggerAwareInterface {
         *
         * @param ILoadBalancer $lb LoadBalancer this instance serves
         * @param BagOStuff $sCache Local server memory cache
-        * @param BagOStuff $cCache Local cluster memory cache
+        * @param WANObjectCache $wCache Local cluster memory cache
         * @param array $options Options map
         */
        public function __construct(
-               ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache, array $options = []
+               ILoadBalancer $lb, BagOStuff $sCache, WANObjectCache $wCache, array $options = []
        );
 
        /**
index 4300e9f..d4e73c9 100644 (file)
@@ -25,6 +25,7 @@ use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
 use Wikimedia\ScopedCallback;
 use BagOStuff;
+use WANObjectCache;
 
 /**
  * Basic DB load monitor with no external dependencies
@@ -37,8 +38,8 @@ class LoadMonitor implements ILoadMonitor {
        protected $parent;
        /** @var BagOStuff */
        protected $srvCache;
-       /** @var BagOStuff */
-       protected $mainCache;
+       /** @var WANObjectCache */
+       protected $wanCache;
        /** @var LoggerInterface */
        protected $replLogger;
 
@@ -48,11 +49,11 @@ class LoadMonitor implements ILoadMonitor {
        const VERSION = 1; // cache key version
 
        public function __construct(
-               ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache, array $options = []
+               ILoadBalancer $lb, BagOStuff $srvCache, WANObjectCache $wCache, array $options = []
        ) {
                $this->parent = $lb;
                $this->srvCache = $srvCache;
-               $this->mainCache = $cache;
+               $this->wanCache = $wCache;
                $this->replLogger = new NullLogger();
 
                $this->movingAveRatio = isset( $options['movingAveRatio'] )
@@ -109,7 +110,7 @@ class LoadMonitor implements ILoadMonitor {
                $staleValue = $value ?: false;
 
                # (b) Check the shared cache and backfill APC
-               $value = $this->mainCache->get( $key );
+               $value = $this->wanCache->get( $key );
                if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
                        $this->srvCache->set( $key, $value, $staleTTL );
                        $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
@@ -119,12 +120,12 @@ class LoadMonitor implements ILoadMonitor {
                $staleValue = $value ?: $staleValue;
 
                # (c) Cache key missing or expired; regenerate and backfill
-               if ( $this->mainCache->lock( $key, 0, 10 ) ) {
-                       # Let this process alone update the cache value
-                       $cache = $this->mainCache;
+               if ( $this->srvCache->lock( $key, 0, 10 ) ) {
+                       # Let only this process update the cache value on this server
+                       $sCache = $this->srvCache;
                        /** @noinspection PhpUnusedLocalVariableInspection */
-                       $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
-                               $cache->unlock( $key );
+                       $unlocker = new ScopedCallback( function () use ( $sCache, $key ) {
+                               $sCache->unlock( $key );
                        } );
                } elseif ( $staleValue ) {
                        # Could not acquire lock but an old cache exists, so use it
@@ -196,7 +197,7 @@ class LoadMonitor implements ILoadMonitor {
                        'weightScales' => $weightScales,
                        'timestamp' => microtime( true )
                ];
-               $this->mainCache->set( $key, $value, $staleTTL );
+               $this->wanCache->set( $key, $value, $staleTTL );
                $this->srvCache->set( $key, $value, $staleTTL );
                $this->replLogger->info( __METHOD__ . ": re-calculated lag times ($key)" );
 
index ff72dbc..f8ad329 100644 (file)
@@ -22,6 +22,7 @@
 namespace Wikimedia\Rdbms;
 
 use BagOStuff;
+use WANObjectCache;
 
 /**
  * Basic MySQL load monitor with no external dependencies
@@ -34,9 +35,9 @@ class LoadMonitorMySQL extends LoadMonitor {
        private $warmCacheRatio;
 
        public function __construct(
-               ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache, array $options = []
+               ILoadBalancer $lb, BagOStuff $srvCache, WANObjectCache $wCache, array $options = []
        ) {
-               parent::__construct( $lb, $srvCache, $cache, $options );
+               parent::__construct( $lb, $srvCache, $wCache, $options );
 
                $this->warmCacheRatio = isset( $options['warmCacheRatio'] )
                        ? $options['warmCacheRatio']
index 8bbf9e5..6dae8cc 100644 (file)
@@ -23,10 +23,11 @@ namespace Wikimedia\Rdbms;
 
 use Psr\Log\LoggerInterface;
 use BagOStuff;
+use WANObjectCache;
 
 class LoadMonitorNull implements ILoadMonitor {
        public function __construct(
-               ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache, array $options = []
+               ILoadBalancer $lb, BagOStuff $sCache, WANObjectCache $wCache, array $options = []
        ) {
        }
 
index f49d46c..5ad1b11 100644 (file)
@@ -1080,7 +1080,12 @@ class SkinTemplate extends Skin {
                                                ),
                                                // uses 'watch' or 'unwatch' message
                                                'text' => $this->msg( $mode )->text(),
-                                               'href' => $title->getLocalURL( [ 'action' => $mode ] )
+                                               'href' => $title->getLocalURL( [ 'action' => $mode ] ),
+                                               // Set a data-mw=interface attribute, which the mediawiki.page.ajax
+                                               // module will look for to make sure it's a trusted link
+                                               'data' => [
+                                                       'mw' => 'interface',
+                                               ],
                                        ];
                                }
                        }
index cd59d6d..6493df8 100644 (file)
@@ -1,4 +1,4 @@
-<table class="{{# tableClasses }}{{ . }} {{/ tableClasses }}">
+<table class="{{# tableClasses }}{{ . }} {{/ tableClasses }}" data-mw-ts="{{{ fullTimestamp }}}">
        <tr>
                <td>
                        <span class="mw-collapsible-toggle mw-collapsible-arrow mw-enhancedchanges-arrow mw-enhancedchanges-arrow-space"></span>
index d7a3aeb..3497d80 100644 (file)
        "rcfilters-restore-default-filters": "Restore default filters",
        "rcfilters-clear-all-filters": "Clear all filters",
        "rcfilters-show-new-changes": "View newest changes",
-       "rcfilters-previous-changes-label": "Previously viewed changes",
        "rcfilters-search-placeholder": "Filter recent changes (browse or start typing)",
        "rcfilters-invalid-filter": "Invalid filter",
        "rcfilters-empty-filter": "No active filters. All contributions are shown.",
        "rcfilters-filterlist-noresults": "No filters found",
        "rcfilters-noresults-conflict": "No results found because the search criteria are in conflict",
        "rcfilters-state-message-subset": "This filter has no effect because its results are included with those of the following, broader {{PLURAL:$2|filter|filters}} (try highlighting to distinguish it): $1",
-       "rcfilters-state-message-fullcoverage": "Selecting all filters in a group is the same as selecting none, so this filter has no effect. Group includes: $1",
+       "rcfilters-state-message-fullcoverage": "Selecting all filters in this group is the same as selecting none, so this filter has no effect. Group includes: $1",
        "rcfilters-filtergroup-authorship": "Contribution authorship",
        "rcfilters-filter-editsbyself-label": "Changes by you",
        "rcfilters-filter-editsbyself-description": "Your own contributions.",
index f7b176c..eeb0e7a 100644 (file)
        "rcfilters-restore-default-filters": "Label for the button that resets filters to defaults",
        "rcfilters-clear-all-filters": "Title for the button that clears all filters",
        "rcfilters-show-new-changes": "Label for the button to show new changes.",
-       "rcfilters-previous-changes-label": "Label to indicate the changes below have been previously viewed.",
        "rcfilters-search-placeholder": "Placeholder for the filter search input.",
        "rcfilters-invalid-filter": "A label for an invalid filter.",
        "rcfilters-empty-filter": "Placeholder for the filter list when no filters were chosen.",
index 144747b..09bd4dc 100644 (file)
@@ -1862,7 +1862,6 @@ return [
                        'rcfilters-restore-default-filters',
                        'rcfilters-clear-all-filters',
                        'rcfilters-show-new-changes',
-                       'rcfilters-previous-changes-label',
                        'rcfilters-search-placeholder',
                        'rcfilters-invalid-filter',
                        'rcfilters-empty-filter',
index 6acc44d..4dc86f6 100644 (file)
        mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
                // Update state
                var changed = false,
-                       active = this.areAnySelected();
-
-               if (
-                       item.isSelected() &&
-                       this.getType() === 'single_option' &&
-                       this.currSelected &&
-                       this.currSelected !== item
-               ) {
-                       this.currSelected.toggleSelected( false );
-               }
-
-               // For 'single_option' groups, check if we just unselected all
-               // items. This should never be the result. If we did unselect
-               // all (like resetting all filters to false) then this group
-               // must choose its default item or the first item in the group
-               if (
-                       this.getType() === 'single_option' &&
-                       !this.getItems().some( function ( filterItem ) {
-                               return filterItem.isSelected();
-                       } )
-               ) {
-                       // Single option means there must be a single option
-                       // selected, so we have to either select the default
-                       // or select the first option
-                       this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
-                               this.getItems()[ 0 ];
-                       this.currSelected.toggleSelected( true );
-                       changed = true;
+                       active = this.areAnySelected(),
+                       model = this;
+
+               if ( this.getType() === 'single_option' ) {
+                       // This group must have one item selected always
+                       // and must never have more than one item selected at a time
+                       if ( this.getSelectedItems().length === 0 ) {
+                               // Nothing is selected anymore
+                               // Select the default or the first item
+                               this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
+                                       this.getItems()[ 0 ];
+                               this.currSelected.toggleSelected( true );
+                               changed = true;
+                       } else if ( this.getSelectedItems().length > 1 ) {
+                               // There is more than one item selected
+                               // This should only happen if the item given
+                               // is the one that is selected, so unselect
+                               // all items that is not it
+                               this.getSelectedItems().forEach( function ( itemModel ) {
+                                       // Note that in case the given item is actually
+                                       // not selected, this loop will end up unselecting
+                                       // all items, which would trigger the case above
+                                       // when the last item is unselected anyways
+                                       var selected = itemModel.getName() === item.getName() &&
+                                               item.isSelected();
+
+                                       itemModel.toggleSelected( selected );
+                                       if ( selected ) {
+                                               model.currSelected = itemModel;
+                                       }
+                               } );
+                               changed = true;
+                       }
                }
 
                if (
index d60e616..31f3f1d 100644 (file)
@@ -1,5 +1,14 @@
 @import 'mw.rcfilters.mixins';
 
+@keyframes fadeBlue {
+       60% {
+               border-top-color: #36c;
+       }
+       100% {
+               border-top-color: #c8ccd1;
+       }
+}
+
 .mw-rcfilters-ui-changesListWrapperWidget {
 
        &-newChanges {
 
        &-previousChangesIndicator {
                margin: 10px 0;
-               color: #36c;
-               border-top: 2px solid #36c;
-               text-align: center;
-
-               &:hover {
-                       color: #72777d;
-                       border-top-color: #72777d;
-                       cursor: pointer;
-               }
+               border-top: 2px solid #c8ccd1;
+               animation: 1s ease fadeBlue;
        }
 
        &-results {
index 42fb5cc..ba3ca97 100644 (file)
                        $message = $( '<div>' )
                                .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results' ),
                        isEmpty = $changesListContent === 'NO_RESULTS',
-                       $lastSeen,
-                       $indicator,
-                       $newChanges = $( [] ),
                        // For enhanced mode, we have to load these modules, which are
                        // not loaded for the 'regular' mode in the backend
                        loaderPromise = mw.user.options.get( 'usenewrc' ) ?
                                this.$element.empty().append( $changesListContent );
 
                                if ( from ) {
-                                       $lastSeen = null;
-                                       this.$element.find( 'li[data-mw-ts]' ).each( function () {
-                                               var $li = $( this ),
-                                                       ts = $li.data( 'mw-ts' );
-
-                                               if ( ts >= from ) {
-                                                       $newChanges = $newChanges.add( $li );
-                                               } else if ( $lastSeen === null ) {
-                                                       $lastSeen = $li;
-                                                       return false;
-                                               }
-                                       } );
-
-                                       if ( $lastSeen ) {
-                                               $indicator = $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' )
-                                                       .text( mw.message( 'rcfilters-previous-changes-label' ).text() );
-
-                                               $indicator.on( 'click', function () {
-                                                       $indicator.detach();
-                                               } );
-
-                                               $lastSeen.before( $indicator );
-                                       }
-
-                                       $newChanges
-                                               .hide()
-                                               .fadeIn( 1000 );
+                                       this.emphasizeNewChanges( from );
                                }
                        }
 
                } );
        };
 
+       /**
+        * Emphasize the elements (or groups) newer than the 'from' parameter
+        * @param {string} from Anything newer than this is considered 'new'
+        */
+       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.emphasizeNewChanges = function ( from ) {
+               var $firstNew,
+                       $indicator,
+                       $newChanges = $( [] ),
+                       selector = this.inEnhancedMode() ?
+                               'table.mw-enhanced-rc[data-mw-ts]' :
+                               'li[data-mw-ts]',
+                       set = this.$element.find( selector ),
+                       length = set.length;
+
+               set.each( function ( index ) {
+                       var $this = $( this ),
+                               ts = $this.data( 'mw-ts' );
+
+                       if ( ts >= from ) {
+                               $newChanges = $newChanges.add( $this );
+                               $firstNew = $this;
+
+                               // guards against putting the marker after the last element
+                               if ( index === ( length - 1 ) ) {
+                                       $firstNew = null;
+                               }
+                       }
+               } );
+
+               if ( $firstNew ) {
+                       $indicator = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' );
+
+                       $firstNew.after( $indicator );
+               }
+
+               $newChanges
+                       .hide()
+                       .fadeIn( 1000 );
+       };
+
        /**
         * Respond to changes list model newChangesExist
         *
         * @param {jQuery|string} $content The content of the updated changes list
         */
        mw.rcfilters.ui.ChangesListWrapperWidget.prototype.setupHighlightContainers = function ( $content ) {
-               var uri = new mw.Uri(),
-                       highlightClass = 'mw-rcfilters-ui-changesListWrapperWidget-highlights',
+               var highlightClass = 'mw-rcfilters-ui-changesListWrapperWidget-highlights',
                        $highlights = $( '<div>' )
                                .addClass( highlightClass )
                                .append(
                        );
                } );
 
-               if (
-                       ( uri.query.enhanced !== undefined && Number( uri.query.enhanced ) ) ||
-                       ( uri.query.enhanced === undefined && Number( mw.user.options.get( 'usenewrc' ) ) )
-               ) {
+               if ( this.inEnhancedMode() ) {
                        // Enhanced RC
                        $content.find( 'td.mw-enhanced-rc' )
                                .parent()
                }
        };
 
+       /**
+        * @return {boolean} Whether the changes are grouped by page
+        */
+       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.inEnhancedMode = function () {
+               var uri = new mw.Uri();
+               return ( uri.query.enhanced !== undefined && Number( uri.query.enhanced ) ) ||
+                       ( uri.query.enhanced === undefined && Number( mw.user.options.get( 'usenewrc' ) ) );
+       };
+
        /**
         * Apply color classes based on filters highlight configuration
         */
index 6322ccd..e56e807 100644 (file)
        );
 
        $( function () {
-               var $links = $( '.mw-watchlink a, a.mw-watchlink' );
-               // Restrict to core interfaces, ignore user-generated content
-               $links = $links.filter( ':not( #bodyContent *, #content * )' );
+               var $links = $( '.mw-watchlink a[data-mw="interface"], a.mw-watchlink[data-mw="interface"]' );
+               if ( !$links.length ) {
+                       // Fallback to the class-based exclusion method for backwards-compatibility
+                       $links = $( '.mw-watchlink a, a.mw-watchlink' );
+                       // Restrict to core interfaces, ignore user-generated content
+                       $links = $links.filter( ':not( #bodyContent *, #content * )' );
+               }
 
                $links.click( function ( e ) {
                        var mwTitle, action, api, $link;