109 parent::__construct(
$name, $restriction );
111 $nonRevisionTypes = [
RC_LOG ];
112 Hooks::run(
'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
114 $this->filterGroupDefinitions = [
116 'name' =>
'registration',
117 'title' =>
'rcfilters-filtergroup-registration',
124 'showHideSuffix' =>
'showhideliu',
126 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
127 &$query_options, &$join_conds
130 $actorQuery = $actorMigration->getJoin(
'rc_user' );
131 $tables += $actorQuery[
'tables'];
132 $join_conds += $actorQuery[
'joins'];
133 $conds[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
135 'isReplacedInStructuredUi' =>
true,
139 'name' =>
'hideanons',
142 'showHideSuffix' =>
'showhideanons',
144 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
145 &$query_options, &$join_conds
148 $actorQuery = $actorMigration->getJoin(
'rc_user' );
149 $tables += $actorQuery[
'tables'];
150 $join_conds += $actorQuery[
'joins'];
151 $conds[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
153 'isReplacedInStructuredUi' =>
true,
159 'name' =>
'userExpLevel',
160 'title' =>
'rcfilters-filtergroup-userExpLevel',
162 'isFullCoverage' =>
true,
165 'name' =>
'unregistered',
166 'label' =>
'rcfilters-filter-user-experience-level-unregistered-label',
167 'description' =>
'rcfilters-filter-user-experience-level-unregistered-description',
168 'cssClassSuffix' =>
'user-unregistered',
169 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
170 return !$rc->getAttribute(
'rc_user' );
174 'name' =>
'registered',
175 'label' =>
'rcfilters-filter-user-experience-level-registered-label',
176 'description' =>
'rcfilters-filter-user-experience-level-registered-description',
177 'cssClassSuffix' =>
'user-registered',
178 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
179 return $rc->getAttribute(
'rc_user' );
183 'name' =>
'newcomer',
184 'label' =>
'rcfilters-filter-user-experience-level-newcomer-label',
185 'description' =>
'rcfilters-filter-user-experience-level-newcomer-description',
186 'cssClassSuffix' =>
'user-newcomer',
187 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
188 $performer = $rc->getPerformer();
189 return $performer && $performer->isLoggedIn() &&
190 $performer->getExperienceLevel() ===
'newcomer';
195 'label' =>
'rcfilters-filter-user-experience-level-learner-label',
196 'description' =>
'rcfilters-filter-user-experience-level-learner-description',
197 'cssClassSuffix' =>
'user-learner',
198 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
199 $performer = $rc->getPerformer();
200 return $performer && $performer->isLoggedIn() &&
201 $performer->getExperienceLevel() ===
'learner';
205 'name' =>
'experienced',
206 'label' =>
'rcfilters-filter-user-experience-level-experienced-label',
207 'description' =>
'rcfilters-filter-user-experience-level-experienced-description',
208 'cssClassSuffix' =>
'user-experienced',
209 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
210 $performer = $rc->getPerformer();
211 return $performer && $performer->isLoggedIn() &&
212 $performer->getExperienceLevel() ===
'experienced';
217 'queryCallable' => [ $this,
'filterOnUserExperienceLevel' ],
221 'name' =>
'authorship',
222 'title' =>
'rcfilters-filtergroup-authorship',
226 'name' =>
'hidemyself',
227 'label' =>
'rcfilters-filter-editsbyself-label',
228 'description' =>
'rcfilters-filter-editsbyself-description',
231 'showHideSuffix' =>
'showhidemine',
233 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
234 &$query_options, &$join_conds
237 $tables += $actorQuery[
'tables'];
238 $join_conds += $actorQuery[
'joins'];
239 $conds[] =
'NOT(' . $actorQuery[
'conds'] .
')';
241 'cssClassSuffix' =>
'self',
242 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
243 return $ctx->getUser()->equals( $rc->getPerformer() );
247 'name' =>
'hidebyothers',
248 'label' =>
'rcfilters-filter-editsbyother-label',
249 'description' =>
'rcfilters-filter-editsbyother-description',
251 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
252 &$query_options, &$join_conds
255 ->getWhere(
$dbr,
'rc_user', $ctx->getUser(),
false );
256 $tables += $actorQuery[
'tables'];
257 $join_conds += $actorQuery[
'joins'];
258 $conds[] = $actorQuery[
'conds'];
260 'cssClassSuffix' =>
'others',
261 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
262 return !$ctx->getUser()->equals( $rc->getPerformer() );
269 'name' =>
'automated',
270 'title' =>
'rcfilters-filtergroup-automated',
274 'name' =>
'hidebots',
275 'label' =>
'rcfilters-filter-bots-label',
276 'description' =>
'rcfilters-filter-bots-description',
279 'showHideSuffix' =>
'showhidebots',
281 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
282 &$query_options, &$join_conds
284 $conds[
'rc_bot'] = 0;
286 'cssClassSuffix' =>
'bot',
287 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
288 return $rc->getAttribute(
'rc_bot' );
292 'name' =>
'hidehumans',
293 'label' =>
'rcfilters-filter-humans-label',
294 'description' =>
'rcfilters-filter-humans-description',
296 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
297 &$query_options, &$join_conds
299 $conds[
'rc_bot'] = 1;
301 'cssClassSuffix' =>
'human',
302 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
303 return !$rc->getAttribute(
'rc_bot' );
312 'name' =>
'significance',
313 'title' =>
'rcfilters-filtergroup-significance',
318 'name' =>
'hideminor',
319 'label' =>
'rcfilters-filter-minor-label',
320 'description' =>
'rcfilters-filter-minor-description',
323 'showHideSuffix' =>
'showhideminor',
325 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
326 &$query_options, &$join_conds
328 $conds[] =
'rc_minor = 0';
330 'cssClassSuffix' =>
'minor',
331 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
332 return $rc->getAttribute(
'rc_minor' );
336 'name' =>
'hidemajor',
337 'label' =>
'rcfilters-filter-major-label',
338 'description' =>
'rcfilters-filter-major-description',
340 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
341 &$query_options, &$join_conds
343 $conds[] =
'rc_minor = 1';
345 'cssClassSuffix' =>
'major',
346 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
347 return !$rc->getAttribute(
'rc_minor' );
354 'name' =>
'lastRevision',
355 'title' =>
'rcfilters-filtergroup-lastRevision',
360 'name' =>
'hidelastrevision',
361 'label' =>
'rcfilters-filter-lastrevision-label',
362 'description' =>
'rcfilters-filter-lastrevision-description',
364 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
365 &$query_options, &$join_conds )
use ( $nonRevisionTypes ) {
366 $conds[] =
$dbr->makeList(
368 'rc_this_oldid <> page_latest',
369 'rc_type' => $nonRevisionTypes,
374 'cssClassSuffix' =>
'last',
375 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
376 return $rc->getAttribute(
'rc_this_oldid' ) === $rc->getAttribute(
'page_latest' );
380 'name' =>
'hidepreviousrevisions',
381 'label' =>
'rcfilters-filter-previousrevision-label',
382 'description' =>
'rcfilters-filter-previousrevision-description',
384 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
385 &$query_options, &$join_conds )
use ( $nonRevisionTypes ) {
386 $conds[] =
$dbr->makeList(
388 'rc_this_oldid = page_latest',
389 'rc_type' => $nonRevisionTypes,
394 'cssClassSuffix' =>
'previous',
395 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
396 return $rc->getAttribute(
'rc_this_oldid' ) !== $rc->getAttribute(
'page_latest' );
404 'name' =>
'changeType',
405 'title' =>
'rcfilters-filtergroup-changetype',
410 'name' =>
'hidepageedits',
411 'label' =>
'rcfilters-filter-pageedits-label',
412 'description' =>
'rcfilters-filter-pageedits-description',
415 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
416 &$query_options, &$join_conds
418 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_EDIT );
420 'cssClassSuffix' =>
'src-mw-edit',
421 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
426 'name' =>
'hidenewpages',
427 'label' =>
'rcfilters-filter-newpages-label',
428 'description' =>
'rcfilters-filter-newpages-description',
431 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
432 &$query_options, &$join_conds
434 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_NEW );
436 'cssClassSuffix' =>
'src-mw-new',
437 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
446 'label' =>
'rcfilters-filter-logactions-label',
447 'description' =>
'rcfilters-filter-logactions-description',
450 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
451 &$query_options, &$join_conds
453 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_LOG );
455 'cssClassSuffix' =>
'src-mw-log',
456 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
465 $this->legacyReviewStatusFilterGroupDefinition = [
467 'name' =>
'legacyReviewStatus',
468 'title' =>
'rcfilters-filtergroup-reviewstatus',
472 'name' =>
'hidepatrolled',
475 'showHideSuffix' =>
'showhidepatr',
477 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
478 &$query_options, &$join_conds
482 'isReplacedInStructuredUi' =>
true,
485 'name' =>
'hideunpatrolled',
487 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
488 &$query_options, &$join_conds
492 'isReplacedInStructuredUi' =>
true,
498 $this->reviewStatusFilterGroupDefinition = [
500 'name' =>
'reviewStatus',
501 'title' =>
'rcfilters-filtergroup-reviewstatus',
503 'isFullCoverage' =>
true,
507 'name' =>
'unpatrolled',
508 'label' =>
'rcfilters-filter-reviewstatus-unpatrolled-label',
509 'description' =>
'rcfilters-filter-reviewstatus-unpatrolled-description',
510 'cssClassSuffix' =>
'reviewstatus-unpatrolled',
511 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
517 'label' =>
'rcfilters-filter-reviewstatus-manual-label',
518 'description' =>
'rcfilters-filter-reviewstatus-manual-description',
519 'cssClassSuffix' =>
'reviewstatus-manual',
520 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
526 'label' =>
'rcfilters-filter-reviewstatus-auto-label',
527 'description' =>
'rcfilters-filter-reviewstatus-auto-description',
528 'cssClassSuffix' =>
'reviewstatus-auto',
529 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
535 'queryCallable' =>
function ( $specialPageClassName, $ctx,
$dbr,
536 &
$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
538 if ( $selected === [] ) {
541 $rcPatrolledValues = [
547 $conds[
'rc_patrolled'] = array_map(
function (
$s )
use ( $rcPatrolledValues ) {
548 return $rcPatrolledValues[
$s ];
554 $this->hideCategorizationFilterDefinition = [
555 'name' =>
'hidecategorization',
556 'label' =>
'rcfilters-filter-categorization-label',
557 'description' =>
'rcfilters-filter-categorization-description',
560 'showHideSuffix' =>
'showhidecategorization',
563 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
564 &$query_options, &$join_conds
568 'cssClassSuffix' =>
'src-mw-categorize',
569 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
584 if ( $group->getConflictingGroups() ) {
587 " specifies conflicts with other groups but these are not supported yet."
592 foreach ( $group->getConflictingFilters()
as $conflictingFilter ) {
593 if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
599 foreach ( $group->getFilters()
as $filter ) {
601 foreach ( $filter->getConflictingFilters()
as $conflictingFilter ) {
603 $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
604 $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
623 $this->rcSubpage = $subpage;
630 if (
$rows ===
false ) {
635 if ( $this->
getRequest()->getVal(
'action' ) ===
'render' ) {
636 $this->
getOutput()->setArticleBodyOnly(
true );
641 if ( $this->
getRequest()->getBool(
'peek' ) ) {
645 if ( $this->
getUser()->isAnon() !==
648 $this->
getOutput()->setStatusCode( 205 );
658 $batch->add( $row->rc_namespace, $row->rc_title );
661 foreach ( $formatter->getPreloadTitles()
as $title ) {
681 $this->
getOutput()->setStatusCode( 500 );
686 if ( $this->
getConfig()->
get(
'EnableWANCacheReaper' ) ) {
690 LoggerFactory::getInstance(
'objectcache' )
709 $knownParams = $this->
getRequest()->getValues(
710 ...array_keys( $this->
getOptions()->getAllValues() )
716 $excludedParams = [
'limit' =>
'',
'days' =>
'',
'enhanced' =>
'',
'from' =>
'' ];
717 $knownParams = array_diff_key( $knownParams, $excludedParams );
723 count( $knownParams ) === 0
727 $this->
getUser()->getOption( static::$savedQueriesPreferenceName ),
731 if ( $savedQueries && isset( $savedQueries[
'default' ] ) ) {
734 if ( isset( $savedQueries[
'version' ] ) && $savedQueries[
'version' ] ===
'2' ) {
735 $savedQueryDefaultID = $savedQueries[
'default' ];
736 $defaultQuery = $savedQueries[
'queries' ][ $savedQueryDefaultID ][
'data' ];
740 $defaultQuery[
'params' ],
741 $defaultQuery[
'highlights' ],
749 unset(
$query[
'title' ] );
757 'wgStructuredChangeFiltersDefaultSavedQueryExists',
763 $this->
getOutput()->addBodyClasses(
'mw-rcfilters-ui-loading' );
780 foreach ( $jsData[
'messageKeys']
as $key ) {
784 $out->addBodyClasses(
'mw-rcfilters-enabled' );
785 $collapsed = $this->
getUser()->getBoolOption( static::$collapsedPreferenceName );
787 $out->addBodyClasses(
'mw-rcfilters-collapsed' );
791 $out->addJsConfigVars(
'wgStructuredChangeFilters', $jsData[
'groups'] );
792 $out->addJsConfigVars(
'wgStructuredChangeFiltersMessages',
$messages );
793 $out->addJsConfigVars(
'wgStructuredChangeFiltersCollapsedState', $collapsed );
795 $out->addJsConfigVars(
796 'wgRCFiltersChangeTags',
799 $out->addJsConfigVars(
800 'StructuredChangeFiltersDisplayConfig',
802 'maxDays' => (
int)$this->
getConfig()->
get(
'RCMaxAge' ) / ( 24 * 3600 ),
803 'limitArray' => $this->
getConfig()->
get(
'RCLinkLimits' ),
805 'daysArray' => $this->
getConfig()->
get(
'RCLinkDays' ),
810 $out->addJsConfigVars(
811 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
812 static::$savedQueriesPreferenceName
814 $out->addJsConfigVars(
815 'wgStructuredChangeFiltersLimitPreferenceName',
816 static::$limitPreferenceName
818 $out->addJsConfigVars(
819 'wgStructuredChangeFiltersDaysPreferenceName',
820 static::$daysPreferenceName
822 $out->addJsConfigVars(
823 'wgStructuredChangeFiltersCollapsedPreferenceName',
824 static::$collapsedPreferenceName
827 $out->addJsConfigVars(
828 'StructuredChangeFiltersLiveUpdatePollingRate',
829 $this->
getConfig()->
get(
'StructuredChangeFiltersLiveUpdatePollingRate' )
832 $out->addBodyClasses(
'mw-rcfilters-disabled' );
844 return $cache->getWithSetCallback(
845 $cache->makeKey(
'changeslistspecialpage-changetags',
$context->getLanguage()->getCode() ),
846 $cache::TTL_MINUTE * 10,
859 $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags );
863 foreach ( $tagHitCounts
as $tagName => $hits ) {
866 isset( $explicitlyDefinedTags[ $tagName ] ) ||
867 isset( $softwareActivatedTags[ $tagName ] )
871 'label' => Sanitizer::stripAllTags(
876 $tagName, self::TAG_DESC_CHARACTER_LIMIT,
$context
878 'cssClass' => Sanitizer::escapeClass(
'mw-tag-' . $tagName ),
885 usort(
$result,
function ( $a, $b ) {
886 return strcasecmp( $a[
'label'], $b[
'label'] );
902 '<div class="mw-changeslist-empty">' .
903 $this->
msg(
'recentchanges-noresult' )->parse() .
913 '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
914 $this->
msg(
'recentchanges-timeout' )->parse() .
932 $this->
buildQuery(
$tables, $fields, $conds, $query_options, $join_conds, $opts );
934 return $this->
doMainQuery(
$tables, $fields, $conds, $query_options, $join_conds, $opts );
943 if ( $this->rcOptions ===
null ) {
944 $this->rcOptions = $this->
setup( $this->rcSubpage );
972 if ( $this->
getConfig()->
get(
'RCWatchCategoryMembership' ) ) {
974 $this->hideCategorizationFilterDefinition
977 $transformedHideCategorizationDef[
'group'] = $changeTypeGroup;
980 $transformedHideCategorizationDef
984 Hooks::run(
'ChangesListSpecialPageStructuredFilters', [ $this ] );
989 $registered = $userExperienceLevel->getFilter(
'registered' );
990 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'newcomer' ) );
991 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'learner' ) );
992 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'experienced' ) );
994 $categoryFilter = $changeTypeGroup->getFilter(
'hidecategorization' );
995 $logactionsFilter = $changeTypeGroup->getFilter(
'hidelog' );
996 $pagecreationFilter = $changeTypeGroup->getFilter(
'hidenewpages' );
999 $hideMinorFilter = $significanceTypeGroup->getFilter(
'hideminor' );
1002 if ( $categoryFilter !==
null ) {
1003 $hideMinorFilter->conflictsWith(
1005 'rcfilters-hideminor-conflicts-typeofchange-global',
1006 'rcfilters-hideminor-conflicts-typeofchange',
1007 'rcfilters-typeofchange-conflicts-hideminor'
1010 $hideMinorFilter->conflictsWith(
1012 'rcfilters-hideminor-conflicts-typeofchange-global',
1013 'rcfilters-hideminor-conflicts-typeofchange',
1014 'rcfilters-typeofchange-conflicts-hideminor'
1016 $hideMinorFilter->conflictsWith(
1017 $pagecreationFilter,
1018 'rcfilters-hideminor-conflicts-typeofchange-global',
1019 'rcfilters-hideminor-conflicts-typeofchange',
1020 'rcfilters-typeofchange-conflicts-hideminor'
1034 return $filterDefinition;
1047 $autoFillPriority = -1;
1048 foreach ( $definition
as $groupDefinition ) {
1049 if ( !isset( $groupDefinition[
'priority'] ) ) {
1050 $groupDefinition[
'priority'] = $autoFillPriority;
1053 $autoFillPriority = $groupDefinition[
'priority'];
1056 $autoFillPriority--;
1058 $className = $groupDefinition[
'class'];
1059 unset( $groupDefinition[
'class'] );
1061 foreach ( $groupDefinition[
'filters']
as &$filterDefinition ) {
1074 foreach ( $this->filterGroups
as $group ) {
1076 foreach ( $group->getFilters()
as $key => $filter ) {
1077 if ( $filter->displaysOnUnstructuredUi( $this ) ) {
1078 $filters[ $key ] = $filter;
1102 if ( $parameters !==
null ) {
1124 $useDefaults = $this->
getRequest()->getInt(
'urlversion' ) !== 2;
1127 foreach ( $this->filterGroups
as $filterGroup ) {
1128 $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1132 $opts->add(
'invert',
false );
1133 $opts->add(
'associated',
false );
1134 $opts->add(
'urlversion', 1 );
1135 $opts->add(
'tagfilter',
'' );
1140 $opts->add(
'from',
'' );
1151 $groupName = $group->
getName();
1153 $this->filterGroups[$groupName] = $group;
1173 return $this->filterGroups[$groupName] ??
null;
1191 'messageKeys' => [],
1194 usort( $this->filterGroups,
function ( $a, $b ) {
1195 return $b->getPriority() <=> $a->getPriority();
1198 foreach ( $this->filterGroups
as $groupName => $group ) {
1199 $groupOutput = $group->getJsData( $this );
1200 if ( $groupOutput !==
null ) {
1201 $output[
'messageKeys'] = array_merge(
1203 $groupOutput[
'messageKeys']
1206 unset( $groupOutput[
'messageKeys'] );
1207 $output[
'groups'][] = $groupOutput;
1223 $opts->fetchValuesFromRequest( $this->
getRequest() );
1235 $stringParameterNameSet = [];
1236 $hideParameterNameSet = [];
1241 foreach ( $this->filterGroups
as $filterGroup ) {
1243 $stringParameterNameSet[$filterGroup->getName()] =
true;
1245 foreach ( $filterGroup->getFilters()
as $filter ) {
1246 $hideParameterNameSet[$filter->getName()] =
true;
1251 $bits = preg_split(
'/\s*,\s*/', trim( $par ) );
1252 foreach ( $bits
as $bit ) {
1254 if ( isset( $hideParameterNameSet[$bit] ) ) {
1257 } elseif ( isset( $hideParameterNameSet[
"hide$bit"] ) ) {
1259 $opts[
"hide$bit"] =
false;
1260 } elseif ( preg_match(
'/^(.*)=(.*)$/', $bit, $m ) ) {
1261 if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1262 $opts[$m[1]] = $m[2];
1277 if ( $isContradictory || $isReplaced ) {
1295 foreach ( $this->filterGroups
as $filterGroup ) {
1297 $filters = $filterGroup->getFilters();
1299 if (
count( $filters ) === 1 ) {
1304 $allInGroupEnabled = array_reduce(
1306 function ( $carry, $filter )
use ( $opts ) {
1307 return $carry && $opts[ $filter->getName() ];
1309 count( $filters ) > 0
1312 if ( $allInGroupEnabled ) {
1313 foreach ( $filters
as $filter ) {
1314 $opts[ $filter->getName() ] =
false;
1335 if ( $opts[
'hideanons'] && $opts[
'hideliu'] ) {
1336 $opts->
reset(
'hideanons' );
1337 if ( !$opts[
'hidebots'] ) {
1338 $opts->
reset(
'hideliu' );
1339 $opts[
'hidehumans'] = 1;
1363 if ( $opts[
'hideanons' ] ) {
1364 $opts->
reset(
'hideanons' );
1365 $opts[
'userExpLevel' ] =
'registered';
1369 if ( $opts[
'hideliu' ] ) {
1370 $opts->
reset(
'hideliu' );
1371 $opts[
'userExpLevel' ] =
'unregistered';
1376 if ( $opts[
'hidepatrolled' ] ) {
1377 $opts->
reset(
'hidepatrolled' );
1378 $opts[
'reviewStatus' ] =
'unpatrolled';
1382 if ( $opts[
'hideunpatrolled' ] ) {
1383 $opts->
reset(
'hideunpatrolled' );
1384 $opts[
'reviewStatus' ] = implode(
1386 [
'manual',
'auto' ]
1405 if (
$value ===
false ) {
1431 foreach ( $this->filterGroups
as $filterGroup ) {
1432 $filterGroup->modifyQuery(
$dbr, $this,
$tables, $fields, $conds,
1433 $query_options, $join_conds, $opts, $isStructuredUI );
1437 if ( $opts[
'namespace' ] !==
'' ) {
1438 $namespaces = explode(
';', $opts[
'namespace' ] );
1440 if ( $opts[
'associated' ] ) {
1441 $associatedNamespaces = array_map(
1451 $operator = $opts[
'invert' ] ?
'!=' :
'=';
1454 $operator = $opts[
'invert' ] ?
'NOT IN' :
'IN';
1458 $conds[] =
"rc_namespace $operator $value";
1462 $cutoff_unixtime = time() - $opts[
'days'] * 3600 * 24;
1463 $cutoff =
$dbr->timestamp( $cutoff_unixtime );
1465 $fromValid = preg_match(
'/^[0-9]{14}$/', $opts[
'from'] );
1466 if ( $fromValid && $opts[
'from'] >
wfTimestamp( TS_MW, $cutoff ) ) {
1467 $cutoff =
$dbr->timestamp( $opts[
'from'] );
1469 $opts->
reset(
'from' );
1472 $conds[] =
'rc_timestamp >= ' .
$dbr->addQuotes( $cutoff );
1491 $fields = array_merge( $rcQuery[
'fields'], $fields );
1492 $join_conds = array_merge( $join_conds, $rcQuery[
'joins'] );
1511 return $dbr->select(
1522 &$query_options, &$join_conds, $opts
1525 'ChangesListSpecialPageQuery',
1526 [ $this->
getName(), &
$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ]
1548 $this->
doHeader( $opts, $rowCount );
1634 # The legend showing what the letters and stuff mean
1636 # Iterates through them and gets the messages for both letter and tooltip
1637 $legendItems =
$context->getConfig()->get(
'RecentChangesFlags' );
1638 if ( !(
$user->useRCPatrol() ||
$user->useNPPatrol() ) ) {
1639 unset( $legendItems[
'unpatrolled'] );
1641 foreach ( $legendItems
as $key => $item ) { # generate items
of the legend
1642 $label = $item[
'legend'] ?? $item[
'title'];
1643 $letter = $item[
'letter'];
1644 $cssClass = $item[
'class'] ?? $key;
1647 [
'class' => $cssClass ],
$context->msg( $letter )->text()
1650 [
'class' => Sanitizer::escapeClass(
'mw-changeslist-legend-' . $key ) ],
1656 [
'class' =>
'mw-plusminus-pos' ],
1657 $context->msg(
'recentchanges-legend-plusminus' )->parse()
1661 [
'class' =>
'mw-changeslist-legend-plusminus' ],
1662 $context->msg(
'recentchanges-label-plusminus' )->text()
1667 $context->msg(
'rcfilters-legend-heading' )->parse() :
1668 $context->msg(
'recentchanges-legend-heading' )->parse();
1671 $collapsedState = $this->
getRequest()->getCookie(
'changeslist-state' );
1672 $collapsedClass = $collapsedState ===
'collapsed' ?
' mw-collapsed' :
'';
1675 '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass .
'">' .
1677 '<div class="mw-collapsible-content">' . $legend .
'</div>' .
1689 $out->addModuleStyles( [
1690 'mediawiki.special.changeslist.legend',
1691 'mediawiki.special.changeslist',
1693 $out->addModules(
'mediawiki.special.changeslist.legend.js' );
1696 $out->addModules(
'mediawiki.rcfilters.filters.ui' );
1697 $out->addModuleStyles(
'mediawiki.rcfilters.filters.base.styles' );
1722 &
$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1732 if (
count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1738 in_array(
'registered', $selectedExpLevels ) &&
1739 in_array(
'unregistered', $selectedExpLevels )
1745 $actorQuery = $actorMigration->getJoin(
'rc_user' );
1746 $tables += $actorQuery[
'tables'];
1747 $join_conds += $actorQuery[
'joins'];
1751 in_array(
'registered', $selectedExpLevels ) &&
1752 !in_array(
'unregistered', $selectedExpLevels )
1754 $conds[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
1758 if ( $selectedExpLevels === [
'unregistered' ] ) {
1759 $conds[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
1764 $join_conds[
'user'] = [
'LEFT JOIN', $actorQuery[
'fields'][
'rc_user'] .
' = user_id' ];
1769 $secondsPerDay = 86400;
1773 $aboveNewcomer =
$dbr->makeList(
1776 'user_registration <= ' .
$dbr->addQuotes(
$dbr->timestamp( $learnerCutoff ) ),
1781 $aboveLearner =
$dbr->makeList(
1784 'user_registration <= ' .
1785 $dbr->addQuotes(
$dbr->timestamp( $experiencedUserCutoff ) ),
1792 if ( in_array(
'unregistered', $selectedExpLevels ) ) {
1793 $selectedExpLevels = array_diff( $selectedExpLevels, [
'unregistered' ] );
1794 $conditions[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
1797 if ( $selectedExpLevels === [
'newcomer' ] ) {
1798 $conditions[] =
"NOT ( $aboveNewcomer )";
1799 } elseif ( $selectedExpLevels === [
'learner' ] ) {
1800 $conditions[] =
$dbr->makeList(
1801 [ $aboveNewcomer,
"NOT ( $aboveLearner )" ],
1804 } elseif ( $selectedExpLevels === [
'experienced' ] ) {
1805 $conditions[] = $aboveLearner;
1806 } elseif ( $selectedExpLevels === [
'learner',
'newcomer' ] ) {
1807 $conditions[] =
"NOT ( $aboveLearner )";
1808 } elseif ( $selectedExpLevels === [
'experienced',
'newcomer' ] ) {
1809 $conditions[] =
$dbr->makeList(
1810 [
"NOT ( $aboveNewcomer )", $aboveLearner ],
1813 } elseif ( $selectedExpLevels === [
'experienced',
'learner' ] ) {
1814 $conditions[] = $aboveNewcomer;
1815 } elseif ( $selectedExpLevels === [
'experienced',
'learner',
'newcomer' ] ) {
1816 $conditions[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
1819 if (
count( $conditions ) > 1 ) {
1821 } elseif (
count( $conditions ) === 1 ) {
1822 $conds[] = reset( $conditions );
1832 if ( $this->
getRequest()->getBool(
'rcfilters' ) ) {
1836 return static::checkStructuredFilterUiEnabled(
1851 return !
$user->getOption(
'rcenhancedfilters-disable' );
1862 return $this->
getUser()->getIntOption( static::$limitPreferenceName );
1874 return floatval( $this->
getUser()->getOption( static::$daysPreferenceName ) );