110 parent::__construct(
$name, $restriction );
112 $nonRevisionTypes = [
RC_LOG ];
113 Hooks::run(
'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
115 $this->filterGroupDefinitions = [
117 'name' =>
'registration',
118 'title' =>
'rcfilters-filtergroup-registration',
125 'showHideSuffix' =>
'showhideliu',
127 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
128 &$query_options, &$join_conds
131 $actorQuery = $actorMigration->getJoin(
'rc_user' );
132 $tables += $actorQuery[
'tables'];
133 $join_conds += $actorQuery[
'joins'];
134 $conds[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
136 'isReplacedInStructuredUi' =>
true,
140 'name' =>
'hideanons',
143 'showHideSuffix' =>
'showhideanons',
145 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
146 &$query_options, &$join_conds
149 $actorQuery = $actorMigration->getJoin(
'rc_user' );
150 $tables += $actorQuery[
'tables'];
151 $join_conds += $actorQuery[
'joins'];
152 $conds[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
154 'isReplacedInStructuredUi' =>
true,
160 'name' =>
'userExpLevel',
161 'title' =>
'rcfilters-filtergroup-userExpLevel',
163 'isFullCoverage' =>
true,
166 'name' =>
'unregistered',
167 'label' =>
'rcfilters-filter-user-experience-level-unregistered-label',
168 'description' =>
'rcfilters-filter-user-experience-level-unregistered-description',
169 'cssClassSuffix' =>
'user-unregistered',
170 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
171 return !$rc->getAttribute(
'rc_user' );
175 'name' =>
'registered',
176 'label' =>
'rcfilters-filter-user-experience-level-registered-label',
177 'description' =>
'rcfilters-filter-user-experience-level-registered-description',
178 'cssClassSuffix' =>
'user-registered',
179 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
180 return $rc->getAttribute(
'rc_user' );
184 'name' =>
'newcomer',
185 'label' =>
'rcfilters-filter-user-experience-level-newcomer-label',
186 'description' =>
'rcfilters-filter-user-experience-level-newcomer-description',
187 'cssClassSuffix' =>
'user-newcomer',
188 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
189 $performer = $rc->getPerformer();
190 return $performer && $performer->isLoggedIn() &&
191 $performer->getExperienceLevel() ===
'newcomer';
196 'label' =>
'rcfilters-filter-user-experience-level-learner-label',
197 'description' =>
'rcfilters-filter-user-experience-level-learner-description',
198 'cssClassSuffix' =>
'user-learner',
199 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
200 $performer = $rc->getPerformer();
201 return $performer && $performer->isLoggedIn() &&
202 $performer->getExperienceLevel() ===
'learner';
206 'name' =>
'experienced',
207 'label' =>
'rcfilters-filter-user-experience-level-experienced-label',
208 'description' =>
'rcfilters-filter-user-experience-level-experienced-description',
209 'cssClassSuffix' =>
'user-experienced',
210 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
211 $performer = $rc->getPerformer();
212 return $performer && $performer->isLoggedIn() &&
213 $performer->getExperienceLevel() ===
'experienced';
218 'queryCallable' => [ $this,
'filterOnUserExperienceLevel' ],
222 'name' =>
'authorship',
223 'title' =>
'rcfilters-filtergroup-authorship',
227 'name' =>
'hidemyself',
228 'label' =>
'rcfilters-filter-editsbyself-label',
229 'description' =>
'rcfilters-filter-editsbyself-description',
232 'showHideSuffix' =>
'showhidemine',
234 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
235 &$query_options, &$join_conds
238 $tables += $actorQuery[
'tables'];
239 $join_conds += $actorQuery[
'joins'];
240 $conds[] =
'NOT(' . $actorQuery[
'conds'] .
')';
242 'cssClassSuffix' =>
'self',
243 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
244 return $ctx->getUser()->equals( $rc->getPerformer() );
248 'name' =>
'hidebyothers',
249 'label' =>
'rcfilters-filter-editsbyother-label',
250 'description' =>
'rcfilters-filter-editsbyother-description',
252 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
253 &$query_options, &$join_conds
256 ->getWhere(
$dbr,
'rc_user', $ctx->getUser(),
false );
257 $tables += $actorQuery[
'tables'];
258 $join_conds += $actorQuery[
'joins'];
259 $conds[] = $actorQuery[
'conds'];
261 'cssClassSuffix' =>
'others',
262 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
263 return !$ctx->getUser()->equals( $rc->getPerformer() );
270 'name' =>
'automated',
271 'title' =>
'rcfilters-filtergroup-automated',
275 'name' =>
'hidebots',
276 'label' =>
'rcfilters-filter-bots-label',
277 'description' =>
'rcfilters-filter-bots-description',
280 'showHideSuffix' =>
'showhidebots',
282 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
283 &$query_options, &$join_conds
285 $conds[
'rc_bot'] = 0;
287 'cssClassSuffix' =>
'bot',
288 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
289 return $rc->getAttribute(
'rc_bot' );
293 'name' =>
'hidehumans',
294 'label' =>
'rcfilters-filter-humans-label',
295 'description' =>
'rcfilters-filter-humans-description',
297 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
298 &$query_options, &$join_conds
300 $conds[
'rc_bot'] = 1;
302 'cssClassSuffix' =>
'human',
303 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
304 return !$rc->getAttribute(
'rc_bot' );
313 'name' =>
'significance',
314 'title' =>
'rcfilters-filtergroup-significance',
319 'name' =>
'hideminor',
320 'label' =>
'rcfilters-filter-minor-label',
321 'description' =>
'rcfilters-filter-minor-description',
324 'showHideSuffix' =>
'showhideminor',
326 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
327 &$query_options, &$join_conds
329 $conds[] =
'rc_minor = 0';
331 'cssClassSuffix' =>
'minor',
332 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
333 return $rc->getAttribute(
'rc_minor' );
337 'name' =>
'hidemajor',
338 'label' =>
'rcfilters-filter-major-label',
339 'description' =>
'rcfilters-filter-major-description',
341 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
342 &$query_options, &$join_conds
344 $conds[] =
'rc_minor = 1';
346 'cssClassSuffix' =>
'major',
347 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
348 return !$rc->getAttribute(
'rc_minor' );
355 'name' =>
'lastRevision',
356 'title' =>
'rcfilters-filtergroup-lastRevision',
361 'name' =>
'hidelastrevision',
362 'label' =>
'rcfilters-filter-lastrevision-label',
363 'description' =>
'rcfilters-filter-lastrevision-description',
365 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
366 &$query_options, &$join_conds )
use ( $nonRevisionTypes ) {
367 $conds[] =
$dbr->makeList(
369 'rc_this_oldid <> page_latest',
370 'rc_type' => $nonRevisionTypes,
375 'cssClassSuffix' =>
'last',
376 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
377 return $rc->getAttribute(
'rc_this_oldid' ) === $rc->getAttribute(
'page_latest' );
381 'name' =>
'hidepreviousrevisions',
382 'label' =>
'rcfilters-filter-previousrevision-label',
383 'description' =>
'rcfilters-filter-previousrevision-description',
385 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
386 &$query_options, &$join_conds )
use ( $nonRevisionTypes ) {
387 $conds[] =
$dbr->makeList(
389 'rc_this_oldid = page_latest',
390 'rc_type' => $nonRevisionTypes,
395 'cssClassSuffix' =>
'previous',
396 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
397 return $rc->getAttribute(
'rc_this_oldid' ) !== $rc->getAttribute(
'page_latest' );
405 'name' =>
'changeType',
406 'title' =>
'rcfilters-filtergroup-changetype',
411 'name' =>
'hidepageedits',
412 'label' =>
'rcfilters-filter-pageedits-label',
413 'description' =>
'rcfilters-filter-pageedits-description',
416 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
417 &$query_options, &$join_conds
419 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_EDIT );
421 'cssClassSuffix' =>
'src-mw-edit',
422 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
427 'name' =>
'hidenewpages',
428 'label' =>
'rcfilters-filter-newpages-label',
429 'description' =>
'rcfilters-filter-newpages-description',
432 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
433 &$query_options, &$join_conds
435 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_NEW );
437 'cssClassSuffix' =>
'src-mw-new',
438 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
447 'label' =>
'rcfilters-filter-logactions-label',
448 'description' =>
'rcfilters-filter-logactions-description',
451 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
452 &$query_options, &$join_conds
454 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_LOG );
456 'cssClassSuffix' =>
'src-mw-log',
457 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
466 $this->legacyReviewStatusFilterGroupDefinition = [
468 'name' =>
'legacyReviewStatus',
469 'title' =>
'rcfilters-filtergroup-reviewstatus',
473 'name' =>
'hidepatrolled',
476 'showHideSuffix' =>
'showhidepatr',
478 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
479 &$query_options, &$join_conds
483 'isReplacedInStructuredUi' =>
true,
486 'name' =>
'hideunpatrolled',
488 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
489 &$query_options, &$join_conds
493 'isReplacedInStructuredUi' =>
true,
499 $this->reviewStatusFilterGroupDefinition = [
501 'name' =>
'reviewStatus',
502 'title' =>
'rcfilters-filtergroup-reviewstatus',
504 'isFullCoverage' =>
true,
508 'name' =>
'unpatrolled',
509 'label' =>
'rcfilters-filter-reviewstatus-unpatrolled-label',
510 'description' =>
'rcfilters-filter-reviewstatus-unpatrolled-description',
511 'cssClassSuffix' =>
'reviewstatus-unpatrolled',
512 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
518 'label' =>
'rcfilters-filter-reviewstatus-manual-label',
519 'description' =>
'rcfilters-filter-reviewstatus-manual-description',
520 'cssClassSuffix' =>
'reviewstatus-manual',
521 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
527 'label' =>
'rcfilters-filter-reviewstatus-auto-label',
528 'description' =>
'rcfilters-filter-reviewstatus-auto-description',
529 'cssClassSuffix' =>
'reviewstatus-auto',
530 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
536 'queryCallable' =>
function ( $specialPageClassName, $ctx,
$dbr,
537 &
$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
539 if ( $selected === [] ) {
542 $rcPatrolledValues = [
548 $conds[
'rc_patrolled'] = array_map(
function (
$s )
use ( $rcPatrolledValues ) {
549 return $rcPatrolledValues[
$s ];
555 $this->hideCategorizationFilterDefinition = [
556 'name' =>
'hidecategorization',
557 'label' =>
'rcfilters-filter-categorization-label',
558 'description' =>
'rcfilters-filter-categorization-description',
561 'showHideSuffix' =>
'showhidecategorization',
564 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
565 &$query_options, &$join_conds
569 'cssClassSuffix' =>
'src-mw-categorize',
570 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
585 if ( $group->getConflictingGroups() ) {
588 " specifies conflicts with other groups but these are not supported yet."
593 foreach ( $group->getConflictingFilters()
as $conflictingFilter ) {
594 if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
600 foreach ( $group->getFilters()
as $filter ) {
602 foreach (
$filter->getConflictingFilters()
as $conflictingFilter ) {
604 $conflictingFilter->activelyInConflictWithFilter(
$filter, $opts ) &&
605 $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
622 $this->rcSubpage = $subpage;
629 if (
$rows ===
false ) {
634 if ( $this->
getRequest()->getVal(
'action' ) ===
'render' ) {
635 $this->
getOutput()->setArticleBodyOnly(
true );
640 if ( $this->
getRequest()->getBool(
'peek' ) ) {
644 if ( $this->
getUser()->isAnon() !==
647 $this->
getOutput()->setStatusCode( 205 );
657 $batch->add( $row->rc_namespace, $row->rc_title );
660 foreach ( $formatter->getPreloadTitles()
as $title ) {
680 $this->
getOutput()->setStatusCode( 500 );
685 if ( $this->
getConfig()->
get(
'EnableWANCacheReaper' ) ) {
689 LoggerFactory::getInstance(
'objectcache' )
708 $knownParams = $this->
getRequest()->getValues(
709 ...array_keys( $this->
getOptions()->getAllValues() )
715 $excludedParams = [
'limit' =>
'',
'days' =>
'',
'enhanced' =>
'',
'from' =>
'' ];
716 $knownParams = array_diff_key( $knownParams, $excludedParams );
722 count( $knownParams ) === 0
726 $this->
getUser()->getOption( static::$savedQueriesPreferenceName ),
730 if ( $savedQueries && isset( $savedQueries[
'default' ] ) ) {
733 if ( isset( $savedQueries[
'version' ] ) && $savedQueries[
'version' ] ===
'2' ) {
734 $savedQueryDefaultID = $savedQueries[
'default' ];
735 $defaultQuery = $savedQueries[
'queries' ][ $savedQueryDefaultID ][
'data' ];
739 $defaultQuery[
'params' ],
740 $defaultQuery[
'highlights' ],
748 unset(
$query[
'title' ] );
756 'wgStructuredChangeFiltersDefaultSavedQueryExists',
762 $this->
getOutput()->addBodyClasses(
'mw-rcfilters-ui-loading' );
779 foreach ( $jsData[
'messageKeys']
as $key ) {
783 $out->addBodyClasses(
'mw-rcfilters-enabled' );
784 $collapsed = $this->
getUser()->getBoolOption( static::$collapsedPreferenceName );
786 $out->addBodyClasses(
'mw-rcfilters-collapsed' );
790 $out->addJsConfigVars(
'wgStructuredChangeFilters', $jsData[
'groups'] );
791 $out->addJsConfigVars(
'wgStructuredChangeFiltersMessages',
$messages );
792 $out->addJsConfigVars(
'wgStructuredChangeFiltersCollapsedState', $collapsed );
794 $out->addJsConfigVars(
795 'StructuredChangeFiltersDisplayConfig',
797 'maxDays' => (
int)$this->
getConfig()->
get(
'RCMaxAge' ) / ( 24 * 3600 ),
798 'limitArray' => $this->
getConfig()->
get(
'RCLinkLimits' ),
800 'daysArray' => $this->
getConfig()->
get(
'RCLinkDays' ),
805 $out->addJsConfigVars(
806 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
807 static::$savedQueriesPreferenceName
809 $out->addJsConfigVars(
810 'wgStructuredChangeFiltersLimitPreferenceName',
811 static::$limitPreferenceName
813 $out->addJsConfigVars(
814 'wgStructuredChangeFiltersDaysPreferenceName',
815 static::$daysPreferenceName
817 $out->addJsConfigVars(
818 'wgStructuredChangeFiltersCollapsedPreferenceName',
819 static::$collapsedPreferenceName
822 $out->addBodyClasses(
'mw-rcfilters-disabled' );
835 'StructuredChangeFiltersEditWatchlistUrl' =>
847 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
848 return $cache->getWithSetCallback(
849 $cache->makeKey(
'changeslistspecialpage-changetags',
$context->getLanguage() ),
850 $cache::TTL_MINUTE * 10,
856 $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
867 foreach ( $tagHitCounts
as $tagName => $hits ) {
871 isset( $explicitlyDefinedTags[ $tagName ] ) ||
872 isset( $softwareActivatedTags[ $tagName ] )
879 'label' => Sanitizer::stripAllTags(
885 self::TAG_DESC_CHARACTER_LIMIT,
888 'cssClass' => Sanitizer::escapeClass(
'mw-tag-' . $tagName ),
895 usort(
$result,
function ( $a, $b ) {
896 return strcasecmp( $a[
'label'], $b[
'label'] );
912 '<div class="mw-changeslist-empty">' .
913 $this->
msg(
'recentchanges-noresult' )->parse() .
923 '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
924 $this->
msg(
'recentchanges-timeout' )->parse() .
942 $this->
buildQuery(
$tables, $fields, $conds, $query_options, $join_conds, $opts );
944 return $this->
doMainQuery(
$tables, $fields, $conds, $query_options, $join_conds, $opts );
953 if ( $this->rcOptions ===
null ) {
954 $this->rcOptions = $this->
setup( $this->rcSubpage );
982 if ( $this->
getConfig()->
get(
'RCWatchCategoryMembership' ) ) {
984 $this->hideCategorizationFilterDefinition
987 $transformedHideCategorizationDef[
'group'] = $changeTypeGroup;
990 $transformedHideCategorizationDef
994 Hooks::run(
'ChangesListSpecialPageStructuredFilters', [ $this ] );
999 $registered = $userExperienceLevel->getFilter(
'registered' );
1000 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'newcomer' ) );
1001 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'learner' ) );
1002 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'experienced' ) );
1004 $categoryFilter = $changeTypeGroup->getFilter(
'hidecategorization' );
1005 $logactionsFilter = $changeTypeGroup->getFilter(
'hidelog' );
1006 $pagecreationFilter = $changeTypeGroup->getFilter(
'hidenewpages' );
1008 $significanceTypeGroup = $this->
getFilterGroup(
'significance' );
1009 $hideMinorFilter = $significanceTypeGroup->getFilter(
'hideminor' );
1012 if ( $categoryFilter !==
null ) {
1013 $hideMinorFilter->conflictsWith(
1015 'rcfilters-hideminor-conflicts-typeofchange-global',
1016 'rcfilters-hideminor-conflicts-typeofchange',
1017 'rcfilters-typeofchange-conflicts-hideminor'
1020 $hideMinorFilter->conflictsWith(
1022 'rcfilters-hideminor-conflicts-typeofchange-global',
1023 'rcfilters-hideminor-conflicts-typeofchange',
1024 'rcfilters-typeofchange-conflicts-hideminor'
1026 $hideMinorFilter->conflictsWith(
1027 $pagecreationFilter,
1028 'rcfilters-hideminor-conflicts-typeofchange-global',
1029 'rcfilters-hideminor-conflicts-typeofchange',
1030 'rcfilters-typeofchange-conflicts-hideminor'
1044 return $filterDefinition;
1058 $autoFillPriority = -1;
1059 foreach ( $definition
as $groupDefinition ) {
1060 if ( !isset( $groupDefinition[
'priority'] ) ) {
1061 $groupDefinition[
'priority'] = $autoFillPriority;
1064 $autoFillPriority = $groupDefinition[
'priority'];
1067 $autoFillPriority--;
1069 $className = $groupDefinition[
'class'];
1070 unset( $groupDefinition[
'class'] );
1072 foreach ( $groupDefinition[
'filters']
as &$filterDefinition ) {
1085 foreach ( $this->filterGroups
as $group ) {
1087 foreach ( $group->getFilters()
as $key =>
$filter ) {
1088 if (
$filter->displaysOnUnstructuredUi( $this ) ) {
1113 if ( $parameters !==
null ) {
1135 $useDefaults = $this->
getRequest()->getInt(
'urlversion' ) !== 2;
1138 foreach ( $this->filterGroups
as $filterGroup ) {
1139 $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1143 $opts->add(
'invert',
false );
1144 $opts->add(
'associated',
false );
1145 $opts->add(
'urlversion', 1 );
1146 $opts->add(
'tagfilter',
'' );
1151 $opts->add(
'from',
'' );
1162 $groupName = $group->
getName();
1164 $this->filterGroups[$groupName] = $group;
1184 return $this->filterGroups[$groupName] ??
null;
1202 'messageKeys' => [],
1205 usort( $this->filterGroups,
function ( $a, $b ) {
1206 return $b->getPriority() <=> $a->getPriority();
1209 foreach ( $this->filterGroups
as $groupName => $group ) {
1210 $groupOutput = $group->getJsData( $this );
1211 if ( $groupOutput !==
null ) {
1212 $output[
'messageKeys'] = array_merge(
1214 $groupOutput[
'messageKeys']
1217 unset( $groupOutput[
'messageKeys'] );
1218 $output[
'groups'][] = $groupOutput;
1234 $opts->fetchValuesFromRequest( $this->
getRequest() );
1246 $stringParameterNameSet = [];
1247 $hideParameterNameSet = [];
1252 foreach ( $this->filterGroups
as $filterGroup ) {
1254 $stringParameterNameSet[$filterGroup->getName()] =
true;
1256 foreach ( $filterGroup->getFilters()
as $filter ) {
1257 $hideParameterNameSet[
$filter->getName()] =
true;
1262 $bits = preg_split(
'/\s*,\s*/', trim( $par ) );
1263 foreach ( $bits
as $bit ) {
1265 if ( isset( $hideParameterNameSet[$bit] ) ) {
1268 } elseif ( isset( $hideParameterNameSet[
"hide$bit"] ) ) {
1270 $opts[
"hide$bit"] =
false;
1271 } elseif ( preg_match(
'/^(.*)=(.*)$/', $bit, $m ) ) {
1272 if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1273 $opts[$m[1]] = $m[2];
1288 if ( $isContradictory || $isReplaced ) {
1306 foreach ( $this->filterGroups
as $filterGroup ) {
1308 $filters = $filterGroup->getFilters();
1310 if (
count( $filters ) === 1 ) {
1315 $allInGroupEnabled = array_reduce(
1318 return $carry && $opts[
$filter->getName() ];
1320 count( $filters ) > 0
1323 if ( $allInGroupEnabled ) {
1325 $opts[
$filter->getName() ] =
false;
1346 if ( $opts[
'hideanons'] && $opts[
'hideliu'] ) {
1347 $opts->
reset(
'hideanons' );
1348 if ( !$opts[
'hidebots'] ) {
1349 $opts->
reset(
'hideliu' );
1350 $opts[
'hidehumans'] = 1;
1374 if ( $opts[
'hideanons' ] ) {
1375 $opts->
reset(
'hideanons' );
1376 $opts[
'userExpLevel' ] =
'registered';
1380 if ( $opts[
'hideliu' ] ) {
1381 $opts->
reset(
'hideliu' );
1382 $opts[
'userExpLevel' ] =
'unregistered';
1387 if ( $opts[
'hidepatrolled' ] ) {
1388 $opts->
reset(
'hidepatrolled' );
1389 $opts[
'reviewStatus' ] =
'unpatrolled';
1393 if ( $opts[
'hideunpatrolled' ] ) {
1394 $opts->
reset(
'hideunpatrolled' );
1395 $opts[
'reviewStatus' ] = implode(
1397 [
'manual',
'auto' ]
1416 if (
$value ===
false ) {
1442 foreach ( $this->filterGroups
as $filterGroup ) {
1443 $filterGroup->modifyQuery(
$dbr, $this,
$tables, $fields, $conds,
1444 $query_options, $join_conds, $opts, $isStructuredUI );
1448 if ( $opts[
'namespace' ] !==
'' ) {
1449 $namespaces = explode(
';', $opts[
'namespace' ] );
1451 if ( $opts[
'associated' ] ) {
1452 $associatedNamespaces = array_map(
1462 $operator = $opts[
'invert' ] ?
'!=' :
'=';
1465 $operator = $opts[
'invert' ] ?
'NOT IN' :
'IN';
1469 $conds[] =
"rc_namespace $operator $value";
1473 $cutoff_unixtime = time() - $opts[
'days'] * 3600 * 24;
1474 $cutoff =
$dbr->timestamp( $cutoff_unixtime );
1476 $fromValid = preg_match(
'/^[0-9]{14}$/', $opts[
'from'] );
1477 if ( $fromValid && $opts[
'from'] >
wfTimestamp( TS_MW, $cutoff ) ) {
1478 $cutoff =
$dbr->timestamp( $opts[
'from'] );
1480 $opts->
reset(
'from' );
1483 $conds[] =
'rc_timestamp >= ' .
$dbr->addQuotes( $cutoff );
1502 $fields = array_merge( $rcQuery[
'fields'], $fields );
1503 $join_conds = array_merge( $join_conds, $rcQuery[
'joins'] );
1522 return $dbr->select(
1533 &$query_options, &$join_conds, $opts
1536 'ChangesListSpecialPageQuery',
1537 [ $this->
getName(), &
$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ]
1559 $this->
doHeader( $opts, $rowCount );
1647 # The legend showing what the letters and stuff mean
1648 $legend = Html::openElement(
'dl' ) .
"\n";
1649 # Iterates through them and gets the messages for both letter and tooltip
1650 $legendItems =
$context->getConfig()->get(
'RecentChangesFlags' );
1651 if ( !(
$user->useRCPatrol() ||
$user->useNPPatrol() ) ) {
1652 unset( $legendItems[
'unpatrolled'] );
1654 foreach ( $legendItems
as $key => $item ) { # generate items
of the legend
1655 $label = $item[
'legend'] ?? $item[
'title'];
1656 $letter = $item[
'letter'];
1657 $cssClass = $item[
'class'] ?? $key;
1659 $legend .= Html::element(
'dt',
1660 [
'class' => $cssClass ],
$context->msg( $letter )->text()
1662 Html::rawElement(
'dd',
1663 [
'class' => Sanitizer::escapeClass(
'mw-changeslist-legend-' . $key ) ],
1668 $legend .= Html::rawElement(
'dt',
1669 [
'class' =>
'mw-plusminus-pos' ],
1670 $context->msg(
'recentchanges-legend-plusminus' )->parse()
1672 $legend .= Html::element(
1674 [
'class' =>
'mw-changeslist-legend-plusminus' ],
1675 $context->msg(
'recentchanges-label-plusminus' )->text()
1677 $legend .= Html::closeElement(
'dl' ) .
"\n";
1680 $context->msg(
'rcfilters-legend-heading' )->parse() :
1681 $context->msg(
'recentchanges-legend-heading' )->parse();
1684 $collapsedState = $this->
getRequest()->getCookie(
'changeslist-state' );
1685 $collapsedClass = $collapsedState ===
'collapsed' ?
' mw-collapsed' :
'';
1688 '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass .
'">' .
1690 '<div class="mw-collapsible-content">' . $legend .
'</div>' .
1702 $out->addModuleStyles( [
1703 'mediawiki.interface.helpers.styles',
1704 'mediawiki.special.changeslist.legend',
1705 'mediawiki.special.changeslist',
1707 $out->addModules(
'mediawiki.special.changeslist.legend.js' );
1710 $out->addModules(
'mediawiki.rcfilters.filters.ui' );
1711 $out->addModuleStyles(
'mediawiki.rcfilters.filters.base.styles' );
1736 &
$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1746 if (
count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1752 in_array(
'registered', $selectedExpLevels ) &&
1753 in_array(
'unregistered', $selectedExpLevels )
1759 $actorQuery = $actorMigration->getJoin(
'rc_user' );
1760 $tables += $actorQuery[
'tables'];
1761 $join_conds += $actorQuery[
'joins'];
1765 in_array(
'registered', $selectedExpLevels ) &&
1766 !in_array(
'unregistered', $selectedExpLevels )
1768 $conds[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
1772 if ( $selectedExpLevels === [
'unregistered' ] ) {
1773 $conds[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
1778 $join_conds[
'user'] = [
'LEFT JOIN', $actorQuery[
'fields'][
'rc_user'] .
' = user_id' ];
1783 $secondsPerDay = 86400;
1787 $aboveNewcomer =
$dbr->makeList(
1790 'user_registration <= ' .
$dbr->addQuotes(
$dbr->timestamp( $learnerCutoff ) ),
1795 $aboveLearner =
$dbr->makeList(
1798 'user_registration <= ' .
1799 $dbr->addQuotes(
$dbr->timestamp( $experiencedUserCutoff ) ),
1806 if ( in_array(
'unregistered', $selectedExpLevels ) ) {
1807 $selectedExpLevels = array_diff( $selectedExpLevels, [
'unregistered' ] );
1808 $conditions[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
1811 if ( $selectedExpLevels === [
'newcomer' ] ) {
1812 $conditions[] =
"NOT ( $aboveNewcomer )";
1813 } elseif ( $selectedExpLevels === [
'learner' ] ) {
1814 $conditions[] =
$dbr->makeList(
1815 [ $aboveNewcomer,
"NOT ( $aboveLearner )" ],
1818 } elseif ( $selectedExpLevels === [
'experienced' ] ) {
1819 $conditions[] = $aboveLearner;
1820 } elseif ( $selectedExpLevels === [
'learner',
'newcomer' ] ) {
1821 $conditions[] =
"NOT ( $aboveLearner )";
1822 } elseif ( $selectedExpLevels === [
'experienced',
'newcomer' ] ) {
1823 $conditions[] =
$dbr->makeList(
1824 [
"NOT ( $aboveNewcomer )", $aboveLearner ],
1827 } elseif ( $selectedExpLevels === [
'experienced',
'learner' ] ) {
1828 $conditions[] = $aboveNewcomer;
1829 } elseif ( $selectedExpLevels === [
'experienced',
'learner',
'newcomer' ] ) {
1830 $conditions[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
1833 if (
count( $conditions ) > 1 ) {
1835 } elseif (
count( $conditions ) === 1 ) {
1836 $conds[] = reset( $conditions );
1846 if ( $this->
getRequest()->getBool(
'rcfilters' ) ) {
1850 return static::checkStructuredFilterUiEnabled(
1865 return !
$user->getOption(
'rcenhancedfilters-disable' );
1876 return $this->
getUser()->getIntOption( static::$limitPreferenceName );
1888 return floatval( $this->
getUser()->getOption( static::$daysPreferenceName ) );