MediaWiki REL1_31
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
40
41 public function __construct( $name = 'Recentchanges', $restriction = '' ) {
42 parent::__construct( $name, $restriction );
43
44 $this->watchlistFilterGroupDefinition = [
45 'name' => 'watchlist',
46 'title' => 'rcfilters-filtergroup-watchlist',
47 'class' => ChangesListStringOptionsFilterGroup::class,
48 'priority' => -9,
49 'isFullCoverage' => true,
50 'filters' => [
51 [
52 'name' => 'watched',
53 'label' => 'rcfilters-filter-watchlist-watched-label',
54 'description' => 'rcfilters-filter-watchlist-watched-description',
55 'cssClassSuffix' => 'watched',
56 'isRowApplicableCallable' => function ( $ctx, $rc ) {
57 return $rc->getAttribute( 'wl_user' );
58 }
59 ],
60 [
61 'name' => 'watchednew',
62 'label' => 'rcfilters-filter-watchlist-watchednew-label',
63 'description' => 'rcfilters-filter-watchlist-watchednew-description',
64 'cssClassSuffix' => 'watchednew',
65 'isRowApplicableCallable' => function ( $ctx, $rc ) {
66 return $rc->getAttribute( 'wl_user' ) &&
67 $rc->getAttribute( 'rc_timestamp' ) &&
68 $rc->getAttribute( 'wl_notificationtimestamp' ) &&
69 $rc->getAttribute( 'rc_timestamp' ) >= $rc->getAttribute( 'wl_notificationtimestamp' );
70 },
71 ],
72 [
73 'name' => 'notwatched',
74 'label' => 'rcfilters-filter-watchlist-notwatched-label',
75 'description' => 'rcfilters-filter-watchlist-notwatched-description',
76 'cssClassSuffix' => 'notwatched',
77 'isRowApplicableCallable' => function ( $ctx, $rc ) {
78 return $rc->getAttribute( 'wl_user' ) === null;
79 },
80 ]
81 ],
83 'queryCallable' => function ( $specialPageClassName, $context, $dbr,
84 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) {
85 sort( $selectedValues );
86 $notwatchedCond = 'wl_user IS NULL';
87 $watchedCond = 'wl_user IS NOT NULL';
88 $newCond = 'rc_timestamp >= wl_notificationtimestamp';
89
90 if ( $selectedValues === [ 'notwatched' ] ) {
91 $conds[] = $notwatchedCond;
92 return;
93 }
94
95 if ( $selectedValues === [ 'watched' ] ) {
96 $conds[] = $watchedCond;
97 return;
98 }
99
100 if ( $selectedValues === [ 'watchednew' ] ) {
101 $conds[] = $dbr->makeList( [
102 $watchedCond,
103 $newCond
104 ], LIST_AND );
105 return;
106 }
107
108 if ( $selectedValues === [ 'notwatched', 'watched' ] ) {
109 // no filters
110 return;
111 }
112
113 if ( $selectedValues === [ 'notwatched', 'watchednew' ] ) {
114 $conds[] = $dbr->makeList( [
115 $notwatchedCond,
116 $dbr->makeList( [
117 $watchedCond,
118 $newCond
119 ], LIST_AND )
120 ], LIST_OR );
121 return;
122 }
123
124 if ( $selectedValues === [ 'watched', 'watchednew' ] ) {
125 $conds[] = $watchedCond;
126 return;
127 }
128
129 if ( $selectedValues === [ 'notwatched', 'watched', 'watchednew' ] ) {
130 // no filters
131 return;
132 }
133 }
134 ];
135 }
136
142 public function execute( $subpage ) {
143 // Backwards-compatibility: redirect to new feed URLs
144 $feedFormat = $this->getRequest()->getVal( 'feed' );
145 if ( !$this->including() && $feedFormat ) {
146 $query = $this->getFeedQuery();
147 $query['feedformat'] = $feedFormat === 'atom' ? 'atom' : 'rss';
148 $this->getOutput()->redirect( wfAppendQuery( wfScript( 'api' ), $query ) );
149
150 return;
151 }
152
153 // 10 seconds server-side caching max
154 $out = $this->getOutput();
155 $out->setCdnMaxage( 10 );
156 // Check if the client has a cached version
157 $lastmod = $this->checkLastModified();
158 if ( $lastmod === false ) {
159 return;
160 }
161
162 $this->addHelpLink(
163 '//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Recent_changes',
164 true
165 );
166 parent::execute( $subpage );
167 }
168
172 protected function transformFilterDefinition( array $filterDefinition ) {
173 if ( isset( $filterDefinition['showHideSuffix'] ) ) {
174 $filterDefinition['showHide'] = 'rc' . $filterDefinition['showHideSuffix'];
175 }
176
177 return $filterDefinition;
178 }
179
183 protected function registerFilters() {
184 parent::registerFilters();
185
186 if (
187 !$this->including() &&
188 $this->getUser()->isLoggedIn() &&
189 $this->getUser()->isAllowed( 'viewmywatchlist' )
190 ) {
191 $this->registerFiltersFromDefinitions( [ $this->watchlistFilterGroupDefinition ] );
192 $watchlistGroup = $this->getFilterGroup( 'watchlist' );
193 $watchlistGroup->getFilter( 'watched' )->setAsSupersetOf(
194 $watchlistGroup->getFilter( 'watchednew' )
195 );
196 }
197
198 $user = $this->getUser();
199
200 $significance = $this->getFilterGroup( 'significance' );
201 $hideMinor = $significance->getFilter( 'hideminor' );
202 $hideMinor->setDefault( $user->getBoolOption( 'hideminor' ) );
203
204 $automated = $this->getFilterGroup( 'automated' );
205 $hideBots = $automated->getFilter( 'hidebots' );
206 $hideBots->setDefault( true );
207
208 $reviewStatus = $this->getFilterGroup( 'reviewStatus' );
209 if ( $reviewStatus !== null ) {
210 // Conditional on feature being available and rights
211 if ( $user->getBoolOption( 'hidepatrolled' ) ) {
212 $reviewStatus->setDefault( 'unpatrolled' );
213 $legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
214 $legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
215 $legacyHidePatrolled->setDefault( true );
216 }
217 }
218
219 $changeType = $this->getFilterGroup( 'changeType' );
220 $hideCategorization = $changeType->getFilter( 'hidecategorization' );
221 if ( $hideCategorization !== null ) {
222 // Conditional on feature being available
223 $hideCategorization->setDefault( $user->getBoolOption( 'hidecategorization' ) );
224 }
225 }
226
232 protected function getCustomFilters() {
233 if ( $this->customFilters === null ) {
234 $this->customFilters = parent::getCustomFilters();
235 Hooks::run( 'SpecialRecentChangesFilters', [ $this, &$this->customFilters ], '1.23' );
236 }
237
239 }
240
247 public function parseParameters( $par, FormOptions $opts ) {
248 parent::parseParameters( $par, $opts );
249
250 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
251 foreach ( $bits as $bit ) {
252 if ( is_numeric( $bit ) ) {
253 $opts['limit'] = $bit;
254 }
255
256 $m = [];
257 if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
258 $opts['limit'] = $m[1];
259 }
260 if ( preg_match( '/^days=(\d+(?:\.\d+)?)$/', $bit, $m ) ) {
261 $opts['days'] = $m[1];
262 }
263 if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
264 $opts['namespace'] = $m[1];
265 }
266 if ( preg_match( '/^tagfilter=(.*)$/', $bit, $m ) ) {
267 $opts['tagfilter'] = $m[1];
268 }
269 }
270 }
271
275 protected function doMainQuery( $tables, $fields, $conds, $query_options,
276 $join_conds, FormOptions $opts
277 ) {
278 $dbr = $this->getDB();
279 $user = $this->getUser();
280
281 $rcQuery = RecentChange::getQueryInfo();
282 $tables = array_merge( $tables, $rcQuery['tables'] );
283 $fields = array_merge( $rcQuery['fields'], $fields );
284 $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
285
286 // JOIN on watchlist for users
287 if ( $user->isLoggedIn() && $user->isAllowed( 'viewmywatchlist' ) ) {
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 runMainQueryHook( &$tables, &$fields, &$conds,
356 &$query_options, &$join_conds, $opts
357 ) {
358 return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts )
359 && Hooks::run(
360 'SpecialRecentChangesQuery',
361 [ &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ],
362 '1.23'
363 );
364 }
365
366 protected function getDB() {
367 return wfGetDB( DB_REPLICA, 'recentchanges' );
368 }
369
370 public function outputFeedLinks() {
371 $this->addFeedLinks( $this->getFeedQuery() );
372 }
373
379 protected function getFeedQuery() {
380 $query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) {
381 // API handles empty parameters in a different way
382 return $value !== '';
383 } );
384 $query['action'] = 'feedrecentchanges';
385 $feedLimit = $this->getConfig()->get( 'FeedLimit' );
386 if ( $query['limit'] > $feedLimit ) {
387 $query['limit'] = $feedLimit;
388 }
389
390 return $query;
391 }
392
399 public function outputChangesList( $rows, $opts ) {
400 $limit = $opts['limit'];
401
402 $showWatcherCount = $this->getConfig()->get( 'RCShowWatchingUsers' )
403 && $this->getUser()->getOption( 'shownumberswatching' );
404 $watcherCache = [];
405
406 $counter = 1;
407 $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups );
408 $list->initChangesListRows( $rows );
409
410 $userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
411 $rclistOutput = $list->beginRecentChangesList();
412 if ( $this->isStructuredFilterUiEnabled() ) {
413 $rclistOutput .= $this->makeLegend();
414 }
415
416 foreach ( $rows as $obj ) {
417 if ( $limit == 0 ) {
418 break;
419 }
420 $rc = RecentChange::newFromRow( $obj );
421
422 # Skip CatWatch entries for hidden cats based on user preference
423 if (
424 $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
425 !$userShowHiddenCats &&
426 $rc->getParam( 'hidden-cat' )
427 ) {
428 continue;
429 }
430
431 $rc->counter = $counter++;
432 # Check if the page has been updated since the last visit
433 if ( $this->getConfig()->get( 'ShowUpdatedMarker' )
434 && !empty( $obj->wl_notificationtimestamp )
435 ) {
436 $rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp );
437 } else {
438 $rc->notificationtimestamp = false; // Default
439 }
440 # Check the number of users watching the page
441 $rc->numberofWatchingusers = 0; // Default
442 if ( $showWatcherCount && $obj->rc_namespace >= 0 ) {
443 if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
444 $watcherCache[$obj->rc_namespace][$obj->rc_title] =
445 MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchers(
446 new TitleValue( (int)$obj->rc_namespace, $obj->rc_title )
447 );
448 }
449 $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
450 }
451
452 $changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
453 if ( $changeLine !== false ) {
454 $rclistOutput .= $changeLine;
455 --$limit;
456 }
457 }
458 $rclistOutput .= $list->endRecentChangesList();
459
460 if ( $rows->numRows() === 0 ) {
461 $this->outputNoResults();
462 if ( !$this->including() ) {
463 $this->getOutput()->setStatusCode( 404 );
464 }
465 } else {
466 $this->getOutput()->addHTML( $rclistOutput );
467 }
468 }
469
476 public function doHeader( $opts, $numRows ) {
477 $this->setTopText( $opts );
478
479 $defaults = $opts->getAllValues();
480 $nondefaults = $opts->getChangedValues();
481
482 $panel = [];
483 if ( !$this->isStructuredFilterUiEnabled() ) {
484 $panel[] = $this->makeLegend();
485 }
486 $panel[] = $this->optionsPanel( $defaults, $nondefaults, $numRows );
487 $panel[] = '<hr />';
488
489 $extraOpts = $this->getExtraOptions( $opts );
490 $extraOptsCount = count( $extraOpts );
491 $count = 0;
492 $submit = ' ' . Xml::submitButton( $this->msg( 'recentchanges-submit' )->text() );
493
494 $out = Xml::openElement( 'table', [ 'class' => 'mw-recentchanges-table' ] );
495 foreach ( $extraOpts as $name => $optionRow ) {
496 # Add submit button to the last row only
497 ++$count;
498 $addSubmit = ( $count === $extraOptsCount ) ? $submit : '';
499
500 $out .= Xml::openElement( 'tr', [ 'class' => $name . 'Form' ] );
501 if ( is_array( $optionRow ) ) {
502 $out .= Xml::tags(
503 'td',
504 [ 'class' => 'mw-label mw-' . $name . '-label' ],
505 $optionRow[0]
506 );
507 $out .= Xml::tags(
508 'td',
509 [ 'class' => 'mw-input' ],
510 $optionRow[1] . $addSubmit
511 );
512 } else {
513 $out .= Xml::tags(
514 'td',
515 [ 'class' => 'mw-input', 'colspan' => 2 ],
516 $optionRow . $addSubmit
517 );
518 }
519 $out .= Xml::closeElement( 'tr' );
520 }
521 $out .= Xml::closeElement( 'table' );
522
523 $unconsumed = $opts->getUnconsumedValues();
524 foreach ( $unconsumed as $key => $value ) {
525 $out .= Html::hidden( $key, $value );
526 }
527
528 $t = $this->getPageTitle();
529 $out .= Html::hidden( 'title', $t->getPrefixedText() );
530 $form = Xml::tags( 'form', [ 'action' => wfScript() ], $out );
531 $panel[] = $form;
532 $panelString = implode( "\n", $panel );
533
534 $rcoptions = Xml::fieldset(
535 $this->msg( 'recentchanges-legend' )->text(),
536 $panelString,
537 [ 'class' => 'rcoptions cloptions' ]
538 );
539
540 // Insert a placeholder for RCFilters
541 if ( $this->isStructuredFilterUiEnabled() ) {
542 $rcfilterContainer = Html::element(
543 'div',
544 [ 'class' => 'rcfilters-container' ]
545 );
546
547 $loadingContainer = Html::rawElement(
548 'div',
549 [ 'class' => 'rcfilters-spinner' ],
550 Html::element(
551 'div',
552 [ 'class' => 'rcfilters-spinner-bounce' ]
553 )
554 );
555
556 // Wrap both with rcfilters-head
557 $this->getOutput()->addHTML(
558 Html::rawElement(
559 'div',
560 [ 'class' => 'rcfilters-head' ],
561 $rcfilterContainer . $rcoptions
562 )
563 );
564
565 // Add spinner
566 $this->getOutput()->addHTML( $loadingContainer );
567 } else {
568 $this->getOutput()->addHTML( $rcoptions );
569 }
570
571 $this->setBottomText( $opts );
572 }
573
579 function setTopText( FormOptions $opts ) {
580 global $wgContLang;
581
582 $message = $this->msg( 'recentchangestext' )->inContentLanguage();
583 if ( !$message->isDisabled() ) {
584 // Parse the message in this weird ugly way to preserve the ability to include interlanguage
585 // links in it (T172461). In the future when T66969 is resolved, perhaps we can just use
586 // $message->parse() instead. This code is copied from Message::parseText().
587 $parserOutput = MessageCache::singleton()->parse(
588 $message->plain(),
589 $this->getPageTitle(),
590 /*linestart*/true,
591 // Message class sets the interface flag to false when parsing in a language different than
592 // user language, and this is wiki content language
593 /*interface*/false,
595 );
596 $content = $parserOutput->getText( [
597 'enableSectionEditLinks' => false,
598 ] );
599 // Add only metadata here (including the language links), text is added below
600 $this->getOutput()->addParserOutputMetadata( $parserOutput );
601
602 $langAttributes = [
603 'lang' => $wgContLang->getHtmlCode(),
604 'dir' => $wgContLang->getDir(),
605 ];
606
607 $topLinksAttributes = [ 'class' => 'mw-recentchanges-toplinks' ];
608
609 if ( $this->isStructuredFilterUiEnabled() ) {
610 // Check whether the widget is already collapsed or expanded
611 $collapsedState = $this->getRequest()->getCookie( 'rcfilters-toplinks-collapsed-state' );
612 // Note that an empty/unset cookie means collapsed, so check for !== 'expanded'
613 $topLinksAttributes[ 'class' ] .= $collapsedState !== 'expanded' ?
614 ' mw-recentchanges-toplinks-collapsed' : '';
615
616 $this->getOutput()->enableOOUI();
617 $contentTitle = new OOUI\ButtonWidget( [
618 'classes' => [ 'mw-recentchanges-toplinks-title' ],
619 'label' => new OOUI\HtmlSnippet( $this->msg( 'rcfilters-other-review-tools' )->parse() ),
620 'framed' => false,
621 'indicator' => $collapsedState !== 'expanded' ? 'down' : 'up',
622 'flags' => [ 'progressive' ],
623 ] );
624
625 $contentWrapper = Html::rawElement( 'div',
626 array_merge(
627 [ 'class' => 'mw-recentchanges-toplinks-content mw-collapsible-content' ],
628 $langAttributes
629 ),
630 $content
631 );
632 $content = $contentTitle . $contentWrapper;
633 } else {
634 // Language direction should be on the top div only
635 // if the title is not there. If it is there, it's
636 // interface direction, and the language/dir attributes
637 // should be on the content itself
638 $topLinksAttributes = array_merge( $topLinksAttributes, $langAttributes );
639 }
640
641 $this->getOutput()->addHTML(
642 Html::rawElement( 'div', $topLinksAttributes, $content )
643 );
644 }
645 }
646
653 function getExtraOptions( $opts ) {
654 $opts->consumeValues( [
655 'namespace', 'invert', 'associated', 'tagfilter'
656 ] );
657
658 $extraOpts = [];
659 $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
660
662 $opts['tagfilter'], false, $this->getContext() );
663 if ( count( $tagFilter ) ) {
664 $extraOpts['tagfilter'] = $tagFilter;
665 }
666
667 // Don't fire the hook for subclasses. (Or should we?)
668 if ( $this->getName() === 'Recentchanges' ) {
669 Hooks::run( 'SpecialRecentChangesPanel', [ &$extraOpts, $opts ] );
670 }
671
672 return $extraOpts;
673 }
674
678 protected function addModules() {
679 parent::addModules();
680 $out = $this->getOutput();
681 $out->addModules( 'mediawiki.special.recentchanges' );
682 }
683
691 public function checkLastModified() {
692 $dbr = $this->getDB();
693 $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', '', __METHOD__ );
694
695 return $lastmod;
696 }
697
704 protected function namespaceFilterForm( FormOptions $opts ) {
705 $nsSelect = Html::namespaceSelector(
706 [ 'selected' => $opts['namespace'], 'all' => '' ],
707 [ 'name' => 'namespace', 'id' => 'namespace' ]
708 );
709 $nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' );
710 $invert = Xml::checkLabel(
711 $this->msg( 'invert' )->text(), 'invert', 'nsinvert',
712 $opts['invert'],
713 [ 'title' => $this->msg( 'tooltip-invert' )->text() ]
714 );
715 $associated = Xml::checkLabel(
716 $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated',
717 $opts['associated'],
718 [ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
719 );
720
721 return [ $nsLabel, "$nsSelect $invert $associated" ];
722 }
723
733 wfDeprecated( __METHOD__, '1.31' );
734
735 $categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
736
737 if ( !count( $categories ) ) {
738 return;
739 }
740
741 # Filter categories
742 $cats = [];
743 foreach ( $categories as $cat ) {
744 $cat = trim( $cat );
745 if ( $cat == '' ) {
746 continue;
747 }
748 $cats[] = $cat;
749 }
750
751 # Filter articles
752 $articles = [];
753 $a2r = [];
754 $rowsarr = [];
755 foreach ( $rows as $k => $r ) {
756 $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
757 $id = $nt->getArticleID();
758 if ( $id == 0 ) {
759 continue; # Page might have been deleted...
760 }
761 if ( !in_array( $id, $articles ) ) {
762 $articles[] = $id;
763 }
764 if ( !isset( $a2r[$id] ) ) {
765 $a2r[$id] = [];
766 }
767 $a2r[$id][] = $k;
768 $rowsarr[$k] = $r;
769 }
770
771 # Shortcut?
772 if ( !count( $articles ) || !count( $cats ) ) {
773 return;
774 }
775
776 # Look up
777 $catFind = new CategoryFinder;
778 $catFind->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' );
779 $match = $catFind->run();
780
781 # Filter
782 $newrows = [];
783 foreach ( $match as $id ) {
784 foreach ( $a2r[$id] as $rev ) {
785 $k = $rev;
786 $newrows[$k] = $rowsarr[$k];
787 }
788 }
789 $rows = new FakeResultWrapper( array_values( $newrows ) );
790 }
791
801 function makeOptionsLink( $title, $override, $options, $active = false ) {
802 $params = $this->convertParamsForLink( $override + $options );
803
804 if ( $active ) {
805 $title = new HtmlArmor( '<strong>' . htmlspecialchars( $title ) . '</strong>' );
806 }
807
808 return $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $title, [
809 'data-params' => json_encode( $override ),
810 'data-keys' => implode( ',', array_keys( $override ) ),
811 ], $params );
812 }
813
822 function optionsPanel( $defaults, $nondefaults, $numRows ) {
823 $options = $nondefaults + $defaults;
824
825 $note = '';
826 $msg = $this->msg( 'rclegend' );
827 if ( !$msg->isDisabled() ) {
828 $note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n";
829 }
830
831 $lang = $this->getLanguage();
832 $user = $this->getUser();
833 $config = $this->getConfig();
834 if ( $options['from'] ) {
835 $resetLink = $this->makeOptionsLink( $this->msg( 'rclistfromreset' ),
836 [ 'from' => '' ], $nondefaults );
837
838 $noteFromMsg = $this->msg( 'rcnotefrom' )
839 ->numParams( $options['limit'] )
840 ->params(
841 $lang->userTimeAndDate( $options['from'], $user ),
842 $lang->userDate( $options['from'], $user ),
843 $lang->userTime( $options['from'], $user )
844 )
845 ->numParams( $numRows );
846 $note .= Html::rawElement(
847 'span',
848 [ 'class' => 'rcnotefrom' ],
849 $noteFromMsg->parse()
850 ) .
851 ' ' .
852 Html::rawElement(
853 'span',
854 [ 'class' => 'rcoptions-listfromreset' ],
855 $this->msg( 'parentheses' )->rawParams( $resetLink )->parse()
856 ) .
857 '<br />';
858 }
859
860 # Sort data for display and make sure it's unique after we've added user data.
861 $linkLimits = $config->get( 'RCLinkLimits' );
862 $linkLimits[] = $options['limit'];
863 sort( $linkLimits );
864 $linkLimits = array_unique( $linkLimits );
865
866 $linkDays = $config->get( 'RCLinkDays' );
867 $linkDays[] = $options['days'];
868 sort( $linkDays );
869 $linkDays = array_unique( $linkDays );
870
871 // limit links
872 $cl = [];
873 foreach ( $linkLimits as $value ) {
874 $cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
875 [ 'limit' => $value ], $nondefaults, $value == $options['limit'] );
876 }
877 $cl = $lang->pipeList( $cl );
878
879 // day links, reset 'from' to none
880 $dl = [];
881 foreach ( $linkDays as $value ) {
882 $dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
883 [ 'days' => $value, 'from' => '' ], $nondefaults, $value == $options['days'] );
884 }
885 $dl = $lang->pipeList( $dl );
886
887 $showhide = [ 'show', 'hide' ];
888
889 $links = [];
890
891 foreach ( $this->getLegacyShowHideFilters() as $key => $filter ) {
892 $msg = $filter->getShowHide();
893 $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] );
894 // Extensions can define additional filters, but don't need to define the corresponding
895 // messages. If they don't exist, just fall back to 'show' and 'hide'.
896 if ( !$linkMessage->exists() ) {
897 $linkMessage = $this->msg( $showhide[1 - $options[$key]] );
898 }
899
900 $link = $this->makeOptionsLink( $linkMessage->text(),
901 [ $key => 1 - $options[$key] ], $nondefaults );
902
903 $attribs = [
904 'class' => "$msg rcshowhideoption clshowhideoption",
905 'data-filter-name' => $filter->getName(),
906 ];
907
908 if ( $filter->isFeatureAvailableOnStructuredUi( $this ) ) {
909 $attribs['data-feature-in-structured-ui'] = true;
910 }
911
912 $links[] = Html::rawElement(
913 'span',
914 $attribs,
915 $this->msg( $msg )->rawParams( $link )->parse()
916 );
917 }
918
919 // show from this onward link
920 $timestamp = wfTimestampNow();
921 $now = $lang->userTimeAndDate( $timestamp, $user );
922 $timenow = $lang->userTime( $timestamp, $user );
923 $datenow = $lang->userDate( $timestamp, $user );
924 $pipedLinks = '<span class="rcshowhide">' . $lang->pipeList( $links ) . '</span>';
925
926 $rclinks = '<span class="rclinks">' . $this->msg( 'rclinks' )->rawParams( $cl, $dl, '' )
927 ->parse() . '</span>';
928
929 $rclistfrom = '<span class="rclistfrom">' . $this->makeOptionsLink(
930 $this->msg( 'rclistfrom' )->rawParams( $now, $timenow, $datenow )->parse(),
931 [ 'from' => $timestamp ],
932 $nondefaults
933 ) . '</span>';
934
935 return "{$note}$rclinks<br />$pipedLinks<br />$rclistfrom";
936 }
937
938 public function isIncludable() {
939 return true;
940 }
941
942 protected function getCacheTTL() {
943 return 60 * 5;
944 }
945
946 public function getDefaultLimit() {
947 $systemPrefValue = $this->getUser()->getIntOption( 'rclimit' );
948 // Prefer the RCFilters-specific preference if RCFilters is enabled
949 if ( $this->isStructuredFilterUiEnabled() ) {
950 return $this->getUser()->getIntOption( static::$limitPreferenceName, $systemPrefValue );
951 }
952
953 // Otherwise, use the system rclimit preference value
954 return $systemPrefValue;
955 }
956}
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
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.
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.
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.
static singleton()
Get the signleton instance of this class.
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.
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
outputChangesList( $rows, $opts)
Build and output the actual changes list.
outputFeedLinks()
Output feed links.
getCustomFilters()
Get all custom filters.
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...
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition design.txt:18
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
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
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:2001
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
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3021
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:2014
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
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:1777
const LIST_OR
Definition Defines.php:56
const LIST_AND
Definition Defines.php:53
const RC_CATEGORIZE
Definition Defines.php:156
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:24
Result wrapper for grabbing data queried from an IDatabase object.
const DB_REPLICA
Definition defines.php:25
$params
if(!isset( $args[0])) $lang