Merge "RCFilters: define consistent interface in ChangesListFilterGroup"
[lhc/web/wiklou.git] / includes / specialpage / ChangesListSpecialPage.php
index d7519d3..67f68ea 100644 (file)
@@ -540,6 +540,8 @@ abstract class ChangesListSpecialPage extends SpecialPage {
        public function execute( $subpage ) {
                $this->rcSubpage = $subpage;
 
+               $this->considerActionsForDefaultSavedQuery();
+
                $rows = $this->getRows();
                $opts = $this->getOptions();
                if ( $rows === false ) {
@@ -591,6 +593,77 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                $this->includeRcFiltersApp();
        }
 
+       /**
+        * Check whether or not the page should load defaults, and if so, whether
+        * a default saved query is relevant to be redirected to. If it is relevant,
+        * redirect properly with all necessary query parameters.
+        */
+       protected function considerActionsForDefaultSavedQuery() {
+               if ( !$this->isStructuredFilterUiEnabled() ) {
+                       return;
+               }
+
+               $knownParams = call_user_func_array(
+                       [ $this->getRequest(), 'getValues' ],
+                       array_keys( $this->getOptions()->getAllValues() )
+               );
+
+               // HACK: Temporarily until we can properly define "sticky" filters and parameters,
+               // we need to exclude several parameters we know should not be counted towards preventing
+               // the loading of defaults.
+               $excludedParams = [ 'limit' => '', 'days' => '', 'enhanced' => '', 'from' => '' ];
+               $knownParams = array_diff_key( $knownParams, $excludedParams );
+
+               if (
+                       // If there are NO known parameters in the URL request
+                       // (that are not excluded) then we need to check into loading
+                       // the default saved query
+                       count( $knownParams ) === 0
+               ) {
+                       // Get the saved queries data and parse it
+                       $savedQueries = FormatJson::decode(
+                               $this->getUser()->getOption( static::$savedQueriesPreferenceName ),
+                               true
+                       );
+
+                       if ( $savedQueries && isset( $savedQueries[ 'default' ] ) ) {
+                               // Only load queries that are 'version' 2, since those
+                               // have parameter representation
+                               if ( isset( $savedQueries[ 'version' ] ) && $savedQueries[ 'version' ] === '2' ) {
+                                       $savedQueryDefaultID = $savedQueries[ 'default' ];
+                                       $defaultQuery = $savedQueries[ 'queries' ][ $savedQueryDefaultID ][ 'data' ];
+
+                                       // Build the entire parameter list
+                                       $query = array_merge(
+                                               $defaultQuery[ 'params' ],
+                                               $defaultQuery[ 'highlights' ],
+                                               [
+                                                       'urlversion' => '2',
+                                               ]
+                                       );
+                                       // Add to the query any parameters that we may have ignored before
+                                       // but are still valid and requested in the URL
+                                       $query = array_merge( $this->getRequest()->getValues(), $query );
+                                       unset( $query[ 'title' ] );
+                                       $this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) );
+                               } else {
+                                       // There's a default, but the version is not 2, and the server can't
+                                       // actually recognize the query itself. This happens if it is before
+                                       // the conversion, so we need to tell the UI to reload saved query as
+                                       // it does the conversion to version 2
+                                       $this->getOutput()->addJsConfigVars(
+                                               'wgStructuredChangeFiltersDefaultSavedQueryExists',
+                                               true
+                                       );
+
+                                       // Add the class that tells the frontend it is still loading
+                                       // another query
+                                       $this->getOutput()->addBodyClasses( 'mw-rcfilters-ui-loading' );
+                               }
+                       }
+               }
+       }
+
        /**
         * Include the modules and configuration for the RCFilters app.
         * Conditional on the user having the feature enabled.
@@ -619,7 +692,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
 
                        $out->addJsConfigVars(
                                'wgRCFiltersChangeTags',
-                               $this->buildChangeTagList()
+                               $this->getChangeTagList()
                        );
                        $out->addJsConfigVars(
                                'StructuredChangeFiltersDisplayConfig',
@@ -632,25 +705,15 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                ]
                        );
 
+                       $out->addJsConfigVars(
+                               'wgStructuredChangeFiltersSavedQueriesPreferenceName',
+                               static::$savedQueriesPreferenceName
+                       );
+
                        $out->addJsConfigVars(
                                'StructuredChangeFiltersLiveUpdatePollingRate',
                                $this->getConfig()->get( 'StructuredChangeFiltersLiveUpdatePollingRate' )
                        );
-
-                       if ( static::$savedQueriesPreferenceName ) {
-                               $savedQueries = FormatJson::decode(
-                                       $this->getUser()->getOption( static::$savedQueriesPreferenceName )
-                               );
-                               if ( $savedQueries && isset( $savedQueries->default ) ) {
-                                       // If there is a default saved query, show a loading spinner,
-                                       // since the frontend is going to reload the results
-                                       $out->addBodyClasses( 'mw-rcfilters-ui-loading' );
-                               }
-                               $out->addJsConfigVars(
-                                       'wgStructuredChangeFiltersSavedQueriesPreferenceName',
-                                       static::$savedQueriesPreferenceName
-                               );
-                       }
                } else {
                        $out->addBodyClasses( 'mw-rcfilters-disabled' );
                }
@@ -661,49 +724,60 @@ abstract class ChangesListSpecialPage extends SpecialPage {
         *
         * @return Array Tag data
         */
-       protected function buildChangeTagList() {
-               $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
-               $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
-
-               // Hit counts disabled for perf reasons, see T169997
-               /*
-               $tagStats = ChangeTags::tagUsageStatistics();
-               $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
-
-               // Sort by hits
-               arsort( $tagHitCounts );
-               */
-               $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags );
-
-               // Build the list and data
-               $result = [];
-               foreach ( $tagHitCounts as $tagName => $hits ) {
-                       if (
-                               // Only get active tags
-                               isset( $explicitlyDefinedTags[ $tagName ] ) ||
-                               isset( $softwareActivatedTags[ $tagName ] )
-                       ) {
-                               // Parse description
-                               $desc = ChangeTags::tagLongDescriptionMessage( $tagName, $this->getContext() );
-
-                               $result[] = [
-                                       'name' => $tagName,
-                                       'label' => Sanitizer::stripAllTags(
-                                               ChangeTags::tagDescription( $tagName, $this->getContext() )
-                                       ),
-                                       'description' => $desc ? Sanitizer::stripAllTags( $desc->parse() ) : '',
-                                       'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
-                                       'hits' => $hits,
-                               ];
-                       }
-               }
+       protected function getChangeTagList() {
+               $cache = ObjectCache::getMainWANInstance();
+               $context = $this->getContext();
+               return $cache->getWithSetCallback(
+                       $cache->makeKey( 'changeslistspecialpage-changetags', $context->getLanguage()->getCode() ),
+                       $cache::TTL_MINUTE * 10,
+                       function () use ( $context ) {
+                               $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
+                               $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
+
+                               // Hit counts disabled for perf reasons, see T169997
+                               /*
+                               $tagStats = ChangeTags::tagUsageStatistics();
+                               $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
+
+                               // Sort by hits
+                               arsort( $tagHitCounts );
+                               */
+                               $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags );
+
+                               // Build the list and data
+                               $result = [];
+                               foreach ( $tagHitCounts as $tagName => $hits ) {
+                                       if (
+                                               // Only get active tags
+                                               isset( $explicitlyDefinedTags[ $tagName ] ) ||
+                                               isset( $softwareActivatedTags[ $tagName ] )
+                                       ) {
+                                               // Parse description
+                                               $desc = ChangeTags::tagLongDescriptionMessage( $tagName, $context );
+
+                                               $result[] = [
+                                                       'name' => $tagName,
+                                                       'label' => Sanitizer::stripAllTags(
+                                                               ChangeTags::tagDescription( $tagName, $context )
+                                                       ),
+                                                       'description' => $desc ? Sanitizer::stripAllTags( $desc->parse() ) : '',
+                                                       'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
+                                                       'hits' => $hits,
+                                               ];
+                                       }
+                               }
 
-               // Instead of sorting by hit count (disabled, see above), sort by display name
-               usort( $result, function ( $a, $b ) {
-                       return strcasecmp( $a['label'], $b['label'] );
-               } );
+                               // Instead of sorting by hit count (disabled, see above), sort by display name
+                               usort( $result, function ( $a, $b ) {
+                                       return strcasecmp( $a['label'], $b['label'] );
+                               } );
 
-               return $result;
+                               return $result;
+                       },
+                       [
+                               'lockTSE' => 30
+                       ]
+               );
        }
 
        /**
@@ -896,6 +970,23 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                return $unstructuredGroupDefinition;
        }
 
+       /**
+        * @return array The legacy show/hide toggle filters
+        */
+       protected function getLegacyShowHideFilters() {
+               $filters = [];
+               foreach ( $this->filterGroups as $group ) {
+                       if ( $group instanceof  ChangesListBooleanFilterGroup ) {
+                               foreach ( $group->getFilters() as $key => $filter ) {
+                                       if ( $filter->displaysOnUnstructuredUi( $this ) ) {
+                                               $filters[ $key ] = $filter;
+                                       }
+                               }
+                       }
+               }
+               return $filters;
+       }
+
        /**
         * Register all the filters, including legacy hook-driven ones.
         * Then create a FormOptions object with options as specified by the user
@@ -936,19 +1027,9 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                // If urlversion=2 is set, ignore the filter defaults and set them all to false/empty
                $useDefaults = $this->getRequest()->getInt( 'urlversion' ) !== 2;
 
-               // Add all filters
                /** @var ChangesListFilterGroup $filterGroup */
                foreach ( $this->filterGroups as $filterGroup ) {
-                       // URL parameters can be per-group, like 'userExpLevel',
-                       // or per-filter, like 'hideminor'.
-                       if ( $filterGroup->isPerGroupRequestParameter() ) {
-                               $opts->add( $filterGroup->getName(), $useDefaults ? $filterGroup->getDefault() : '' );
-                       } else {
-                               /** @var ChangesListBooleanFilter $filter */
-                               foreach ( $filterGroup->getFilters() as $filter ) {
-                                       $opts->add( $filter->getName(), $useDefaults ? $filter->getDefault( $structuredUI ) : false );
-                               }
-                       }
+                       $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
                }
 
                $opts->add( 'namespace', '', FormOptions::STRING );
@@ -957,6 +1038,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                $opts->add( 'urlversion', 1 );
                $opts->add( 'tagfilter', '' );
 
+               $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT );
+               $opts->add( 'limit', $this->getDefaultLimit(), FormOptions::INT );
+
+               $opts->add( 'from', '' );
+
                return $opts;
        }
 
@@ -1074,9 +1160,9 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                // or per-filter, like 'hideminor'.
 
                foreach ( $this->filterGroups as $filterGroup ) {
-                       if ( $filterGroup->isPerGroupRequestParameter() ) {
+                       if ( $filterGroup instanceof ChangesListStringOptionsFilterGroup ) {
                                $stringParameterNameSet[$filterGroup->getName()] = true;
-                       } elseif ( $filterGroup->getType() === ChangesListBooleanFilterGroup::TYPE ) {
+                       } elseif ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
                                foreach ( $filterGroup->getFilters() as $filter ) {
                                        $hideParameterNameSet[$filter->getName()] = true;
                                }
@@ -1106,10 +1192,16 @@ abstract class ChangesListSpecialPage extends SpecialPage {
         * @param FormOptions $opts
         */
        public function validateOptions( FormOptions $opts ) {
-               if ( $this->fixContradictoryOptions( $opts ) ) {
+               $isContradictory = $this->fixContradictoryOptions( $opts );
+               $isReplaced = $this->replaceOldOptions( $opts );
+
+               if ( $isContradictory || $isReplaced ) {
                        $query = wfArrayToCgi( $this->convertParamsForLink( $opts->getChangedValues() ) );
                        $this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) );
                }
+
+               $opts->validateIntBounds( 'limit', 0, 5000 );
+               $opts->validateBounds( 'days', 0, $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
        }
 
        /**
@@ -1174,6 +1266,34 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                return false;
        }
 
+       /**
+        * Replace old options 'hideanons' or 'hideliu' with structured UI equivalent
+        *
+        * @param FormOptions $opts
+        * @return bool True if the change was made
+        */
+       public function replaceOldOptions( FormOptions $opts ) {
+               if ( !$this->isStructuredFilterUiEnabled() ) {
+                       return false;
+               }
+
+               // At this point 'hideanons' and 'hideliu' cannot be both true,
+               // because fixBackwardsCompatibilityOptions resets (at least) 'hideanons' in such case
+               if ( $opts[ 'hideanons' ] ) {
+                       $opts->reset( 'hideanons' );
+                       $opts[ 'userExpLevel' ] = 'registered';
+                       return true;
+               }
+
+               if ( $opts[ 'hideliu' ] ) {
+                       $opts->reset( 'hideliu' );
+                       $opts[ 'userExpLevel' ] = 'unregistered';
+                       return true;
+               }
+
+               return false;
+       }
+
        /**
         * Convert parameters values from true/false to 1/0
         * so they are not omitted by wfArrayToCgi()
@@ -1209,20 +1329,10 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                $dbr = $this->getDB();
                $isStructuredUI = $this->isStructuredFilterUiEnabled();
 
+               /** @var ChangesListFilterGroup $filterGroup */
                foreach ( $this->filterGroups as $filterGroup ) {
-                       // URL parameters can be per-group, like 'userExpLevel',
-                       // or per-filter, like 'hideminor'.
-                       if ( $filterGroup->isPerGroupRequestParameter() ) {
-                               $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds,
-                                       $query_options, $join_conds, $opts[$filterGroup->getName()] );
-                       } else {
-                               foreach ( $filterGroup->getFilters() as $filter ) {
-                                       if ( $filter->isActive( $opts, $isStructuredUI ) ) {
-                                               $filter->modifyQuery( $dbr, $this, $tables, $fields, $conds,
-                                                       $query_options, $join_conds );
-                                       }
-                               }
-                       }
+                       $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds,
+                               $query_options, $join_conds, $opts, $isStructuredUI );
                }
 
                // Namespace filtering
@@ -1249,6 +1359,19 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        }
                        $conds[] = "rc_namespace $operator $value";
                }
+
+               // Calculate cutoff
+               $cutoff_unixtime = time() - $opts['days'] * 3600 * 24;
+               $cutoff = $dbr->timestamp( $cutoff_unixtime );
+
+               $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
+               if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
+                       $cutoff = $dbr->timestamp( $opts['from'] );
+               } else {
+                       $opts->reset( 'from' );
+               }
+
+               $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
        }
 
        /**
@@ -1435,8 +1558,10 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        $context->msg( 'recentchanges-legend-heading' )->parse();
 
                # Collapsible
+               $collapsedState = $this->getRequest()->getCookie( 'changeslist-state' );
+               $collapsedClass = $collapsedState === 'collapsed' ? ' mw-collapsed' : '';
                $legend =
-                       '<div class="mw-changeslist-legend">' .
+                       '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass . '">' .
                                $legendHeading .
                                '<div class="mw-collapsible-content">' . $legend . '</div>' .
                        '</div>';