MediaWiki REL1_31
ChangesListSpecialPage.php
Go to the documentation of this file.
1<?php
28
35abstract class ChangesListSpecialPage extends SpecialPage {
41
47
52 protected static $daysPreferenceName;
53
58 protected static $limitPreferenceName;
59
61 protected $rcSubpage;
62
64 protected $rcOptions;
65
67 protected $customFilters;
68
69 // Order of both groups and filters is significant; first is top-most priority,
70 // descending from there.
71 // 'showHideSuffix' is a shortcut to and avoid spelling out
72 // details specific to subclasses here.
87
88 // Same format as filterGroupDefinitions, but for a single group (reviewStatus)
89 // that is registered conditionally.
91
92 // Single filter group registered conditionally
94
95 // Single filter group registered conditionally
97
104 protected $filterGroups = [];
105
106 public function __construct( $name, $restriction ) {
107 parent::__construct( $name, $restriction );
108
109 $nonRevisionTypes = [ RC_LOG ];
110 Hooks::run( 'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
111
112 $this->filterGroupDefinitions = [
113 [
114 'name' => 'registration',
115 'title' => 'rcfilters-filtergroup-registration',
116 'class' => ChangesListBooleanFilterGroup::class,
117 'filters' => [
118 [
119 'name' => 'hideliu',
120 // rcshowhideliu-show, rcshowhideliu-hide,
121 // wlshowhideliu
122 'showHideSuffix' => 'showhideliu',
123 'default' => false,
124 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
125 &$query_options, &$join_conds
126 ) {
127 $actorMigration = ActorMigration::newMigration();
128 $actorQuery = $actorMigration->getJoin( 'rc_user' );
129 $tables += $actorQuery['tables'];
130 $join_conds += $actorQuery['joins'];
131 $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
132 },
133 'isReplacedInStructuredUi' => true,
134
135 ],
136 [
137 'name' => 'hideanons',
138 // rcshowhideanons-show, rcshowhideanons-hide,
139 // wlshowhideanons
140 'showHideSuffix' => 'showhideanons',
141 'default' => false,
142 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
143 &$query_options, &$join_conds
144 ) {
145 $actorMigration = ActorMigration::newMigration();
146 $actorQuery = $actorMigration->getJoin( 'rc_user' );
147 $tables += $actorQuery['tables'];
148 $join_conds += $actorQuery['joins'];
149 $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
150 },
151 'isReplacedInStructuredUi' => true,
152 ]
153 ],
154 ],
155
156 [
157 'name' => 'userExpLevel',
158 'title' => 'rcfilters-filtergroup-userExpLevel',
159 'class' => ChangesListStringOptionsFilterGroup::class,
160 'isFullCoverage' => true,
161 'filters' => [
162 [
163 'name' => 'unregistered',
164 'label' => 'rcfilters-filter-user-experience-level-unregistered-label',
165 'description' => 'rcfilters-filter-user-experience-level-unregistered-description',
166 'cssClassSuffix' => 'user-unregistered',
167 'isRowApplicableCallable' => function ( $ctx, $rc ) {
168 return !$rc->getAttribute( 'rc_user' );
169 }
170 ],
171 [
172 'name' => 'registered',
173 'label' => 'rcfilters-filter-user-experience-level-registered-label',
174 'description' => 'rcfilters-filter-user-experience-level-registered-description',
175 'cssClassSuffix' => 'user-registered',
176 'isRowApplicableCallable' => function ( $ctx, $rc ) {
177 return $rc->getAttribute( 'rc_user' );
178 }
179 ],
180 [
181 'name' => 'newcomer',
182 'label' => 'rcfilters-filter-user-experience-level-newcomer-label',
183 'description' => 'rcfilters-filter-user-experience-level-newcomer-description',
184 'cssClassSuffix' => 'user-newcomer',
185 'isRowApplicableCallable' => function ( $ctx, $rc ) {
186 $performer = $rc->getPerformer();
187 return $performer && $performer->isLoggedIn() &&
188 $performer->getExperienceLevel() === 'newcomer';
189 }
190 ],
191 [
192 'name' => 'learner',
193 'label' => 'rcfilters-filter-user-experience-level-learner-label',
194 'description' => 'rcfilters-filter-user-experience-level-learner-description',
195 'cssClassSuffix' => 'user-learner',
196 'isRowApplicableCallable' => function ( $ctx, $rc ) {
197 $performer = $rc->getPerformer();
198 return $performer && $performer->isLoggedIn() &&
199 $performer->getExperienceLevel() === 'learner';
200 },
201 ],
202 [
203 'name' => 'experienced',
204 'label' => 'rcfilters-filter-user-experience-level-experienced-label',
205 'description' => 'rcfilters-filter-user-experience-level-experienced-description',
206 'cssClassSuffix' => 'user-experienced',
207 'isRowApplicableCallable' => function ( $ctx, $rc ) {
208 $performer = $rc->getPerformer();
209 return $performer && $performer->isLoggedIn() &&
210 $performer->getExperienceLevel() === 'experienced';
211 },
212 ]
213 ],
215 'queryCallable' => [ $this, 'filterOnUserExperienceLevel' ],
216 ],
217
218 [
219 'name' => 'authorship',
220 'title' => 'rcfilters-filtergroup-authorship',
221 'class' => ChangesListBooleanFilterGroup::class,
222 'filters' => [
223 [
224 'name' => 'hidemyself',
225 'label' => 'rcfilters-filter-editsbyself-label',
226 'description' => 'rcfilters-filter-editsbyself-description',
227 // rcshowhidemine-show, rcshowhidemine-hide,
228 // wlshowhidemine
229 'showHideSuffix' => 'showhidemine',
230 'default' => false,
231 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
232 &$query_options, &$join_conds
233 ) {
234 $actorQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $ctx->getUser() );
235 $tables += $actorQuery['tables'];
236 $join_conds += $actorQuery['joins'];
237 $conds[] = 'NOT(' . $actorQuery['conds'] . ')';
238 },
239 'cssClassSuffix' => 'self',
240 'isRowApplicableCallable' => function ( $ctx, $rc ) {
241 return $ctx->getUser()->equals( $rc->getPerformer() );
242 },
243 ],
244 [
245 'name' => 'hidebyothers',
246 'label' => 'rcfilters-filter-editsbyother-label',
247 'description' => 'rcfilters-filter-editsbyother-description',
248 'default' => false,
249 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
250 &$query_options, &$join_conds
251 ) {
252 $actorQuery = ActorMigration::newMigration()
253 ->getWhere( $dbr, 'rc_user', $ctx->getUser(), false );
254 $tables += $actorQuery['tables'];
255 $join_conds += $actorQuery['joins'];
256 $conds[] = $actorQuery['conds'];
257 },
258 'cssClassSuffix' => 'others',
259 'isRowApplicableCallable' => function ( $ctx, $rc ) {
260 return !$ctx->getUser()->equals( $rc->getPerformer() );
261 },
262 ]
263 ]
264 ],
265
266 [
267 'name' => 'automated',
268 'title' => 'rcfilters-filtergroup-automated',
269 'class' => ChangesListBooleanFilterGroup::class,
270 'filters' => [
271 [
272 'name' => 'hidebots',
273 'label' => 'rcfilters-filter-bots-label',
274 'description' => 'rcfilters-filter-bots-description',
275 // rcshowhidebots-show, rcshowhidebots-hide,
276 // wlshowhidebots
277 'showHideSuffix' => 'showhidebots',
278 'default' => false,
279 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
280 &$query_options, &$join_conds
281 ) {
282 $conds['rc_bot'] = 0;
283 },
284 'cssClassSuffix' => 'bot',
285 'isRowApplicableCallable' => function ( $ctx, $rc ) {
286 return $rc->getAttribute( 'rc_bot' );
287 },
288 ],
289 [
290 'name' => 'hidehumans',
291 'label' => 'rcfilters-filter-humans-label',
292 'description' => 'rcfilters-filter-humans-description',
293 'default' => false,
294 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
295 &$query_options, &$join_conds
296 ) {
297 $conds['rc_bot'] = 1;
298 },
299 'cssClassSuffix' => 'human',
300 'isRowApplicableCallable' => function ( $ctx, $rc ) {
301 return !$rc->getAttribute( 'rc_bot' );
302 },
303 ]
304 ]
305 ],
306
307 // significance (conditional)
308
309 [
310 'name' => 'significance',
311 'title' => 'rcfilters-filtergroup-significance',
312 'class' => ChangesListBooleanFilterGroup::class,
313 'priority' => -6,
314 'filters' => [
315 [
316 'name' => 'hideminor',
317 'label' => 'rcfilters-filter-minor-label',
318 'description' => 'rcfilters-filter-minor-description',
319 // rcshowhideminor-show, rcshowhideminor-hide,
320 // wlshowhideminor
321 'showHideSuffix' => 'showhideminor',
322 'default' => false,
323 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
324 &$query_options, &$join_conds
325 ) {
326 $conds[] = 'rc_minor = 0';
327 },
328 'cssClassSuffix' => 'minor',
329 'isRowApplicableCallable' => function ( $ctx, $rc ) {
330 return $rc->getAttribute( 'rc_minor' );
331 }
332 ],
333 [
334 'name' => 'hidemajor',
335 'label' => 'rcfilters-filter-major-label',
336 'description' => 'rcfilters-filter-major-description',
337 'default' => false,
338 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
339 &$query_options, &$join_conds
340 ) {
341 $conds[] = 'rc_minor = 1';
342 },
343 'cssClassSuffix' => 'major',
344 'isRowApplicableCallable' => function ( $ctx, $rc ) {
345 return !$rc->getAttribute( 'rc_minor' );
346 }
347 ]
348 ]
349 ],
350
351 [
352 'name' => 'lastRevision',
353 'title' => 'rcfilters-filtergroup-lastRevision',
354 'class' => ChangesListBooleanFilterGroup::class,
355 'priority' => -7,
356 'filters' => [
357 [
358 'name' => 'hidelastrevision',
359 'label' => 'rcfilters-filter-lastrevision-label',
360 'description' => 'rcfilters-filter-lastrevision-description',
361 'default' => false,
362 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
363 &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
364 $conds[] = $dbr->makeList(
365 [
366 'rc_this_oldid <> page_latest',
367 'rc_type' => $nonRevisionTypes,
368 ],
369 LIST_OR
370 );
371 },
372 'cssClassSuffix' => 'last',
373 'isRowApplicableCallable' => function ( $ctx, $rc ) {
374 return $rc->getAttribute( 'rc_this_oldid' ) === $rc->getAttribute( 'page_latest' );
375 }
376 ],
377 [
378 'name' => 'hidepreviousrevisions',
379 'label' => 'rcfilters-filter-previousrevision-label',
380 'description' => 'rcfilters-filter-previousrevision-description',
381 'default' => false,
382 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
383 &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
384 $conds[] = $dbr->makeList(
385 [
386 'rc_this_oldid = page_latest',
387 'rc_type' => $nonRevisionTypes,
388 ],
389 LIST_OR
390 );
391 },
392 'cssClassSuffix' => 'previous',
393 'isRowApplicableCallable' => function ( $ctx, $rc ) {
394 return $rc->getAttribute( 'rc_this_oldid' ) !== $rc->getAttribute( 'page_latest' );
395 }
396 ]
397 ]
398 ],
399
400 // With extensions, there can be change types that will not be hidden by any of these.
401 [
402 'name' => 'changeType',
403 'title' => 'rcfilters-filtergroup-changetype',
404 'class' => ChangesListBooleanFilterGroup::class,
405 'priority' => -8,
406 'filters' => [
407 [
408 'name' => 'hidepageedits',
409 'label' => 'rcfilters-filter-pageedits-label',
410 'description' => 'rcfilters-filter-pageedits-description',
411 'default' => false,
412 'priority' => -2,
413 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
414 &$query_options, &$join_conds
415 ) {
416 $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_EDIT );
417 },
418 'cssClassSuffix' => 'src-mw-edit',
419 'isRowApplicableCallable' => function ( $ctx, $rc ) {
420 return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_EDIT;
421 },
422 ],
423 [
424 'name' => 'hidenewpages',
425 'label' => 'rcfilters-filter-newpages-label',
426 'description' => 'rcfilters-filter-newpages-description',
427 'default' => false,
428 'priority' => -3,
429 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
430 &$query_options, &$join_conds
431 ) {
432 $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_NEW );
433 },
434 'cssClassSuffix' => 'src-mw-new',
435 'isRowApplicableCallable' => function ( $ctx, $rc ) {
436 return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW;
437 },
438 ],
439
440 // hidecategorization
441
442 [
443 'name' => 'hidelog',
444 'label' => 'rcfilters-filter-logactions-label',
445 'description' => 'rcfilters-filter-logactions-description',
446 'default' => false,
447 'priority' => -5,
448 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
449 &$query_options, &$join_conds
450 ) {
451 $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG );
452 },
453 'cssClassSuffix' => 'src-mw-log',
454 'isRowApplicableCallable' => function ( $ctx, $rc ) {
455 return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG;
456 }
457 ],
458 ],
459 ],
460
461 ];
462
463 $this->legacyReviewStatusFilterGroupDefinition = [
464 [
465 'name' => 'legacyReviewStatus',
466 'title' => 'rcfilters-filtergroup-reviewstatus',
467 'class' => ChangesListBooleanFilterGroup::class,
468 'filters' => [
469 [
470 'name' => 'hidepatrolled',
471 // rcshowhidepatr-show, rcshowhidepatr-hide
472 // wlshowhidepatr
473 'showHideSuffix' => 'showhidepatr',
474 'default' => false,
475 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
476 &$query_options, &$join_conds
477 ) {
478 $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
479 },
480 'isReplacedInStructuredUi' => true,
481 ],
482 [
483 'name' => 'hideunpatrolled',
484 'default' => false,
485 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
486 &$query_options, &$join_conds
487 ) {
488 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
489 },
490 'isReplacedInStructuredUi' => true,
491 ],
492 ],
493 ]
494 ];
495
496 $this->reviewStatusFilterGroupDefinition = [
497 [
498 'name' => 'reviewStatus',
499 'title' => 'rcfilters-filtergroup-reviewstatus',
500 'class' => ChangesListStringOptionsFilterGroup::class,
501 'isFullCoverage' => true,
502 'priority' => -5,
503 'filters' => [
504 [
505 'name' => 'unpatrolled',
506 'label' => 'rcfilters-filter-reviewstatus-unpatrolled-label',
507 'description' => 'rcfilters-filter-reviewstatus-unpatrolled-description',
508 'cssClassSuffix' => 'reviewstatus-unpatrolled',
509 'isRowApplicableCallable' => function ( $ctx, $rc ) {
510 return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED;
511 },
512 ],
513 [
514 'name' => 'manual',
515 'label' => 'rcfilters-filter-reviewstatus-manual-label',
516 'description' => 'rcfilters-filter-reviewstatus-manual-description',
517 'cssClassSuffix' => 'reviewstatus-manual',
518 'isRowApplicableCallable' => function ( $ctx, $rc ) {
519 return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_PATROLLED;
520 },
521 ],
522 [
523 'name' => 'auto',
524 'label' => 'rcfilters-filter-reviewstatus-auto-label',
525 'description' => 'rcfilters-filter-reviewstatus-auto-description',
526 'cssClassSuffix' => 'reviewstatus-auto',
527 'isRowApplicableCallable' => function ( $ctx, $rc ) {
528 return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED;
529 },
530 ],
531 ],
533 'queryCallable' => function ( $specialPageClassName, $ctx, $dbr,
534 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
535 ) {
536 if ( $selected === [] ) {
537 return;
538 }
539 $rcPatrolledValues = [
540 'unpatrolled' => RecentChange::PRC_UNPATROLLED,
541 'manual' => RecentChange::PRC_PATROLLED,
542 'auto' => RecentChange::PRC_AUTOPATROLLED,
543 ];
544 // e.g. rc_patrolled IN (0, 2)
545 $conds['rc_patrolled'] = array_map( function ( $s ) use ( $rcPatrolledValues ) {
546 return $rcPatrolledValues[ $s ];
547 }, $selected );
548 }
549 ]
550 ];
551
552 $this->hideCategorizationFilterDefinition = [
553 'name' => 'hidecategorization',
554 'label' => 'rcfilters-filter-categorization-label',
555 'description' => 'rcfilters-filter-categorization-description',
556 // rcshowhidecategorization-show, rcshowhidecategorization-hide.
557 // wlshowhidecategorization
558 'showHideSuffix' => 'showhidecategorization',
559 'default' => false,
560 'priority' => -4,
561 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
562 &$query_options, &$join_conds
563 ) {
564 $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE );
565 },
566 'cssClassSuffix' => 'src-mw-categorize',
567 'isRowApplicableCallable' => function ( $ctx, $rc ) {
568 return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE;
569 },
570 ];
571 }
572
578 protected function areFiltersInConflict() {
579 $opts = $this->getOptions();
581 foreach ( $this->getFilterGroups() as $group ) {
582 if ( $group->getConflictingGroups() ) {
584 $group->getName() .
585 " specifies conflicts with other groups but these are not supported yet."
586 );
587 }
588
590 foreach ( $group->getConflictingFilters() as $conflictingFilter ) {
591 if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
592 return true;
593 }
594 }
595
597 foreach ( $group->getFilters() as $filter ) {
599 foreach ( $filter->getConflictingFilters() as $conflictingFilter ) {
600 if (
601 $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
602 $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
603 ) {
604 return true;
605 }
606 }
607
608 }
609
610 }
611
612 return false;
613 }
614
620 public function execute( $subpage ) {
621 $this->rcSubpage = $subpage;
622
623 $this->considerActionsForDefaultSavedQuery( $subpage );
624
625 $opts = $this->getOptions();
626 try {
627 $rows = $this->getRows();
628 if ( $rows === false ) {
629 $rows = new FakeResultWrapper( [] );
630 }
631
632 // Used by Structured UI app to get results without MW chrome
633 if ( $this->getRequest()->getVal( 'action' ) === 'render' ) {
634 $this->getOutput()->setArticleBodyOnly( true );
635 }
636
637 // Used by "live update" and "view newest" to check
638 // if there's new changes with minimal data transfer
639 if ( $this->getRequest()->getBool( 'peek' ) ) {
640 $code = $rows->numRows() > 0 ? 200 : 204;
641 $this->getOutput()->setStatusCode( $code );
642
643 if ( $this->getUser()->isAnon() !==
644 $this->getRequest()->getFuzzyBool( 'isAnon' )
645 ) {
646 $this->getOutput()->setStatusCode( 205 );
647 }
648
649 return;
650 }
651
652 $batch = new LinkBatch;
653 foreach ( $rows as $row ) {
654 $batch->add( NS_USER, $row->rc_user_text );
655 $batch->add( NS_USER_TALK, $row->rc_user_text );
656 $batch->add( $row->rc_namespace, $row->rc_title );
657 if ( $row->rc_source === RecentChange::SRC_LOG ) {
658 $formatter = LogFormatter::newFromRow( $row );
659 foreach ( $formatter->getPreloadTitles() as $title ) {
660 $batch->addObj( $title );
661 }
662 }
663 }
664 $batch->execute();
665
666 $this->setHeaders();
667 $this->outputHeader();
668 $this->addModules();
669 $this->webOutput( $rows, $opts );
670
671 $rows->free();
672 } catch ( DBQueryTimeoutError $timeoutException ) {
673 MWExceptionHandler::logException( $timeoutException );
674
675 $this->setHeaders();
676 $this->outputHeader();
677 $this->addModules();
678
679 $this->getOutput()->setStatusCode( 500 );
680 $this->webOutputHeader( 0, $opts );
681 $this->outputTimeout();
682 }
683
684 if ( $this->getConfig()->get( 'EnableWANCacheReaper' ) ) {
685 // Clean up any bad page entries for titles showing up in RC
686 DeferredUpdates::addUpdate( new WANCacheReapUpdate(
687 $this->getDB(),
688 LoggerFactory::getInstance( 'objectcache' )
689 ) );
690 }
691
692 $this->includeRcFiltersApp();
693 }
694
702 protected function considerActionsForDefaultSavedQuery( $subpage ) {
703 if ( !$this->isStructuredFilterUiEnabled() || $this->including() ) {
704 return;
705 }
706
707 $knownParams = call_user_func_array(
708 [ $this->getRequest(), 'getValues' ],
709 array_keys( $this->getOptions()->getAllValues() )
710 );
711
712 // HACK: Temporarily until we can properly define "sticky" filters and parameters,
713 // we need to exclude several parameters we know should not be counted towards preventing
714 // the loading of defaults.
715 $excludedParams = [ 'limit' => '', 'days' => '', 'enhanced' => '', 'from' => '' ];
716 $knownParams = array_diff_key( $knownParams, $excludedParams );
717
718 if (
719 // If there are NO known parameters in the URL request
720 // (that are not excluded) then we need to check into loading
721 // the default saved query
722 count( $knownParams ) === 0
723 ) {
724 // Get the saved queries data and parse it
725 $savedQueries = FormatJson::decode(
726 $this->getUser()->getOption( static::$savedQueriesPreferenceName ),
727 true
728 );
729
730 if ( $savedQueries && isset( $savedQueries[ 'default' ] ) ) {
731 // Only load queries that are 'version' 2, since those
732 // have parameter representation
733 if ( isset( $savedQueries[ 'version' ] ) && $savedQueries[ 'version' ] === '2' ) {
734 $savedQueryDefaultID = $savedQueries[ 'default' ];
735 $defaultQuery = $savedQueries[ 'queries' ][ $savedQueryDefaultID ][ 'data' ];
736
737 // Build the entire parameter list
738 $query = array_merge(
739 $defaultQuery[ 'params' ],
740 $defaultQuery[ 'highlights' ],
741 [
742 'urlversion' => '2',
743 ]
744 );
745 // Add to the query any parameters that we may have ignored before
746 // but are still valid and requested in the URL
747 $query = array_merge( $this->getRequest()->getValues(), $query );
748 unset( $query[ 'title' ] );
749 $this->getOutput()->redirect( $this->getPageTitle( $subpage )->getCanonicalURL( $query ) );
750 } else {
751 // There's a default, but the version is not 2, and the server can't
752 // actually recognize the query itself. This happens if it is before
753 // the conversion, so we need to tell the UI to reload saved query as
754 // it does the conversion to version 2
755 $this->getOutput()->addJsConfigVars(
756 'wgStructuredChangeFiltersDefaultSavedQueryExists',
757 true
758 );
759
760 // Add the class that tells the frontend it is still loading
761 // another query
762 $this->getOutput()->addBodyClasses( 'mw-rcfilters-ui-loading' );
763 }
764 }
765 }
766 }
767
774 protected function includeRcFiltersApp() {
775 $out = $this->getOutput();
776 if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
777 $jsData = $this->getStructuredFilterJsData();
778
779 $messages = [];
780 foreach ( $jsData['messageKeys'] as $key ) {
781 $messages[$key] = $this->msg( $key )->plain();
782 }
783
784 $out->addBodyClasses( 'mw-rcfilters-enabled' );
785
786 $out->addHTML(
789 )
790 );
791
792 $out->addJsConfigVars( 'wgStructuredChangeFilters', $jsData['groups'] );
793
794 $out->addJsConfigVars(
795 'wgRCFiltersChangeTags',
796 $this->getChangeTagList()
797 );
798 $out->addJsConfigVars(
799 'StructuredChangeFiltersDisplayConfig',
800 [
801 'maxDays' => (int)$this->getConfig()->get( 'RCMaxAge' ) / ( 24 * 3600 ), // Translate to days
802 'limitArray' => $this->getConfig()->get( 'RCLinkLimits' ),
803 'limitDefault' => $this->getDefaultLimit(),
804 'daysArray' => $this->getConfig()->get( 'RCLinkDays' ),
805 'daysDefault' => $this->getDefaultDays(),
806 ]
807 );
808
809 $out->addJsConfigVars(
810 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
811 static::$savedQueriesPreferenceName
812 );
813 $out->addJsConfigVars(
814 'wgStructuredChangeFiltersLimitPreferenceName',
815 static::$limitPreferenceName
816 );
817 $out->addJsConfigVars(
818 'wgStructuredChangeFiltersDaysPreferenceName',
819 static::$daysPreferenceName
820 );
821
822 $out->addJsConfigVars(
823 'StructuredChangeFiltersLiveUpdatePollingRate',
824 $this->getConfig()->get( 'StructuredChangeFiltersLiveUpdatePollingRate' )
825 );
826 } else {
827 $out->addBodyClasses( 'mw-rcfilters-disabled' );
828 }
829 }
830
836 protected function getChangeTagList() {
837 $cache = ObjectCache::getMainWANInstance();
838 $context = $this->getContext();
839 return $cache->getWithSetCallback(
840 $cache->makeKey( 'changeslistspecialpage-changetags', $context->getLanguage()->getCode() ),
841 $cache::TTL_MINUTE * 10,
842 function () use ( $context ) {
843 $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
844 $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
845
846 // Hit counts disabled for perf reasons, see T169997
847 /*
848 $tagStats = ChangeTags::tagUsageStatistics();
849 $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
850
851 // Sort by hits
852 arsort( $tagHitCounts );
853 */
854 $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags );
855
856 // Build the list and data
857 $result = [];
858 foreach ( $tagHitCounts as $tagName => $hits ) {
859 if (
860 // Only get active tags
861 isset( $explicitlyDefinedTags[ $tagName ] ) ||
862 isset( $softwareActivatedTags[ $tagName ] )
863 ) {
864 $result[] = [
865 'name' => $tagName,
866 'label' => Sanitizer::stripAllTags(
868 ),
869 'description' =>
871 $tagName, self::TAG_DESC_CHARACTER_LIMIT, $context
872 ),
873 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
874 'hits' => $hits,
875 ];
876 }
877 }
878
879 // Instead of sorting by hit count (disabled, see above), sort by display name
880 usort( $result, function ( $a, $b ) {
881 return strcasecmp( $a['label'], $b['label'] );
882 } );
883
884 return $result;
885 },
886 [
887 'lockTSE' => 30
888 ]
889 );
890 }
891
895 protected function outputNoResults() {
896 $this->getOutput()->addHTML(
897 '<div class="mw-changeslist-empty">' .
898 $this->msg( 'recentchanges-noresult' )->parse() .
899 '</div>'
900 );
901 }
902
906 protected function outputTimeout() {
907 $this->getOutput()->addHTML(
908 '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
909 $this->msg( 'recentchanges-timeout' )->parse() .
910 '</div>'
911 );
912 }
913
919 public function getRows() {
920 $opts = $this->getOptions();
921
922 $tables = [];
923 $fields = [];
924 $conds = [];
925 $query_options = [];
926 $join_conds = [];
927 $this->buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
928
929 return $this->doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
930 }
931
937 public function getOptions() {
938 if ( $this->rcOptions === null ) {
939 $this->rcOptions = $this->setup( $this->rcSubpage );
940 }
941
942 return $this->rcOptions;
943 }
944
954 protected function registerFilters() {
955 $this->registerFiltersFromDefinitions( $this->filterGroupDefinitions );
956
957 // Make sure this is not being transcluded (we don't want to show this
958 // information to all users just because the user that saves the edit can
959 // patrol or is logged in)
960 if ( !$this->including() && $this->getUser()->useRCPatrol() ) {
961 $this->registerFiltersFromDefinitions( $this->legacyReviewStatusFilterGroupDefinition );
962 $this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition );
963 }
964
965 $changeTypeGroup = $this->getFilterGroup( 'changeType' );
966
967 if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) {
968 $transformedHideCategorizationDef = $this->transformFilterDefinition(
969 $this->hideCategorizationFilterDefinition
970 );
971
972 $transformedHideCategorizationDef['group'] = $changeTypeGroup;
973
974 $hideCategorization = new ChangesListBooleanFilter(
975 $transformedHideCategorizationDef
976 );
977 }
978
979 Hooks::run( 'ChangesListSpecialPageStructuredFilters', [ $this ] );
980
981 $unstructuredGroupDefinition =
983 $this->getCustomFilters()
984 );
985 $this->registerFiltersFromDefinitions( [ $unstructuredGroupDefinition ] );
986
987 $userExperienceLevel = $this->getFilterGroup( 'userExpLevel' );
988 $registered = $userExperienceLevel->getFilter( 'registered' );
989 $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'newcomer' ) );
990 $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'learner' ) );
991 $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'experienced' ) );
992
993 $categoryFilter = $changeTypeGroup->getFilter( 'hidecategorization' );
994 $logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' );
995 $pagecreationFilter = $changeTypeGroup->getFilter( 'hidenewpages' );
996
997 $significanceTypeGroup = $this->getFilterGroup( 'significance' );
998 $hideMinorFilter = $significanceTypeGroup->getFilter( 'hideminor' );
999
1000 // categoryFilter is conditional; see registerFilters
1001 if ( $categoryFilter !== null ) {
1002 $hideMinorFilter->conflictsWith(
1003 $categoryFilter,
1004 'rcfilters-hideminor-conflicts-typeofchange-global',
1005 'rcfilters-hideminor-conflicts-typeofchange',
1006 'rcfilters-typeofchange-conflicts-hideminor'
1007 );
1008 }
1009 $hideMinorFilter->conflictsWith(
1010 $logactionsFilter,
1011 'rcfilters-hideminor-conflicts-typeofchange-global',
1012 'rcfilters-hideminor-conflicts-typeofchange',
1013 'rcfilters-typeofchange-conflicts-hideminor'
1014 );
1015 $hideMinorFilter->conflictsWith(
1016 $pagecreationFilter,
1017 'rcfilters-hideminor-conflicts-typeofchange-global',
1018 'rcfilters-hideminor-conflicts-typeofchange',
1019 'rcfilters-typeofchange-conflicts-hideminor'
1020 );
1021 }
1022
1032 protected function transformFilterDefinition( array $filterDefinition ) {
1033 return $filterDefinition;
1034 }
1035
1045 protected function registerFiltersFromDefinitions( array $definition ) {
1046 $autoFillPriority = -1;
1047 foreach ( $definition as $groupDefinition ) {
1048 if ( !isset( $groupDefinition['priority'] ) ) {
1049 $groupDefinition['priority'] = $autoFillPriority;
1050 } else {
1051 // If it's explicitly specified, start over the auto-fill
1052 $autoFillPriority = $groupDefinition['priority'];
1053 }
1054
1055 $autoFillPriority--;
1056
1057 $className = $groupDefinition['class'];
1058 unset( $groupDefinition['class'] );
1059
1060 foreach ( $groupDefinition['filters'] as &$filterDefinition ) {
1061 $filterDefinition = $this->transformFilterDefinition( $filterDefinition );
1062 }
1063
1064 $this->registerFilterGroup( new $className( $groupDefinition ) );
1065 }
1066 }
1067
1075 // Special internal unstructured group
1076 $unstructuredGroupDefinition = [
1077 'name' => 'unstructured',
1078 'class' => ChangesListBooleanFilterGroup::class,
1079 'priority' => -1, // Won't display in structured
1080 'filters' => [],
1081 ];
1082
1083 foreach ( $customFilters as $name => $params ) {
1084 $unstructuredGroupDefinition['filters'][] = [
1085 'name' => $name,
1086 'showHide' => $params['msg'],
1087 'default' => $params['default'],
1088 ];
1089 }
1090
1091 return $unstructuredGroupDefinition;
1092 }
1093
1097 protected function getLegacyShowHideFilters() {
1098 $filters = [];
1099 foreach ( $this->filterGroups as $group ) {
1100 if ( $group instanceof ChangesListBooleanFilterGroup ) {
1101 foreach ( $group->getFilters() as $key => $filter ) {
1102 if ( $filter->displaysOnUnstructuredUi( $this ) ) {
1103 $filters[ $key ] = $filter;
1104 }
1105 }
1106 }
1107 }
1108 return $filters;
1109 }
1110
1119 public function setup( $parameters ) {
1120 $this->registerFilters();
1121
1122 $opts = $this->getDefaultOptions();
1123
1124 $opts = $this->fetchOptionsFromRequest( $opts );
1125
1126 // Give precedence to subpage syntax
1127 if ( $parameters !== null ) {
1128 $this->parseParameters( $parameters, $opts );
1129 }
1130
1131 $this->validateOptions( $opts );
1132
1133 return $opts;
1134 }
1135
1145 public function getDefaultOptions() {
1146 $opts = new FormOptions();
1147 $structuredUI = $this->isStructuredFilterUiEnabled();
1148 // If urlversion=2 is set, ignore the filter defaults and set them all to false/empty
1149 $useDefaults = $this->getRequest()->getInt( 'urlversion' ) !== 2;
1150
1152 foreach ( $this->filterGroups as $filterGroup ) {
1153 $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1154 }
1155
1156 $opts->add( 'namespace', '', FormOptions::STRING );
1157 $opts->add( 'invert', false );
1158 $opts->add( 'associated', false );
1159 $opts->add( 'urlversion', 1 );
1160 $opts->add( 'tagfilter', '' );
1161
1162 $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT );
1163 $opts->add( 'limit', $this->getDefaultLimit(), FormOptions::INT );
1164
1165 $opts->add( 'from', '' );
1166
1167 return $opts;
1168 }
1169
1176 $groupName = $group->getName();
1177
1178 $this->filterGroups[$groupName] = $group;
1179 }
1180
1186 protected function getFilterGroups() {
1187 return $this->filterGroups;
1188 }
1189
1197 public function getFilterGroup( $groupName ) {
1198 return isset( $this->filterGroups[$groupName] ) ?
1199 $this->filterGroups[$groupName] :
1200 null;
1201 }
1202
1203 // Currently, this intentionally only includes filters that display
1204 // in the structured UI. This can be changed easily, though, if we want
1205 // to include data on filters that use the unstructured UI. messageKeys is a
1206 // special top-level value, with the value being an array of the message keys to
1207 // send to the client.
1215 public function getStructuredFilterJsData() {
1216 $output = [
1217 'groups' => [],
1218 'messageKeys' => [],
1219 ];
1220
1221 usort( $this->filterGroups, function ( $a, $b ) {
1222 return $b->getPriority() - $a->getPriority();
1223 } );
1224
1225 foreach ( $this->filterGroups as $groupName => $group ) {
1226 $groupOutput = $group->getJsData( $this );
1227 if ( $groupOutput !== null ) {
1228 $output['messageKeys'] = array_merge(
1229 $output['messageKeys'],
1230 $groupOutput['messageKeys']
1231 );
1232
1233 unset( $groupOutput['messageKeys'] );
1234 $output['groups'][] = $groupOutput;
1235 }
1236 }
1237
1238 return $output;
1239 }
1240
1247 protected function getCustomFilters() {
1248 if ( $this->customFilters === null ) {
1249 $this->customFilters = [];
1250 Hooks::run( 'ChangesListSpecialPageFilters', [ $this, &$this->customFilters ], '1.29' );
1251 }
1252
1253 return $this->customFilters;
1254 }
1255
1264 protected function fetchOptionsFromRequest( $opts ) {
1265 $opts->fetchValuesFromRequest( $this->getRequest() );
1266
1267 return $opts;
1268 }
1269
1276 public function parseParameters( $par, FormOptions $opts ) {
1277 $stringParameterNameSet = [];
1278 $hideParameterNameSet = [];
1279
1280 // URL parameters can be per-group, like 'userExpLevel',
1281 // or per-filter, like 'hideminor'.
1282
1283 foreach ( $this->filterGroups as $filterGroup ) {
1284 if ( $filterGroup instanceof ChangesListStringOptionsFilterGroup ) {
1285 $stringParameterNameSet[$filterGroup->getName()] = true;
1286 } elseif ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
1287 foreach ( $filterGroup->getFilters() as $filter ) {
1288 $hideParameterNameSet[$filter->getName()] = true;
1289 }
1290 }
1291 }
1292
1293 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
1294 foreach ( $bits as $bit ) {
1295 $m = [];
1296 if ( isset( $hideParameterNameSet[$bit] ) ) {
1297 // hidefoo => hidefoo=true
1298 $opts[$bit] = true;
1299 } elseif ( isset( $hideParameterNameSet["hide$bit"] ) ) {
1300 // foo => hidefoo=false
1301 $opts["hide$bit"] = false;
1302 } elseif ( preg_match( '/^(.*)=(.*)$/', $bit, $m ) ) {
1303 if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1304 $opts[$m[1]] = $m[2];
1305 }
1306 }
1307 }
1308 }
1309
1315 public function validateOptions( FormOptions $opts ) {
1316 $isContradictory = $this->fixContradictoryOptions( $opts );
1317 $isReplaced = $this->replaceOldOptions( $opts );
1318
1319 if ( $isContradictory || $isReplaced ) {
1321 $this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) );
1322 }
1323
1324 $opts->validateIntBounds( 'limit', 0, 5000 );
1325 $opts->validateBounds( 'days', 0, $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
1326 }
1327
1334 private function fixContradictoryOptions( FormOptions $opts ) {
1335 $fixed = $this->fixBackwardsCompatibilityOptions( $opts );
1336
1337 foreach ( $this->filterGroups as $filterGroup ) {
1338 if ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
1339 $filters = $filterGroup->getFilters();
1340
1341 if ( count( $filters ) === 1 ) {
1342 // legacy boolean filters should not be considered
1343 continue;
1344 }
1345
1346 $allInGroupEnabled = array_reduce(
1347 $filters,
1348 function ( $carry, $filter ) use ( $opts ) {
1349 return $carry && $opts[ $filter->getName() ];
1350 },
1351 /* initialValue */ count( $filters ) > 0
1352 );
1353
1354 if ( $allInGroupEnabled ) {
1355 foreach ( $filters as $filter ) {
1356 $opts[ $filter->getName() ] = false;
1357 }
1358
1359 $fixed = true;
1360 }
1361 }
1362 }
1363
1364 return $fixed;
1365 }
1366
1377 if ( $opts['hideanons'] && $opts['hideliu'] ) {
1378 $opts->reset( 'hideanons' );
1379 if ( !$opts['hidebots'] ) {
1380 $opts->reset( 'hideliu' );
1381 $opts['hidehumans'] = 1;
1382 }
1383
1384 return true;
1385 }
1386
1387 return false;
1388 }
1389
1396 public function replaceOldOptions( FormOptions $opts ) {
1397 if ( !$this->isStructuredFilterUiEnabled() ) {
1398 return false;
1399 }
1400
1401 $changed = false;
1402
1403 // At this point 'hideanons' and 'hideliu' cannot be both true,
1404 // because fixBackwardsCompatibilityOptions resets (at least) 'hideanons' in such case
1405 if ( $opts[ 'hideanons' ] ) {
1406 $opts->reset( 'hideanons' );
1407 $opts[ 'userExpLevel' ] = 'registered';
1408 $changed = true;
1409 }
1410
1411 if ( $opts[ 'hideliu' ] ) {
1412 $opts->reset( 'hideliu' );
1413 $opts[ 'userExpLevel' ] = 'unregistered';
1414 $changed = true;
1415 }
1416
1417 if ( $this->getFilterGroup( 'legacyReviewStatus' ) ) {
1418 if ( $opts[ 'hidepatrolled' ] ) {
1419 $opts->reset( 'hidepatrolled' );
1420 $opts[ 'reviewStatus' ] = 'unpatrolled';
1421 $changed = true;
1422 }
1423
1424 if ( $opts[ 'hideunpatrolled' ] ) {
1425 $opts->reset( 'hideunpatrolled' );
1426 $opts[ 'reviewStatus' ] = implode(
1428 [ 'manual', 'auto' ]
1429 );
1430 $changed = true;
1431 }
1432 }
1433
1434 return $changed;
1435 }
1436
1445 protected function convertParamsForLink( $params ) {
1446 foreach ( $params as &$value ) {
1447 if ( $value === false ) {
1448 $value = '0';
1449 }
1450 }
1451 unset( $value );
1452 return $params;
1453 }
1454
1466 protected function buildQuery( &$tables, &$fields, &$conds, &$query_options,
1467 &$join_conds, FormOptions $opts
1468 ) {
1469 $dbr = $this->getDB();
1470 $isStructuredUI = $this->isStructuredFilterUiEnabled();
1471
1473 foreach ( $this->filterGroups as $filterGroup ) {
1474 $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds,
1475 $query_options, $join_conds, $opts, $isStructuredUI );
1476 }
1477
1478 // Namespace filtering
1479 if ( $opts[ 'namespace' ] !== '' ) {
1480 $namespaces = explode( ';', $opts[ 'namespace' ] );
1481
1482 if ( $opts[ 'associated' ] ) {
1483 $associatedNamespaces = array_map(
1484 function ( $ns ) {
1485 return MWNamespace::getAssociated( $ns );
1486 },
1488 );
1489 $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
1490 }
1491
1492 if ( count( $namespaces ) === 1 ) {
1493 $operator = $opts[ 'invert' ] ? '!=' : '=';
1494 $value = $dbr->addQuotes( reset( $namespaces ) );
1495 } else {
1496 $operator = $opts[ 'invert' ] ? 'NOT IN' : 'IN';
1497 sort( $namespaces );
1498 $value = '(' . $dbr->makeList( $namespaces ) . ')';
1499 }
1500 $conds[] = "rc_namespace $operator $value";
1501 }
1502
1503 // Calculate cutoff
1504 $cutoff_unixtime = time() - $opts['days'] * 3600 * 24;
1505 $cutoff = $dbr->timestamp( $cutoff_unixtime );
1506
1507 $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
1508 if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
1509 $cutoff = $dbr->timestamp( $opts['from'] );
1510 } else {
1511 $opts->reset( 'from' );
1512 }
1513
1514 $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
1515 }
1516
1528 protected function doMainQuery( $tables, $fields, $conds,
1529 $query_options, $join_conds, FormOptions $opts
1530 ) {
1531 $rcQuery = RecentChange::getQueryInfo();
1532 $tables = array_merge( $tables, $rcQuery['tables'] );
1533 $fields = array_merge( $rcQuery['fields'], $fields );
1534 $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
1535
1537 $tables,
1538 $fields,
1539 $conds,
1540 $join_conds,
1541 $query_options,
1542 ''
1543 );
1544
1545 if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
1546 $opts )
1547 ) {
1548 return false;
1549 }
1550
1551 $dbr = $this->getDB();
1552
1553 return $dbr->select(
1554 $tables,
1555 $fields,
1556 $conds,
1557 __METHOD__,
1558 $query_options,
1559 $join_conds
1560 );
1561 }
1562
1563 protected function runMainQueryHook( &$tables, &$fields, &$conds,
1564 &$query_options, &$join_conds, $opts
1565 ) {
1566 return Hooks::run(
1567 'ChangesListSpecialPageQuery',
1568 [ $this->getName(), &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ]
1569 );
1570 }
1571
1577 protected function getDB() {
1578 return wfGetDB( DB_REPLICA );
1579 }
1580
1587 private function webOutputHeader( $rowCount, $opts ) {
1588 if ( !$this->including() ) {
1589 $this->outputFeedLinks();
1590 $this->doHeader( $opts, $rowCount );
1591 }
1592 }
1593
1600 public function webOutput( $rows, $opts ) {
1601 $this->webOutputHeader( $rows->numRows(), $opts );
1602
1603 $this->outputChangesList( $rows, $opts );
1604 }
1605
1609 public function outputFeedLinks() {
1610 // nothing by default
1611 }
1612
1619 abstract public function outputChangesList( $rows, $opts );
1620
1627 public function doHeader( $opts, $numRows ) {
1628 $this->setTopText( $opts );
1629
1630 // @todo Lots of stuff should be done here.
1631
1632 $this->setBottomText( $opts );
1633 }
1634
1641 public function setTopText( FormOptions $opts ) {
1642 // nothing by default
1643 }
1644
1651 public function setBottomText( FormOptions $opts ) {
1652 // nothing by default
1653 }
1654
1664 public function getExtraOptions( $opts ) {
1665 return [];
1666 }
1667
1673 public function makeLegend() {
1674 $context = $this->getContext();
1675 $user = $context->getUser();
1676 # The legend showing what the letters and stuff mean
1677 $legend = Html::openElement( 'dl' ) . "\n";
1678 # Iterates through them and gets the messages for both letter and tooltip
1679 $legendItems = $context->getConfig()->get( 'RecentChangesFlags' );
1680 if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
1681 unset( $legendItems['unpatrolled'] );
1682 }
1683 foreach ( $legendItems as $key => $item ) { # generate items of the legend
1684 $label = isset( $item['legend'] ) ? $item['legend'] : $item['title'];
1685 $letter = $item['letter'];
1686 $cssClass = isset( $item['class'] ) ? $item['class'] : $key;
1687
1688 $legend .= Html::element( 'dt',
1689 [ 'class' => $cssClass ], $context->msg( $letter )->text()
1690 ) . "\n" .
1691 Html::rawElement( 'dd',
1692 [ 'class' => Sanitizer::escapeClass( 'mw-changeslist-legend-' . $key ) ],
1693 $context->msg( $label )->parse()
1694 ) . "\n";
1695 }
1696 # (+-123)
1697 $legend .= Html::rawElement( 'dt',
1698 [ 'class' => 'mw-plusminus-pos' ],
1699 $context->msg( 'recentchanges-legend-plusminus' )->parse()
1700 ) . "\n";
1701 $legend .= Html::element(
1702 'dd',
1703 [ 'class' => 'mw-changeslist-legend-plusminus' ],
1704 $context->msg( 'recentchanges-label-plusminus' )->text()
1705 ) . "\n";
1706 $legend .= Html::closeElement( 'dl' ) . "\n";
1707
1708 $legendHeading = $this->isStructuredFilterUiEnabled() ?
1709 $context->msg( 'rcfilters-legend-heading' )->parse() :
1710 $context->msg( 'recentchanges-legend-heading' )->parse();
1711
1712 # Collapsible
1713 $collapsedState = $this->getRequest()->getCookie( 'changeslist-state' );
1714 $collapsedClass = $collapsedState === 'collapsed' ? ' mw-collapsed' : '';
1715
1716 $legend =
1717 '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass . '">' .
1718 $legendHeading .
1719 '<div class="mw-collapsible-content">' . $legend . '</div>' .
1720 '</div>';
1721
1722 return $legend;
1723 }
1724
1728 protected function addModules() {
1729 $out = $this->getOutput();
1730 // Styles and behavior for the legend box (see makeLegend())
1731 $out->addModuleStyles( [
1732 'mediawiki.special.changeslist.legend',
1733 'mediawiki.special.changeslist',
1734 ] );
1735 $out->addModules( 'mediawiki.special.changeslist.legend.js' );
1736
1737 if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
1738 $out->addModules( 'mediawiki.rcfilters.filters.ui' );
1739 $out->addModuleStyles( 'mediawiki.rcfilters.filters.base.styles' );
1740 }
1741 }
1742
1743 protected function getGroupName() {
1744 return 'changes';
1745 }
1746
1763 public function filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr,
1764 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1765 ) {
1766 global $wgLearnerEdits,
1770
1771 $LEVEL_COUNT = 5;
1772
1773 // If all levels are selected, don't filter
1774 if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1775 return;
1776 }
1777
1778 // both 'registered' and 'unregistered', experience levels, if any, are included in 'registered'
1779 if (
1780 in_array( 'registered', $selectedExpLevels ) &&
1781 in_array( 'unregistered', $selectedExpLevels )
1782 ) {
1783 return;
1784 }
1785
1786 $actorMigration = ActorMigration::newMigration();
1787 $actorQuery = $actorMigration->getJoin( 'rc_user' );
1788 $tables += $actorQuery['tables'];
1789 $join_conds += $actorQuery['joins'];
1790
1791 // 'registered' but not 'unregistered', experience levels, if any, are included in 'registered'
1792 if (
1793 in_array( 'registered', $selectedExpLevels ) &&
1794 !in_array( 'unregistered', $selectedExpLevels )
1795 ) {
1796 $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
1797 return;
1798 }
1799
1800 if ( $selectedExpLevels === [ 'unregistered' ] ) {
1801 $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
1802 return;
1803 }
1804
1805 $tables[] = 'user';
1806 $join_conds['user'] = [ 'LEFT JOIN', $actorQuery['fields']['rc_user'] . ' = user_id' ];
1807
1808 if ( $now === 0 ) {
1809 $now = time();
1810 }
1811 $secondsPerDay = 86400;
1812 $learnerCutoff = $now - $wgLearnerMemberSince * $secondsPerDay;
1813 $experiencedUserCutoff = $now - $wgExperiencedUserMemberSince * $secondsPerDay;
1814
1815 $aboveNewcomer = $dbr->makeList(
1816 [
1817 'user_editcount >= ' . intval( $wgLearnerEdits ),
1818 'user_registration <= ' . $dbr->addQuotes( $dbr->timestamp( $learnerCutoff ) ),
1819 ],
1820 IDatabase::LIST_AND
1821 );
1822
1823 $aboveLearner = $dbr->makeList(
1824 [
1825 'user_editcount >= ' . intval( $wgExperiencedUserEdits ),
1826 'user_registration <= ' .
1827 $dbr->addQuotes( $dbr->timestamp( $experiencedUserCutoff ) ),
1828 ],
1829 IDatabase::LIST_AND
1830 );
1831
1832 $conditions = [];
1833
1834 if ( in_array( 'unregistered', $selectedExpLevels ) ) {
1835 $selectedExpLevels = array_diff( $selectedExpLevels, [ 'unregistered' ] );
1836 $conditions[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
1837 }
1838
1839 if ( $selectedExpLevels === [ 'newcomer' ] ) {
1840 $conditions[] = "NOT ( $aboveNewcomer )";
1841 } elseif ( $selectedExpLevels === [ 'learner' ] ) {
1842 $conditions[] = $dbr->makeList(
1843 [ $aboveNewcomer, "NOT ( $aboveLearner )" ],
1844 IDatabase::LIST_AND
1845 );
1846 } elseif ( $selectedExpLevels === [ 'experienced' ] ) {
1847 $conditions[] = $aboveLearner;
1848 } elseif ( $selectedExpLevels === [ 'learner', 'newcomer' ] ) {
1849 $conditions[] = "NOT ( $aboveLearner )";
1850 } elseif ( $selectedExpLevels === [ 'experienced', 'newcomer' ] ) {
1851 $conditions[] = $dbr->makeList(
1852 [ "NOT ( $aboveNewcomer )", $aboveLearner ],
1853 IDatabase::LIST_OR
1854 );
1855 } elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) {
1856 $conditions[] = $aboveNewcomer;
1857 } elseif ( $selectedExpLevels === [ 'experienced', 'learner', 'newcomer' ] ) {
1858 $conditions[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
1859 }
1860
1861 if ( count( $conditions ) > 1 ) {
1862 $conds[] = $dbr->makeList( $conditions, IDatabase::LIST_OR );
1863 } elseif ( count( $conditions ) === 1 ) {
1864 $conds[] = reset( $conditions );
1865 }
1866 }
1867
1874 if ( $this->getRequest()->getBool( 'rcfilters' ) ) {
1875 return true;
1876 }
1877
1878 return static::checkStructuredFilterUiEnabled(
1879 $this->getConfig(),
1880 $this->getUser()
1881 );
1882 }
1883
1891 if ( $this->getConfig()->get( 'StructuredChangeFiltersShowPreference' ) ) {
1892 return !$this->getUser()->getDefaultOption( 'rcenhancedfilters-disable' );
1893 } else {
1894 return $this->getUser()->getDefaultOption( 'rcenhancedfilters' );
1895 }
1896 }
1897
1906 public static function checkStructuredFilterUiEnabled( Config $config, User $user ) {
1907 if ( $config->get( 'StructuredChangeFiltersShowPreference' ) ) {
1908 return !$user->getOption( 'rcenhancedfilters-disable' );
1909 } else {
1910 return $user->getOption( 'rcenhancedfilters' );
1911 }
1912 }
1913
1921 public function getDefaultLimit() {
1922 return $this->getUser()->getIntOption( static::$limitPreferenceName );
1923 }
1924
1933 public function getDefaultDays() {
1934 return floatval( $this->getUser()->getOption( static::$daysPreferenceName ) );
1935 }
1936}
within a display generated by the Derivative if and wherever such third party notices normally appear The contents of the NOTICE file are for informational purposes only and do not modify the License You may add Your own attribution notices within Derivative Works that You alongside or as an addendum to the NOTICE text from the provided that such additional attribution notices cannot be construed as modifying the License You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for or distribution of Your or for any such Derivative Works as a provided Your and distribution of the Work otherwise complies with the conditions stated in this License Submission of Contributions Unless You explicitly state any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this without any additional terms or conditions Notwithstanding the nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions Trademarks This License does not grant permission to use the trade service or product names of the except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file Disclaimer of Warranty Unless required by applicable law or agreed to in Licensor provides the WITHOUT WARRANTIES OR CONDITIONS OF ANY either express or including
$wgLearnerMemberSince
Name of the external diff engine to use.
$wgExperiencedUserMemberSince
Name of the external diff engine to use.
$wgLearnerEdits
The following variables define 3 user experience levels:
$wgExperiencedUserEdits
Name of the external diff 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.
$messages
static truncateTagDescription( $tag, $length, IContextSource $context)
Get truncated message for the tag's long description.
static tagDescription( $tag, IContextSource $context)
Get a short description for a tag.
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the valid_tag table of the database.
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)
Special page which uses a ChangesList to show query results.
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() Bug 36524.
parseParameters( $par, FormOptions $opts)
Process $par and put options found in $opts.
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
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.
isStructuredFilterUiEnabledByDefault()
Check whether the structured filter UI is enabled by default (regardless of this particular user's se...
static string $savedQueriesPreferenceName
Preference name for saved queries.
getFilterGroupDefinitionFromLegacyCustomFilters(array $customFilters)
Get filter group definition from legacy custom filters.
static checkStructuredFilterUiEnabled(Config $config, User $user)
Static method to check whether StructuredFilter UI is enabled for the given user.
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.
getCustomFilters()
Get custom show/hide filters using deprecated ChangesListSpecialPageFilters hook.
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.
__construct( $name, $restriction)
outputFeedLinks()
Output feed links.
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.
execute( $subpage)
Main execution point.
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.
getChangeTagList()
Fetch the change tags list for the front end.
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...
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.
Represents a filter group with multiple string options.
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
Helper class to keep track of options when mixing links and form elements.
reset( $name)
Delete the option value.
const FLOAT
Float type, maps guessType() to WebRequest::getFloat()
validateBounds( $name, $min, $max)
Constrain a numeric value for a given option to a given range.
const STRING
String type, maps guessType() to WebRequest::getText()
validateIntBounds( $name, $min, $max)
const INT
Integer type, maps guessType() to WebRequest::getInt()
getChangedValues()
Return options modified as an array ( name => value )
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:34
add( $ns, $dbkey)
Definition LinkBatch.php:80
static newFromRow( $row)
Handy shortcut for constructing a formatter directly from database row.
PSR-3 logger instance factory.
static makeInlineScript( $script)
Returns an HTML script tag that runs given JS code after startup and base modules.
static makeMessageSetScript( $messages)
Returns JS code which, when called, will register a given list of messages.
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.
getContext()
Gets the context this SpecialPage is executed in.
msg( $key)
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.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:53
Class for fixing stale WANObjectCache keys using a purge event source.
Error thrown when a query times out.
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
when a variable name is used in a function
Definition design.txt:94
namespace being checked & $result
Definition hooks.txt:2323
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition hooks.txt:2783
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2255
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1015
namespace and then decline to actually register it & $namespaces
Definition hooks.txt:934
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition hooks.txt:865
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition hooks.txt:2811
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:864
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition hooks.txt:1620
const RC_NEW
Definition Defines.php:153
const LIST_OR
Definition Defines.php:56
const RC_LOG
Definition Defines.php:154
const NS_USER_TALK
Definition Defines.php:77
const RC_EDIT
Definition Defines.php:152
const RC_CATEGORIZE
Definition Defines.php:156
Interface for configuration instances.
Definition Config.php:28
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
Result wrapper for grabbing data queried from an IDatabase object.
$batch
Definition linkcache.txt:23
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
$params