112 parent::__construct( $name, $restriction );
114 $nonRevisionTypes = [
RC_LOG ];
115 $this->
getHookRunner()->onSpecialWatchlistGetNonRevisionTypes( $nonRevisionTypes );
117 $this->filterGroupDefinitions = [
119 'name' =>
'registration',
120 'title' =>
'rcfilters-filtergroup-registration',
121 'class' => ChangesListBooleanFilterGroup::class,
127 'showHideSuffix' =>
'showhideliu',
129 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
130 &$query_options, &$join_conds
132 $actorMigration = ActorMigration::newMigration();
133 $actorQuery = $actorMigration->getJoin(
'rc_user' );
134 $tables += $actorQuery[
'tables'];
135 $join_conds += $actorQuery[
'joins'];
136 $conds[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
138 'isReplacedInStructuredUi' =>
true,
142 'name' =>
'hideanons',
145 'showHideSuffix' =>
'showhideanons',
147 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
148 &$query_options, &$join_conds
150 $actorMigration = ActorMigration::newMigration();
151 $actorQuery = $actorMigration->getJoin(
'rc_user' );
152 $tables += $actorQuery[
'tables'];
153 $join_conds += $actorQuery[
'joins'];
154 $conds[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
156 'isReplacedInStructuredUi' =>
true,
162 'name' =>
'userExpLevel',
163 'title' =>
'rcfilters-filtergroup-user-experience-level',
164 'class' => ChangesListStringOptionsFilterGroup::class,
165 'isFullCoverage' =>
true,
168 'name' =>
'unregistered',
169 'label' =>
'rcfilters-filter-user-experience-level-unregistered-label',
170 'description' =>
'rcfilters-filter-user-experience-level-unregistered-description',
171 'cssClassSuffix' =>
'user-unregistered',
172 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
173 return !$rc->getAttribute(
'rc_user' );
177 'name' =>
'registered',
178 'label' =>
'rcfilters-filter-user-experience-level-registered-label',
179 'description' =>
'rcfilters-filter-user-experience-level-registered-description',
180 'cssClassSuffix' =>
'user-registered',
181 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
182 return $rc->getAttribute(
'rc_user' );
186 'name' =>
'newcomer',
187 'label' =>
'rcfilters-filter-user-experience-level-newcomer-label',
188 'description' =>
'rcfilters-filter-user-experience-level-newcomer-description',
189 'cssClassSuffix' =>
'user-newcomer',
190 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
191 $performer = $rc->getPerformer();
192 return $performer && $performer->isLoggedIn() &&
193 $performer->getExperienceLevel() ===
'newcomer';
198 'label' =>
'rcfilters-filter-user-experience-level-learner-label',
199 'description' =>
'rcfilters-filter-user-experience-level-learner-description',
200 'cssClassSuffix' =>
'user-learner',
201 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
202 $performer = $rc->getPerformer();
203 return $performer && $performer->isLoggedIn() &&
204 $performer->getExperienceLevel() ===
'learner';
208 'name' =>
'experienced',
209 'label' =>
'rcfilters-filter-user-experience-level-experienced-label',
210 'description' =>
'rcfilters-filter-user-experience-level-experienced-description',
211 'cssClassSuffix' =>
'user-experienced',
212 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
213 $performer = $rc->getPerformer();
214 return $performer && $performer->isLoggedIn() &&
215 $performer->getExperienceLevel() ===
'experienced';
220 'queryCallable' => [ $this,
'filterOnUserExperienceLevel' ],
224 'name' =>
'authorship',
225 'title' =>
'rcfilters-filtergroup-authorship',
226 'class' => ChangesListBooleanFilterGroup::class,
229 'name' =>
'hidemyself',
230 'label' =>
'rcfilters-filter-editsbyself-label',
231 'description' =>
'rcfilters-filter-editsbyself-description',
234 'showHideSuffix' =>
'showhidemine',
236 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
237 &$query_options, &$join_conds
239 $actorQuery = ActorMigration::newMigration()->getWhere(
$dbr,
'rc_user', $ctx->getUser() );
240 $tables += $actorQuery[
'tables'];
241 $join_conds += $actorQuery[
'joins'];
242 $conds[] =
'NOT(' . $actorQuery[
'conds'] .
')';
244 'cssClassSuffix' =>
'self',
245 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
246 return $ctx->getUser()->equals( $rc->getPerformer() );
250 'name' =>
'hidebyothers',
251 'label' =>
'rcfilters-filter-editsbyother-label',
252 'description' =>
'rcfilters-filter-editsbyother-description',
254 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
255 &$query_options, &$join_conds
257 $actorQuery = ActorMigration::newMigration()
258 ->getWhere(
$dbr,
'rc_user', $ctx->getUser(),
false );
259 $tables += $actorQuery[
'tables'];
260 $join_conds += $actorQuery[
'joins'];
261 $conds[] = $actorQuery[
'conds'];
263 'cssClassSuffix' =>
'others',
264 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
265 return !$ctx->getUser()->equals( $rc->getPerformer() );
272 'name' =>
'automated',
273 'title' =>
'rcfilters-filtergroup-automated',
274 'class' => ChangesListBooleanFilterGroup::class,
277 'name' =>
'hidebots',
278 'label' =>
'rcfilters-filter-bots-label',
279 'description' =>
'rcfilters-filter-bots-description',
282 'showHideSuffix' =>
'showhidebots',
284 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
285 &$query_options, &$join_conds
287 $conds[
'rc_bot'] = 0;
289 'cssClassSuffix' =>
'bot',
290 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
291 return $rc->getAttribute(
'rc_bot' );
295 'name' =>
'hidehumans',
296 'label' =>
'rcfilters-filter-humans-label',
297 'description' =>
'rcfilters-filter-humans-description',
299 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
300 &$query_options, &$join_conds
302 $conds[
'rc_bot'] = 1;
304 'cssClassSuffix' =>
'human',
305 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
306 return !$rc->getAttribute(
'rc_bot' );
315 'name' =>
'significance',
316 'title' =>
'rcfilters-filtergroup-significance',
317 'class' => ChangesListBooleanFilterGroup::class,
321 'name' =>
'hideminor',
322 'label' =>
'rcfilters-filter-minor-label',
323 'description' =>
'rcfilters-filter-minor-description',
326 'showHideSuffix' =>
'showhideminor',
328 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
329 &$query_options, &$join_conds
331 $conds[] =
'rc_minor = 0';
333 'cssClassSuffix' =>
'minor',
334 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
335 return $rc->getAttribute(
'rc_minor' );
339 'name' =>
'hidemajor',
340 'label' =>
'rcfilters-filter-major-label',
341 'description' =>
'rcfilters-filter-major-description',
343 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
344 &$query_options, &$join_conds
346 $conds[] =
'rc_minor = 1';
348 'cssClassSuffix' =>
'major',
349 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
350 return !$rc->getAttribute(
'rc_minor' );
357 'name' =>
'lastRevision',
358 'title' =>
'rcfilters-filtergroup-lastrevision',
359 'class' => ChangesListBooleanFilterGroup::class,
363 'name' =>
'hidelastrevision',
364 'label' =>
'rcfilters-filter-lastrevision-label',
365 'description' =>
'rcfilters-filter-lastrevision-description',
367 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
368 &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
369 $conds[] =
$dbr->makeList(
371 'rc_this_oldid <> page_latest',
372 'rc_type' => $nonRevisionTypes,
377 'cssClassSuffix' =>
'last',
378 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
379 return $rc->getAttribute(
'rc_this_oldid' ) === $rc->getAttribute(
'page_latest' );
383 'name' =>
'hidepreviousrevisions',
384 'label' =>
'rcfilters-filter-previousrevision-label',
385 'description' =>
'rcfilters-filter-previousrevision-description',
387 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
388 &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
389 $conds[] =
$dbr->makeList(
391 'rc_this_oldid = page_latest',
392 'rc_type' => $nonRevisionTypes,
397 'cssClassSuffix' =>
'previous',
398 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
399 return $rc->getAttribute(
'rc_this_oldid' ) !== $rc->getAttribute(
'page_latest' );
407 'name' =>
'changeType',
408 'title' =>
'rcfilters-filtergroup-changetype',
409 'class' => ChangesListBooleanFilterGroup::class,
413 'name' =>
'hidepageedits',
414 'label' =>
'rcfilters-filter-pageedits-label',
415 'description' =>
'rcfilters-filter-pageedits-description',
418 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
419 &$query_options, &$join_conds
421 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_EDIT );
423 'cssClassSuffix' =>
'src-mw-edit',
424 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
425 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_EDIT;
429 'name' =>
'hidenewpages',
430 'label' =>
'rcfilters-filter-newpages-label',
431 'description' =>
'rcfilters-filter-newpages-description',
434 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
435 &$query_options, &$join_conds
437 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_NEW );
439 'cssClassSuffix' =>
'src-mw-new',
440 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
441 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_NEW;
449 'label' =>
'rcfilters-filter-logactions-label',
450 'description' =>
'rcfilters-filter-logactions-description',
453 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
454 &$query_options, &$join_conds
456 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_LOG );
458 'cssClassSuffix' =>
'src-mw-log',
459 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
460 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_LOG;
468 $this->legacyReviewStatusFilterGroupDefinition = [
470 'name' =>
'legacyReviewStatus',
471 'title' =>
'rcfilters-filtergroup-reviewstatus',
472 'class' => ChangesListBooleanFilterGroup::class,
475 'name' =>
'hidepatrolled',
478 'showHideSuffix' =>
'showhidepatr',
480 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
481 &$query_options, &$join_conds
483 $conds[
'rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
485 'isReplacedInStructuredUi' =>
true,
488 'name' =>
'hideunpatrolled',
490 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
491 &$query_options, &$join_conds
493 $conds[] =
'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
495 'isReplacedInStructuredUi' =>
true,
501 $this->reviewStatusFilterGroupDefinition = [
503 'name' =>
'reviewStatus',
504 'title' =>
'rcfilters-filtergroup-reviewstatus',
505 'class' => ChangesListStringOptionsFilterGroup::class,
506 'isFullCoverage' =>
true,
510 'name' =>
'unpatrolled',
511 'label' =>
'rcfilters-filter-reviewstatus-unpatrolled-label',
512 'description' =>
'rcfilters-filter-reviewstatus-unpatrolled-description',
513 'cssClassSuffix' =>
'reviewstatus-unpatrolled',
514 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
515 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED;
520 'label' =>
'rcfilters-filter-reviewstatus-manual-label',
521 'description' =>
'rcfilters-filter-reviewstatus-manual-description',
522 'cssClassSuffix' =>
'reviewstatus-manual',
523 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
524 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_PATROLLED;
529 'label' =>
'rcfilters-filter-reviewstatus-auto-label',
530 'description' =>
'rcfilters-filter-reviewstatus-auto-description',
531 'cssClassSuffix' =>
'reviewstatus-auto',
532 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
533 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED;
538 'queryCallable' =>
function ( $specialPageClassName, $ctx,
$dbr,
539 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
541 if ( $selected === [] ) {
544 $rcPatrolledValues = [
545 'unpatrolled' => RecentChange::PRC_UNPATROLLED,
546 'manual' => RecentChange::PRC_PATROLLED,
547 'auto' => RecentChange::PRC_AUTOPATROLLED,
550 $conds[
'rc_patrolled'] = array_map(
function (
$s ) use ( $rcPatrolledValues ) {
551 return $rcPatrolledValues[
$s ];
557 $this->hideCategorizationFilterDefinition = [
558 'name' =>
'hidecategorization',
559 'label' =>
'rcfilters-filter-categorization-label',
560 'description' =>
'rcfilters-filter-categorization-description',
563 'showHideSuffix' =>
'showhidecategorization',
566 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
567 &$query_options, &$join_conds
571 'cssClassSuffix' =>
'src-mw-categorize',
572 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
573 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_CATEGORIZE;
587 if ( $group->getConflictingGroups() ) {
590 " specifies conflicts with other groups but these are not supported yet."
595 foreach ( $group->getConflictingFilters() as $conflictingFilter ) {
596 if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
602 foreach ( $group->getFilters() as $filter ) {
604 foreach ( $filter->getConflictingFilters() as $conflictingFilter ) {
606 $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
607 $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
624 $this->rcSubpage = $subpage;
629 if ( $this->
getConfig()->
get(
'WatchlistExpiry' ) ) {
631 $this->
getOutput()->addModules(
'mediawiki.special.changeslist.watchlistexpiry' );
637 if ( $rows ===
false ) {
642 if ( $this->
getRequest()->getVal(
'action' ) ===
'render' ) {
643 $this->
getOutput()->setArticleBodyOnly(
true );
648 if ( $this->
getRequest()->getBool(
'peek' ) ) {
649 $code = $rows->numRows() > 0 ? 200 : 204;
650 $this->
getOutput()->setStatusCode( $code );
652 if ( $this->
getUser()->isAnon() !==
655 $this->
getOutput()->setStatusCode( 205 );
662 foreach ( $rows as $row ) {
665 $batch->add( $row->rc_namespace, $row->rc_title );
666 if ( $row->rc_source === RecentChange::SRC_LOG ) {
668 foreach ( $formatter->getPreloadTitles() as
$title ) {
682 MWExceptionHandler::logException( $timeoutException );
688 $this->
getOutput()->setStatusCode( 500 );
693 if ( $this->
getConfig()->
get(
'EnableWANCacheReaper' ) ) {
697 LoggerFactory::getInstance(
'objectcache' )
716 $knownParams = $this->
getRequest()->getValues(
717 ...array_keys( $this->
getOptions()->getAllValues() )
723 $excludedParams = [
'limit' =>
'',
'days' =>
'',
'enhanced' =>
'',
'from' =>
'' ];
724 $knownParams = array_diff_key( $knownParams, $excludedParams );
730 count( $knownParams ) === 0
732 $prefJson = $this->
getUser()->getOption( static::$savedQueriesPreferenceName );
735 $savedQueries = $prefJson ? FormatJson::decode( $prefJson,
true ) :
false;
737 if ( $savedQueries && isset( $savedQueries[
'default' ] ) ) {
740 if ( isset( $savedQueries[
'version' ] ) && $savedQueries[
'version' ] ===
'2' ) {
741 $savedQueryDefaultID = $savedQueries[
'default' ];
742 $defaultQuery = $savedQueries[
'queries' ][ $savedQueryDefaultID ][
'data' ];
745 $query = array_merge(
746 $defaultQuery[
'params' ],
747 $defaultQuery[
'highlights' ],
754 $query = array_merge( $this->
getRequest()->getValues(), $query );
755 unset( $query[
'title' ] );
763 'wgStructuredChangeFiltersDefaultSavedQueryExists',
769 $this->
getOutput()->addBodyClasses(
'mw-rcfilters-ui-loading' );
781 $linkDays = $this->
getConfig()->get(
'RCLinkDays' );
782 $filterByAge = $this->
getConfig()->get(
'RCFilterByAge' );
783 $maxAge = $this->
getConfig()->get(
'RCMaxAge' );
784 if ( $filterByAge ) {
790 $maxAgeDays = $maxAge / ( 3600 * 24 );
791 foreach ( $linkDays as $i => $days ) {
792 if ( $days >= $maxAgeDays ) {
793 array_splice( $linkDays, $i + 1 );
813 foreach ( $jsData[
'messageKeys'] as $key ) {
814 $messages[$key] = $this->
msg( $key )->plain();
817 $out->addBodyClasses(
'mw-rcfilters-enabled' );
818 $collapsed = $this->
getUser()->getBoolOption( static::$collapsedPreferenceName );
820 $out->addBodyClasses(
'mw-rcfilters-collapsed' );
824 $out->addJsConfigVars(
'wgStructuredChangeFilters', $jsData[
'groups'] );
825 $out->addJsConfigVars(
'wgStructuredChangeFiltersMessages', $messages );
826 $out->addJsConfigVars(
'wgStructuredChangeFiltersCollapsedState', $collapsed );
828 $out->addJsConfigVars(
829 'StructuredChangeFiltersDisplayConfig',
831 'maxDays' => (
int)$this->
getConfig()->
get(
'RCMaxAge' ) / ( 24 * 3600 ),
832 'limitArray' => $this->
getConfig()->
get(
'RCLinkLimits' ),
839 $out->addJsConfigVars(
840 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
841 static::$savedQueriesPreferenceName
843 $out->addJsConfigVars(
844 'wgStructuredChangeFiltersLimitPreferenceName',
845 static::$limitPreferenceName
847 $out->addJsConfigVars(
848 'wgStructuredChangeFiltersDaysPreferenceName',
849 static::$daysPreferenceName
851 $out->addJsConfigVars(
852 'wgStructuredChangeFiltersCollapsedPreferenceName',
853 static::$collapsedPreferenceName
856 $out->addBodyClasses(
'mw-rcfilters-disabled' );
871 'RCFiltersChangeTags' => self::getChangeTagListSummary( $context ),
872 'StructuredChangeFiltersEditWatchlistUrl' =>
886 'RCFiltersChangeTags' => self::getChangeTagList( $context ),
887 'StructuredChangeFiltersEditWatchlistUrl' =>
913 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
914 return $cache->getWithSetCallback(
915 $cache->makeKey(
'ChangesListSpecialPage-changeTagListSummary', $context->
getLanguage() ),
916 WANObjectCache::TTL_DAY,
917 function ( $oldValue, &$ttl, array &$setOpts ) use ( $context ) {
922 $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
925 foreach ( $tagHitCounts as $tagName => $hits ) {
929 isset( $explicitlyDefinedTags[ $tagName ] ) ||
930 isset( $softwareActivatedTags[ $tagName ] )
936 if ( $labelMsg ===
false ) {
943 'labelMsg' => $labelMsg,
944 'label' => $labelMsg->plain(),
945 'descriptionMsg' => $descriptionMsg,
946 'description' => $descriptionMsg ? $descriptionMsg->plain() :
'',
947 'cssClass' => Sanitizer::escapeClass(
'mw-tag-' . $tagName ),
971 $tags = self::getChangeTagListSummary( $context );
972 $language = MediaWikiServices::getInstance()->getLanguageFactory()
974 foreach ( $tags as &$tagInfo ) {
975 $tagInfo[
'label'] = Sanitizer::stripAllTags( $tagInfo[
'labelMsg']->parse() );
976 $tagInfo[
'description'] = $tagInfo[
'descriptionMsg'] ?
977 $language->truncateForVisual(
978 Sanitizer::stripAllTags( $tagInfo[
'descriptionMsg']->parse() ),
979 self::TAG_DESC_CHARACTER_LIMIT
982 unset( $tagInfo[
'labelMsg'] );
983 unset( $tagInfo[
'descriptionMsg'] );
987 usort( $tags,
function ( $a, $b ) {
988 return strcasecmp( $a[
'label'], $b[
'label'] );
998 '<div class="mw-changeslist-empty">' .
999 $this->
msg(
'recentchanges-noresult' )->parse() .
1009 '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
1010 $this->
msg(
'recentchanges-timeout' )->parse() .
1026 $query_options = [];
1028 $this->
buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1030 return $this->
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1039 if ( $this->rcOptions ===
null ) {
1040 $this->rcOptions = $this->
setup( $this->rcSubpage );
1043 return $this->rcOptions;
1068 if ( $this->
getConfig()->
get(
'RCWatchCategoryMembership' ) ) {
1070 $this->hideCategorizationFilterDefinition
1073 $transformedHideCategorizationDef[
'group'] = $changeTypeGroup;
1076 $transformedHideCategorizationDef
1080 $this->
getHookRunner()->onChangesListSpecialPageStructuredFilters( $this );
1085 $registered = $userExperienceLevel->getFilter(
'registered' );
1086 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'newcomer' ) );
1087 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'learner' ) );
1088 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'experienced' ) );
1090 $categoryFilter = $changeTypeGroup->getFilter(
'hidecategorization' );
1091 $logactionsFilter = $changeTypeGroup->getFilter(
'hidelog' );
1092 $pagecreationFilter = $changeTypeGroup->getFilter(
'hidenewpages' );
1094 $significanceTypeGroup = $this->
getFilterGroup(
'significance' );
1095 $hideMinorFilter = $significanceTypeGroup->getFilter(
'hideminor' );
1098 if ( $categoryFilter !==
null ) {
1099 $hideMinorFilter->conflictsWith(
1101 'rcfilters-hideminor-conflicts-typeofchange-global',
1102 'rcfilters-hideminor-conflicts-typeofchange',
1103 'rcfilters-typeofchange-conflicts-hideminor'
1106 $hideMinorFilter->conflictsWith(
1108 'rcfilters-hideminor-conflicts-typeofchange-global',
1109 'rcfilters-hideminor-conflicts-typeofchange',
1110 'rcfilters-typeofchange-conflicts-hideminor'
1112 $hideMinorFilter->conflictsWith(
1113 $pagecreationFilter,
1114 'rcfilters-hideminor-conflicts-typeofchange-global',
1115 'rcfilters-hideminor-conflicts-typeofchange',
1116 'rcfilters-typeofchange-conflicts-hideminor'
1130 return $filterDefinition;
1144 $autoFillPriority = -1;
1145 foreach ( $definition as $groupDefinition ) {
1146 if ( !isset( $groupDefinition[
'priority'] ) ) {
1147 $groupDefinition[
'priority'] = $autoFillPriority;
1150 $autoFillPriority = $groupDefinition[
'priority'];
1153 $autoFillPriority--;
1155 $className = $groupDefinition[
'class'];
1156 unset( $groupDefinition[
'class'] );
1158 foreach ( $groupDefinition[
'filters'] as &$filterDefinition ) {
1171 foreach ( $this->filterGroups as $group ) {
1173 foreach ( $group->getFilters() as $key => $filter ) {
1174 if ( $filter->displaysOnUnstructuredUi() ) {
1175 $filters[ $key ] = $filter;
1199 if ( $parameters !==
null ) {
1221 $useDefaults = $this->
getRequest()->getInt(
'urlversion' ) !== 2;
1224 foreach ( $this->filterGroups as $filterGroup ) {
1225 $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1228 $opts->add(
'namespace',
'', FormOptions::STRING );
1229 $opts->add(
'invert',
false );
1230 $opts->add(
'associated',
false );
1231 $opts->add(
'urlversion', 1 );
1232 $opts->add(
'tagfilter',
'' );
1234 $opts->add(
'days', $this->
getDefaultDays(), FormOptions::FLOAT );
1237 $opts->add(
'from',
'' );
1248 $groupName = $group->
getName();
1250 $this->filterGroups[$groupName] = $group;
1259 return $this->filterGroups;
1270 return $this->filterGroups[$groupName] ??
null;
1289 'messageKeys' => [],
1292 usort( $this->filterGroups,
function ( $a, $b ) {
1293 return $b->getPriority() <=> $a->getPriority();
1296 foreach ( $this->filterGroups as $groupName => $group ) {
1297 $groupOutput = $group->getJsData();
1298 if ( $groupOutput !==
null ) {
1299 $output[
'messageKeys'] = array_merge(
1300 $output[
'messageKeys'],
1301 $groupOutput[
'messageKeys']
1304 unset( $groupOutput[
'messageKeys'] );
1305 $output[
'groups'][] = $groupOutput;
1321 $opts->fetchValuesFromRequest( $this->
getRequest() );
1333 $stringParameterNameSet = [];
1334 $hideParameterNameSet = [];
1339 foreach ( $this->filterGroups as $filterGroup ) {
1341 $stringParameterNameSet[$filterGroup->getName()] =
true;
1343 foreach ( $filterGroup->getFilters() as $filter ) {
1344 $hideParameterNameSet[$filter->getName()] =
true;
1349 $bits = preg_split(
'/\s*,\s*/', trim( $par ) );
1350 foreach ( $bits as $bit ) {
1352 if ( isset( $hideParameterNameSet[$bit] ) ) {
1355 } elseif ( isset( $hideParameterNameSet[
"hide$bit"] ) ) {
1357 $opts[
"hide$bit"] =
false;
1358 } elseif ( preg_match(
'/^(.*)=(.*)$/', $bit, $m ) ) {
1359 if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1360 $opts[$m[1]] = $m[2];
1375 if ( $isContradictory || $isReplaced ) {
1393 foreach ( $this->filterGroups as $filterGroup ) {
1395 $filters = $filterGroup->getFilters();
1397 if ( count( $filters ) === 1 ) {
1402 $allInGroupEnabled = array_reduce(
1404 function ( $carry, $filter ) use ( $opts ) {
1405 return $carry && $opts[ $filter->getName() ];
1407 count( $filters ) > 0
1410 if ( $allInGroupEnabled ) {
1411 foreach ( $filters as $filter ) {
1412 $opts[ $filter->getName() ] =
false;
1433 if ( $opts[
'hideanons'] && $opts[
'hideliu'] ) {
1434 $opts->
reset(
'hideanons' );
1435 if ( !$opts[
'hidebots'] ) {
1436 $opts->
reset(
'hideliu' );
1437 $opts[
'hidehumans'] = 1;
1461 if ( $opts[
'hideanons' ] ) {
1462 $opts->
reset(
'hideanons' );
1463 $opts[
'userExpLevel' ] =
'registered';
1467 if ( $opts[
'hideliu' ] ) {
1468 $opts->
reset(
'hideliu' );
1469 $opts[
'userExpLevel' ] =
'unregistered';
1474 if ( $opts[
'hidepatrolled' ] ) {
1475 $opts->
reset(
'hidepatrolled' );
1476 $opts[
'reviewStatus' ] =
'unpatrolled';
1480 if ( $opts[
'hideunpatrolled' ] ) {
1481 $opts->
reset(
'hideunpatrolled' );
1482 $opts[
'reviewStatus' ] = implode(
1484 [
'manual',
'auto' ]
1502 foreach ( $params as &$value ) {
1503 if ( $value ===
false ) {
1522 protected function buildQuery( &$tables, &$fields, &$conds, &$query_options,
1529 foreach ( $this->filterGroups as $filterGroup ) {
1530 $filterGroup->modifyQuery(
$dbr, $this, $tables, $fields, $conds,
1531 $query_options, $join_conds, $opts, $isStructuredUI );
1535 if ( $opts[
'namespace' ] !==
'' ) {
1536 $namespaces = explode(
';', $opts[
'namespace' ] );
1540 if ( $opts[
'associated' ] ) {
1541 $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1542 $associatedNamespaces = array_map(
1543 function ( $ns ) use ( $namespaceInfo ){
1544 return $namespaceInfo->getAssociated( $ns );
1548 function ( $ns ) use ( $namespaceInfo ) {
1549 return $namespaceInfo->hasTalkNamespace( $ns );
1553 $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
1556 if ( count( $namespaces ) === 1 ) {
1557 $operator = $opts[
'invert' ] ?
'!=' :
'=';
1558 $value =
$dbr->addQuotes( reset( $namespaces ) );
1560 $operator = $opts[
'invert' ] ?
'NOT IN' :
'IN';
1561 sort( $namespaces );
1562 $value =
'(' .
$dbr->makeList( $namespaces ) .
')';
1564 $conds[] =
"rc_namespace $operator $value";
1568 $cutoff_unixtime = time() - $opts[
'days'] * 3600 * 24;
1569 $cutoff =
$dbr->timestamp( $cutoff_unixtime );
1571 $fromValid = preg_match(
'/^[0-9]{14}$/', $opts[
'from'] );
1572 if ( $fromValid && $opts[
'from'] >
wfTimestamp( TS_MW, $cutoff ) ) {
1573 $cutoff =
$dbr->timestamp( $opts[
'from'] );
1575 $opts->
reset(
'from' );
1578 $conds[] =
'rc_timestamp >= ' .
$dbr->addQuotes( $cutoff );
1595 $rcQuery = RecentChange::getQueryInfo();
1596 $tables = array_merge( $tables, $rcQuery[
'tables'] );
1597 $fields = array_merge( $rcQuery[
'fields'], $fields );
1598 $join_conds = array_merge( $join_conds, $rcQuery[
'joins'] );
1609 if ( !$this->
runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
1617 return $dbr->select(
1628 &$query_options, &$join_conds, $opts
1630 return $this->
getHookRunner()->onChangesListSpecialPageQuery(
1631 $this->
getName(), $tables, $fields, $conds, $query_options, $join_conds, $opts );
1652 $this->
doHeader( $opts, $rowCount );
1739 $user = $context->getUser();
1740 # The legend showing what the letters and stuff mean
1741 $legend = Html::openElement(
'dl' ) .
"\n";
1742 # Iterates through them and gets the messages for both letter and tooltip
1743 $legendItems = $context->getConfig()->get(
'RecentChangesFlags' );
1744 if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
1745 unset( $legendItems[
'unpatrolled'] );
1747 foreach ( $legendItems as $key => $item ) { # generate items of the legend
1748 $label = $item[
'legend'] ?? $item[
'title'];
1749 $letter = $item[
'letter'];
1750 $cssClass = $item[
'class'] ?? $key;
1752 $legend .= Html::element(
'dt',
1753 [
'class' => $cssClass ], $context->msg( $letter )->text()
1755 Html::rawElement(
'dd',
1756 [
'class' => Sanitizer::escapeClass(
'mw-changeslist-legend-' . $key ) ],
1757 $context->msg( $label )->parse()
1761 $legend .= Html::rawElement(
'dt',
1762 [
'class' =>
'mw-plusminus-pos' ],
1763 $context->msg(
'recentchanges-legend-plusminus' )->parse()
1765 $legend .= Html::element(
1767 [
'class' =>
'mw-changeslist-legend-plusminus' ],
1768 $context->msg(
'recentchanges-label-plusminus' )->text()
1771 if ( $context->getConfig()->get(
'WatchlistExpiry' ) ) {
1772 $widget =
new IconWidget( [
1774 'classes' => [
'mw-changesList-watchlistExpiry' ],
1777 $watchlistLabelId =
'mw-changeslist-watchlistExpiry-label';
1778 $widget->getIconElement()->setAttributes( [
1780 'aria-labelledby' => $watchlistLabelId,
1782 $legend .= Html::rawElement(
1784 [
'class' =>
'mw-changeslist-legend-watchlistexpiry' ],
1787 $legend .= Html::element(
1789 [
'class' =>
'mw-changeslist-legend-watchlistexpiry',
'id' => $watchlistLabelId ],
1790 $context->msg(
'recentchanges-legend-watchlistexpiry' )->text()
1793 $legend .= Html::closeElement(
'dl' ) .
"\n";
1796 $context->msg(
'rcfilters-legend-heading' )->parse() :
1797 $context->msg(
'recentchanges-legend-heading' )->parse();
1800 $collapsedState = $this->
getRequest()->getCookie(
'changeslist-state' );
1801 $collapsedClass = $collapsedState ===
'collapsed' ?
' mw-collapsed' :
'';
1804 '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass .
'">' .
1806 '<div class="mw-collapsible-content">' . $legend .
'</div>' .
1818 $out->addModuleStyles( [
1819 'mediawiki.interface.helpers.styles',
1820 'mediawiki.special.changeslist.legend',
1821 'mediawiki.special.changeslist',
1823 $out->addModules(
'mediawiki.special.changeslist.legend.js' );
1826 $out->addModules(
'mediawiki.rcfilters.filters.ui' );
1827 $out->addModuleStyles(
'mediawiki.rcfilters.filters.base.styles' );
1852 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1862 if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1868 in_array(
'registered', $selectedExpLevels ) &&
1869 in_array(
'unregistered', $selectedExpLevels )
1874 $actorMigration = ActorMigration::newMigration();
1875 $actorQuery = $actorMigration->getJoin(
'rc_user' );
1876 $tables += $actorQuery[
'tables'];
1877 $join_conds += $actorQuery[
'joins'];
1881 in_array(
'registered', $selectedExpLevels ) &&
1882 !in_array(
'unregistered', $selectedExpLevels )
1884 $conds[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
1888 if ( $selectedExpLevels === [
'unregistered' ] ) {
1889 $conds[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
1894 $join_conds[
'user'] = [
'LEFT JOIN', $actorQuery[
'fields'][
'rc_user'] .
' = user_id' ];
1899 $secondsPerDay = 86400;
1903 $aboveNewcomer =
$dbr->makeList(
1906 'user_registration <= ' .
$dbr->addQuotes(
$dbr->timestamp( $learnerCutoff ) ),
1911 $aboveLearner =
$dbr->makeList(
1914 'user_registration <= ' .
1915 $dbr->addQuotes(
$dbr->timestamp( $experiencedUserCutoff ) ),
1922 if ( in_array(
'unregistered', $selectedExpLevels ) ) {
1923 $selectedExpLevels = array_diff( $selectedExpLevels, [
'unregistered' ] );
1924 $conditions[] = $actorMigration->isAnon( $actorQuery[
'fields'][
'rc_user'] );
1927 if ( $selectedExpLevels === [
'newcomer' ] ) {
1928 $conditions[] =
"NOT ( $aboveNewcomer )";
1929 } elseif ( $selectedExpLevels === [
'learner' ] ) {
1930 $conditions[] =
$dbr->makeList(
1931 [ $aboveNewcomer,
"NOT ( $aboveLearner )" ],
1934 } elseif ( $selectedExpLevels === [
'experienced' ] ) {
1935 $conditions[] = $aboveLearner;
1936 } elseif ( $selectedExpLevels === [
'learner',
'newcomer' ] ) {
1937 $conditions[] =
"NOT ( $aboveLearner )";
1938 } elseif ( $selectedExpLevels === [
'experienced',
'newcomer' ] ) {
1939 $conditions[] =
$dbr->makeList(
1940 [
"NOT ( $aboveNewcomer )", $aboveLearner ],
1943 } elseif ( $selectedExpLevels === [
'experienced',
'learner' ] ) {
1944 $conditions[] = $aboveNewcomer;
1945 } elseif ( $selectedExpLevels === [
'experienced',
'learner',
'newcomer' ] ) {
1946 $conditions[] = $actorMigration->isNotAnon( $actorQuery[
'fields'][
'rc_user'] );
1949 if ( count( $conditions ) > 1 ) {
1950 $conds[] =
$dbr->makeList( $conditions, IDatabase::LIST_OR );
1951 } elseif ( count( $conditions ) === 1 ) {
1952 $conds[] = reset( $conditions );
1962 if ( $this->
getRequest()->getBool(
'rcfilters' ) ) {
1966 return static::checkStructuredFilterUiEnabled( $this->
getUser() );
1977 if ( $user instanceof
Config ) {
1978 wfDeprecated( __METHOD__ .
' with Config argument',
'1.34' );
1979 $user = func_get_arg( 1 );
1981 return !$user->getOption(
'rcenhancedfilters-disable' );
1992 return $this->
getUser()->getIntOption( static::$limitPreferenceName );
2004 return floatval( $this->
getUser()->getOption( static::$daysPreferenceName ) );
2008 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2009 $symbolicFilters = [
2010 'all-contents' => $nsInfo->getSubjectNamespaces(),
2011 'all-discussions' => $nsInfo->getTalkNamespaces(),
2013 $additionalNamespaces = [];
2014 foreach ( $symbolicFilters as $name => $values ) {
2015 if ( in_array( $name, $namespaces ) ) {
2016 $additionalNamespaces = array_merge( $additionalNamespaces, $values );
2019 $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) );
2020 $namespaces = array_merge( $namespaces, $additionalNamespaces );
2021 return array_unique( $namespaces );
$wgLearnerMemberSince
Specify the difference engine to use.
$wgExperiencedUserMemberSince
Specify the difference engine to use.
$wgLearnerEdits
The following variables define 3 user experience levels:
$wgExperiencedUserEdits
Specify the difference engine to use.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
If the group is active, any unchecked filters will translate to hide parameters in the URL.
Represents a hide-based boolean filter (used on ChangesListSpecialPage and descendants)
Represents a filter group (used on ChangesListSpecialPage and descendants)
Represents a filter (used on ChangesListSpecialPage and descendants)
getConflictingFilters()
Get filters conflicting with this filter.
Special page which uses a ChangesList to show query results.
static getRcFiltersConfigVars(ResourceLoaderContext $context)
Get config vars to export with the mediawiki.rcfilters.filters.ui module.
const TAG_DESC_CHARACTER_LIMIT
Maximum length of a tag description in UTF-8 characters.
validateOptions(FormOptions $opts)
Validate a FormOptions object generated by getDefaultOptions() with values already populated.
getDefaultOptions()
Get a FormOptions object containing the default options.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
setTopText(FormOptions $opts)
Send the text to be displayed before the options.
filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now=0)
Filter on users' experience levels; this will not be called if nothing is selected.
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
getDefaultLimit()
Get the default value of the number of changes to display when loading the result set.
registerFiltersFromDefinitions(array $definition)
Register filters from a definition object.
convertParamsForLink( $params)
Convert parameters values from true/false to 1/0 so they are not omitted by wfArrayToCgi() T38524.
parseParameters( $par, FormOptions $opts)
Process $par and put options found in $opts.
static getChangeTagListSummary(ResourceLoaderContext $context)
Get information about change tags, without parsing messages, for getRcFiltersConfigSummary().
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
$hideCategorizationFilterDefinition
replaceOldOptions(FormOptions $opts)
Replace old options with their structured UI equivalents.
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
getExtraOptions( $opts)
Get options to be displayed in a form.
setup( $parameters)
Register all the filters, including legacy hook-driven ones.
static getChangeTagList(ResourceLoaderContext $context)
Get information about change tags to export to JS via getRcFiltersConfigVars().
static string $savedQueriesPreferenceName
Preference name for saved queries.
registerFilters()
Register all filters and their groups (including those from hooks), plus handle conflicts and default...
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
getDefaultDays()
Get the default value of the number of days to display when loading the result set.
fixBackwardsCompatibilityOptions(FormOptions $opts)
Fix a special case (hideanons=1 and hideliu=1) in a special way, for backwards compatibility.
expandSymbolicNamespaceFilters(array $namespaces)
outputNoResults()
Add the "no results" message to the output.
static string $limitPreferenceName
Preference name for 'limit'.
static string $daysPreferenceName
Preference name for 'days'.
getFilterGroups()
Gets the currently registered filters groups.
registerFilterGroup(ChangesListFilterGroup $group)
Register a structured changes list filter group.
addModules()
Add page-specific modules.
fixContradictoryOptions(FormOptions $opts)
Fix invalid options by resetting pairs that should never appear together.
static getRcFiltersConfigSummary(ResourceLoaderContext $context)
Get essential data about getRcFiltersConfigVars() for change detection.
__construct( $name, $restriction)
outputFeedLinks()
Output feed links.
$legacyReviewStatusFilterGroupDefinition
getLegacyShowHideFilters()
$reviewStatusFilterGroupDefinition
outputTimeout()
Add the "timeout" message to the output.
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
fetchOptionsFromRequest( $opts)
Fetch values for a FormOptions object from the WebRequest associated with this instance.
getOptions()
Get the current FormOptions for this request.
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
Process the query.
setBottomText(FormOptions $opts)
Send the text to be displayed after the options.
getStructuredFilterJsData()
Gets structured filter information needed by JS.
buildQuery(&$tables, &$fields, &$conds, &$query_options, &$join_conds, FormOptions $opts)
Sets appropriate tables, fields, conditions, etc.
webOutputHeader( $rowCount, $opts)
Send header output to the OutputPage object, only called if not using feeds.
ChangesListFilterGroup[] $filterGroups
Filter groups, and their contained filters This is an associative array (with group name as key) of C...
makeLegend()
Return the legend displayed within the fieldset.
webOutput( $rows, $opts)
Send output to the OutputPage object, only called if not used feeds.
considerActionsForDefaultSavedQuery( $subpage)
Check whether or not the page should load defaults, and if so, whether a default saved query is relev...
transformFilterDefinition(array $filterDefinition)
Transforms filter definition to prepare it for constructor.
getDB()
Return a IDatabase object for reading.
static checkStructuredFilterUiEnabled( $user)
Static method to check whether StructuredFilter UI is enabled for the given user.
outputChangesList( $rows, $opts)
Build and output the actual changes list.
getRows()
Get the database result for this special page instance.
array $filterGroupDefinitions
Definition information for the filters and their groups.
includeRcFiltersApp()
Include the modules and configuration for the RCFilters app.
static string $collapsedPreferenceName
Preference name for collapsing the active filter display.
Represents a filter group with multiple string options.
const SEPARATOR
Delimiter.
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Context object that contains information about the state of a specific ResourceLoader web request.
Parent class for all special pages.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
getName()
Get the name of this Special Page.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!...
getOutput()
Get the OutputPage being used for this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
including( $x=null)
Whether the special page is being evaluated via transclusion.
Class for fixing stale WANObjectCache keys using a purge event source.
Interface for configuration instances.