111 parent::__construct( $name, $restriction );
113 $nonRevisionTypes = [
RC_LOG ];
114 Hooks::run(
'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
116 $this->filterGroupDefinitions = [
118 'name' =>
'registration',
119 'title' =>
'rcfilters-filtergroup-registration',
120 'class' => ChangesListBooleanFilterGroup::class,
126 'showHideSuffix' =>
'showhideliu',
128 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
129 &$query_options, &$join_conds
132 $actorQuery = $actorMigration->getJoin(
'rc_user' );
133 $tables += $actorQuery[
'tables'];
134 $join_conds += $actorQuery[
'joins'];
135 $conds[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
137 'isReplacedInStructuredUi' =>
true,
141 'name' =>
'hideanons',
144 'showHideSuffix' =>
'showhideanons',
146 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
147 &$query_options, &$join_conds
150 $actorQuery = $actorMigration->getJoin(
'rc_user' );
151 $tables += $actorQuery[
'tables'];
152 $join_conds += $actorQuery[
'joins'];
153 $conds[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
155 'isReplacedInStructuredUi' =>
true,
161 'name' =>
'userExpLevel',
162 'title' =>
'rcfilters-filtergroup-user-experience-level',
163 'class' => ChangesListStringOptionsFilterGroup::class,
164 'isFullCoverage' =>
true,
167 'name' =>
'unregistered',
168 'label' =>
'rcfilters-filter-user-experience-level-unregistered-label',
169 'description' =>
'rcfilters-filter-user-experience-level-unregistered-description',
170 'cssClassSuffix' =>
'user-unregistered',
171 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
172 return !$rc->getAttribute(
'rc_user' );
176 'name' =>
'registered',
177 'label' =>
'rcfilters-filter-user-experience-level-registered-label',
178 'description' =>
'rcfilters-filter-user-experience-level-registered-description',
179 'cssClassSuffix' =>
'user-registered',
180 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
181 return $rc->getAttribute(
'rc_user' );
185 'name' =>
'newcomer',
186 'label' =>
'rcfilters-filter-user-experience-level-newcomer-label',
187 'description' =>
'rcfilters-filter-user-experience-level-newcomer-description',
188 'cssClassSuffix' =>
'user-newcomer',
189 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
190 $performer = $rc->getPerformer();
191 return $performer && $performer->isLoggedIn() &&
192 $performer->getExperienceLevel() ===
'newcomer';
197 'label' =>
'rcfilters-filter-user-experience-level-learner-label',
198 'description' =>
'rcfilters-filter-user-experience-level-learner-description',
199 'cssClassSuffix' =>
'user-learner',
200 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
201 $performer = $rc->getPerformer();
202 return $performer && $performer->isLoggedIn() &&
203 $performer->getExperienceLevel() ===
'learner';
207 'name' =>
'experienced',
208 'label' =>
'rcfilters-filter-user-experience-level-experienced-label',
209 'description' =>
'rcfilters-filter-user-experience-level-experienced-description',
210 'cssClassSuffix' =>
'user-experienced',
211 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
212 $performer = $rc->getPerformer();
213 return $performer && $performer->isLoggedIn() &&
214 $performer->getExperienceLevel() ===
'experienced';
219 'queryCallable' => [ $this,
'filterOnUserExperienceLevel' ],
223 'name' =>
'authorship',
224 'title' =>
'rcfilters-filtergroup-authorship',
225 'class' => ChangesListBooleanFilterGroup::class,
228 'name' =>
'hidemyself',
229 'label' =>
'rcfilters-filter-editsbyself-label',
230 'description' =>
'rcfilters-filter-editsbyself-description',
233 'showHideSuffix' =>
'showhidemine',
235 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
236 &$query_options, &$join_conds
239 $tables += $actorQuery[
'tables'];
240 $join_conds += $actorQuery[
'joins'];
241 $conds[] =
'NOT(' . $actorQuery[
'conds'] .
')';
243 'cssClassSuffix' =>
'self',
244 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
245 return $ctx->getUser()->equals( $rc->getPerformer() );
249 'name' =>
'hidebyothers',
250 'label' =>
'rcfilters-filter-editsbyother-label',
251 'description' =>
'rcfilters-filter-editsbyother-description',
253 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
254 &$query_options, &$join_conds
257 ->getWhere(
$dbr,
'rc_user', $ctx->getUser(), false );
258 $tables += $actorQuery[
'tables'];
259 $join_conds += $actorQuery[
'joins'];
260 $conds[] = $actorQuery[
'conds'];
262 'cssClassSuffix' =>
'others',
263 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
264 return !$ctx->getUser()->equals( $rc->getPerformer() );
271 'name' =>
'automated',
272 'title' =>
'rcfilters-filtergroup-automated',
273 'class' => ChangesListBooleanFilterGroup::class,
276 'name' =>
'hidebots',
277 'label' =>
'rcfilters-filter-bots-label',
278 'description' =>
'rcfilters-filter-bots-description',
281 'showHideSuffix' =>
'showhidebots',
283 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
284 &$query_options, &$join_conds
286 $conds[
'rc_bot'] = 0;
288 'cssClassSuffix' =>
'bot',
289 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
290 return $rc->getAttribute(
'rc_bot' );
294 'name' =>
'hidehumans',
295 'label' =>
'rcfilters-filter-humans-label',
296 'description' =>
'rcfilters-filter-humans-description',
298 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
299 &$query_options, &$join_conds
301 $conds[
'rc_bot'] = 1;
303 'cssClassSuffix' =>
'human',
304 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
305 return !$rc->getAttribute(
'rc_bot' );
314 'name' =>
'significance',
315 'title' =>
'rcfilters-filtergroup-significance',
316 'class' => ChangesListBooleanFilterGroup::class,
320 'name' =>
'hideminor',
321 'label' =>
'rcfilters-filter-minor-label',
322 'description' =>
'rcfilters-filter-minor-description',
325 'showHideSuffix' =>
'showhideminor',
327 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
328 &$query_options, &$join_conds
330 $conds[] =
'rc_minor = 0';
332 'cssClassSuffix' =>
'minor',
333 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
334 return $rc->getAttribute(
'rc_minor' );
338 'name' =>
'hidemajor',
339 'label' =>
'rcfilters-filter-major-label',
340 'description' =>
'rcfilters-filter-major-description',
342 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
343 &$query_options, &$join_conds
345 $conds[] =
'rc_minor = 1';
347 'cssClassSuffix' =>
'major',
348 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
349 return !$rc->getAttribute(
'rc_minor' );
356 'name' =>
'lastRevision',
357 'title' =>
'rcfilters-filtergroup-lastrevision',
358 'class' => ChangesListBooleanFilterGroup::class,
362 'name' =>
'hidelastrevision',
363 'label' =>
'rcfilters-filter-lastrevision-label',
364 'description' =>
'rcfilters-filter-lastrevision-description',
366 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
367 &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
368 $conds[] =
$dbr->makeList(
370 'rc_this_oldid <> page_latest',
371 'rc_type' => $nonRevisionTypes,
376 'cssClassSuffix' =>
'last',
377 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
378 return $rc->getAttribute(
'rc_this_oldid' ) === $rc->getAttribute(
'page_latest' );
382 'name' =>
'hidepreviousrevisions',
383 'label' =>
'rcfilters-filter-previousrevision-label',
384 'description' =>
'rcfilters-filter-previousrevision-description',
386 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
387 &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
388 $conds[] =
$dbr->makeList(
390 'rc_this_oldid = page_latest',
391 'rc_type' => $nonRevisionTypes,
396 'cssClassSuffix' =>
'previous',
397 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
398 return $rc->getAttribute(
'rc_this_oldid' ) !== $rc->getAttribute(
'page_latest' );
406 'name' =>
'changeType',
407 'title' =>
'rcfilters-filtergroup-changetype',
408 'class' => ChangesListBooleanFilterGroup::class,
412 'name' =>
'hidepageedits',
413 'label' =>
'rcfilters-filter-pageedits-label',
414 'description' =>
'rcfilters-filter-pageedits-description',
417 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
418 &$query_options, &$join_conds
420 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_EDIT );
422 'cssClassSuffix' =>
'src-mw-edit',
423 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
428 'name' =>
'hidenewpages',
429 'label' =>
'rcfilters-filter-newpages-label',
430 'description' =>
'rcfilters-filter-newpages-description',
433 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
434 &$query_options, &$join_conds
436 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_NEW );
438 'cssClassSuffix' =>
'src-mw-new',
439 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
448 'label' =>
'rcfilters-filter-logactions-label',
449 'description' =>
'rcfilters-filter-logactions-description',
452 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
453 &$query_options, &$join_conds
455 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_LOG );
457 'cssClassSuffix' =>
'src-mw-log',
458 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
467 $this->legacyReviewStatusFilterGroupDefinition = [
469 'name' =>
'legacyReviewStatus',
470 'title' =>
'rcfilters-filtergroup-reviewstatus',
471 'class' => ChangesListBooleanFilterGroup::class,
474 'name' =>
'hidepatrolled',
477 'showHideSuffix' =>
'showhidepatr',
479 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
480 &$query_options, &$join_conds
484 'isReplacedInStructuredUi' =>
true,
487 'name' =>
'hideunpatrolled',
489 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
490 &$query_options, &$join_conds
494 'isReplacedInStructuredUi' =>
true,
500 $this->reviewStatusFilterGroupDefinition = [
502 'name' =>
'reviewStatus',
503 'title' =>
'rcfilters-filtergroup-reviewstatus',
504 'class' => ChangesListStringOptionsFilterGroup::class,
505 'isFullCoverage' =>
true,
509 'name' =>
'unpatrolled',
510 'label' =>
'rcfilters-filter-reviewstatus-unpatrolled-label',
511 'description' =>
'rcfilters-filter-reviewstatus-unpatrolled-description',
512 'cssClassSuffix' =>
'reviewstatus-unpatrolled',
513 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
519 'label' =>
'rcfilters-filter-reviewstatus-manual-label',
520 'description' =>
'rcfilters-filter-reviewstatus-manual-description',
521 'cssClassSuffix' =>
'reviewstatus-manual',
522 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
528 'label' =>
'rcfilters-filter-reviewstatus-auto-label',
529 'description' =>
'rcfilters-filter-reviewstatus-auto-description',
530 'cssClassSuffix' =>
'reviewstatus-auto',
531 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
537 'queryCallable' =>
function ( $specialPageClassName, $ctx,
$dbr,
538 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
540 if ( $selected === [] ) {
543 $rcPatrolledValues = [
549 $conds[
'rc_patrolled'] = array_map(
function (
$s ) use ( $rcPatrolledValues ) {
550 return $rcPatrolledValues[
$s ];
556 $this->hideCategorizationFilterDefinition = [
557 'name' =>
'hidecategorization',
558 'label' =>
'rcfilters-filter-categorization-label',
559 'description' =>
'rcfilters-filter-categorization-description',
562 'showHideSuffix' =>
'showhidecategorization',
565 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
566 &$query_options, &$join_conds
570 'cssClassSuffix' =>
'src-mw-categorize',
571 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
586 if ( $group->getConflictingGroups() ) {
589 " specifies conflicts with other groups but these are not supported yet."
594 foreach ( $group->getConflictingFilters() as $conflictingFilter ) {
595 if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
601 foreach ( $group->getFilters() as
$filter ) {
603 foreach (
$filter->getConflictingFilters() as $conflictingFilter ) {
605 $conflictingFilter->activelyInConflictWithFilter(
$filter, $opts ) &&
606 $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' ) ) {
642 $code = $rows->numRows() > 0 ? 200 : 204;
643 $this->
getOutput()->setStatusCode( $code );
645 if ( $this->
getUser()->isAnon() !==
648 $this->
getOutput()->setStatusCode( 205 );
655 foreach ( $rows as $row ) {
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' ];
739 $query = array_merge(
740 $defaultQuery[
'params' ],
741 $defaultQuery[
'highlights' ],
748 $query = array_merge( $this->
getRequest()->getValues(), $query );
749 unset( $query[
'title' ] );
757 'wgStructuredChangeFiltersDefaultSavedQueryExists',
763 $this->
getOutput()->addBodyClasses(
'mw-rcfilters-ui-loading' );
775 $linkDays = $this->
getConfig()->get(
'RCLinkDays' );
776 $filterByAge = $this->
getConfig()->get(
'RCFilterByAge' );
777 $maxAge = $this->
getConfig()->get(
'RCMaxAge' );
778 if ( $filterByAge ) {
784 $maxAgeDays = $maxAge / ( 3600 * 24 );
785 foreach ( $linkDays as $i => $days ) {
786 if ( $days >= $maxAgeDays ) {
787 array_splice( $linkDays, $i + 1 );
807 foreach ( $jsData[
'messageKeys'] as $key ) {
808 $messages[$key] = $this->
msg( $key )->plain();
811 $out->addBodyClasses(
'mw-rcfilters-enabled' );
812 $collapsed = $this->
getUser()->getBoolOption( static::$collapsedPreferenceName );
814 $out->addBodyClasses(
'mw-rcfilters-collapsed' );
818 $out->addJsConfigVars(
'wgStructuredChangeFilters', $jsData[
'groups'] );
819 $out->addJsConfigVars(
'wgStructuredChangeFiltersMessages', $messages );
820 $out->addJsConfigVars(
'wgStructuredChangeFiltersCollapsedState', $collapsed );
822 $out->addJsConfigVars(
823 'StructuredChangeFiltersDisplayConfig',
825 'maxDays' => (
int)$this->
getConfig()->
get(
'RCMaxAge' ) / ( 24 * 3600 ),
826 'limitArray' => $this->
getConfig()->
get(
'RCLinkLimits' ),
833 $out->addJsConfigVars(
834 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
835 static::$savedQueriesPreferenceName
837 $out->addJsConfigVars(
838 'wgStructuredChangeFiltersLimitPreferenceName',
839 static::$limitPreferenceName
841 $out->addJsConfigVars(
842 'wgStructuredChangeFiltersDaysPreferenceName',
843 static::$daysPreferenceName
845 $out->addJsConfigVars(
846 'wgStructuredChangeFiltersCollapsedPreferenceName',
847 static::$collapsedPreferenceName
850 $out->addBodyClasses(
'mw-rcfilters-disabled' );
866 'StructuredChangeFiltersEditWatchlistUrl' =>
881 'StructuredChangeFiltersEditWatchlistUrl' =>
907 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
908 return $cache->getWithSetCallback(
909 $cache->makeKey(
'ChangesListSpecialPage-changeTagListSummary',
$context->getLanguage() ),
911 function ( $oldValue, &$ttl, array &$setOpts ) use (
$context ) {
916 $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
919 foreach ( $tagHitCounts as $tagName => $hits ) {
923 isset( $explicitlyDefinedTags[ $tagName ] ) ||
924 isset( $softwareActivatedTags[ $tagName ] )
930 if ( $labelMsg ===
false ) {
937 'labelMsg' => $labelMsg,
938 'label' => $labelMsg->plain(),
939 'descriptionMsg' => $descriptionMsg,
940 'description' => $descriptionMsg ? $descriptionMsg->plain() :
'',
941 'cssClass' => Sanitizer::escapeClass(
'mw-tag-' . $tagName ),
967 foreach ( $tags as &$tagInfo ) {
968 $tagInfo[
'label'] = Sanitizer::stripAllTags( $tagInfo[
'labelMsg']->parse() );
969 $tagInfo[
'description'] = $tagInfo[
'descriptionMsg'] ?
970 $language->truncateForVisual(
971 Sanitizer::stripAllTags( $tagInfo[
'descriptionMsg']->parse() ),
972 self::TAG_DESC_CHARACTER_LIMIT
975 unset( $tagInfo[
'labelMsg'] );
976 unset( $tagInfo[
'descriptionMsg'] );
980 usort( $tags,
function ( $a, $b ) {
981 return strcasecmp( $a[
'label'], $b[
'label'] );
991 '<div class="mw-changeslist-empty">' .
992 $this->
msg(
'recentchanges-noresult' )->parse() .
1002 '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
1003 $this->
msg(
'recentchanges-timeout' )->parse() .
1019 $query_options = [];
1021 $this->
buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1023 return $this->
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1032 if ( $this->rcOptions ===
null ) {
1033 $this->rcOptions = $this->
setup( $this->rcSubpage );
1061 if ( $this->
getConfig()->
get(
'RCWatchCategoryMembership' ) ) {
1063 $this->hideCategorizationFilterDefinition
1066 $transformedHideCategorizationDef[
'group'] = $changeTypeGroup;
1069 $transformedHideCategorizationDef
1073 Hooks::run(
'ChangesListSpecialPageStructuredFilters', [ $this ] );
1078 $registered = $userExperienceLevel->getFilter(
'registered' );
1079 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'newcomer' ) );
1080 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'learner' ) );
1081 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'experienced' ) );
1083 $categoryFilter = $changeTypeGroup->getFilter(
'hidecategorization' );
1084 $logactionsFilter = $changeTypeGroup->getFilter(
'hidelog' );
1085 $pagecreationFilter = $changeTypeGroup->getFilter(
'hidenewpages' );
1087 $significanceTypeGroup = $this->
getFilterGroup(
'significance' );
1088 $hideMinorFilter = $significanceTypeGroup->getFilter(
'hideminor' );
1091 if ( $categoryFilter !==
null ) {
1092 $hideMinorFilter->conflictsWith(
1094 'rcfilters-hideminor-conflicts-typeofchange-global',
1095 'rcfilters-hideminor-conflicts-typeofchange',
1096 'rcfilters-typeofchange-conflicts-hideminor'
1099 $hideMinorFilter->conflictsWith(
1101 'rcfilters-hideminor-conflicts-typeofchange-global',
1102 'rcfilters-hideminor-conflicts-typeofchange',
1103 'rcfilters-typeofchange-conflicts-hideminor'
1105 $hideMinorFilter->conflictsWith(
1106 $pagecreationFilter,
1107 'rcfilters-hideminor-conflicts-typeofchange-global',
1108 'rcfilters-hideminor-conflicts-typeofchange',
1109 'rcfilters-typeofchange-conflicts-hideminor'
1123 return $filterDefinition;
1137 $autoFillPriority = -1;
1138 foreach ( $definition as $groupDefinition ) {
1139 if ( !isset( $groupDefinition[
'priority'] ) ) {
1140 $groupDefinition[
'priority'] = $autoFillPriority;
1143 $autoFillPriority = $groupDefinition[
'priority'];
1146 $autoFillPriority--;
1148 $className = $groupDefinition[
'class'];
1149 unset( $groupDefinition[
'class'] );
1151 foreach ( $groupDefinition[
'filters'] as &$filterDefinition ) {
1164 foreach ( $this->filterGroups as $group ) {
1166 foreach ( $group->getFilters() as $key =>
$filter ) {
1167 if (
$filter->displaysOnUnstructuredUi( $this ) ) {
1192 if ( $parameters !==
null ) {
1214 $useDefaults = $this->
getRequest()->getInt(
'urlversion' ) !== 2;
1217 foreach ( $this->filterGroups as $filterGroup ) {
1218 $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1222 $opts->add(
'invert',
false );
1223 $opts->add(
'associated',
false );
1224 $opts->add(
'urlversion', 1 );
1225 $opts->add(
'tagfilter',
'' );
1230 $opts->add(
'from',
'' );
1241 $groupName = $group->
getName();
1243 $this->filterGroups[$groupName] = $group;
1263 return $this->filterGroups[$groupName] ??
null;
1282 'messageKeys' => [],
1285 usort( $this->filterGroups,
function ( $a, $b ) {
1286 return $b->getPriority() <=> $a->getPriority();
1289 foreach ( $this->filterGroups as $groupName => $group ) {
1290 $groupOutput = $group->getJsData( $this );
1291 if ( $groupOutput !==
null ) {
1292 $output[
'messageKeys'] = array_merge(
1294 $groupOutput[
'messageKeys']
1297 unset( $groupOutput[
'messageKeys'] );
1298 $output[
'groups'][] = $groupOutput;
1314 $opts->fetchValuesFromRequest( $this->
getRequest() );
1326 $stringParameterNameSet = [];
1327 $hideParameterNameSet = [];
1332 foreach ( $this->filterGroups as $filterGroup ) {
1334 $stringParameterNameSet[$filterGroup->getName()] =
true;
1336 foreach ( $filterGroup->getFilters() as
$filter ) {
1337 $hideParameterNameSet[
$filter->getName()] =
true;
1342 $bits = preg_split(
'/\s*,\s*/', trim( $par ) );
1343 foreach ( $bits as $bit ) {
1345 if ( isset( $hideParameterNameSet[$bit] ) ) {
1348 } elseif ( isset( $hideParameterNameSet[
"hide$bit"] ) ) {
1350 $opts[
"hide$bit"] =
false;
1351 } elseif ( preg_match(
'/^(.*)=(.*)$/', $bit, $m ) ) {
1352 if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1353 $opts[$m[1]] = $m[2];
1368 if ( $isContradictory || $isReplaced ) {
1386 foreach ( $this->filterGroups as $filterGroup ) {
1388 $filters = $filterGroup->getFilters();
1390 if ( count( $filters ) === 1 ) {
1395 $allInGroupEnabled = array_reduce(
1397 function ( $carry,
$filter ) use ( $opts ) {
1398 return $carry && $opts[
$filter->getName() ];
1400 count( $filters ) > 0
1403 if ( $allInGroupEnabled ) {
1404 foreach ( $filters as
$filter ) {
1405 $opts[
$filter->getName() ] =
false;
1426 if ( $opts[
'hideanons'] && $opts[
'hideliu'] ) {
1427 $opts->
reset(
'hideanons' );
1428 if ( !$opts[
'hidebots'] ) {
1429 $opts->
reset(
'hideliu' );
1430 $opts[
'hidehumans'] = 1;
1454 if ( $opts[
'hideanons' ] ) {
1455 $opts->
reset(
'hideanons' );
1456 $opts[
'userExpLevel' ] =
'registered';
1460 if ( $opts[
'hideliu' ] ) {
1461 $opts->
reset(
'hideliu' );
1462 $opts[
'userExpLevel' ] =
'unregistered';
1467 if ( $opts[
'hidepatrolled' ] ) {
1468 $opts->
reset(
'hidepatrolled' );
1469 $opts[
'reviewStatus' ] =
'unpatrolled';
1473 if ( $opts[
'hideunpatrolled' ] ) {
1474 $opts->
reset(
'hideunpatrolled' );
1475 $opts[
'reviewStatus' ] = implode(
1477 [
'manual',
'auto' ]
1495 foreach ( $params as &$value ) {
1496 if ( $value ===
false ) {
1515 protected function buildQuery( &$tables, &$fields, &$conds, &$query_options,
1522 foreach ( $this->filterGroups as $filterGroup ) {
1523 $filterGroup->modifyQuery(
$dbr, $this, $tables, $fields, $conds,
1524 $query_options, $join_conds, $opts, $isStructuredUI );
1528 if ( $opts[
'namespace' ] !==
'' ) {
1529 $namespaces = explode(
';', $opts[
'namespace' ] );
1533 if ( $opts[
'associated' ] ) {
1534 $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1535 $associatedNamespaces = array_map(
1536 function ( $ns ) use ( $namespaceInfo ){
1537 return $namespaceInfo->getAssociated( $ns );
1541 function ( $ns ) use ( $namespaceInfo ) {
1542 return $namespaceInfo->hasTalkNamespace( $ns );
1546 $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
1549 if ( count( $namespaces ) === 1 ) {
1550 $operator = $opts[
'invert' ] ?
'!=' :
'=';
1551 $value =
$dbr->addQuotes( reset( $namespaces ) );
1553 $operator = $opts[
'invert' ] ?
'NOT IN' :
'IN';
1554 sort( $namespaces );
1555 $value =
'(' .
$dbr->makeList( $namespaces ) .
')';
1557 $conds[] =
"rc_namespace $operator $value";
1561 $cutoff_unixtime = time() - $opts[
'days'] * 3600 * 24;
1562 $cutoff =
$dbr->timestamp( $cutoff_unixtime );
1564 $fromValid = preg_match(
'/^[0-9]{14}$/', $opts[
'from'] );
1565 if ( $fromValid && $opts[
'from'] >
wfTimestamp( TS_MW, $cutoff ) ) {
1566 $cutoff =
$dbr->timestamp( $opts[
'from'] );
1568 $opts->
reset(
'from' );
1571 $conds[] =
'rc_timestamp >= ' .
$dbr->addQuotes( $cutoff );
1589 $tables = array_merge( $tables, $rcQuery[
'tables'] );
1590 $fields = array_merge( $rcQuery[
'fields'], $fields );
1591 $join_conds = array_merge( $join_conds, $rcQuery[
'joins'] );
1602 if ( !$this->
runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
1610 return $dbr->select(
1621 &$query_options, &$join_conds, $opts
1624 'ChangesListSpecialPageQuery',
1625 [ $this->
getName(), &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ]
1647 $this->
doHeader( $opts, $rowCount );
1735 # The legend showing what the letters and stuff mean
1736 $legend = Html::openElement(
'dl' ) .
"\n";
1737 # Iterates through them and gets the messages for both letter and tooltip
1738 $legendItems =
$context->getConfig()->get(
'RecentChangesFlags' );
1739 if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
1740 unset( $legendItems[
'unpatrolled'] );
1742 foreach ( $legendItems as $key => $item ) { # generate items of the legend
1743 $label = $item[
'legend'] ?? $item[
'title'];
1744 $letter = $item[
'letter'];
1745 $cssClass = $item[
'class'] ?? $key;
1747 $legend .= Html::element(
'dt',
1748 [
'class' => $cssClass ],
$context->msg( $letter )->text()
1750 Html::rawElement(
'dd',
1751 [
'class' => Sanitizer::escapeClass(
'mw-changeslist-legend-' . $key ) ],
1756 $legend .= Html::rawElement(
'dt',
1757 [
'class' =>
'mw-plusminus-pos' ],
1758 $context->msg(
'recentchanges-legend-plusminus' )->parse()
1760 $legend .= Html::element(
1762 [
'class' =>
'mw-changeslist-legend-plusminus' ],
1763 $context->msg(
'recentchanges-label-plusminus' )->text()
1765 $legend .= Html::closeElement(
'dl' ) .
"\n";
1768 $context->msg(
'rcfilters-legend-heading' )->parse() :
1769 $context->msg(
'recentchanges-legend-heading' )->parse();
1772 $collapsedState = $this->
getRequest()->getCookie(
'changeslist-state' );
1773 $collapsedClass = $collapsedState ===
'collapsed' ?
' mw-collapsed' :
'';
1776 '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass .
'">' .
1778 '<div class="mw-collapsible-content">' . $legend .
'</div>' .
1790 $out->addModuleStyles( [
1791 'mediawiki.interface.helpers.styles',
1792 'mediawiki.special.changeslist.legend',
1793 'mediawiki.special.changeslist',
1795 $out->addModules(
'mediawiki.special.changeslist.legend.js' );
1798 $out->addModules(
'mediawiki.rcfilters.filters.ui' );
1799 $out->addModuleStyles(
'mediawiki.rcfilters.filters.base.styles' );
1824 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1834 if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1840 in_array(
'registered', $selectedExpLevels ) &&
1841 in_array(
'unregistered', $selectedExpLevels )
1847 $actorQuery = $actorMigration->getJoin(
'rc_user' );
1848 $tables += $actorQuery[
'tables'];
1849 $join_conds += $actorQuery[
'joins'];
1853 in_array(
'registered', $selectedExpLevels ) &&
1854 !in_array(
'unregistered', $selectedExpLevels )
1856 $conds[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
1860 if ( $selectedExpLevels === [
'unregistered' ] ) {
1861 $conds[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
1866 $join_conds[
'user'] = [
'LEFT JOIN', $actorQuery[
'fields'][
'rc_user'] .
' = user_id' ];
1871 $secondsPerDay = 86400;
1875 $aboveNewcomer =
$dbr->makeList(
1878 'user_registration <= ' .
$dbr->addQuotes(
$dbr->timestamp( $learnerCutoff ) ),
1883 $aboveLearner =
$dbr->makeList(
1886 'user_registration <= ' .
1887 $dbr->addQuotes(
$dbr->timestamp( $experiencedUserCutoff ) ),
1894 if ( in_array(
'unregistered', $selectedExpLevels ) ) {
1895 $selectedExpLevels = array_diff( $selectedExpLevels, [
'unregistered' ] );
1896 $conditions[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
1899 if ( $selectedExpLevels === [
'newcomer' ] ) {
1900 $conditions[] =
"NOT ( $aboveNewcomer )";
1901 } elseif ( $selectedExpLevels === [
'learner' ] ) {
1902 $conditions[] =
$dbr->makeList(
1903 [ $aboveNewcomer,
"NOT ( $aboveLearner )" ],
1906 } elseif ( $selectedExpLevels === [
'experienced' ] ) {
1907 $conditions[] = $aboveLearner;
1908 } elseif ( $selectedExpLevels === [
'learner',
'newcomer' ] ) {
1909 $conditions[] =
"NOT ( $aboveLearner )";
1910 } elseif ( $selectedExpLevels === [
'experienced',
'newcomer' ] ) {
1911 $conditions[] =
$dbr->makeList(
1912 [
"NOT ( $aboveNewcomer )", $aboveLearner ],
1915 } elseif ( $selectedExpLevels === [
'experienced',
'learner' ] ) {
1916 $conditions[] = $aboveNewcomer;
1917 } elseif ( $selectedExpLevels === [
'experienced',
'learner',
'newcomer' ] ) {
1918 $conditions[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
1921 if ( count( $conditions ) > 1 ) {
1923 } elseif ( count( $conditions ) === 1 ) {
1924 $conds[] = reset( $conditions );
1934 if ( $this->
getRequest()->getBool(
'rcfilters' ) ) {
1938 return static::checkStructuredFilterUiEnabled( $this->
getUser() );
1949 if ( $user instanceof
Config ) {
1950 wfDeprecated( __METHOD__ .
' with Config argument',
'1.34' );
1951 $user = func_get_arg( 1 );
1953 return !$user->getOption(
'rcenhancedfilters-disable' );
1964 return $this->
getUser()->getIntOption( static::$limitPreferenceName );
1976 return floatval( $this->
getUser()->getOption( static::$daysPreferenceName ) );
1980 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1981 $symbolicFilters = [
1982 'all-contents' => $nsInfo->getSubjectNamespaces(),
1983 'all-discussions' => $nsInfo->getTalkNamespaces(),
1985 $additionalNamespaces = [];
1986 foreach ( $symbolicFilters as $name => $values ) {
1987 if ( in_array( $name, $namespaces ) ) {
1988 $additionalNamespaces = array_merge( $additionalNamespaces, $values );
1991 $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) );
1992 $namespaces = array_merge( $namespaces, $additionalNamespaces );
1993 return array_unique( $namespaces );