MediaWiki REL1_30
SpecialRecentchanges.php
Go to the documentation of this file.
1<?php
27
34
35 protected static $savedQueriesPreferenceName = 'rcfilters-saved-queries';
36
38
39 // @codingStandardsIgnoreStart Needed "useless" override to change parameters.
40 public function __construct( $name = 'Recentchanges', $restriction = '' ) {
41 parent::__construct( $name, $restriction );
42
43 $this->watchlistFilterGroupDefinition = [
44 'name' => 'watchlist',
45 'title' => 'rcfilters-filtergroup-watchlist',
46 'class' => ChangesListStringOptionsFilterGroup::class,
47 'priority' => -9,
48 'isFullCoverage' => true,
49 'filters' => [
50 [
51 'name' => 'watched',
52 'label' => 'rcfilters-filter-watchlist-watched-label',
53 'description' => 'rcfilters-filter-watchlist-watched-description',
54 'cssClassSuffix' => 'watched',
55 'isRowApplicableCallable' => function ( $ctx, $rc ) {
56 return $rc->getAttribute( 'wl_user' );
57 }
58 ],
59 [
60 'name' => 'watchednew',
61 'label' => 'rcfilters-filter-watchlist-watchednew-label',
62 'description' => 'rcfilters-filter-watchlist-watchednew-description',
63 'cssClassSuffix' => 'watchednew',
64 'isRowApplicableCallable' => function ( $ctx, $rc ) {
65 return $rc->getAttribute( 'wl_user' ) &&
66 $rc->getAttribute( 'rc_timestamp' ) &&
67 $rc->getAttribute( 'wl_notificationtimestamp' ) &&
68 $rc->getAttribute( 'rc_timestamp' ) >= $rc->getAttribute( 'wl_notificationtimestamp' );
69 },
70 ],
71 [
72 'name' => 'notwatched',
73 'label' => 'rcfilters-filter-watchlist-notwatched-label',
74 'description' => 'rcfilters-filter-watchlist-notwatched-description',
75 'cssClassSuffix' => 'notwatched',
76 'isRowApplicableCallable' => function ( $ctx, $rc ) {
77 return $rc->getAttribute( 'wl_user' ) === null;
78 },
79 ]
80 ],
82 'queryCallable' => function ( $specialPageClassName, $context, $dbr,
83 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) {
84 sort( $selectedValues );
85 $notwatchedCond = 'wl_user IS NULL';
86 $watchedCond = 'wl_user IS NOT NULL';
87 $newCond = 'rc_timestamp >= wl_notificationtimestamp';
88
89 if ( $selectedValues === [ 'notwatched' ] ) {
90 $conds[] = $notwatchedCond;
91 return;
92 }
93
94 if ( $selectedValues === [ 'watched' ] ) {
95 $conds[] = $watchedCond;
96 return;
97 }
98
99 if ( $selectedValues === [ 'watchednew' ] ) {
100 $conds[] = $dbr->makeList( [
101 $watchedCond,
102 $newCond
103 ], LIST_AND );
104 return;
105 }
106
107 if ( $selectedValues === [ 'notwatched', 'watched' ] ) {
108 // no filters
109 return;
110 }
111
112 if ( $selectedValues === [ 'notwatched', 'watchednew' ] ) {
113 $conds[] = $dbr->makeList( [
114 $notwatchedCond,
115 $dbr->makeList( [
116 $watchedCond,
117 $newCond
118 ], LIST_AND )
119 ], LIST_OR );
120 return;
121 }
122
123 if ( $selectedValues === [ 'watched', 'watchednew' ] ) {
124 $conds[] = $watchedCond;
125 return;
126 }
127
128 if ( $selectedValues === [ 'notwatched', 'watched', 'watchednew' ] ) {
129 // no filters
130 return;
131 }
132 }
133 ];
134 }
135 // @codingStandardsIgnoreEnd
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 if ( $this->isStructuredFilterUiEnabled() ) {
169 $out->addJsConfigVars( 'wgStructuredChangeFiltersLiveUpdateSupported', true );
170 }
171 }
172
176 protected function transformFilterDefinition( array $filterDefinition ) {
177 if ( isset( $filterDefinition['showHideSuffix'] ) ) {
178 $filterDefinition['showHide'] = 'rc' . $filterDefinition['showHideSuffix'];
179 }
180
181 return $filterDefinition;
182 }
183
187 protected function registerFilters() {
188 parent::registerFilters();
189
190 if (
191 !$this->including() &&
192 $this->getUser()->isLoggedIn() &&
193 $this->getUser()->isAllowed( 'viewmywatchlist' )
194 ) {
195 $this->registerFiltersFromDefinitions( [ $this->watchlistFilterGroupDefinition ] );
196 $watchlistGroup = $this->getFilterGroup( 'watchlist' );
197 $watchlistGroup->getFilter( 'watched' )->setAsSupersetOf(
198 $watchlistGroup->getFilter( 'watchednew' )
199 );
200 }
201
202 $user = $this->getUser();
203
204 $significance = $this->getFilterGroup( 'significance' );
205 $hideMinor = $significance->getFilter( 'hideminor' );
206 $hideMinor->setDefault( $user->getBoolOption( 'hideminor' ) );
207
208 $automated = $this->getFilterGroup( 'automated' );
209 $hideBots = $automated->getFilter( 'hidebots' );
210 $hideBots->setDefault( true );
211
212 $reviewStatus = $this->getFilterGroup( 'reviewStatus' );
213 if ( $reviewStatus !== null ) {
214 // Conditional on feature being available and rights
215 $hidePatrolled = $reviewStatus->getFilter( 'hidepatrolled' );
216 $hidePatrolled->setDefault( $user->getBoolOption( 'hidepatrolled' ) );
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 public function getDefaultOptions() {
233 $opts = parent::getDefaultOptions();
234
235 $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT );
236 $opts->add( 'limit', $this->getDefaultLimit() );
237 $opts->add( 'from', '' );
238
239 $opts->add( 'categories', '' );
240 $opts->add( 'categories_any', false );
241
242 return $opts;
243 }
244
250 protected function getCustomFilters() {
251 if ( $this->customFilters === null ) {
252 $this->customFilters = parent::getCustomFilters();
253 Hooks::run( 'SpecialRecentChangesFilters', [ $this, &$this->customFilters ], '1.23' );
254 }
255
257 }
258
265 public function parseParameters( $par, FormOptions $opts ) {
266 parent::parseParameters( $par, $opts );
267
268 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
269 foreach ( $bits as $bit ) {
270 if ( is_numeric( $bit ) ) {
271 $opts['limit'] = $bit;
272 }
273
274 $m = [];
275 if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
276 $opts['limit'] = $m[1];
277 }
278 if ( preg_match( '/^days=(\d+(?:\.\d+)?)$/', $bit, $m ) ) {
279 $opts['days'] = $m[1];
280 }
281 if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
282 $opts['namespace'] = $m[1];
283 }
284 if ( preg_match( '/^tagfilter=(.*)$/', $bit, $m ) ) {
285 $opts['tagfilter'] = $m[1];
286 }
287 }
288 }
289
290 public function validateOptions( FormOptions $opts ) {
291 $opts->validateIntBounds( 'limit', 0, 5000 );
292 $opts->validateBounds( 'days', 0, $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
293 parent::validateOptions( $opts );
294 }
295
299 protected function buildQuery( &$tables, &$fields, &$conds,
300 &$query_options, &$join_conds, FormOptions $opts
301 ) {
302 $dbr = $this->getDB();
303 parent::buildQuery( $tables, $fields, $conds,
304 $query_options, $join_conds, $opts );
305
306 // Calculate cutoff
307 $cutoff_unixtime = time() - $opts['days'] * 3600 * 24;
308 $cutoff = $dbr->timestamp( $cutoff_unixtime );
309
310 $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
311 if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
312 $cutoff = $dbr->timestamp( $opts['from'] );
313 } else {
314 $opts->reset( 'from' );
315 }
316
317 $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
318 }
319
323 protected function doMainQuery( $tables, $fields, $conds, $query_options,
324 $join_conds, FormOptions $opts
325 ) {
326 $dbr = $this->getDB();
327 $user = $this->getUser();
328
329 $tables[] = 'recentchanges';
330 $fields = array_merge( RecentChange::selectFields(), $fields );
331
332 // JOIN on watchlist for users
333 if ( $user->isLoggedIn() && $user->isAllowed( 'viewmywatchlist' ) ) {
334 $tables[] = 'watchlist';
335 $fields[] = 'wl_user';
336 $fields[] = 'wl_notificationtimestamp';
337 $join_conds['watchlist'] = [ 'LEFT JOIN', [
338 'wl_user' => $user->getId(),
339 'wl_title=rc_title',
340 'wl_namespace=rc_namespace'
341 ] ];
342 }
343
344 // JOIN on page, used for 'last revision' filter highlight
345 $tables[] = 'page';
346 $fields[] = 'page_latest';
347 $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
348
349 $tagFilter = $opts['tagfilter'] ? explode( '|', $opts['tagfilter'] ) : [];
351 $tables,
352 $fields,
353 $conds,
354 $join_conds,
355 $query_options,
356 $tagFilter
357 );
358
359 if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
360 $opts )
361 ) {
362 return false;
363 }
364
365 if ( $this->areFiltersInConflict() ) {
366 return false;
367 }
368
369 $orderByAndLimit = [
370 'ORDER BY' => 'rc_timestamp DESC',
371 'LIMIT' => $opts['limit']
372 ];
373 if ( in_array( 'DISTINCT', $query_options ) ) {
374 // ChangeTags::modifyDisplayQuery() adds DISTINCT when filtering on multiple tags.
375 // In order to prevent DISTINCT from causing query performance problems,
376 // we have to GROUP BY the primary key. This in turn requires us to add
377 // the primary key to the end of the ORDER BY, and the old ORDER BY to the
378 // start of the GROUP BY
379 $orderByAndLimit['ORDER BY'] = 'rc_timestamp DESC, rc_id DESC';
380 $orderByAndLimit['GROUP BY'] = 'rc_timestamp, rc_id';
381 }
382 // array_merge() is used intentionally here so that hooks can, should
383 // they so desire, override the ORDER BY / LIMIT condition(s); prior to
384 // MediaWiki 1.26 this used to use the plus operator instead, which meant
385 // that extensions weren't able to change these conditions
386 $query_options = array_merge( $orderByAndLimit, $query_options );
387 $rows = $dbr->select(
388 $tables,
389 $fields,
390 // rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough
391 // knowledge to use an index merge if it wants (it may use some other index though).
392 $conds + [ 'rc_new' => [ 0, 1 ] ],
393 __METHOD__,
394 $query_options,
395 $join_conds
396 );
397
398 // Build the final data
399 if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) {
400 $this->filterByCategories( $rows, $opts );
401 }
402
403 return $rows;
404 }
405
406 protected function runMainQueryHook( &$tables, &$fields, &$conds,
407 &$query_options, &$join_conds, $opts
408 ) {
409 return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts )
410 && Hooks::run(
411 'SpecialRecentChangesQuery',
412 [ &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ],
413 '1.23'
414 );
415 }
416
417 protected function getDB() {
418 return wfGetDB( DB_REPLICA, 'recentchanges' );
419 }
420
421 public function outputFeedLinks() {
422 $this->addFeedLinks( $this->getFeedQuery() );
423 }
424
430 protected function getFeedQuery() {
431 $query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) {
432 // API handles empty parameters in a different way
433 return $value !== '';
434 } );
435 $query['action'] = 'feedrecentchanges';
436 $feedLimit = $this->getConfig()->get( 'FeedLimit' );
437 if ( $query['limit'] > $feedLimit ) {
438 $query['limit'] = $feedLimit;
439 }
440
441 return $query;
442 }
443
450 public function outputChangesList( $rows, $opts ) {
451 $limit = $opts['limit'];
452
453 $showWatcherCount = $this->getConfig()->get( 'RCShowWatchingUsers' )
454 && $this->getUser()->getOption( 'shownumberswatching' );
455 $watcherCache = [];
456
457 $counter = 1;
458 $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups );
459 $list->initChangesListRows( $rows );
460
461 $userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
462 $rclistOutput = $list->beginRecentChangesList();
463 if ( $this->isStructuredFilterUiEnabled() ) {
464 $rclistOutput .= $this->makeLegend();
465 }
466
467 foreach ( $rows as $obj ) {
468 if ( $limit == 0 ) {
469 break;
470 }
471 $rc = RecentChange::newFromRow( $obj );
472
473 # Skip CatWatch entries for hidden cats based on user preference
474 if (
475 $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
476 !$userShowHiddenCats &&
477 $rc->getParam( 'hidden-cat' )
478 ) {
479 continue;
480 }
481
482 $rc->counter = $counter++;
483 # Check if the page has been updated since the last visit
484 if ( $this->getConfig()->get( 'ShowUpdatedMarker' )
485 && !empty( $obj->wl_notificationtimestamp )
486 ) {
487 $rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp );
488 } else {
489 $rc->notificationtimestamp = false; // Default
490 }
491 # Check the number of users watching the page
492 $rc->numberofWatchingusers = 0; // Default
493 if ( $showWatcherCount && $obj->rc_namespace >= 0 ) {
494 if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
495 $watcherCache[$obj->rc_namespace][$obj->rc_title] =
496 MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchers(
497 new TitleValue( (int)$obj->rc_namespace, $obj->rc_title )
498 );
499 }
500 $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
501 }
502
503 $changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
504 if ( $changeLine !== false ) {
505 $rclistOutput .= $changeLine;
506 --$limit;
507 }
508 }
509 $rclistOutput .= $list->endRecentChangesList();
510
511 if ( $rows->numRows() === 0 ) {
512 $this->outputNoResults();
513 if ( !$this->including() ) {
514 $this->getOutput()->setStatusCode( 404 );
515 }
516 } else {
517 $this->getOutput()->addHTML( $rclistOutput );
518 }
519 }
520
527 public function doHeader( $opts, $numRows ) {
528 $this->setTopText( $opts );
529
530 $defaults = $opts->getAllValues();
531 $nondefaults = $opts->getChangedValues();
532
533 $panel = [];
534 if ( !$this->isStructuredFilterUiEnabled() ) {
535 $panel[] = $this->makeLegend();
536 }
537 $panel[] = $this->optionsPanel( $defaults, $nondefaults, $numRows );
538 $panel[] = '<hr />';
539
540 $extraOpts = $this->getExtraOptions( $opts );
541 $extraOptsCount = count( $extraOpts );
542 $count = 0;
543 $submit = ' ' . Xml::submitButton( $this->msg( 'recentchanges-submit' )->text() );
544
545 $out = Xml::openElement( 'table', [ 'class' => 'mw-recentchanges-table' ] );
546 foreach ( $extraOpts as $name => $optionRow ) {
547 # Add submit button to the last row only
548 ++$count;
549 $addSubmit = ( $count === $extraOptsCount ) ? $submit : '';
550
551 $out .= Xml::openElement( 'tr', [ 'class' => $name . 'Form' ] );
552 if ( is_array( $optionRow ) ) {
553 $out .= Xml::tags(
554 'td',
555 [ 'class' => 'mw-label mw-' . $name . '-label' ],
556 $optionRow[0]
557 );
558 $out .= Xml::tags(
559 'td',
560 [ 'class' => 'mw-input' ],
561 $optionRow[1] . $addSubmit
562 );
563 } else {
564 $out .= Xml::tags(
565 'td',
566 [ 'class' => 'mw-input', 'colspan' => 2 ],
567 $optionRow . $addSubmit
568 );
569 }
570 $out .= Xml::closeElement( 'tr' );
571 }
572 $out .= Xml::closeElement( 'table' );
573
574 $unconsumed = $opts->getUnconsumedValues();
575 foreach ( $unconsumed as $key => $value ) {
576 $out .= Html::hidden( $key, $value );
577 }
578
579 $t = $this->getPageTitle();
580 $out .= Html::hidden( 'title', $t->getPrefixedText() );
581 $form = Xml::tags( 'form', [ 'action' => wfScript() ], $out );
582 $panel[] = $form;
583 $panelString = implode( "\n", $panel );
584
585 $rcoptions = Xml::fieldset(
586 $this->msg( 'recentchanges-legend' )->text(),
587 $panelString,
588 [ 'class' => 'rcoptions cloptions' ]
589 );
590
591 // Insert a placeholder for RCFilters
592 if ( $this->isStructuredFilterUiEnabled() ) {
593 $rcfilterContainer = Html::element(
594 'div',
595 [ 'class' => 'rcfilters-container' ]
596 );
597
598 $loadingContainer = Html::rawElement(
599 'div',
600 [ 'class' => 'rcfilters-spinner' ],
601 Html::element(
602 'div',
603 [ 'class' => 'rcfilters-spinner-bounce' ]
604 )
605 );
606
607 // Wrap both with rcfilters-head
608 $this->getOutput()->addHTML(
609 Html::rawElement(
610 'div',
611 [ 'class' => 'rcfilters-head' ],
612 $rcfilterContainer . $rcoptions
613 )
614 );
615
616 // Add spinner
617 $this->getOutput()->addHTML( $loadingContainer );
618 } else {
619 $this->getOutput()->addHTML( $rcoptions );
620 }
621
622 $this->setBottomText( $opts );
623 }
624
630 function setTopText( FormOptions $opts ) {
632
633 $message = $this->msg( 'recentchangestext' )->inContentLanguage();
634 if ( !$message->isDisabled() ) {
635 // Parse the message in this weird ugly way to preserve the ability to include interlanguage
636 // links in it (T172461). In the future when T66969 is resolved, perhaps we can just use
637 // $message->parse() instead. This code is copied from Message::parseText().
638 $parserOutput = MessageCache::singleton()->parse(
639 $message->plain(),
640 $this->getPageTitle(),
641 /*linestart*/true,
642 // Message class sets the interface flag to false when parsing in a language different than
643 // user language, and this is wiki content language
644 /*interface*/false,
646 );
647 $content = $parserOutput->getText();
648 // Add only metadata here (including the language links), text is added below
649 $this->getOutput()->addParserOutputMetadata( $parserOutput );
650
651 $langAttributes = [
652 'lang' => $wgContLang->getHtmlCode(),
653 'dir' => $wgContLang->getDir(),
654 ];
655
656 $topLinksAttributes = [ 'class' => 'mw-recentchanges-toplinks' ];
657
658 if ( $this->isStructuredFilterUiEnabled() ) {
659 $contentTitle = Html::rawElement( 'div',
660 [ 'class' => 'mw-recentchanges-toplinks-title' ],
661 $this->msg( 'rcfilters-other-review-tools' )->parse()
662 );
663 $contentWrapper = Html::rawElement( 'div',
664 array_merge( [ 'class' => 'mw-collapsible-content' ], $langAttributes ),
665 $content
666 );
667 $content = $contentTitle . $contentWrapper;
668 } else {
669 // Language direction should be on the top div only
670 // if the title is not there. If it is there, it's
671 // interface direction, and the language/dir attributes
672 // should be on the content itself
673 $topLinksAttributes = array_merge( $topLinksAttributes, $langAttributes );
674 }
675
676 $this->getOutput()->addHTML(
677 Html::rawElement( 'div', $topLinksAttributes, $content )
678 );
679 }
680 }
681
688 function getExtraOptions( $opts ) {
689 $opts->consumeValues( [
690 'namespace', 'invert', 'associated', 'tagfilter', 'categories', 'categories_any'
691 ] );
692
693 $extraOpts = [];
694 $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
695
696 if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) {
697 $extraOpts['category'] = $this->categoryFilterForm( $opts );
698 }
699
701 $opts['tagfilter'], false, $this->getContext() );
702 if ( count( $tagFilter ) ) {
703 $extraOpts['tagfilter'] = $tagFilter;
704 }
705
706 // Don't fire the hook for subclasses. (Or should we?)
707 if ( $this->getName() === 'Recentchanges' ) {
708 Hooks::run( 'SpecialRecentChangesPanel', [ &$extraOpts, $opts ] );
709 }
710
711 return $extraOpts;
712 }
713
717 protected function addModules() {
718 parent::addModules();
719 $out = $this->getOutput();
720 $out->addModules( 'mediawiki.special.recentchanges' );
721 }
722
730 public function checkLastModified() {
731 $dbr = $this->getDB();
732 $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ );
733
734 return $lastmod;
735 }
736
743 protected function namespaceFilterForm( FormOptions $opts ) {
744 $nsSelect = Html::namespaceSelector(
745 [ 'selected' => $opts['namespace'], 'all' => '' ],
746 [ 'name' => 'namespace', 'id' => 'namespace' ]
747 );
748 $nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' );
749 $invert = Xml::checkLabel(
750 $this->msg( 'invert' )->text(), 'invert', 'nsinvert',
751 $opts['invert'],
752 [ 'title' => $this->msg( 'tooltip-invert' )->text() ]
753 );
754 $associated = Xml::checkLabel(
755 $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated',
756 $opts['associated'],
757 [ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
758 );
759
760 return [ $nsLabel, "$nsSelect $invert $associated" ];
761 }
762
769 protected function categoryFilterForm( FormOptions $opts ) {
770 list( $label, $input ) = Xml::inputLabelSep( $this->msg( 'rc_categories' )->text(),
771 'categories', 'mw-categories', false, $opts['categories'] );
772
773 $input .= ' ' . Xml::checkLabel( $this->msg( 'rc_categories_any' )->text(),
774 'categories_any', 'mw-categories_any', $opts['categories_any'] );
775
776 return [ $label, $input ];
777 }
778
786 $categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
787
788 if ( !count( $categories ) ) {
789 return;
790 }
791
792 # Filter categories
793 $cats = [];
794 foreach ( $categories as $cat ) {
795 $cat = trim( $cat );
796 if ( $cat == '' ) {
797 continue;
798 }
799 $cats[] = $cat;
800 }
801
802 # Filter articles
803 $articles = [];
804 $a2r = [];
805 $rowsarr = [];
806 foreach ( $rows as $k => $r ) {
807 $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
808 $id = $nt->getArticleID();
809 if ( $id == 0 ) {
810 continue; # Page might have been deleted...
811 }
812 if ( !in_array( $id, $articles ) ) {
813 $articles[] = $id;
814 }
815 if ( !isset( $a2r[$id] ) ) {
816 $a2r[$id] = [];
817 }
818 $a2r[$id][] = $k;
819 $rowsarr[$k] = $r;
820 }
821
822 # Shortcut?
823 if ( !count( $articles ) || !count( $cats ) ) {
824 return;
825 }
826
827 # Look up
828 $catFind = new CategoryFinder;
829 $catFind->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' );
830 $match = $catFind->run();
831
832 # Filter
833 $newrows = [];
834 foreach ( $match as $id ) {
835 foreach ( $a2r[$id] as $rev ) {
836 $k = $rev;
837 $newrows[$k] = $rowsarr[$k];
838 }
839 }
840 $rows = new FakeResultWrapper( array_values( $newrows ) );
841 }
842
852 function makeOptionsLink( $title, $override, $options, $active = false ) {
853 $params = $this->convertParamsForLink( $override + $options );
854
855 if ( $active ) {
856 $title = new HtmlArmor( '<strong>' . htmlspecialchars( $title ) . '</strong>' );
857 }
858
859 return $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $title, [
860 'data-params' => json_encode( $override ),
861 'data-keys' => implode( ',', array_keys( $override ) ),
862 ], $params );
863 }
864
873 function optionsPanel( $defaults, $nondefaults, $numRows ) {
874 $options = $nondefaults + $defaults;
875
876 $note = '';
877 $msg = $this->msg( 'rclegend' );
878 if ( !$msg->isDisabled() ) {
879 $note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n";
880 }
881
882 $lang = $this->getLanguage();
883 $user = $this->getUser();
884 $config = $this->getConfig();
885 if ( $options['from'] ) {
886 $resetLink = $this->makeOptionsLink( $this->msg( 'rclistfromreset' ),
887 [ 'from' => '' ], $nondefaults );
888
889 $noteFromMsg = $this->msg( 'rcnotefrom' )
890 ->numParams( $options['limit'] )
891 ->params(
892 $lang->userTimeAndDate( $options['from'], $user ),
893 $lang->userDate( $options['from'], $user ),
894 $lang->userTime( $options['from'], $user )
895 )
896 ->numParams( $numRows );
897 $note .= Html::rawElement(
898 'span',
899 [ 'class' => 'rcnotefrom' ],
900 $noteFromMsg->parse()
901 ) .
902 ' ' .
903 Html::rawElement(
904 'span',
905 [ 'class' => 'rcoptions-listfromreset' ],
906 $this->msg( 'parentheses' )->rawParams( $resetLink )->parse()
907 ) .
908 '<br />';
909 }
910
911 # Sort data for display and make sure it's unique after we've added user data.
912 $linkLimits = $config->get( 'RCLinkLimits' );
913 $linkLimits[] = $options['limit'];
914 sort( $linkLimits );
915 $linkLimits = array_unique( $linkLimits );
916
917 $linkDays = $config->get( 'RCLinkDays' );
918 $linkDays[] = $options['days'];
919 sort( $linkDays );
920 $linkDays = array_unique( $linkDays );
921
922 // limit links
923 $cl = [];
924 foreach ( $linkLimits as $value ) {
925 $cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
926 [ 'limit' => $value ], $nondefaults, $value == $options['limit'] );
927 }
928 $cl = $lang->pipeList( $cl );
929
930 // day links, reset 'from' to none
931 $dl = [];
932 foreach ( $linkDays as $value ) {
933 $dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
934 [ 'days' => $value, 'from' => '' ], $nondefaults, $value == $options['days'] );
935 }
936 $dl = $lang->pipeList( $dl );
937
938 $showhide = [ 'show', 'hide' ];
939
940 $links = [];
941
943
944 foreach ( $filterGroups as $groupName => $group ) {
945 if ( !$group->isPerGroupRequestParameter() ) {
946 foreach ( $group->getFilters() as $key => $filter ) {
947 if ( $filter->displaysOnUnstructuredUi( $this ) ) {
948 $msg = $filter->getShowHide();
949 $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] );
950 // Extensions can define additional filters, but don't need to define the corresponding
951 // messages. If they don't exist, just fall back to 'show' and 'hide'.
952 if ( !$linkMessage->exists() ) {
953 $linkMessage = $this->msg( $showhide[1 - $options[$key]] );
954 }
955
956 $link = $this->makeOptionsLink( $linkMessage->text(),
957 [ $key => 1 - $options[$key] ], $nondefaults );
958
959 $attribs = [
960 'class' => "$msg rcshowhideoption clshowhideoption",
961 'data-filter-name' => $filter->getName(),
962 ];
963
964 if ( $filter->isFeatureAvailableOnStructuredUi( $this ) ) {
965 $attribs['data-feature-in-structured-ui'] = true;
966 }
967
968 $links[] = Html::rawElement(
969 'span',
970 $attribs,
971 $this->msg( $msg )->rawParams( $link )->escaped()
972 );
973 }
974 }
975 }
976 }
977
978 // show from this onward link
979 $timestamp = wfTimestampNow();
980 $now = $lang->userTimeAndDate( $timestamp, $user );
981 $timenow = $lang->userTime( $timestamp, $user );
982 $datenow = $lang->userDate( $timestamp, $user );
983 $pipedLinks = '<span class="rcshowhide">' . $lang->pipeList( $links ) . '</span>';
984
985 $rclinks = '<span class="rclinks">' . $this->msg( 'rclinks' )->rawParams( $cl, $dl, '' )
986 ->parse() . '</span>';
987
988 $rclistfrom = '<span class="rclistfrom">' . $this->makeOptionsLink(
989 $this->msg( 'rclistfrom' )->rawParams( $now, $timenow, $datenow )->parse(),
990 [ 'from' => $timestamp ],
991 $nondefaults
992 ) . '</span>';
993
994 return "{$note}$rclinks<br />$pipedLinks<br />$rclistfrom";
995 }
996
997 public function isIncludable() {
998 return true;
999 }
1000
1001 protected function getCacheTTL() {
1002 return 60 * 5;
1003 }
1004
1005 function getDefaultLimit() {
1006 return $this->getUser()->getIntOption( 'rclimit' );
1007 }
1008
1009 function getDefaultDays() {
1010 return floatval( $this->getUser()->getOption( 'rcdays' ) );
1011 }
1012}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
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...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
The "CategoryFinder" class takes a list of articles, creates an internal representation of all their ...
seed( $articleIds, $categories, $mode='AND')
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.
getFilterGroups()
Gets the currently registered filters groups.
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.
$filterGroups
Filter groups, and their contained filters This is an associative array (with group name as key) of C...
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.
reset( $name)
Delete the option value.
const FLOAT
Float type, maps guessType() to WebRequest::getFloat()
validateBounds( $name, $min, $max)
Constrain a numeric value for a given option to a given range.
validateIntBounds( $name, $min, $max)
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.
static newFromRow( $row)
static selectFields()
Return the list of recentchanges fields that should be selected to create a new recentchanges object.
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.
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.
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.
categoryFilterForm(FormOptions $opts)
Create an input to filter changes by categories.
__construct( $name='Recentchanges', $restriction='')
namespaceFilterForm(FormOptions $opts)
Creates the choose namespace selection.
validateOptions(FormOptions $opts)
Validate a FormOptions object generated by getDefaultOptions() with values already populated.
getDefaultDays()
Get the default value of the number of days to display when loading the result set.
getDefaultOptions()
Get a FormOptions object containing the default options.
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.
buildQuery(&$tables, &$fields, &$conds, &$query_options, &$join_conds, FormOptions $opts)
@inheritDoc
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...
Result wrapper for grabbing data queried from an IDatabase object.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
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
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
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
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
const LIST_OR
Definition Defines.php:47
const LIST_AND
Definition Defines.php:44
const RC_CATEGORIZE
Definition Defines.php:147
the array() calling protocol came about after MediaWiki 1.4rc1.
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:2746
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:1013
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:1971
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:2780
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:962
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:862
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:2989
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
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:1984
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:1610
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:1760
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:24
if(is_array($mode)) switch( $mode) $input
const DB_REPLICA
Definition defines.php:25
$params
if(!isset( $args[0])) $lang