78 parent::__construct(
$name, $restriction );
80 $this->filterGroupDefinitions = [
82 'name' =>
'registration',
83 'title' =>
'rcfilters-filtergroup-registration',
88 'label' =>
'rcfilters-filter-registered-label',
89 'description' =>
'rcfilters-filter-registered-description',
92 'showHideSuffix' =>
'showhideliu',
94 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
95 &$query_options, &$join_conds ) {
97 $conds[] =
'rc_user = 0';
99 'cssClassSuffix' =>
'liu',
100 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
101 return $rc->getAttribute(
'rc_user' );
106 'name' =>
'hideanons',
107 'label' =>
'rcfilters-filter-unregistered-label',
108 'description' =>
'rcfilters-filter-unregistered-description',
111 'showHideSuffix' =>
'showhideanons',
113 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
114 &$query_options, &$join_conds ) {
116 $conds[] =
'rc_user != 0';
118 'cssClassSuffix' =>
'anon',
119 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
120 return !$rc->getAttribute(
'rc_user' );
127 'name' =>
'userExpLevel',
128 'title' =>
'rcfilters-filtergroup-userExpLevel',
131 'isFullCoverage' =>
false,
134 'name' =>
'newcomer',
135 'label' =>
'rcfilters-filter-user-experience-level-newcomer-label',
136 'description' =>
'rcfilters-filter-user-experience-level-newcomer-description',
137 'cssClassSuffix' =>
'user-newcomer',
138 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
139 $performer = $rc->getPerformer();
140 return $performer && $performer->isLoggedIn() &&
141 $performer->getExperienceLevel() ===
'newcomer';
146 'label' =>
'rcfilters-filter-user-experience-level-learner-label',
147 'description' =>
'rcfilters-filter-user-experience-level-learner-description',
148 'cssClassSuffix' =>
'user-learner',
149 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
150 $performer = $rc->getPerformer();
151 return $performer && $performer->isLoggedIn() &&
152 $performer->getExperienceLevel() ===
'learner';
156 'name' =>
'experienced',
157 'label' =>
'rcfilters-filter-user-experience-level-experienced-label',
158 'description' =>
'rcfilters-filter-user-experience-level-experienced-description',
159 'cssClassSuffix' =>
'user-experienced',
160 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
161 $performer = $rc->getPerformer();
162 return $performer && $performer->isLoggedIn() &&
163 $performer->getExperienceLevel() ===
'experienced';
168 'queryCallable' => [ $this,
'filterOnUserExperienceLevel' ],
172 'name' =>
'authorship',
173 'title' =>
'rcfilters-filtergroup-authorship',
177 'name' =>
'hidemyself',
178 'label' =>
'rcfilters-filter-editsbyself-label',
179 'description' =>
'rcfilters-filter-editsbyself-description',
182 'showHideSuffix' =>
'showhidemine',
184 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
185 &$query_options, &$join_conds ) {
187 $user = $ctx->getUser();
188 $conds[] =
'rc_user_text != ' .
$dbr->addQuotes(
$user->getName() );
190 'cssClassSuffix' =>
'self',
191 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
192 return $ctx->getUser()->equals( $rc->getPerformer() );
196 'name' =>
'hidebyothers',
197 'label' =>
'rcfilters-filter-editsbyother-label',
198 'description' =>
'rcfilters-filter-editsbyother-description',
200 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
201 &$query_options, &$join_conds ) {
203 $user = $ctx->getUser();
204 $conds[] =
'rc_user_text = ' .
$dbr->addQuotes(
$user->getName() );
206 'cssClassSuffix' =>
'others',
207 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
208 return !$ctx->getUser()->equals( $rc->getPerformer() );
215 'name' =>
'automated',
216 'title' =>
'rcfilters-filtergroup-automated',
220 'name' =>
'hidebots',
221 'label' =>
'rcfilters-filter-bots-label',
222 'description' =>
'rcfilters-filter-bots-description',
225 'showHideSuffix' =>
'showhidebots',
227 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
228 &$query_options, &$join_conds ) {
230 $conds[] =
'rc_bot = 0';
232 'cssClassSuffix' =>
'bot',
233 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
234 return $rc->getAttribute(
'rc_bot' );
238 'name' =>
'hidehumans',
239 'label' =>
'rcfilters-filter-humans-label',
240 'description' =>
'rcfilters-filter-humans-description',
242 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
243 &$query_options, &$join_conds ) {
245 $conds[] =
'rc_bot = 1';
247 'cssClassSuffix' =>
'human',
248 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
249 return !$rc->getAttribute(
'rc_bot' );
258 'name' =>
'significance',
259 'title' =>
'rcfilters-filtergroup-significance',
264 'name' =>
'hideminor',
265 'label' =>
'rcfilters-filter-minor-label',
266 'description' =>
'rcfilters-filter-minor-description',
269 'showHideSuffix' =>
'showhideminor',
271 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
272 &$query_options, &$join_conds ) {
274 $conds[] =
'rc_minor = 0';
276 'cssClassSuffix' =>
'minor',
277 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
278 return $rc->getAttribute(
'rc_minor' );
282 'name' =>
'hidemajor',
283 'label' =>
'rcfilters-filter-major-label',
284 'description' =>
'rcfilters-filter-major-description',
286 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
287 &$query_options, &$join_conds ) {
289 $conds[] =
'rc_minor = 1';
291 'cssClassSuffix' =>
'major',
292 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
293 return !$rc->getAttribute(
'rc_minor' );
301 'name' =>
'changeType',
302 'title' =>
'rcfilters-filtergroup-changetype',
306 'name' =>
'hidepageedits',
307 'label' =>
'rcfilters-filter-pageedits-label',
308 'description' =>
'rcfilters-filter-pageedits-description',
311 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
312 &$query_options, &$join_conds ) {
314 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_EDIT );
316 'cssClassSuffix' =>
'src-mw-edit',
317 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
322 'name' =>
'hidenewpages',
323 'label' =>
'rcfilters-filter-newpages-label',
324 'description' =>
'rcfilters-filter-newpages-description',
327 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
328 &$query_options, &$join_conds ) {
330 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_NEW );
332 'cssClassSuffix' =>
'src-mw-new',
333 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
342 'label' =>
'rcfilters-filter-logactions-label',
343 'description' =>
'rcfilters-filter-logactions-description',
346 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
347 &$query_options, &$join_conds ) {
349 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_LOG );
351 'cssClassSuffix' =>
'src-mw-log',
352 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
360 $this->reviewStatusFilterGroupDefinition = [
362 'name' =>
'reviewStatus',
363 'title' =>
'rcfilters-filtergroup-reviewstatus',
368 'name' =>
'hidepatrolled',
369 'label' =>
'rcfilters-filter-patrolled-label',
370 'description' =>
'rcfilters-filter-patrolled-description',
373 'showHideSuffix' =>
'showhidepatr',
375 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
376 &$query_options, &$join_conds ) {
378 $conds[] =
'rc_patrolled = 0';
380 'cssClassSuffix' =>
'patrolled',
381 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
382 return $rc->getAttribute(
'rc_patrolled' );
386 'name' =>
'hideunpatrolled',
387 'label' =>
'rcfilters-filter-unpatrolled-label',
388 'description' =>
'rcfilters-filter-unpatrolled-description',
390 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
391 &$query_options, &$join_conds ) {
393 $conds[] =
'rc_patrolled = 1';
395 'cssClassSuffix' =>
'unpatrolled',
396 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
397 return !$rc->getAttribute(
'rc_patrolled' );
404 $this->hideCategorizationFilterDefinition = [
405 'name' =>
'hidecategorization',
406 'label' =>
'rcfilters-filter-categorization-label',
407 'description' =>
'rcfilters-filter-categorization-description',
410 'showHideSuffix' =>
'showhidecategorization',
413 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &
$tables, &$fields, &$conds,
414 &$query_options, &$join_conds ) {
418 'cssClassSuffix' =>
'src-mw-categorize',
419 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
435 if ( $group->getConflictingGroups() ) {
438 " specifies conflicts with other groups but these are not supported yet."
443 foreach ( $group->getConflictingFilters()
as $conflictingFilter ) {
444 if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
450 foreach ( $group->getFilters()
as $filter ) {
453 foreach ( $filter->getConflictingFilters()
as $conflictingFilter ) {
455 $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
456 $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
475 $this->rcSubpage = $subpage;
483 if ( $rows ===
false ) {
487 $this->
getOutput()->setStatusCode( 404 );
494 foreach ( $rows
as $row ) {
497 $batch->add( $row->rc_namespace, $row->rc_title );
500 foreach ( $formatter->getPreloadTitles()
as $title ) {
510 if ( $this->
getConfig()->
get(
'EnableWANCacheReaper' ) ) {
514 LoggerFactory::getInstance(
'objectcache' )
524 '<div class="mw-changeslist-empty">' .
525 $this->
msg(
'recentchanges-noresult' )->parse() .
543 $this->
buildQuery(
$tables, $fields, $conds, $query_options, $join_conds, $opts );
545 return $this->
doMainQuery(
$tables, $fields, $conds, $query_options, $join_conds, $opts );
554 if ( $this->rcOptions ===
null ) {
555 $this->rcOptions = $this->
setup( $this->rcSubpage );
582 if ( $this->
getConfig()->
get(
'RCWatchCategoryMembership' ) ) {
584 $this->hideCategorizationFilterDefinition
587 $transformedHideCategorizationDef[
'group'] = $changeTypeGroup;
590 $transformedHideCategorizationDef
594 Hooks::run(
'ChangesListSpecialPageStructuredFilters', [ $this ] );
596 $unstructuredGroupDefinition =
605 $anons = $registration->getFilter(
'hideanons' );
610 $userExperienceLevel->conflictsWith(
612 'rcfilters-filtergroup-user-experience-level-conflicts-unregistered-global',
613 'rcfilters-filtergroup-user-experience-level-conflicts-unregistered',
614 'rcfilters-filter-unregistered-conflicts-user-experience-level'
617 $categoryFilter = $changeTypeGroup->getFilter(
'hidecategorization' );
618 $logactionsFilter = $changeTypeGroup->getFilter(
'hidelog' );
619 $pagecreationFilter = $changeTypeGroup->getFilter(
'hidenewpages' );
622 $hideMinorFilter = $significanceTypeGroup->getFilter(
'hideminor' );
625 if ( $categoryFilter !==
null ) {
626 $hideMinorFilter->conflictsWith(
628 'rcfilters-hideminor-conflicts-typeofchange-global',
629 'rcfilters-hideminor-conflicts-typeofchange',
630 'rcfilters-typeofchange-conflicts-hideminor'
633 $hideMinorFilter->conflictsWith(
635 'rcfilters-hideminor-conflicts-typeofchange-global',
636 'rcfilters-hideminor-conflicts-typeofchange',
637 'rcfilters-typeofchange-conflicts-hideminor'
639 $hideMinorFilter->conflictsWith(
641 'rcfilters-hideminor-conflicts-typeofchange-global',
642 'rcfilters-hideminor-conflicts-typeofchange',
643 'rcfilters-typeofchange-conflicts-hideminor'
657 return $filterDefinition;
669 $autoFillPriority = -1;
670 foreach ( $definition
as $groupDefinition ) {
671 if ( !isset( $groupDefinition[
'priority'] ) ) {
672 $groupDefinition[
'priority'] = $autoFillPriority;
675 $autoFillPriority = $groupDefinition[
'priority'];
680 $className = $groupDefinition[
'class'];
681 unset( $groupDefinition[
'class'] );
683 foreach ( $groupDefinition[
'filters']
as &$filterDefinition ) {
699 $unstructuredGroupDefinition = [
700 'name' =>
'unstructured',
707 $unstructuredGroupDefinition[
'filters'][] = [
710 'default' =>
$params[
'default'],
714 return $unstructuredGroupDefinition;
725 public function setup( $parameters ) {
733 if ( $parameters !==
null ) {
754 $structuredUI = $this->
getUser()->getOption(
'rcenhancedfilters' );
757 foreach ( $this->filterGroups
as $filterGroup ) {
760 if ( $filterGroup->isPerGroupRequestParameter() ) {
761 $opts->add( $filterGroup->getName(), $filterGroup->getDefault() );
763 foreach ( $filterGroup->getFilters()
as $filter ) {
764 $opts->add( $filter->getName(), $filter->getDefault( $structuredUI ) );
770 $opts->add(
'invert',
false );
771 $opts->add(
'associated',
false );
782 $groupName = $group->
getName();
784 $this->filterGroups[$groupName] = $group;
804 return isset( $this->filterGroups[$groupName] ) ?
805 $this->filterGroups[$groupName] :
829 usort( $this->filterGroups,
function ( $a, $b ) {
830 return $b->getPriority() - $a->getPriority();
833 foreach ( $this->filterGroups
as $groupName => $group ) {
834 $groupOutput = $group->getJsData( $this );
835 if ( $groupOutput !==
null ) {
836 $output[
'messageKeys'] = array_merge(
838 $groupOutput[
'messageKeys']
841 unset( $groupOutput[
'messageKeys'] );
842 $output[
'groups'][] = $groupOutput;
856 if ( $this->customFilters ===
null ) {
857 $this->customFilters = [];
858 Hooks::run(
'ChangesListSpecialPageFilters', [ $this, &$this->customFilters ],
'1.29' );
873 $opts->fetchValuesFromRequest( $this->
getRequest() );
885 $stringParameterNameSet = [];
886 $hideParameterNameSet = [];
891 foreach ( $this->filterGroups
as $filterGroup ) {
892 if ( $filterGroup->isPerGroupRequestParameter() ) {
893 $stringParameterNameSet[$filterGroup->getName()] =
true;
895 foreach ( $filterGroup->getFilters()
as $filter ) {
896 $hideParameterNameSet[$filter->getName()] =
true;
901 $bits = preg_split(
'/\s*,\s*/', trim( $par ) );
902 foreach ( $bits
as $bit ) {
904 if ( isset( $hideParameterNameSet[$bit] ) ) {
907 } elseif ( isset( $hideParameterNameSet[
"hide$bit"] ) ) {
909 $opts[
"hide$bit"] =
false;
910 } elseif ( preg_match(
'/^(.*)=(.*)$/', $bit, $m ) ) {
911 if ( isset( $stringParameterNameSet[$m[1]] ) ) {
912 $opts[$m[1]] = $m[2];
945 foreach ( $this->filterGroups
as $filterGroup ) {
948 if ( $filterGroup->isPerGroupRequestParameter() ) {
949 $filterGroup->modifyQuery(
$dbr, $this,
$tables, $fields, $conds,
950 $query_options, $join_conds, $opts[$filterGroup->getName()] );
952 foreach ( $filterGroup->getFilters()
as $filter ) {
953 if ( $opts[$filter->getName()] ) {
954 $filter->modifyQuery(
$dbr, $this,
$tables, $fields, $conds,
955 $query_options, $join_conds );
962 if ( $opts[
'namespace'] !==
'' ) {
963 $selectedNS =
$dbr->addQuotes( $opts[
'namespace'] );
964 $operator = $opts[
'invert'] ?
'!=' :
'=';
965 $boolean = $opts[
'invert'] ?
'AND' :
'OR';
968 if ( !$opts[
'associated'] ) {
969 $condition =
"rc_namespace $operator $selectedNS";
972 $associatedNS =
$dbr->addQuotes(
975 $condition =
"(rc_namespace $operator $selectedNS "
977 .
" rc_namespace $operator $associatedNS)";
980 $conds[] = $condition;
1027 return $dbr->select(
1038 &$query_options, &$join_conds, $opts
1041 'ChangesListSpecialPageQuery',
1042 [ $this->
getName(), &
$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ]
1064 $this->
doHeader( $opts, $rows->numRows() );
1140 # The legend showing what the letters and stuff mean
1142 # Iterates through them and gets the messages for both letter and tooltip
1144 if ( !(
$user->useRCPatrol() ||
$user->useNPPatrol() ) ) {
1145 unset( $legendItems[
'unpatrolled'] );
1147 foreach ( $legendItems
as $key => $item ) { # generate items
of the legend
1148 $label = isset( $item[
'legend'] ) ? $item[
'legend'] : $item[
'title'];
1149 $letter = $item[
'letter'];
1150 $cssClass = isset( $item[
'class'] ) ? $item[
'class'] : $key;
1153 [
'class' => $cssClass ],
$context->
msg( $letter )->text()
1156 [
'class' => Sanitizer::escapeClass(
'mw-changeslist-legend-' . $key ) ],
1162 [
'class' =>
'mw-plusminus-pos' ],
1163 $context->
msg(
'recentchanges-legend-plusminus' )->parse()
1167 [
'class' =>
'mw-changeslist-legend-plusminus' ],
1168 $context->
msg(
'recentchanges-label-plusminus' )->text()
1174 '<div class="mw-changeslist-legend">' .
1175 $context->
msg(
'recentchanges-legend-heading' )->parse() .
1176 '<div class="mw-collapsible-content">' . $legend .
'</div>' .
1188 $out->addModuleStyles( [
1189 'mediawiki.special.changeslist.legend',
1190 'mediawiki.special.changeslist',
1192 $out->addModules(
'mediawiki.special.changeslist.legend.js' );
1214 &
$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels ) {
1217 $wgExperiencedUserEdits,
1218 $wgLearnerMemberSince,
1219 $wgExperiencedUserMemberSince;
1225 if (
count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1226 $conds[] =
'rc_user != 0';
1231 $join_conds[
'user'] = [
'LEFT JOIN',
'rc_user = user_id' ];
1234 $secondsPerDay = 86400;
1235 $learnerCutoff = $now - $wgLearnerMemberSince * $secondsPerDay;
1236 $experiencedUserCutoff = $now - $wgExperiencedUserMemberSince * $secondsPerDay;
1238 $aboveNewcomer =
$dbr->makeList(
1240 'user_editcount >= ' . intval( $wgLearnerEdits ),
1241 'user_registration <= ' .
$dbr->timestamp( $learnerCutoff ),
1246 $aboveLearner =
$dbr->makeList(
1248 'user_editcount >= ' . intval( $wgExperiencedUserEdits ),
1249 'user_registration <= ' .
$dbr->timestamp( $experiencedUserCutoff ),
1254 if ( $selectedExpLevels === [
'newcomer' ] ) {
1255 $conds[] =
"NOT ( $aboveNewcomer )";
1256 } elseif ( $selectedExpLevels === [
'learner' ] ) {
1257 $conds[] =
$dbr->makeList(
1258 [ $aboveNewcomer,
"NOT ( $aboveLearner )" ],
1261 } elseif ( $selectedExpLevels === [
'experienced' ] ) {
1262 $conds[] = $aboveLearner;
1263 } elseif ( $selectedExpLevels === [
'learner',
'newcomer' ] ) {
1264 $conds[] =
"NOT ( $aboveLearner )";
1265 } elseif ( $selectedExpLevels === [
'experienced',
'newcomer' ] ) {
1266 $conds[] =
$dbr->makeList(
1267 [
"NOT ( $aboveNewcomer )", $aboveLearner ],
1270 } elseif ( $selectedExpLevels === [
'experienced',
'learner' ] ) {
1271 $conds[] = $aboveNewcomer;