MediaWiki REL1_34
SpecialRecentChanges.php
Go to the documentation of this file.
1<?php
27
34
35 protected static $savedQueriesPreferenceName = 'rcfilters-saved-queries';
36 protected static $daysPreferenceName = 'rcdays'; // Use general RecentChanges preference
37 protected static $limitPreferenceName = 'rcfilters-limit'; // Use RCFilters-specific preference
38 protected static $collapsedPreferenceName = 'rcfilters-rc-collapsed';
39
41
42 public function __construct( $name = 'Recentchanges', $restriction = '' ) {
43 parent::__construct( $name, $restriction );
44
45 $this->watchlistFilterGroupDefinition = [
46 'name' => 'watchlist',
47 'title' => 'rcfilters-filtergroup-watchlist',
48 'class' => ChangesListStringOptionsFilterGroup::class,
49 'priority' => -9,
50 'isFullCoverage' => true,
51 'filters' => [
52 [
53 'name' => 'watched',
54 'label' => 'rcfilters-filter-watchlist-watched-label',
55 'description' => 'rcfilters-filter-watchlist-watched-description',
56 'cssClassSuffix' => 'watched',
57 'isRowApplicableCallable' => function ( $ctx, $rc ) {
58 return $rc->getAttribute( 'wl_user' );
59 }
60 ],
61 [
62 'name' => 'watchednew',
63 'label' => 'rcfilters-filter-watchlist-watchednew-label',
64 'description' => 'rcfilters-filter-watchlist-watchednew-description',
65 'cssClassSuffix' => 'watchednew',
66 'isRowApplicableCallable' => function ( $ctx, $rc ) {
67 return $rc->getAttribute( 'wl_user' ) &&
68 $rc->getAttribute( 'rc_timestamp' ) &&
69 $rc->getAttribute( 'wl_notificationtimestamp' ) &&
70 $rc->getAttribute( 'rc_timestamp' ) >= $rc->getAttribute( 'wl_notificationtimestamp' );
71 },
72 ],
73 [
74 'name' => 'notwatched',
75 'label' => 'rcfilters-filter-watchlist-notwatched-label',
76 'description' => 'rcfilters-filter-watchlist-notwatched-description',
77 'cssClassSuffix' => 'notwatched',
78 'isRowApplicableCallable' => function ( $ctx, $rc ) {
79 return $rc->getAttribute( 'wl_user' ) === null;
80 },
81 ]
82 ],
84 'queryCallable' => function ( $specialPageClassName, $context, $dbr,
85 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) {
86 sort( $selectedValues );
87 $notwatchedCond = 'wl_user IS NULL';
88 $watchedCond = 'wl_user IS NOT NULL';
89 $newCond = 'rc_timestamp >= wl_notificationtimestamp';
90
91 if ( $selectedValues === [ 'notwatched' ] ) {
92 $conds[] = $notwatchedCond;
93 return;
94 }
95
96 if ( $selectedValues === [ 'watched' ] ) {
97 $conds[] = $watchedCond;
98 return;
99 }
100
101 if ( $selectedValues === [ 'watchednew' ] ) {
102 $conds[] = $dbr->makeList( [
103 $watchedCond,
104 $newCond
105 ], LIST_AND );
106 return;
107 }
108
109 if ( $selectedValues === [ 'notwatched', 'watched' ] ) {
110 // no filters
111 return;
112 }
113
114 if ( $selectedValues === [ 'notwatched', 'watchednew' ] ) {
115 $conds[] = $dbr->makeList( [
116 $notwatchedCond,
117 $dbr->makeList( [
118 $watchedCond,
119 $newCond
120 ], LIST_AND )
121 ], LIST_OR );
122 return;
123 }
124
125 if ( $selectedValues === [ 'watched', 'watchednew' ] ) {
126 $conds[] = $watchedCond;
127 return;
128 }
129
130 if ( $selectedValues === [ 'notwatched', 'watched', 'watchednew' ] ) {
131 // no filters
132 return;
133 }
134 }
135 ];
136 }
137
141 public function execute( $subpage ) {
142 // Backwards-compatibility: redirect to new feed URLs
143 $feedFormat = $this->getRequest()->getVal( 'feed' );
144 if ( !$this->including() && $feedFormat ) {
145 $query = $this->getFeedQuery();
146 $query['feedformat'] = $feedFormat === 'atom' ? 'atom' : 'rss';
147 $this->getOutput()->redirect( wfAppendQuery( wfScript( 'api' ), $query ) );
148
149 return;
150 }
151
152 // 10 seconds server-side caching max
153 $out = $this->getOutput();
154 $out->setCdnMaxage( 10 );
155 // Check if the client has a cached version
156 $lastmod = $this->checkLastModified();
157 if ( $lastmod === false ) {
158 return;
159 }
160
161 $this->addHelpLink(
162 'https://meta.wikimedia.org/wiki/Special:MyLanguage/Help:Recent_changes',
163 true
164 );
165 parent::execute( $subpage );
166 }
167
171 protected function transformFilterDefinition( array $filterDefinition ) {
172 if ( isset( $filterDefinition['showHideSuffix'] ) ) {
173 $filterDefinition['showHide'] = 'rc' . $filterDefinition['showHideSuffix'];
174 }
175
176 return $filterDefinition;
177 }
178
182 protected function registerFilters() {
183 parent::registerFilters();
184
185 if (
186 !$this->including() &&
187 $this->getUser()->isLoggedIn() &&
188 MediaWikiServices::getInstance()
190 ->userHasRight( $this->getUser(), 'viewmywatchlist' )
191 ) {
192 $this->registerFiltersFromDefinitions( [ $this->watchlistFilterGroupDefinition ] );
193 $watchlistGroup = $this->getFilterGroup( 'watchlist' );
194 $watchlistGroup->getFilter( 'watched' )->setAsSupersetOf(
195 $watchlistGroup->getFilter( 'watchednew' )
196 );
197 }
198
199 $user = $this->getUser();
200
201 $significance = $this->getFilterGroup( 'significance' );
203 $hideMinor = $significance->getFilter( 'hideminor' );
204 '@phan-var ChangesListBooleanFilter $hideMinor';
205 $hideMinor->setDefault( $user->getBoolOption( 'hideminor' ) );
206
207 $automated = $this->getFilterGroup( 'automated' );
209 $hideBots = $automated->getFilter( 'hidebots' );
210 '@phan-var ChangesListBooleanFilter $hideBots';
211 $hideBots->setDefault( true );
212
214 $reviewStatus = $this->getFilterGroup( 'reviewStatus' );
215 '@phan-var ChangesListStringOptionsFilterGroup|null $reviewStatus';
216 if ( $reviewStatus !== null ) {
217 // Conditional on feature being available and rights
218 if ( $user->getBoolOption( 'hidepatrolled' ) ) {
219 $reviewStatus->setDefault( 'unpatrolled' );
220 $legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
222 $legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
223 '@phan-var ChangesListBooleanFilter $legacyHidePatrolled';
224 $legacyHidePatrolled->setDefault( true );
225 }
226 }
227
228 $changeType = $this->getFilterGroup( 'changeType' );
230 $hideCategorization = $changeType->getFilter( 'hidecategorization' );
231 '@phan-var ChangesListBooleanFilter $hideCategorization';
232 if ( $hideCategorization !== null ) {
233 // Conditional on feature being available
234 $hideCategorization->setDefault( $user->getBoolOption( 'hidecategorization' ) );
235 }
236 }
237
244 public function parseParameters( $par, FormOptions $opts ) {
245 parent::parseParameters( $par, $opts );
246
247 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
248 foreach ( $bits as $bit ) {
249 if ( is_numeric( $bit ) ) {
250 $opts['limit'] = $bit;
251 }
252
253 $m = [];
254 if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
255 $opts['limit'] = $m[1];
256 }
257 if ( preg_match( '/^days=(\d+(?:\.\d+)?)$/', $bit, $m ) ) {
258 $opts['days'] = $m[1];
259 }
260 if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
261 $opts['namespace'] = $m[1];
262 }
263 if ( preg_match( '/^tagfilter=(.*)$/', $bit, $m ) ) {
264 $opts['tagfilter'] = $m[1];
265 }
266 }
267 }
268
272 protected function doMainQuery( $tables, $fields, $conds, $query_options,
273 $join_conds, FormOptions $opts
274 ) {
275 $dbr = $this->getDB();
276 $user = $this->getUser();
277
278 $rcQuery = RecentChange::getQueryInfo();
279 $tables = array_merge( $tables, $rcQuery['tables'] );
280 $fields = array_merge( $rcQuery['fields'], $fields );
281 $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
282
283 // JOIN on watchlist for users
284 if ( $user->isLoggedIn() && MediaWikiServices::getInstance()
285 ->getPermissionManager()
286 ->userHasRight( $user, 'viewmywatchlist' )
287 ) {
288 $tables[] = 'watchlist';
289 $fields[] = 'wl_user';
290 $fields[] = 'wl_notificationtimestamp';
291 $join_conds['watchlist'] = [ 'LEFT JOIN', [
292 'wl_user' => $user->getId(),
293 'wl_title=rc_title',
294 'wl_namespace=rc_namespace'
295 ] ];
296 }
297
298 // JOIN on page, used for 'last revision' filter highlight
299 $tables[] = 'page';
300 $fields[] = 'page_latest';
301 $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
302
303 $tagFilter = $opts['tagfilter'] ? explode( '|', $opts['tagfilter'] ) : [];
305 $tables,
306 $fields,
307 $conds,
308 $join_conds,
309 $query_options,
310 $tagFilter
311 );
312
313 if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
314 $opts )
315 ) {
316 return false;
317 }
318
319 if ( $this->areFiltersInConflict() ) {
320 return false;
321 }
322
323 $orderByAndLimit = [
324 'ORDER BY' => 'rc_timestamp DESC',
325 'LIMIT' => $opts['limit']
326 ];
327 if ( in_array( 'DISTINCT', $query_options ) ) {
328 // ChangeTags::modifyDisplayQuery() adds DISTINCT when filtering on multiple tags.
329 // In order to prevent DISTINCT from causing query performance problems,
330 // we have to GROUP BY the primary key. This in turn requires us to add
331 // the primary key to the end of the ORDER BY, and the old ORDER BY to the
332 // start of the GROUP BY
333 $orderByAndLimit['ORDER BY'] = 'rc_timestamp DESC, rc_id DESC';
334 $orderByAndLimit['GROUP BY'] = 'rc_timestamp, rc_id';
335 }
336 // array_merge() is used intentionally here so that hooks can, should
337 // they so desire, override the ORDER BY / LIMIT condition(s); prior to
338 // MediaWiki 1.26 this used to use the plus operator instead, which meant
339 // that extensions weren't able to change these conditions
340 $query_options = array_merge( $orderByAndLimit, $query_options );
341 $rows = $dbr->select(
342 $tables,
343 $fields,
344 // rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough
345 // knowledge to use an index merge if it wants (it may use some other index though).
346 $conds + [ 'rc_new' => [ 0, 1 ] ],
347 __METHOD__,
348 $query_options,
349 $join_conds
350 );
351
352 return $rows;
353 }
354
355 protected function getDB() {
356 return wfGetDB( DB_REPLICA, 'recentchanges' );
357 }
358
359 public function outputFeedLinks() {
360 $this->addFeedLinks( $this->getFeedQuery() );
361 }
362
368 protected function getFeedQuery() {
369 $query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) {
370 // API handles empty parameters in a different way
371 return $value !== '';
372 } );
373 $query['action'] = 'feedrecentchanges';
374 $feedLimit = $this->getConfig()->get( 'FeedLimit' );
375 if ( $query['limit'] > $feedLimit ) {
376 $query['limit'] = $feedLimit;
377 }
378
379 return $query;
380 }
381
388 public function outputChangesList( $rows, $opts ) {
389 $limit = $opts['limit'];
390
391 $showWatcherCount = $this->getConfig()->get( 'RCShowWatchingUsers' )
392 && $this->getUser()->getOption( 'shownumberswatching' );
393 $watcherCache = [];
394
395 $counter = 1;
396 $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups );
397 $list->initChangesListRows( $rows );
398
399 $userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
400 $rclistOutput = $list->beginRecentChangesList();
401 if ( $this->isStructuredFilterUiEnabled() ) {
402 $rclistOutput .= $this->makeLegend();
403 }
404
405 foreach ( $rows as $obj ) {
406 if ( $limit == 0 ) {
407 break;
408 }
409 $rc = RecentChange::newFromRow( $obj );
410
411 # Skip CatWatch entries for hidden cats based on user preference
412 if (
413 $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
414 !$userShowHiddenCats &&
415 $rc->getParam( 'hidden-cat' )
416 ) {
417 continue;
418 }
419
420 $rc->counter = $counter++;
421 # Check if the page has been updated since the last visit
422 if ( $this->getConfig()->get( 'ShowUpdatedMarker' )
423 && !empty( $obj->wl_notificationtimestamp )
424 ) {
425 $rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp );
426 } else {
427 $rc->notificationtimestamp = false; // Default
428 }
429 # Check the number of users watching the page
430 $rc->numberofWatchingusers = 0; // Default
431 if ( $showWatcherCount && $obj->rc_namespace >= 0 ) {
432 if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
433 $watcherCache[$obj->rc_namespace][$obj->rc_title] =
434 MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchers(
435 new TitleValue( (int)$obj->rc_namespace, $obj->rc_title )
436 );
437 }
438 $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
439 }
440
441 $changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
442 if ( $changeLine !== false ) {
443 $rclistOutput .= $changeLine;
444 --$limit;
445 }
446 }
447 $rclistOutput .= $list->endRecentChangesList();
448
449 if ( $rows->numRows() === 0 ) {
450 $this->outputNoResults();
451 if ( !$this->including() ) {
452 $this->getOutput()->setStatusCode( 404 );
453 }
454 } else {
455 $this->getOutput()->addHTML( $rclistOutput );
456 }
457 }
458
465 public function doHeader( $opts, $numRows ) {
466 $this->setTopText( $opts );
467
468 $defaults = $opts->getAllValues();
469 $nondefaults = $opts->getChangedValues();
470
471 $panel = [];
472 if ( !$this->isStructuredFilterUiEnabled() ) {
473 $panel[] = $this->makeLegend();
474 }
475 $panel[] = $this->optionsPanel( $defaults, $nondefaults, $numRows );
476 $panel[] = '<hr />';
477
478 $extraOpts = $this->getExtraOptions( $opts );
479 $extraOptsCount = count( $extraOpts );
480 $count = 0;
481 $submit = ' ' . Xml::submitButton( $this->msg( 'recentchanges-submit' )->text() );
482
483 $out = Xml::openElement( 'table', [ 'class' => 'mw-recentchanges-table' ] );
484 foreach ( $extraOpts as $name => $optionRow ) {
485 # Add submit button to the last row only
486 ++$count;
487 $addSubmit = ( $count === $extraOptsCount ) ? $submit : '';
488
489 $out .= Xml::openElement( 'tr', [ 'class' => $name . 'Form' ] );
490 if ( is_array( $optionRow ) ) {
491 $out .= Xml::tags(
492 'td',
493 [ 'class' => 'mw-label mw-' . $name . '-label' ],
494 $optionRow[0]
495 );
496 $out .= Xml::tags(
497 'td',
498 [ 'class' => 'mw-input' ],
499 $optionRow[1] . $addSubmit
500 );
501 } else {
502 $out .= Xml::tags(
503 'td',
504 [ 'class' => 'mw-input', 'colspan' => 2 ],
505 $optionRow . $addSubmit
506 );
507 }
508 $out .= Xml::closeElement( 'tr' );
509 }
510 $out .= Xml::closeElement( 'table' );
511
512 $unconsumed = $opts->getUnconsumedValues();
513 foreach ( $unconsumed as $key => $value ) {
514 $out .= Html::hidden( $key, $value );
515 }
516
517 $t = $this->getPageTitle();
518 $out .= Html::hidden( 'title', $t->getPrefixedText() );
519 $form = Xml::tags( 'form', [ 'action' => wfScript() ], $out );
520 $panel[] = $form;
521 $panelString = implode( "\n", $panel );
522
523 $rcoptions = Xml::fieldset(
524 $this->msg( 'recentchanges-legend' )->text(),
525 $panelString,
526 [ 'class' => 'rcoptions cloptions' ]
527 );
528
529 // Insert a placeholder for RCFilters
530 if ( $this->isStructuredFilterUiEnabled() ) {
531 $rcfilterContainer = Html::element(
532 'div',
533 // TODO: Remove deprecated rcfilters-container class
534 [ 'class' => 'rcfilters-container mw-rcfilters-container' ]
535 );
536
537 $loadingContainer = Html::rawElement(
538 'div',
539 [ 'class' => 'mw-rcfilters-spinner' ],
540 Html::element(
541 'div',
542 [ 'class' => 'mw-rcfilters-spinner-bounce' ]
543 )
544 );
545
546 // Wrap both with rcfilters-head
547 $this->getOutput()->addHTML(
548 Html::rawElement(
549 'div',
550 // TODO: Remove deprecated rcfilters-head class
551 [ 'class' => 'rcfilters-head mw-rcfilters-head' ],
552 $rcfilterContainer . $rcoptions
553 )
554 );
555
556 // Add spinner
557 $this->getOutput()->addHTML( $loadingContainer );
558 } else {
559 $this->getOutput()->addHTML( $rcoptions );
560 }
561
562 $this->setBottomText( $opts );
563 }
564
570 function setTopText( FormOptions $opts ) {
571 $message = $this->msg( 'recentchangestext' )->inContentLanguage();
572 if ( !$message->isDisabled() ) {
573 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
574 // Parse the message in this weird ugly way to preserve the ability to include interlanguage
575 // links in it (T172461). In the future when T66969 is resolved, perhaps we can just use
576 // $message->parse() instead. This code is copied from Message::parseText().
577 $parserOutput = MessageCache::singleton()->parse(
578 $message->plain(),
579 $this->getPageTitle(),
580 /*linestart*/true,
581 // Message class sets the interface flag to false when parsing in a language different than
582 // user language, and this is wiki content language
583 /*interface*/false,
584 $contLang
585 );
586 $content = $parserOutput->getText( [
587 'enableSectionEditLinks' => false,
588 ] );
589 // Add only metadata here (including the language links), text is added below
590 $this->getOutput()->addParserOutputMetadata( $parserOutput );
591
592 $langAttributes = [
593 'lang' => $contLang->getHtmlCode(),
594 'dir' => $contLang->getDir(),
595 ];
596
597 $topLinksAttributes = [ 'class' => 'mw-recentchanges-toplinks' ];
598
599 if ( $this->isStructuredFilterUiEnabled() ) {
600 // Check whether the widget is already collapsed or expanded
601 $collapsedState = $this->getRequest()->getCookie( 'rcfilters-toplinks-collapsed-state' );
602 // Note that an empty/unset cookie means collapsed, so check for !== 'expanded'
603 $topLinksAttributes[ 'class' ] .= $collapsedState !== 'expanded' ?
604 ' mw-recentchanges-toplinks-collapsed' : '';
605
606 $this->getOutput()->enableOOUI();
607 $contentTitle = new OOUI\ButtonWidget( [
608 'classes' => [ 'mw-recentchanges-toplinks-title' ],
609 'label' => new OOUI\HtmlSnippet( $this->msg( 'rcfilters-other-review-tools' )->parse() ),
610 'framed' => false,
611 'indicator' => $collapsedState !== 'expanded' ? 'down' : 'up',
612 'flags' => [ 'progressive' ],
613 ] );
614
615 $contentWrapper = Html::rawElement( 'div',
616 array_merge(
617 [ 'class' => 'mw-recentchanges-toplinks-content mw-collapsible-content' ],
618 $langAttributes
619 ),
621 );
622 $content = $contentTitle . $contentWrapper;
623 } else {
624 // Language direction should be on the top div only
625 // if the title is not there. If it is there, it's
626 // interface direction, and the language/dir attributes
627 // should be on the content itself
628 $topLinksAttributes = array_merge( $topLinksAttributes, $langAttributes );
629 }
630
631 $this->getOutput()->addHTML(
632 Html::rawElement( 'div', $topLinksAttributes, $content )
633 );
634 }
635 }
636
643 function getExtraOptions( $opts ) {
644 $opts->consumeValues( [
645 'namespace', 'invert', 'associated', 'tagfilter'
646 ] );
647
648 $extraOpts = [];
649 $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
650
652 $opts['tagfilter'], false, $this->getContext() );
653 if ( count( $tagFilter ) ) {
654 $extraOpts['tagfilter'] = $tagFilter;
655 }
656
657 // Don't fire the hook for subclasses. (Or should we?)
658 if ( $this->getName() === 'Recentchanges' ) {
659 Hooks::run( 'SpecialRecentChangesPanel', [ &$extraOpts, $opts ] );
660 }
661
662 return $extraOpts;
663 }
664
668 protected function addModules() {
669 parent::addModules();
670 $out = $this->getOutput();
671 $out->addModules( 'mediawiki.special.recentchanges' );
672 }
673
681 public function checkLastModified() {
682 $dbr = $this->getDB();
683 $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', '', __METHOD__ );
684
685 return $lastmod;
686 }
687
694 protected function namespaceFilterForm( FormOptions $opts ) {
695 $nsSelect = Html::namespaceSelector(
696 [ 'selected' => $opts['namespace'], 'all' => '', 'in-user-lang' => true ],
697 [ 'name' => 'namespace', 'id' => 'namespace' ]
698 );
699 $nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' );
700 $attribs = [ 'class' => [ 'mw-input-with-label' ] ];
701 // Hide the checkboxes when the namespace filter is set to 'all'.
702 if ( $opts['namespace'] === '' ) {
703 $attribs['class'][] = 'mw-input-hidden';
704 }
705 $invert = Html::rawElement( 'span', $attribs, Xml::checkLabel(
706 $this->msg( 'invert' )->text(), 'invert', 'nsinvert',
707 $opts['invert'],
708 [ 'title' => $this->msg( 'tooltip-invert' )->text() ]
709 ) );
710 $associated = Html::rawElement( 'span', $attribs, Xml::checkLabel(
711 $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated',
712 $opts['associated'],
713 [ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
714 ) );
715
716 return [ $nsLabel, "$nsSelect $invert $associated" ];
717 }
718
727 function filterByCategories( &$rows, FormOptions $opts ) {
728 wfDeprecated( __METHOD__, '1.31' );
729
730 $categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
731
732 if ( $categories === [] ) {
733 return;
734 }
735
736 # Filter categories
737 $cats = [];
738 foreach ( $categories as $cat ) {
739 $cat = trim( $cat );
740 if ( $cat == '' ) {
741 continue;
742 }
743 $cats[] = $cat;
744 }
745
746 # Filter articles
747 $articles = [];
748 $a2r = [];
749 $rowsarr = [];
750 foreach ( $rows as $k => $r ) {
751 $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
752 $id = $nt->getArticleID();
753 if ( $id == 0 ) {
754 continue; # Page might have been deleted...
755 }
756 if ( !in_array( $id, $articles ) ) {
757 $articles[] = $id;
758 }
759 if ( !isset( $a2r[$id] ) ) {
760 $a2r[$id] = [];
761 }
762 $a2r[$id][] = $k;
763 $rowsarr[$k] = $r;
764 }
765
766 # Shortcut?
767 if ( $articles === [] || $cats === [] ) {
768 return;
769 }
770
771 # Look up
772 $catFind = new CategoryFinder;
773 $catFind->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' );
774 $match = $catFind->run();
775
776 # Filter
777 $newrows = [];
778 foreach ( $match as $id ) {
779 foreach ( $a2r[$id] as $rev ) {
780 $k = $rev;
781 $newrows[$k] = $rowsarr[$k];
782 }
783 }
784 $rows = new FakeResultWrapper( array_values( $newrows ) );
785 }
786
796 function makeOptionsLink( $title, $override, $options, $active = false ) {
797 $params = $this->convertParamsForLink( $override + $options );
798
799 if ( $active ) {
800 $title = new HtmlArmor( '<strong>' . htmlspecialchars( $title ) . '</strong>' );
801 }
802
803 return $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $title, [
804 'data-params' => json_encode( $override ),
805 'data-keys' => implode( ',', array_keys( $override ) ),
806 ], $params );
807 }
808
817 function optionsPanel( $defaults, $nondefaults, $numRows ) {
818 $options = $nondefaults + $defaults;
819
820 $note = '';
821 $msg = $this->msg( 'rclegend' );
822 if ( !$msg->isDisabled() ) {
823 $note .= Html::rawElement(
824 'div',
825 [ 'class' => 'mw-rclegend' ],
826 $msg->parse()
827 );
828 }
829
830 $lang = $this->getLanguage();
831 $user = $this->getUser();
832 $config = $this->getConfig();
833 if ( $options['from'] ) {
834 $resetLink = $this->makeOptionsLink( $this->msg( 'rclistfromreset' ),
835 [ 'from' => '' ], $nondefaults );
836
837 $noteFromMsg = $this->msg( 'rcnotefrom' )
838 ->numParams( $options['limit'] )
839 ->params(
840 $lang->userTimeAndDate( $options['from'], $user ),
841 $lang->userDate( $options['from'], $user ),
842 $lang->userTime( $options['from'], $user )
843 )
844 ->numParams( $numRows );
845 $note .= Html::rawElement(
846 'span',
847 [ 'class' => 'rcnotefrom' ],
848 $noteFromMsg->parse()
849 ) .
850 ' ' .
851 Html::rawElement(
852 'span',
853 [ 'class' => 'rcoptions-listfromreset' ],
854 $this->msg( 'parentheses' )->rawParams( $resetLink )->parse()
855 ) .
856 '<br />';
857 }
858
859 # Sort data for display and make sure it's unique after we've added user data.
860 $linkLimits = $config->get( 'RCLinkLimits' );
861 $linkLimits[] = $options['limit'];
862 sort( $linkLimits );
863 $linkLimits = array_unique( $linkLimits );
864
865 $linkDays = $this->getLinkDays();
866 $linkDays[] = $options['days'];
867 sort( $linkDays );
868 $linkDays = array_unique( $linkDays );
869
870 // limit links
871 $cl = [];
872 foreach ( $linkLimits as $value ) {
873 $cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
874 [ 'limit' => $value ], $nondefaults, $value == $options['limit'] );
875 }
876 $cl = $lang->pipeList( $cl );
877
878 // day links, reset 'from' to none
879 $dl = [];
880 foreach ( $linkDays as $value ) {
881 $dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
882 [ 'days' => $value, 'from' => '' ], $nondefaults, $value == $options['days'] );
883 }
884 $dl = $lang->pipeList( $dl );
885
886 $showhide = [ 'show', 'hide' ];
887
888 $links = [];
889
890 foreach ( $this->getLegacyShowHideFilters() as $key => $filter ) {
891 $msg = $filter->getShowHide();
892 $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] );
893 // Extensions can define additional filters, but don't need to define the corresponding
894 // messages. If they don't exist, just fall back to 'show' and 'hide'.
895 if ( !$linkMessage->exists() ) {
896 $linkMessage = $this->msg( $showhide[1 - $options[$key]] );
897 }
898
899 $link = $this->makeOptionsLink( $linkMessage->text(),
900 [ $key => 1 - $options[$key] ], $nondefaults );
901
902 $attribs = [
903 'class' => "$msg rcshowhideoption clshowhideoption",
904 'data-filter-name' => $filter->getName(),
905 ];
906
907 if ( $filter->isFeatureAvailableOnStructuredUi( $this ) ) {
908 $attribs['data-feature-in-structured-ui'] = true;
909 }
910
911 $links[] = Html::rawElement(
912 'span',
913 $attribs,
914 $this->msg( $msg )->rawParams( $link )->parse()
915 );
916 }
917
918 // show from this onward link
919 $timestamp = wfTimestampNow();
920 $now = $lang->userTimeAndDate( $timestamp, $user );
921 $timenow = $lang->userTime( $timestamp, $user );
922 $datenow = $lang->userDate( $timestamp, $user );
923 $pipedLinks = '<span class="rcshowhide">' . $lang->pipeList( $links ) . '</span>';
924
925 $rclinks = Html::rawElement(
926 'span',
927 [ 'class' => 'rclinks' ],
928 $this->msg( 'rclinks' )->rawParams( $cl, $dl, '' )->parse()
929 );
930
931 $rclistfrom = Html::rawElement(
932 'span',
933 [ 'class' => 'rclistfrom' ],
934 $this->makeOptionsLink(
935 $this->msg( 'rclistfrom' )->plaintextParams( $now, $timenow, $datenow )->parse(),
936 [ 'from' => $timestamp, 'fromFormatted' => $now ],
937 $nondefaults
938 )
939 );
940
941 return "{$note}$rclinks<br />$pipedLinks<br />$rclistfrom";
942 }
943
944 public function isIncludable() {
945 return true;
946 }
947
948 protected function getCacheTTL() {
949 return 60 * 5;
950 }
951
952 public function getDefaultLimit() {
953 $systemPrefValue = $this->getUser()->getIntOption( 'rclimit' );
954 // Prefer the RCFilters-specific preference if RCFilters is enabled
955 if ( $this->isStructuredFilterUiEnabled() ) {
956 return $this->getUser()->getIntOption( static::$limitPreferenceName, $systemPrefValue );
957 }
958
959 // Otherwise, use the system rclimit preference value
960 return $systemPrefValue;
961 }
962}
getPermissionManager()
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
The "CategoryFinder" class takes a list of articles, creates an internal representation of all their ...
seed( $articleIds, $categories, $mode='AND', $maxdepth=-1)
Initializes the instance.
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Special page which uses a ChangesList to show query results.
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
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.
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
outputNoResults()
Add the "no results" message to the output.
getOptions()
Get the current FormOptions for this request.
setBottomText(FormOptions $opts)
Send the text to be displayed after the options.
makeLegend()
Return the legend displayed within the fieldset.
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
static newFromContext(IContextSource $context, array $groups=[])
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
Helper class to keep track of options when mixing links and form elements.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:28
MediaWikiServices is the service locator for the application scope of MediaWiki.
getName()
Get the name of this Special Page.
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
addFeedLinks( $params)
Adds RSS/atom links.
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.
getLanguage()
Shortcut to get user's language.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
including( $x=null)
Whether the special page is being evaluated via transclusion.
A special page that lists last changes made to the wiki.
filterByCategories(&$rows, FormOptions $opts)
Filter $rows by categories set in $opts.
optionsPanel( $defaults, $nondefaults, $numRows)
Creates the options panel.
isIncludable()
Whether it's allowed to transclude the special page via {{Special:Foo/params}}.
setTopText(FormOptions $opts)
Send the text to be displayed above the options.
getExtraOptions( $opts)
Get options to be displayed in a form.
makeOptionsLink( $title, $override, $options, $active=false)
Makes change an option link which carries all the other options.
getDB()
Return a IDatabase object for reading.
addModules()
Add page-specific modules.
getFeedQuery()
Get URL query parameters for action=feedrecentchanges API feed of current recent changes view.
checkLastModified()
Get last modified date, for client caching Don't use this if we are using the patrol feature,...
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
transformFilterDefinition(array $filterDefinition)
Transforms filter definition to prepare it for constructor.See overrides of this method as well....
parseParameters( $par, FormOptions $opts)
Process $par and put options found in $opts.
getDefaultLimit()
Get the default value of the number of changes to display when loading the result set.
__construct( $name='Recentchanges', $restriction='')
namespaceFilterForm(FormOptions $opts)
Creates the choose namespace selection.
outputChangesList( $rows, $opts)
Build and output the actual changes list.
outputFeedLinks()
Output feed links.
registerFilters()
Register all filters and their groups (including those from hooks), plus handle conflicts and default...
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
Process the query.bool|IResultWrapper Result or false
Represents a page (or page fragment) title within MediaWiki.
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
const LIST_OR
Definition Defines.php:51
const LIST_AND
Definition Defines.php:48
const RC_CATEGORIZE
Definition Defines.php:135
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:29
Result wrapper for grabbing data queried from an IDatabase object.
$context
Definition load.php:45
$filter
const DB_REPLICA
Definition defines.php:25
$content
Definition router.php:78
if(!isset( $args[0])) $lang