MediaWiki REL1_32
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
143 public function execute( $subpage ) {
144 // Backwards-compatibility: redirect to new feed URLs
145 $feedFormat = $this->getRequest()->getVal( 'feed' );
146 if ( !$this->including() && $feedFormat ) {
147 $query = $this->getFeedQuery();
148 $query['feedformat'] = $feedFormat === 'atom' ? 'atom' : 'rss';
149 $this->getOutput()->redirect( wfAppendQuery( wfScript( 'api' ), $query ) );
150
151 return;
152 }
153
154 // 10 seconds server-side caching max
155 $out = $this->getOutput();
156 $out->setCdnMaxage( 10 );
157 // Check if the client has a cached version
158 $lastmod = $this->checkLastModified();
159 if ( $lastmod === false ) {
160 return;
161 }
162
163 $this->addHelpLink(
164 '//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Recent_changes',
165 true
166 );
167 parent::execute( $subpage );
168 }
169
173 protected function transformFilterDefinition( array $filterDefinition ) {
174 if ( isset( $filterDefinition['showHideSuffix'] ) ) {
175 $filterDefinition['showHide'] = 'rc' . $filterDefinition['showHideSuffix'];
176 }
177
178 return $filterDefinition;
179 }
180
184 protected function registerFilters() {
185 parent::registerFilters();
186
187 if (
188 !$this->including() &&
189 $this->getUser()->isLoggedIn() &&
190 $this->getUser()->isAllowed( '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' );
202 $hideMinor = $significance->getFilter( 'hideminor' );
203 $hideMinor->setDefault( $user->getBoolOption( 'hideminor' ) );
204
205 $automated = $this->getFilterGroup( 'automated' );
206 $hideBots = $automated->getFilter( 'hidebots' );
207 $hideBots->setDefault( true );
208
209 $reviewStatus = $this->getFilterGroup( 'reviewStatus' );
210 if ( $reviewStatus !== null ) {
211 // Conditional on feature being available and rights
212 if ( $user->getBoolOption( 'hidepatrolled' ) ) {
213 $reviewStatus->setDefault( 'unpatrolled' );
214 $legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
215 $legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
216 $legacyHidePatrolled->setDefault( true );
217 }
218 }
219
220 $changeType = $this->getFilterGroup( 'changeType' );
221 $hideCategorization = $changeType->getFilter( 'hidecategorization' );
222 if ( $hideCategorization !== null ) {
223 // Conditional on feature being available
224 $hideCategorization->setDefault( $user->getBoolOption( 'hidecategorization' ) );
225 }
226 }
227
234 public function parseParameters( $par, FormOptions $opts ) {
235 parent::parseParameters( $par, $opts );
236
237 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
238 foreach ( $bits as $bit ) {
239 if ( is_numeric( $bit ) ) {
240 $opts['limit'] = $bit;
241 }
242
243 $m = [];
244 if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
245 $opts['limit'] = $m[1];
246 }
247 if ( preg_match( '/^days=(\d+(?:\.\d+)?)$/', $bit, $m ) ) {
248 $opts['days'] = $m[1];
249 }
250 if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
251 $opts['namespace'] = $m[1];
252 }
253 if ( preg_match( '/^tagfilter=(.*)$/', $bit, $m ) ) {
254 $opts['tagfilter'] = $m[1];
255 }
256 }
257 }
258
262 protected function doMainQuery( $tables, $fields, $conds, $query_options,
263 $join_conds, FormOptions $opts
264 ) {
265 $dbr = $this->getDB();
266 $user = $this->getUser();
267
268 $rcQuery = RecentChange::getQueryInfo();
269 $tables = array_merge( $tables, $rcQuery['tables'] );
270 $fields = array_merge( $rcQuery['fields'], $fields );
271 $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
272
273 // JOIN on watchlist for users
274 if ( $user->isLoggedIn() && $user->isAllowed( 'viewmywatchlist' ) ) {
275 $tables[] = 'watchlist';
276 $fields[] = 'wl_user';
277 $fields[] = 'wl_notificationtimestamp';
278 $join_conds['watchlist'] = [ 'LEFT JOIN', [
279 'wl_user' => $user->getId(),
280 'wl_title=rc_title',
281 'wl_namespace=rc_namespace'
282 ] ];
283 }
284
285 // JOIN on page, used for 'last revision' filter highlight
286 $tables[] = 'page';
287 $fields[] = 'page_latest';
288 $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
289
290 $tagFilter = $opts['tagfilter'] ? explode( '|', $opts['tagfilter'] ) : [];
291 ChangeTags::modifyDisplayQuery(
292 $tables,
293 $fields,
294 $conds,
295 $join_conds,
296 $query_options,
297 $tagFilter
298 );
299
300 if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
301 $opts )
302 ) {
303 return false;
304 }
305
306 if ( $this->areFiltersInConflict() ) {
307 return false;
308 }
309
310 $orderByAndLimit = [
311 'ORDER BY' => 'rc_timestamp DESC',
312 'LIMIT' => $opts['limit']
313 ];
314 if ( in_array( 'DISTINCT', $query_options ) ) {
315 // ChangeTags::modifyDisplayQuery() adds DISTINCT when filtering on multiple tags.
316 // In order to prevent DISTINCT from causing query performance problems,
317 // we have to GROUP BY the primary key. This in turn requires us to add
318 // the primary key to the end of the ORDER BY, and the old ORDER BY to the
319 // start of the GROUP BY
320 $orderByAndLimit['ORDER BY'] = 'rc_timestamp DESC, rc_id DESC';
321 $orderByAndLimit['GROUP BY'] = 'rc_timestamp, rc_id';
322 }
323 // array_merge() is used intentionally here so that hooks can, should
324 // they so desire, override the ORDER BY / LIMIT condition(s); prior to
325 // MediaWiki 1.26 this used to use the plus operator instead, which meant
326 // that extensions weren't able to change these conditions
327 $query_options = array_merge( $orderByAndLimit, $query_options );
328 $rows = $dbr->select(
329 $tables,
330 $fields,
331 // rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough
332 // knowledge to use an index merge if it wants (it may use some other index though).
333 $conds + [ 'rc_new' => [ 0, 1 ] ],
334 __METHOD__,
335 $query_options,
336 $join_conds
337 );
338
339 return $rows;
340 }
341
342 protected function getDB() {
343 return wfGetDB( DB_REPLICA, 'recentchanges' );
344 }
345
346 public function outputFeedLinks() {
347 $this->addFeedLinks( $this->getFeedQuery() );
348 }
349
355 protected function getFeedQuery() {
356 $query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) {
357 // API handles empty parameters in a different way
358 return $value !== '';
359 } );
360 $query['action'] = 'feedrecentchanges';
361 $feedLimit = $this->getConfig()->get( 'FeedLimit' );
362 if ( $query['limit'] > $feedLimit ) {
363 $query['limit'] = $feedLimit;
364 }
365
366 return $query;
367 }
368
375 public function outputChangesList( $rows, $opts ) {
376 $limit = $opts['limit'];
377
378 $showWatcherCount = $this->getConfig()->get( 'RCShowWatchingUsers' )
379 && $this->getUser()->getOption( 'shownumberswatching' );
380 $watcherCache = [];
381
382 $counter = 1;
383 $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups );
384 $list->initChangesListRows( $rows );
385
386 $userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
387 $rclistOutput = $list->beginRecentChangesList();
388 if ( $this->isStructuredFilterUiEnabled() ) {
389 $rclistOutput .= $this->makeLegend();
390 }
391
392 foreach ( $rows as $obj ) {
393 if ( $limit == 0 ) {
394 break;
395 }
396 $rc = RecentChange::newFromRow( $obj );
397
398 # Skip CatWatch entries for hidden cats based on user preference
399 if (
400 $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
401 !$userShowHiddenCats &&
402 $rc->getParam( 'hidden-cat' )
403 ) {
404 continue;
405 }
406
407 $rc->counter = $counter++;
408 # Check if the page has been updated since the last visit
409 if ( $this->getConfig()->get( 'ShowUpdatedMarker' )
410 && !empty( $obj->wl_notificationtimestamp )
411 ) {
412 $rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp );
413 } else {
414 $rc->notificationtimestamp = false; // Default
415 }
416 # Check the number of users watching the page
417 $rc->numberofWatchingusers = 0; // Default
418 if ( $showWatcherCount && $obj->rc_namespace >= 0 ) {
419 if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
420 $watcherCache[$obj->rc_namespace][$obj->rc_title] =
421 MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchers(
422 new TitleValue( (int)$obj->rc_namespace, $obj->rc_title )
423 );
424 }
425 $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
426 }
427
428 $changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
429 if ( $changeLine !== false ) {
430 $rclistOutput .= $changeLine;
431 --$limit;
432 }
433 }
434 $rclistOutput .= $list->endRecentChangesList();
435
436 if ( $rows->numRows() === 0 ) {
437 $this->outputNoResults();
438 if ( !$this->including() ) {
439 $this->getOutput()->setStatusCode( 404 );
440 }
441 } else {
442 $this->getOutput()->addHTML( $rclistOutput );
443 }
444 }
445
452 public function doHeader( $opts, $numRows ) {
453 $this->setTopText( $opts );
454
455 $defaults = $opts->getAllValues();
456 $nondefaults = $opts->getChangedValues();
457
458 $panel = [];
459 if ( !$this->isStructuredFilterUiEnabled() ) {
460 $panel[] = $this->makeLegend();
461 }
462 $panel[] = $this->optionsPanel( $defaults, $nondefaults, $numRows );
463 $panel[] = '<hr />';
464
465 $extraOpts = $this->getExtraOptions( $opts );
466 $extraOptsCount = count( $extraOpts );
467 $count = 0;
468 $submit = ' ' . Xml::submitButton( $this->msg( 'recentchanges-submit' )->text() );
469
470 $out = Xml::openElement( 'table', [ 'class' => 'mw-recentchanges-table' ] );
471 foreach ( $extraOpts as $name => $optionRow ) {
472 # Add submit button to the last row only
473 ++$count;
474 $addSubmit = ( $count === $extraOptsCount ) ? $submit : '';
475
476 $out .= Xml::openElement( 'tr', [ 'class' => $name . 'Form' ] );
477 if ( is_array( $optionRow ) ) {
478 $out .= Xml::tags(
479 'td',
480 [ 'class' => 'mw-label mw-' . $name . '-label' ],
481 $optionRow[0]
482 );
483 $out .= Xml::tags(
484 'td',
485 [ 'class' => 'mw-input' ],
486 $optionRow[1] . $addSubmit
487 );
488 } else {
489 $out .= Xml::tags(
490 'td',
491 [ 'class' => 'mw-input', 'colspan' => 2 ],
492 $optionRow . $addSubmit
493 );
494 }
495 $out .= Xml::closeElement( 'tr' );
496 }
497 $out .= Xml::closeElement( 'table' );
498
499 $unconsumed = $opts->getUnconsumedValues();
500 foreach ( $unconsumed as $key => $value ) {
501 $out .= Html::hidden( $key, $value );
502 }
503
504 $t = $this->getPageTitle();
505 $out .= Html::hidden( 'title', $t->getPrefixedText() );
506 $form = Xml::tags( 'form', [ 'action' => wfScript() ], $out );
507 $panel[] = $form;
508 $panelString = implode( "\n", $panel );
509
510 $rcoptions = Xml::fieldset(
511 $this->msg( 'recentchanges-legend' )->text(),
512 $panelString,
513 [ 'class' => 'rcoptions cloptions' ]
514 );
515
516 // Insert a placeholder for RCFilters
517 if ( $this->isStructuredFilterUiEnabled() ) {
518 $rcfilterContainer = Html::element(
519 'div',
520 [ 'class' => 'rcfilters-container' ]
521 );
522
523 $loadingContainer = Html::rawElement(
524 'div',
525 [ 'class' => 'rcfilters-spinner' ],
526 Html::element(
527 'div',
528 [ 'class' => 'rcfilters-spinner-bounce' ]
529 )
530 );
531
532 // Wrap both with rcfilters-head
533 $this->getOutput()->addHTML(
534 Html::rawElement(
535 'div',
536 [ 'class' => 'rcfilters-head' ],
537 $rcfilterContainer . $rcoptions
538 )
539 );
540
541 // Add spinner
542 $this->getOutput()->addHTML( $loadingContainer );
543 } else {
544 $this->getOutput()->addHTML( $rcoptions );
545 }
546
547 $this->setBottomText( $opts );
548 }
549
555 function setTopText( FormOptions $opts ) {
556 $message = $this->msg( 'recentchangestext' )->inContentLanguage();
557 if ( !$message->isDisabled() ) {
558 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
559 // Parse the message in this weird ugly way to preserve the ability to include interlanguage
560 // links in it (T172461). In the future when T66969 is resolved, perhaps we can just use
561 // $message->parse() instead. This code is copied from Message::parseText().
562 $parserOutput = MessageCache::singleton()->parse(
563 $message->plain(),
564 $this->getPageTitle(),
565 /*linestart*/true,
566 // Message class sets the interface flag to false when parsing in a language different than
567 // user language, and this is wiki content language
568 /*interface*/false,
569 $contLang
570 );
571 $content = $parserOutput->getText( [
572 'enableSectionEditLinks' => false,
573 ] );
574 // Add only metadata here (including the language links), text is added below
575 $this->getOutput()->addParserOutputMetadata( $parserOutput );
576
577 $langAttributes = [
578 'lang' => $contLang->getHtmlCode(),
579 'dir' => $contLang->getDir(),
580 ];
581
582 $topLinksAttributes = [ 'class' => 'mw-recentchanges-toplinks' ];
583
584 if ( $this->isStructuredFilterUiEnabled() ) {
585 // Check whether the widget is already collapsed or expanded
586 $collapsedState = $this->getRequest()->getCookie( 'rcfilters-toplinks-collapsed-state' );
587 // Note that an empty/unset cookie means collapsed, so check for !== 'expanded'
588 $topLinksAttributes[ 'class' ] .= $collapsedState !== 'expanded' ?
589 ' mw-recentchanges-toplinks-collapsed' : '';
590
591 $this->getOutput()->enableOOUI();
592 $contentTitle = new OOUI\ButtonWidget( [
593 'classes' => [ 'mw-recentchanges-toplinks-title' ],
594 'label' => new OOUI\HtmlSnippet( $this->msg( 'rcfilters-other-review-tools' )->parse() ),
595 'framed' => false,
596 'indicator' => $collapsedState !== 'expanded' ? 'down' : 'up',
597 'flags' => [ 'progressive' ],
598 ] );
599
600 $contentWrapper = Html::rawElement( 'div',
601 array_merge(
602 [ 'class' => 'mw-recentchanges-toplinks-content mw-collapsible-content' ],
603 $langAttributes
604 ),
606 );
607 $content = $contentTitle . $contentWrapper;
608 } else {
609 // Language direction should be on the top div only
610 // if the title is not there. If it is there, it's
611 // interface direction, and the language/dir attributes
612 // should be on the content itself
613 $topLinksAttributes = array_merge( $topLinksAttributes, $langAttributes );
614 }
615
616 $this->getOutput()->addHTML(
617 Html::rawElement( 'div', $topLinksAttributes, $content )
618 );
619 }
620 }
621
628 function getExtraOptions( $opts ) {
629 $opts->consumeValues( [
630 'namespace', 'invert', 'associated', 'tagfilter'
631 ] );
632
633 $extraOpts = [];
634 $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
635
636 $tagFilter = ChangeTags::buildTagFilterSelector(
637 $opts['tagfilter'], false, $this->getContext() );
638 if ( count( $tagFilter ) ) {
639 $extraOpts['tagfilter'] = $tagFilter;
640 }
641
642 // Don't fire the hook for subclasses. (Or should we?)
643 if ( $this->getName() === 'Recentchanges' ) {
644 Hooks::run( 'SpecialRecentChangesPanel', [ &$extraOpts, $opts ] );
645 }
646
647 return $extraOpts;
648 }
649
653 protected function addModules() {
654 parent::addModules();
655 $out = $this->getOutput();
656 $out->addModules( 'mediawiki.special.recentchanges' );
657 }
658
666 public function checkLastModified() {
667 $dbr = $this->getDB();
668 $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', '', __METHOD__ );
669
670 return $lastmod;
671 }
672
679 protected function namespaceFilterForm( FormOptions $opts ) {
680 $nsSelect = Html::namespaceSelector(
681 [ 'selected' => $opts['namespace'], 'all' => '' ],
682 [ 'name' => 'namespace', 'id' => 'namespace' ]
683 );
684 $nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' );
685 $invert = Xml::checkLabel(
686 $this->msg( 'invert' )->text(), 'invert', 'nsinvert',
687 $opts['invert'],
688 [ 'title' => $this->msg( 'tooltip-invert' )->text() ]
689 );
690 $associated = Xml::checkLabel(
691 $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated',
692 $opts['associated'],
693 [ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
694 );
695
696 return [ $nsLabel, "$nsSelect $invert $associated" ];
697 }
698
708 wfDeprecated( __METHOD__, '1.31' );
709
710 $categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
711
712 if ( !count( $categories ) ) {
713 return;
714 }
715
716 # Filter categories
717 $cats = [];
718 foreach ( $categories as $cat ) {
719 $cat = trim( $cat );
720 if ( $cat == '' ) {
721 continue;
722 }
723 $cats[] = $cat;
724 }
725
726 # Filter articles
727 $articles = [];
728 $a2r = [];
729 $rowsarr = [];
730 foreach ( $rows as $k => $r ) {
731 $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
732 $id = $nt->getArticleID();
733 if ( $id == 0 ) {
734 continue; # Page might have been deleted...
735 }
736 if ( !in_array( $id, $articles ) ) {
737 $articles[] = $id;
738 }
739 if ( !isset( $a2r[$id] ) ) {
740 $a2r[$id] = [];
741 }
742 $a2r[$id][] = $k;
743 $rowsarr[$k] = $r;
744 }
745
746 # Shortcut?
747 if ( !count( $articles ) || !count( $cats ) ) {
748 return;
749 }
750
751 # Look up
752 $catFind = new CategoryFinder;
753 $catFind->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' );
754 $match = $catFind->run();
755
756 # Filter
757 $newrows = [];
758 foreach ( $match as $id ) {
759 foreach ( $a2r[$id] as $rev ) {
760 $k = $rev;
761 $newrows[$k] = $rowsarr[$k];
762 }
763 }
764 $rows = new FakeResultWrapper( array_values( $newrows ) );
765 }
766
776 function makeOptionsLink( $title, $override, $options, $active = false ) {
777 $params = $this->convertParamsForLink( $override + $options );
778
779 if ( $active ) {
780 $title = new HtmlArmor( '<strong>' . htmlspecialchars( $title ) . '</strong>' );
781 }
782
783 return $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $title, [
784 'data-params' => json_encode( $override ),
785 'data-keys' => implode( ',', array_keys( $override ) ),
786 ], $params );
787 }
788
797 function optionsPanel( $defaults, $nondefaults, $numRows ) {
798 $options = $nondefaults + $defaults;
799
800 $note = '';
801 $msg = $this->msg( 'rclegend' );
802 if ( !$msg->isDisabled() ) {
803 $note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n";
804 }
805
806 $lang = $this->getLanguage();
807 $user = $this->getUser();
808 $config = $this->getConfig();
809 if ( $options['from'] ) {
810 $resetLink = $this->makeOptionsLink( $this->msg( 'rclistfromreset' ),
811 [ 'from' => '' ], $nondefaults );
812
813 $noteFromMsg = $this->msg( 'rcnotefrom' )
814 ->numParams( $options['limit'] )
815 ->params(
816 $lang->userTimeAndDate( $options['from'], $user ),
817 $lang->userDate( $options['from'], $user ),
818 $lang->userTime( $options['from'], $user )
819 )
820 ->numParams( $numRows );
821 $note .= Html::rawElement(
822 'span',
823 [ 'class' => 'rcnotefrom' ],
824 $noteFromMsg->parse()
825 ) .
826 ' ' .
827 Html::rawElement(
828 'span',
829 [ 'class' => 'rcoptions-listfromreset' ],
830 $this->msg( 'parentheses' )->rawParams( $resetLink )->parse()
831 ) .
832 '<br />';
833 }
834
835 # Sort data for display and make sure it's unique after we've added user data.
836 $linkLimits = $config->get( 'RCLinkLimits' );
837 $linkLimits[] = $options['limit'];
838 sort( $linkLimits );
839 $linkLimits = array_unique( $linkLimits );
840
841 $linkDays = $config->get( 'RCLinkDays' );
842 $linkDays[] = $options['days'];
843 sort( $linkDays );
844 $linkDays = array_unique( $linkDays );
845
846 // limit links
847 $cl = [];
848 foreach ( $linkLimits as $value ) {
849 $cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
850 [ 'limit' => $value ], $nondefaults, $value == $options['limit'] );
851 }
852 $cl = $lang->pipeList( $cl );
853
854 // day links, reset 'from' to none
855 $dl = [];
856 foreach ( $linkDays as $value ) {
857 $dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
858 [ 'days' => $value, 'from' => '' ], $nondefaults, $value == $options['days'] );
859 }
860 $dl = $lang->pipeList( $dl );
861
862 $showhide = [ 'show', 'hide' ];
863
864 $links = [];
865
866 foreach ( $this->getLegacyShowHideFilters() as $key => $filter ) {
867 $msg = $filter->getShowHide();
868 $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] );
869 // Extensions can define additional filters, but don't need to define the corresponding
870 // messages. If they don't exist, just fall back to 'show' and 'hide'.
871 if ( !$linkMessage->exists() ) {
872 $linkMessage = $this->msg( $showhide[1 - $options[$key]] );
873 }
874
875 $link = $this->makeOptionsLink( $linkMessage->text(),
876 [ $key => 1 - $options[$key] ], $nondefaults );
877
878 $attribs = [
879 'class' => "$msg rcshowhideoption clshowhideoption",
880 'data-filter-name' => $filter->getName(),
881 ];
882
883 if ( $filter->isFeatureAvailableOnStructuredUi( $this ) ) {
884 $attribs['data-feature-in-structured-ui'] = true;
885 }
886
887 $links[] = Html::rawElement(
888 'span',
889 $attribs,
890 $this->msg( $msg )->rawParams( $link )->parse()
891 );
892 }
893
894 // show from this onward link
895 $timestamp = wfTimestampNow();
896 $now = $lang->userTimeAndDate( $timestamp, $user );
897 $timenow = $lang->userTime( $timestamp, $user );
898 $datenow = $lang->userDate( $timestamp, $user );
899 $pipedLinks = '<span class="rcshowhide">' . $lang->pipeList( $links ) . '</span>';
900
901 $rclinks = '<span class="rclinks">' . $this->msg( 'rclinks' )->rawParams( $cl, $dl, '' )
902 ->parse() . '</span>';
903
904 $rclistfrom = '<span class="rclistfrom">' . $this->makeOptionsLink(
905 $this->msg( 'rclistfrom' )->rawParams( $now, $timenow, $datenow )->parse(),
906 [ 'from' => $timestamp ],
907 $nondefaults
908 ) . '</span>';
909
910 return "{$note}$rclinks<br />$pipedLinks<br />$rclistfrom";
911 }
912
913 public function isIncludable() {
914 return true;
915 }
916
917 protected function getCacheTTL() {
918 return 60 * 5;
919 }
920
921 public function getDefaultLimit() {
922 $systemPrefValue = $this->getUser()->getIntOption( 'rclimit' );
923 // Prefer the RCFilters-specific preference if RCFilters is enabled
924 if ( $this->isStructuredFilterUiEnabled() ) {
925 return $this->getUser()->getIntOption( static::$limitPreferenceName, $systemPrefValue );
926 }
927
928 // Otherwise, use the system rclimit preference value
929 return $systemPrefValue;
930 }
931}
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
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
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.
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)
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.
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.
execute( $subpage)
Main execution point.
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)
@inheritDoc
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.
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
@inheritDoc
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...
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:2857
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2050
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:2885
this hook is for auditing only 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:1035
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:894
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3106
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition hooks.txt:2063
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:1656
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1818
const LIST_OR
Definition Defines.php:46
const LIST_AND
Definition Defines.php:43
const RC_CATEGORIZE
Definition Defines.php:146
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:24
Result wrapper for grabbing data queried from an IDatabase object.
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$content
const DB_REPLICA
Definition defines.php:25
$params
if(!isset( $args[0])) $lang