*/
use MediaWiki\Logger\LoggerFactory;
use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IDatabase;
/**
* Special page which uses a ChangesList to show query results.
*/
private $filterGroupDefinitions;
+ // Same format as filterGroupDefinitions, but for a single group (reviewStatus)
+ // that is registered conditionally.
+ private $reviewStatusFilterGroupDefinition;
+
+ // Single filter registered conditionally
+ private $hideCategorizationFilterDefinition;
+
/**
* Filter groups, and their contained filters
* This is an associative array (with group name as key) of ChangesListFilterGroup objects.
]
],
+ // reviewStatus (conditional)
+
[
- 'name' => 'reviewStatus',
- 'title' => 'rcfilters-filtergroup-reviewstatus',
+ 'name' => 'lastRevision',
+ 'title' => 'rcfilters-filtergroup-lastRevision',
'class' => ChangesListBooleanFilterGroup::class,
+ 'priority' => -7,
'filters' => [
[
- 'name' => 'hidepatrolled',
- 'label' => 'rcfilters-filter-patrolled-label',
- 'description' => 'rcfilters-filter-patrolled-description',
- // rcshowhidepatr-show, rcshowhidepatr-hide
- // wlshowhidepatr
- 'showHideSuffix' => 'showhidepatr',
+ 'name' => 'hidelastrevision',
+ 'label' => 'rcfilters-filter-lastrevision-label',
+ 'description' => 'rcfilters-filter-lastrevision-description',
'default' => false,
- 'isAllowedCallable' => function ( $pageClassName, $context ) {
- return $context->getUser()->useRCPatrol();
- },
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds ) {
-
- $conds[] = 'rc_patrolled = 0';
+ $conds[] = 'rc_this_oldid <> page_latest';
},
- 'cssClassSuffix' => 'patrolled',
+ 'cssClassSuffix' => 'last',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
- return $rc->getAttribute( 'rc_patrolled' );
- },
+ return $rc->getAttribute( 'rc_this_oldid' ) === $rc->getAttribute( 'page_latest' );
+ }
],
[
- 'name' => 'hideunpatrolled',
- 'label' => 'rcfilters-filter-unpatrolled-label',
- 'description' => 'rcfilters-filter-unpatrolled-description',
+ 'name' => 'hidepreviousrevisions',
+ 'label' => 'rcfilters-filter-previousrevision-label',
+ 'description' => 'rcfilters-filter-previousrevision-description',
'default' => false,
- 'isAllowedCallable' => function ( $pageClassName, $context ) {
- return $context->getUser()->useRCPatrol();
- },
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds ) {
-
- $conds[] = 'rc_patrolled = 1';
+ $conds[] = 'rc_this_oldid = page_latest';
},
- 'cssClassSuffix' => 'unpatrolled',
+ 'cssClassSuffix' => 'previous',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
- return !$rc->getAttribute( 'rc_patrolled' );
- },
- ],
- ],
+ return $rc->getAttribute( 'rc_this_oldid' ) !== $rc->getAttribute( 'page_latest' );
+ }
+ ]
+ ]
],
[
'name' => 'significance',
'title' => 'rcfilters-filtergroup-significance',
'class' => ChangesListBooleanFilterGroup::class,
+ 'priority' => -6,
'filters' => [
[
'name' => 'hideminor',
'label' => 'rcfilters-filter-pageedits-label',
'description' => 'rcfilters-filter-pageedits-description',
'default' => false,
+ 'priority' => -2,
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds ) {
'label' => 'rcfilters-filter-newpages-label',
'description' => 'rcfilters-filter-newpages-description',
'default' => false,
+ 'priority' => -3,
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds ) {
return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW;
},
],
+
+ // hidecategorization
+
+ [
+ 'name' => 'hidelog',
+ 'label' => 'rcfilters-filter-logactions-label',
+ 'description' => 'rcfilters-filter-logactions-description',
+ 'default' => false,
+ 'priority' => -5,
+ 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
+ &$query_options, &$join_conds ) {
+
+ $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG );
+ },
+ 'cssClassSuffix' => 'src-mw-log',
+ 'isRowApplicableCallable' => function ( $ctx, $rc ) {
+ return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG;
+ }
+ ],
+ ],
+ ],
+
+ [
+ 'name' => 'watchlist',
+ 'title' => 'rcfilters-filtergroup-watchlist',
+ 'class' => ChangesListStringOptionsFilterGroup::class,
+ 'isFullCoverage' => true,
+ 'filters' => [
[
- 'name' => 'hidecategorization',
- 'label' => 'rcfilters-filter-categorization-label',
- 'description' => 'rcfilters-filter-categorization-description',
- // rcshowhidecategorization-show, rcshowhidecategorization-hide.
- // wlshowhidecategorization
- 'showHideSuffix' => 'showhidecategorization',
- 'isAllowedCallable' => function ( $pageClassName, $context ) {
- return $context->getConfig()->get( 'RCWatchCategoryMembership' );
+ 'name' => 'watched',
+ 'label' => 'rcfilters-filter-watchlist-watched-label',
+ 'description' => 'rcfilters-filter-watchlist-watched-description',
+ 'cssClassSuffix' => 'watched',
+ 'isRowApplicableCallable' => function ( $ctx, $rc ) {
+ return $rc->getAttribute( 'wl_user' );
+ }
+ ],
+ [
+ 'name' => 'watchednew',
+ 'label' => 'rcfilters-filter-watchlist-watchednew-label',
+ 'description' => 'rcfilters-filter-watchlist-watchednew-description',
+ 'cssClassSuffix' => 'watchednew',
+ 'isRowApplicableCallable' => function ( $ctx, $rc ) {
+ return $rc->getAttribute( 'wl_user' ) &&
+ $rc->getAttribute( 'rc_timestamp' ) > $rc->getAttribute( 'wl_notificationtimestamp' );
},
+ ],
+ [
+ 'name' => 'notwatched',
+ 'label' => 'rcfilters-filter-watchlist-notwatched-label',
+ 'description' => 'rcfilters-filter-watchlist-notwatched-description',
+ 'cssClassSuffix' => 'notwatched',
+ 'isRowApplicableCallable' => function ( $ctx, $rc ) {
+ return $rc->getAttribute( 'wl_user' ) === null;
+ },
+ ]
+ ],
+ 'default' => ChangesListStringOptionsFilterGroup::NONE,
+ 'queryCallable' => function ( $specialPageClassName, $context, $dbr,
+ &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) {
+ sort( $selectedValues );
+ $notwatchedCond = 'wl_user IS NULL';
+ $watchedCond = 'wl_user IS NOT NULL';
+ $newCond = 'rc_timestamp >= wl_notificationtimestamp';
+
+ if ( $selectedValues === [ 'notwatched' ] ) {
+ $conds[] = $notwatchedCond;
+ return;
+ }
+
+ if ( $selectedValues === [ 'watched' ] ) {
+ $conds[] = $watchedCond;
+ return;
+ }
+
+ if ( $selectedValues === [ 'watchednew' ] ) {
+ $conds[] = $dbr->makeList( [
+ $watchedCond,
+ $newCond
+ ], LIST_AND );
+ return;
+ }
+
+ if ( $selectedValues === [ 'notwatched', 'watched' ] ) {
+ // no filters
+ return;
+ }
+
+ if ( $selectedValues === [ 'notwatched', 'watchednew' ] ) {
+ $conds[] = $dbr->makeList( [
+ $notwatchedCond,
+ $dbr->makeList( [
+ $watchedCond,
+ $newCond
+ ], LIST_AND )
+ ], LIST_OR );
+ return;
+ }
+
+ if ( $selectedValues === [ 'watched', 'watchednew' ] ) {
+ $conds[] = $watchedCond;
+ return;
+ }
+
+ if ( $selectedValues === [ 'notwatched', 'watched', 'watchednew' ] ) {
+ // no filters
+ return;
+ }
+ },
+ ],
+ ];
+
+ $this->reviewStatusFilterGroupDefinition = [
+ [
+ 'name' => 'reviewStatus',
+ 'title' => 'rcfilters-filtergroup-reviewstatus',
+ 'class' => ChangesListBooleanFilterGroup::class,
+ 'priority' => -5,
+ 'filters' => [
+ [
+ 'name' => 'hidepatrolled',
+ 'label' => 'rcfilters-filter-patrolled-label',
+ 'description' => 'rcfilters-filter-patrolled-description',
+ // rcshowhidepatr-show, rcshowhidepatr-hide
+ // wlshowhidepatr
+ 'showHideSuffix' => 'showhidepatr',
'default' => false,
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds ) {
- $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE );
+ $conds[] = 'rc_patrolled = 0';
},
- 'cssClassSuffix' => 'src-mw-categorize',
+ 'cssClassSuffix' => 'patrolled',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
- return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE;
+ return $rc->getAttribute( 'rc_patrolled' );
},
],
[
- 'name' => 'hidelog',
- 'label' => 'rcfilters-filter-logactions-label',
- 'description' => 'rcfilters-filter-logactions-description',
+ 'name' => 'hideunpatrolled',
+ 'label' => 'rcfilters-filter-unpatrolled-label',
+ 'description' => 'rcfilters-filter-unpatrolled-description',
'default' => false,
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds ) {
- $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG );
+ $conds[] = 'rc_patrolled = 1';
},
- 'cssClassSuffix' => 'src-mw-log',
+ 'cssClassSuffix' => 'unpatrolled',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
- return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG;
- }
+ return !$rc->getAttribute( 'rc_patrolled' );
+ },
],
],
- ],
+ ]
+ ];
+
+ $this->hideCategorizationFilterDefinition = [
+ 'name' => 'hidecategorization',
+ 'label' => 'rcfilters-filter-categorization-label',
+ 'description' => 'rcfilters-filter-categorization-description',
+ // rcshowhidecategorization-show, rcshowhidecategorization-hide.
+ // wlshowhidecategorization
+ 'showHideSuffix' => 'showhidecategorization',
+ 'default' => false,
+ 'priority' => -4,
+ 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
+ &$query_options, &$join_conds ) {
+
+ $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE );
+ },
+ 'cssClassSuffix' => 'src-mw-categorize',
+ 'isRowApplicableCallable' => function ( $ctx, $rc ) {
+ return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE;
+ },
];
}
+ /**
+ * Check if filters are in conflict and guaranteed to return no results.
+ *
+ * @return bool
+ */
+ protected function areFiltersInConflict() {
+ $opts = $this->getOptions();
+ /** @var ChangesListFilterGroup $group */
+ foreach ( $this->getFilterGroups() as $group ) {
+
+ if ( $group->getConflictingGroups() ) {
+ wfLogWarning(
+ $group->getName() .
+ " specifies conflicts with other groups but these are not supported yet."
+ );
+ }
+
+ /** @var ChangesListFilter $conflictingFilter */
+ foreach ( $group->getConflictingFilters() as $conflictingFilter ) {
+ if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
+ return true;
+ }
+ }
+
+ /** @var ChangesListFilter $filter */
+ foreach ( $group->getFilters() as $filter ) {
+
+ /** @var ChangesListFilter $conflictingFilter */
+ foreach ( $filter->getConflictingFilters() as $conflictingFilter ) {
+ if (
+ $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
+ $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
+ ) {
+ return true;
+ }
+ }
+
+ }
+
+ }
+
+ return false;
+ }
+
/**
* Main execution point
*
if ( $rows === false ) {
if ( !$this->including() ) {
$this->doHeader( $opts, 0 );
+ $this->outputNoResults();
$this->getOutput()->setStatusCode( 404 );
}
}
}
$batch->execute();
-
$this->webOutput( $rows, $opts );
$rows->free();
}
}
+ /**
+ * Add the "no results" message to the output
+ */
+ protected function outputNoResults() {
+ $this->getOutput()->addHTML(
+ '<div class="mw-changeslist-empty">' .
+ $this->msg( 'recentchanges-noresult' )->parse() .
+ '</div>'
+ );
+ }
+
/**
* Get the database result for this special page instance. Used by ApiFeedRecentChanges.
*
}
/**
- * Register all filters and their groups, plus conflicts
+ * Register all filters and their groups (including those from hooks), plus handle
+ * conflicts and defaults.
*
* You might want to customize these in the same method, in subclasses. You can
* call getFilterGroup to access a group, and (on the group) getFilter to access a
protected function registerFilters() {
$this->registerFiltersFromDefinitions( $this->filterGroupDefinitions );
+ // Make sure this is not being transcluded (we don't want to show this
+ // information to all users just because the user that saves the edit can
+ // patrol)
+ if ( !$this->including() && $this->getUser()->useRCPatrol() ) {
+ $this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition );
+ }
+
+ $changeTypeGroup = $this->getFilterGroup( 'changeType' );
+
+ if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) {
+ $transformedHideCategorizationDef = $this->transformFilterDefinition(
+ $this->hideCategorizationFilterDefinition
+ );
+
+ $transformedHideCategorizationDef['group'] = $changeTypeGroup;
+
+ $hideCategorization = new ChangesListBooleanFilter(
+ $transformedHideCategorizationDef
+ );
+ }
+
Hooks::run( 'ChangesListSpecialPageStructuredFilters', [ $this ] );
$unstructuredGroupDefinition =
'rcfilters-filter-unregistered-conflicts-user-experience-level'
);
- $changeTypeGroup = $this->getFilterGroup( 'changeType' );
$categoryFilter = $changeTypeGroup->getFilter( 'hidecategorization' );
$logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' );
$pagecreationFilter = $changeTypeGroup->getFilter( 'hidenewpages' );
$significanceTypeGroup = $this->getFilterGroup( 'significance' );
$hideMinorFilter = $significanceTypeGroup->getFilter( 'hideminor' );
- $hideMinorFilter->conflictsWith(
- $categoryFilter,
- 'rcfilters-hideminor-conflicts-typeofchange-global',
- 'rcfilters-hideminor-conflicts-typeofchange',
- 'rcfilters-typeofchange-conflicts-hideminor'
- );
+ // categoryFilter is conditional; see registerFilters
+ if ( $categoryFilter !== null ) {
+ $hideMinorFilter->conflictsWith(
+ $categoryFilter,
+ 'rcfilters-hideminor-conflicts-typeofchange-global',
+ 'rcfilters-hideminor-conflicts-typeofchange',
+ 'rcfilters-typeofchange-conflicts-hideminor'
+ );
+ }
$hideMinorFilter->conflictsWith(
$logactionsFilter,
'rcfilters-hideminor-conflicts-typeofchange-global',
'rcfilters-hideminor-conflicts-typeofchange',
'rcfilters-typeofchange-conflicts-hideminor'
);
+
+ $watchlistGroup = $this->getFilterGroup( 'watchlist' );
+ $watchlistGroup->getFilter( 'watched' )->setAsSupersetOf(
+ $watchlistGroup->getFilter( 'watchednew' )
+ );
+ }
+
+ /**
+ * Transforms filter definition to prepare it for constructor.
+ *
+ * See overrides of this method as well.
+ *
+ * @param array $filterDefinition Original filter definition
+ *
+ * @return array Transformed definition
+ */
+ protected function transformFilterDefinition( array $filterDefinition ) {
+ return $filterDefinition;
}
/**
* Array specifying groups and their filters; see Filter and
* ChangesListFilterGroup constructors.
*
- * There is light processing to simplify core maintenance. See overrides
- * of this method as well.
+ * There is light processing to simplify core maintenance.
*/
protected function registerFiltersFromDefinitions( array $definition ) {
- $priority = -1;
+ $autoFillPriority = -1;
foreach ( $definition as $groupDefinition ) {
- $groupDefinition['priority'] = $priority;
- $priority--;
+ if ( !isset( $groupDefinition['priority'] ) ) {
+ $groupDefinition['priority'] = $autoFillPriority;
+ } else {
+ // If it's explicitly specified, start over the auto-fill
+ $autoFillPriority = $groupDefinition['priority'];
+ }
+
+ $autoFillPriority--;
$className = $groupDefinition['class'];
unset( $groupDefinition['class'] );
+
+ foreach ( $groupDefinition['filters'] as &$filterDefinition ) {
+ $filterDefinition = $this->transformFilterDefinition( $filterDefinition );
+ }
+
$this->registerFilterGroup( new $className( $groupDefinition ) );
}
}
public function getDefaultOptions() {
$config = $this->getConfig();
$opts = new FormOptions();
+ $structuredUI = $this->getUser()->getOption( 'rcenhancedfilters' );
// Add all filters
foreach ( $this->filterGroups as $filterGroup ) {
$opts->add( $filterGroup->getName(), $filterGroup->getDefault() );
} else {
foreach ( $filterGroup->getFilters() as $filter ) {
- $opts->add( $filter->getName(), $filter->getDefault() );
+ $opts->add( $filter->getName(), $filter->getDefault( $structuredUI ) );
}
}
}
- $opts->add( 'namespace', '', FormOptions::INTNULL );
+ $opts->add( 'namespace', '', FormOptions::STRING );
$opts->add( 'invert', false );
$opts->add( 'associated', false );
$query_options, $join_conds, $opts[$filterGroup->getName()] );
} else {
foreach ( $filterGroup->getFilters() as $filter ) {
- if ( $opts[$filter->getName()] && $filter->isAllowed( $this ) ) {
+ if ( $opts[$filter->getName()] ) {
$filter->modifyQuery( $dbr, $this, $tables, $fields, $conds,
$query_options, $join_conds );
}
}
// Namespace filtering
- if ( $opts['namespace'] !== '' ) {
- $selectedNS = $dbr->addQuotes( $opts['namespace'] );
- $operator = $opts['invert'] ? '!=' : '=';
- $boolean = $opts['invert'] ? 'AND' : 'OR';
-
- // Namespace association (T4429)
- if ( !$opts['associated'] ) {
- $condition = "rc_namespace $operator $selectedNS";
- } else {
- // Also add the associated namespace
- $associatedNS = $dbr->addQuotes(
- MWNamespace::getAssociated( $opts['namespace'] )
+ if ( $opts[ 'namespace' ] !== '' ) {
+ $namespaces = explode( ',', $opts[ 'namespace' ] );
+
+ if ( $opts[ 'associated' ] ) {
+ $associatedNamespaces = array_map(
+ function ( $ns ) {
+ return MWNamespace::getAssociated( $ns );
+ },
+ $namespaces
);
- $condition = "(rc_namespace $operator $selectedNS "
- . $boolean
- . " rc_namespace $operator $associatedNS)";
+ $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
}
- $conds[] = $condition;
+ if ( count( $namespaces ) === 1 ) {
+ $operator = $opts[ 'invert' ] ? '!=' : '=';
+ $value = $dbr->addQuotes( reset( $namespaces ) );
+ } else {
+ $operator = $opts[ 'invert' ] ? 'NOT IN' : 'IN';
+ sort( $namespaces );
+ $value = '(' . $dbr->makeList( $namespaces ) . ')';
+ }
+ $conds[] = "rc_namespace $operator $value";
}
}
* @param array $selectedExpLevels The allowed active values, sorted
*/
public function filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr,
- &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels ) {
+ &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0 ) {
global $wgLearnerEdits,
- $wgExperiencedUserEdits,
- $wgLearnerMemberSince,
- $wgExperiencedUserMemberSince;
+ $wgExperiencedUserEdits,
+ $wgLearnerMemberSince,
+ $wgExperiencedUserMemberSince;
$LEVEL_COUNT = 3;
$tables[] = 'user';
$join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ];
- $now = time();
+ if ( $now === 0 ) {
+ $now = time();
+ }
$secondsPerDay = 86400;
$learnerCutoff = $now - $wgLearnerMemberSince * $secondsPerDay;
$experiencedUserCutoff = $now - $wgExperiencedUserMemberSince * $secondsPerDay;
);
if ( $selectedExpLevels === [ 'newcomer' ] ) {
- $conds[] = "NOT ( $aboveNewcomer )";
+ $conds[] = "NOT ( $aboveNewcomer )";
} elseif ( $selectedExpLevels === [ 'learner' ] ) {
$conds[] = $dbr->makeList(
[ $aboveNewcomer, "NOT ( $aboveLearner )" ],