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
131 $actorMigration = ActorMigration::newMigration();
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
149 $actorMigration = ActorMigration::newMigration();
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
238 $actorQuery = ActorMigration::newMigration()->getWhere(
$dbr,
'rc_user', $ctx->getUser() );
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
256 $actorQuery = ActorMigration::newMigration()
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 ) {
424 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_EDIT;
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 ) {
440 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_NEW;
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 ) {
459 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_LOG;
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
482 $conds[
'rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
484 'isReplacedInStructuredUi' =>
true,
487 'name' =>
'hideunpatrolled',
489 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables, &$fields, &$conds,
490 &$query_options, &$join_conds
492 $conds[] =
'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
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 ) {
514 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED;
519 'label' =>
'rcfilters-filter-reviewstatus-manual-label',
520 'description' =>
'rcfilters-filter-reviewstatus-manual-description',
521 'cssClassSuffix' =>
'reviewstatus-manual',
522 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
523 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_PATROLLED;
528 'label' =>
'rcfilters-filter-reviewstatus-auto-label',
529 'description' =>
'rcfilters-filter-reviewstatus-auto-description',
530 'cssClassSuffix' =>
'reviewstatus-auto',
531 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
532 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED;
537 'queryCallable' =>
function ( $specialPageClassName, $ctx,
$dbr,
538 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
540 if ( $selected === [] ) {
543 $rcPatrolledValues = [
544 'unpatrolled' => RecentChange::PRC_UNPATROLLED,
545 'manual' => RecentChange::PRC_PATROLLED,
546 'auto' => RecentChange::PRC_AUTOPATROLLED,
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 ) {
572 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_CATEGORIZE;
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 );
659 if ( $row->rc_source === RecentChange::SRC_LOG ) {
661 foreach ( $formatter->getPreloadTitles() as
$title ) {
675 MWExceptionHandler::logException( $timeoutException );
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
726 $savedQueries = FormatJson::decode(
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() ),
910 WANObjectCache::TTL_DAY,
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 ),
966 $language = Language::factory(
$context->getLanguage() );
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(
1293 $output[
'messageKeys'],
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 );
1588 $rcQuery = RecentChange::getQueryInfo();
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 )
1846 $actorMigration = ActorMigration::newMigration();
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 ) {
1922 $conds[] =
$dbr->makeList( $conditions, IDatabase::LIST_OR );
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 );
$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)
Throws 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.
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.
$filterGroups
Filter groups, and their contained filters This is an associative array (with group name as key) of C...
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.
$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.
getUser()
Shortcut to get the User executing 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,...
getContext()
Gets the context this SpecialPage is executed in.
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.