MediaWiki master
ChangesList.php
Go to the documentation of this file.
1<?php
8
15use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
27use MediaWiki\Pager\PagerTools;
36use OOUI\IconWidget;
37use RuntimeException;
38use stdClass;
42
52 use ProtectedHookAccessorTrait;
53
54 public const CSS_CLASS_PREFIX = 'mw-changeslist-';
55
57 protected $watchlist = false;
59 protected $lastdate;
61 protected $message;
63 protected $rc_cache;
65 protected $rcCacheIndex;
67 protected $rclistOpen;
69 protected $rcMoveIndex;
70
73
75 protected $watchMsgCache;
76
80 protected $linkRenderer;
81
86
91
95 protected $filterGroups;
96
100 protected $tagsCache;
101
105 protected $userLinkCache;
106
107 private LogFormatterFactory $logFormatterFactory;
108
110
115 public function __construct( $context, ?ChangesListFilterGroupContainer $filterGroups = null ) {
116 $this->setContext( $context );
117 $this->preCacheMessages();
118 $this->watchMsgCache = new MapCacheLRU( 50 );
119 $this->filterGroups = $filterGroups ?? new ChangesListFilterGroupContainer();
120
121 $services = MediaWikiServices::getInstance();
122 $this->linkRenderer = $services->getLinkRenderer();
123 $this->commentFormatter = $services->getRowCommentFormatter();
124 $this->logFormatterFactory = $services->getLogFormatterFactory();
125 $this->userLinkRenderer = $services->getUserLinkRenderer();
126 $this->tagsCache = new MapCacheLRU( 50 );
127 $this->userLinkCache = new MapCacheLRU( 50 );
128 }
129
138 public static function newFromContext(
139 IContextSource $context,
140 ?ChangesListFilterGroupContainer $groups = null
141 ) {
142 $user = $context->getUser();
143 $sk = $context->getSkin();
144 $services = MediaWikiServices::getInstance();
145 $list = null;
146 $groups ??= new ChangesListFilterGroupContainer();
147 if ( ( new HookRunner( $services->getHookContainer() ) )->onFetchChangesList( $user, $sk, $list, $groups ) ) {
148 $userOptionsLookup = $services->getUserOptionsLookup();
149 $new = $context->getRequest()->getBool(
150 'enhanced',
151 $userOptionsLookup->getBoolOption( $user, 'usenewrc' )
152 );
153
154 return $new ?
155 new EnhancedChangesList( $context, $groups ) :
156 new OldChangesList( $context, $groups );
157 } else {
158 return $list;
159 }
160 }
161
173 public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
174 throw new RuntimeException( 'recentChangesLine should be implemented' );
175 }
176
183 protected function getHighlightsContainerDiv() {
184 $highlightColorDivs = '';
185 foreach ( [ 'none', 'c1', 'c2', 'c3', 'c4', 'c5' ] as $color ) {
186 $highlightColorDivs .= Html::rawElement(
187 'div',
188 [
189 'class' => 'mw-rcfilters-ui-highlights-color-' . $color,
190 'data-color' => $color
191 ]
192 );
193 }
194
195 return Html::rawElement(
196 'div',
197 [ 'class' => 'mw-rcfilters-ui-highlights' ],
198 $highlightColorDivs
199 );
200 }
201
206 public function setWatchlistDivs( $value = true ) {
207 $this->watchlist = $value;
208 }
209
214 public function isWatchlist() {
215 return (bool)$this->watchlist;
216 }
217
222 private function preCacheMessages() {
223 // @phan-suppress-next-line MediaWikiNoIssetIfDefined False positives when documented as nullable
224 if ( !isset( $this->message ) ) {
225 $this->message = [];
226 foreach ( [
227 'cur', 'diff', 'hist', 'enhancedrc-history', 'last', 'blocklink', 'history',
228 'semicolon-separator', 'pipe-separator', 'word-separator' ] as $msg
229 ) {
230 $this->message[$msg] = $this->msg( $msg )->escaped();
231 }
232 }
233 }
234
241 public function recentChangesFlags( $flags, $nothing = "\u{00A0}" ) {
242 $f = '';
243 foreach (
244 $this->getConfig()->get( MainConfigNames::RecentChangesFlags ) as $flag => $_
245 ) {
246 $f .= isset( $flags[$flag] ) && $flags[$flag]
247 ? self::flag( $flag, $this->getContext() )
248 : $nothing;
249 }
250
251 return $f;
252 }
253
262 protected function getHTMLClasses( $rc, $watched ) {
263 $classes = [ self::CSS_CLASS_PREFIX . 'line' ];
264 $logType = $rc->mAttribs['rc_log_type'];
265
266 if ( $logType ) {
267 $classes[] = self::CSS_CLASS_PREFIX . 'log';
268 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'log-' . $logType );
269 } else {
270 $classes[] = self::CSS_CLASS_PREFIX . 'edit';
271 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns' .
272 $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] );
273 }
274
275 // Indicate watched status on the line to allow for more
276 // comprehensive styling.
277 $classes[] = $watched && $rc->mAttribs['rc_timestamp'] >= $watched
278 ? self::CSS_CLASS_PREFIX . 'line-watched'
279 : self::CSS_CLASS_PREFIX . 'line-not-watched';
280
281 $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rc ) );
282
283 return $classes;
284 }
285
293 protected function getHTMLClassesForFilters( $rc ) {
294 $classes = [];
295
296 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns-' .
297 $rc->mAttribs['rc_namespace'] );
298
299 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
300 $classes[] = Sanitizer::escapeClass(
301 self::CSS_CLASS_PREFIX .
302 'ns-' .
303 ( $nsInfo->isTalk( $rc->mAttribs['rc_namespace'] ) ? 'talk' : 'subject' )
304 );
305
306 $this->filterGroups->applyCssClassIfNeeded( $this->getContext(), $rc, $classes );
307
308 return $classes;
309 }
310
321 public static function flag( $flag, ?IContextSource $context = null ) {
322 static $map = [ 'minoredit' => 'minor', 'botedit' => 'bot' ];
323 static $flagInfos = null;
324
325 if ( $flagInfos === null ) {
326 $recentChangesFlags = MediaWikiServices::getInstance()->getMainConfig()
328 $flagInfos = [];
329 foreach ( $recentChangesFlags as $key => $value ) {
330 $flagInfos[$key]['letter'] = $value['letter'];
331 $flagInfos[$key]['title'] = $value['title'];
332 // Allow customized class name, fall back to flag name
333 $flagInfos[$key]['class'] = $value['class'] ?? $key;
334 }
335 }
336
337 $context = $context ?: RequestContext::getMain();
338
339 // Inconsistent naming, kept for b/c
340 if ( isset( $map[$flag] ) ) {
341 $flag = $map[$flag];
342 }
343
344 $info = $flagInfos[$flag];
345 return Html::element( 'abbr', [
346 'class' => $info['class'],
347 'title' => wfMessage( $info['title'] )->setContext( $context )->text(),
348 ], wfMessage( $info['letter'] )->setContext( $context )->text() );
349 }
350
355 public function beginRecentChangesList() {
356 $this->rc_cache = [];
357 $this->rcMoveIndex = 0;
358 $this->rcCacheIndex = 0;
359 $this->lastdate = '';
360 $this->rclistOpen = false;
361 $this->getOutput()->addModuleStyles( [
362 'mediawiki.interface.helpers.styles',
363 'mediawiki.special.changeslist'
364 ] );
365
366 return '<div class="mw-changeslist">';
367 }
368
372 public function initChangesListRows( $rows ) {
373 $this->getHookRunner()->onChangesListInitRows( $this, $rows );
374 $this->formattedComments = $this->commentFormatter->createBatch()
375 ->comments(
376 $this->commentFormatter->rows( $rows )
377 ->commentKey( 'rc_comment' )
378 ->namespaceField( 'rc_namespace' )
379 ->titleField( 'rc_title' )
380 ->indexField( 'rc_id' )
381 )
382 ->useBlock()
383 ->execute();
384 }
385
396 public static function showCharacterDifference( $old, $new, ?IContextSource $context = null ) {
397 if ( !$context ) {
398 $context = RequestContext::getMain();
399 }
400
401 $new = (int)$new;
402 $old = (int)$old;
403 $szdiff = $new - $old;
404
405 $lang = $context->getLanguage();
406 $config = $context->getConfig();
407 $code = $lang->getCode();
408 static $fastCharDiff = [];
409 if ( !isset( $fastCharDiff[$code] ) ) {
410 $fastCharDiff[$code] = $config->get( MainConfigNames::MiserMode )
411 || $context->msg( 'rc-change-size' )->plain() === '$1';
412 }
413
414 $formattedSize = $lang->formatNum( $szdiff );
415
416 if ( !$fastCharDiff[$code] ) {
417 $formattedSize = $context->msg( 'rc-change-size', $formattedSize )->text();
418 }
419
420 if ( abs( $szdiff ) > abs( $config->get( MainConfigNames::RCChangedSizeThreshold ) ) ) {
421 $tag = 'strong';
422 } else {
423 $tag = 'span';
424 }
425
426 if ( $szdiff === 0 ) {
427 $formattedSizeClass = 'mw-plusminus-null';
428 } elseif ( $szdiff > 0 ) {
429 $formattedSize = '+' . $formattedSize;
430 $formattedSizeClass = 'mw-plusminus-pos';
431 } else {
432 $formattedSizeClass = 'mw-plusminus-neg';
433 }
434 $formattedSizeClass .= ' mw-diff-bytes';
435
436 $formattedTotalSize = $context->msg( 'rc-change-size-new' )->numParams( $new )->text();
437
438 return Html::element( $tag,
439 [ 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ],
440 $formattedSize );
441 }
442
450 public function formatCharacterDifference( RecentChange $old, ?RecentChange $new = null ) {
451 $oldlen = $old->mAttribs['rc_old_len'];
452
453 if ( $new ) {
454 $newlen = $new->mAttribs['rc_new_len'];
455 } else {
456 $newlen = $old->mAttribs['rc_new_len'];
457 }
458
459 if ( $oldlen === null || $newlen === null ) {
460 return '';
461 }
462
463 return self::showCharacterDifference( $oldlen, $newlen, $this->getContext() );
464 }
465
470 public function endRecentChangesList() {
471 $out = $this->rclistOpen ? "</ul>\n" : '';
472 $out .= '</div>';
473
474 return $out;
475 }
476
490 public static function revDateLink(
491 RevisionRecord $rev,
492 Authority $performer,
493 Language $lang,
494 $title = null,
495 $className = ''
496 ) {
497 $ts = $rev->getTimestamp();
498 $time = $lang->userTime( $ts, $performer->getUser() );
499 $date = $lang->userTimeAndDate( $ts, $performer->getUser() );
500 $class = trim( 'mw-changeslist-date ' . $className );
501 if ( $rev->userCan( RevisionRecord::DELETED_TEXT, $performer ) ) {
502 $link = Html::rawElement( 'bdi', [ 'dir' => $lang->getDir() ],
503 MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
504 $title ?? $rev->getPageAsLinkTarget(),
505 $date,
506 [ 'class' => $class ],
507 [ 'oldid' => $rev->getId() ]
508 )
509 );
510 } else {
511 $link = htmlspecialchars( $date );
512 }
513 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
514 $class = Linker::getRevisionDeletedClass( $rev ) . " $class";
515 $link = "<span class=\"$class\">$link</span>";
516 }
517 return Html::element( 'span', [
518 'class' => 'mw-changeslist-time'
519 ], $time ) . $link;
520 }
521
526 public function insertDateHeader( &$s, $rc_timestamp ) {
527 # Make date header if necessary
528 $date = $this->getLanguage()->userDate( $rc_timestamp, $this->getUser() );
529 if ( $date != $this->lastdate ) {
530 if ( $this->lastdate != '' ) {
531 $s .= "</ul>\n";
532 }
533 $s .= Html::element( 'h4', [], $date ) . "\n<ul class=\"special\">";
534 $this->lastdate = $date;
535 $this->rclistOpen = true;
536 }
537 }
538
545 public function insertLog( &$s, $title, $logtype, $useParentheses = true ) {
546 $page = new LogPage( $logtype );
547 $logname = $page->getName()->setContext( $this->getContext() )->text();
548 $link = $this->linkRenderer->makeKnownLink( $title, $logname, [
549 'class' => $useParentheses ? '' : 'mw-changeslist-links'
550 ] );
551 if ( $useParentheses ) {
552 $s .= $this->msg( 'parentheses' )->rawParams(
553 $link
554 )->escaped();
555 } else {
556 $s .= $link;
557 }
558 }
559
565 public function insertDiffHist( &$s, &$rc, $unpatrolled = null ) {
566 # Diff link
567 if (
568 $rc->mAttribs['rc_source'] === RecentChange::SRC_NEW ||
569 $rc->mAttribs['rc_source'] === RecentChange::SRC_LOG
570 ) {
571 $diffLink = $this->message['diff'];
572 } elseif ( !self::userCan( $rc, RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
573 $diffLink = $this->message['diff'];
574 } else {
575 $query = [
576 'curid' => $rc->mAttribs['rc_cur_id'],
577 'diff' => $rc->mAttribs['rc_this_oldid'],
578 'oldid' => $rc->mAttribs['rc_last_oldid']
579 ];
580
581 $diffLink = $this->linkRenderer->makeKnownLink(
582 $rc->getTitle(),
583 new HtmlArmor( $this->message['diff'] ),
584 [ 'class' => 'mw-changeslist-diff' ],
585 $query
586 );
587 }
588 $histLink = $this->linkRenderer->makeKnownLink(
589 $rc->getTitle(),
590 new HtmlArmor( $this->message['hist'] ),
591 [ 'class' => 'mw-changeslist-history' ],
592 [
593 'curid' => $rc->mAttribs['rc_cur_id'],
594 'action' => 'history'
595 ]
596 );
597
598 $s .= Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ],
599 Html::rawElement( 'span', [], $diffLink ) .
600 Html::rawElement( 'span', [], $histLink )
601 ) .
602 ' <span class="mw-changeslist-separator"></span> ';
603 }
604
615 public function getArticleLink( &$rc, $unpatrolled, $watched ) {
616 $params = [];
617 if ( $rc->getTitle()->isRedirect() ) {
618 $params = [ 'redirect' => 'no' ];
619 }
620
621 $articlelink = $this->linkRenderer->makeLink(
622 $rc->getTitle(),
623 null,
624 [ 'class' => 'mw-changeslist-title' ],
625 $params
626 );
627 if ( static::isDeleted( $rc, RevisionRecord::DELETED_TEXT ) ) {
628 $class = 'history-deleted';
629 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
630 $class .= ' mw-history-suppressed';
631 }
632 $articlelink = '<span class="' . $class . '">' . $articlelink . '</span>';
633 }
634 $dir = $this->getLanguage()->getDir();
635 $articlelink = Html::rawElement( 'bdi', [ 'dir' => $dir ], $articlelink );
636 # To allow for boldening pages watched by this user
637 # Don't wrap result of this with another tag, see T376814
638 $articlelink = "<span class=\"mw-title\">{$articlelink}</span>";
639
640 # TODO: Deprecate the $s argument, it seems happily unused.
641 $s = '';
642 $this->getHookRunner()->onChangesListInsertArticleLink( $this, $articlelink,
643 $s, $rc, $unpatrolled, $watched );
644
645 // Watchlist expiry icon.
646 $watchlistExpiry = '';
647 // @phan-suppress-next-line MediaWikiNoIssetIfDefined
648 if ( isset( $rc->watchlistExpiry ) && $rc->watchlistExpiry ) {
649 $watchlistExpiry = $this->getWatchlistExpiry( $rc );
650 }
651
652 return "{$s} {$articlelink}{$watchlistExpiry}";
653 }
654
661 public function getWatchlistExpiry( RecentChange $recentChange ): string {
662 $item = WatchedItem::newFromRecentChange( $recentChange, $this->getUser() );
663 // Guard against expired items, even though they shouldn't come here.
664 if ( $item->isExpired() ) {
665 return '';
666 }
667 $daysLeftText = $item->getExpiryInDaysText( $this->getContext() );
668 // Matching widget is also created in ChangesListSpecialPage, for the legend.
669 $widget = new IconWidget( [
670 'icon' => 'clock',
671 'title' => $daysLeftText,
672 'classes' => [ 'mw-changesList-watchlistExpiry' ],
673 ] );
674 $widget->setAttributes( [
675 // Add labels for assistive technologies.
676 'role' => 'img',
677 'aria-label' => $this->msg( 'watchlist-expires-in-aria-label' )->text(),
678 // Days-left is used in resources/src/mediawiki.special.changeslist.watchlistexpiry/watchlistexpiry.js
679 'data-days-left' => $item->getExpiryInDays(),
680 ] );
681 // Add spaces around the widget (the page title is to one side,
682 // and a semicolon or opening-parenthesis to the other).
683 return " $widget ";
684 }
685
694 public function getTimestamp( $rc ) {
695 // This uses the semi-colon separator unless there's a watchlist expiry date for the entry,
696 // because in that case the timestamp is preceded by a clock icon.
697 // A space is important after `.mw-changeslist-separator--semicolon` to make sure
698 // that whatever comes before it is distinguishable.
699 // (Otherwise your have the text of titles pushing up against the timestamp)
700 // A specific element is used for this purpose rather than styling `.mw-changeslist-date`
701 // as the `.mw-changeslist-date` class is used in a variety
702 // of other places with a different position and the information proceeding getTimestamp can vary.
703 // The `.mw-changeslist-time` class allows us to distinguish from `.mw-changeslist-date` elements that
704 // contain the full date (month, year) and adds consistency with Special:Contributions
705 // and other pages.
706 $separatorClass = $rc->watchlistExpiry ? 'mw-changeslist-separator' : 'mw-changeslist-separator--semicolon';
707 return Html::element( 'span', [ 'class' => $separatorClass ] ) . $this->message['word-separator'] .
708 '<span class="mw-changeslist-date mw-changeslist-time">' .
709 htmlspecialchars( $this->getLanguage()->userTime(
710 $rc->mAttribs['rc_timestamp'],
711 $this->getUser()
712 ) ) . '</span> <span class="mw-changeslist-separator"></span> ';
713 }
714
721 public function insertTimestamp( &$s, $rc ) {
722 $s .= $this->getTimestamp( $rc );
723 }
724
731 public function insertUserRelatedLinks( &$s, &$rc ) {
732 if ( static::isDeleted( $rc, RevisionRecord::DELETED_USER ) ) {
733 $deletedClass = 'history-deleted';
734 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
735 $deletedClass .= ' mw-history-suppressed';
736 }
737 $s .= ' <span class="' . $deletedClass . '">' .
738 $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
739 } else {
740 $s .= $this->linkRenderer->makeUserLink(
741 $rc->getPerformerIdentity(),
742 $this
743 );
744 # Don't wrap result of this with another tag, see T376814
745 $s .= $this->userLinkCache->getWithSetCallback(
746 $this->userLinkCache->makeKey(
747 $rc->mAttribs['rc_user_text'],
748 $this->getUser()->getName(),
749 $this->getLanguage()->getCode()
750 ),
751 // The text content of tools is not wrapped with parentheses or "piped".
752 // This will be handled in CSS (T205581).
753 static fn () => Linker::userToolLinks(
754 $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'],
755 false, 0, null,
756 false
757 )
758 );
759 }
760 }
761
768 public function insertLogEntry( $rc ) {
769 $entry = DatabaseLogEntry::newFromRow( $rc->mAttribs );
770 $formatter = $this->logFormatterFactory->newFromEntry( $entry );
771 $formatter->setContext( $this->getContext() );
772 $formatter->setShowUserToolLinks( true );
773
774 $comment = $formatter->getComment();
775 if ( $comment !== '' ) {
776 $dir = $this->getLanguage()->getDir();
777 $comment = Html::rawElement( 'bdi', [ 'dir' => $dir ], $comment );
778 }
779
780 $html = $formatter->getActionText() . $this->message['word-separator'] . $comment .
781 $this->message['word-separator'] . $formatter->getActionLinks();
782 $classes = [ 'mw-changeslist-log-entry' ];
783 $attribs = [];
784
785 // Let extensions add data to the outputted log entry in a similar way to the LogEventsListLineEnding hook
786 $this->getHookRunner()->onChangesListInsertLogEntry( $entry, $this->getContext(), $html, $classes, $attribs );
787 $attribs = array_filter( $attribs,
788 Sanitizer::isReservedDataAttribute( ... ),
789 ARRAY_FILTER_USE_KEY
790 );
791 $attribs['class'] = $classes;
792
793 return Html::openElement( 'span', $attribs ) . $html . Html::closeElement( 'span' );
794 }
795
801 public function insertComment( $rc ) {
802 if ( static::isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) {
803 $deletedClass = 'history-deleted';
804 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
805 $deletedClass .= ' mw-history-suppressed';
806 }
807 return ' <span class="' . $deletedClass . ' comment">' .
808 $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
809 } elseif ( isset( $rc->mAttribs['rc_id'] )
810 && isset( $this->formattedComments[$rc->mAttribs['rc_id']] )
811 ) {
812 return $this->formattedComments[$rc->mAttribs['rc_id']];
813 } else {
814 return $this->commentFormatter->formatBlock(
815 $rc->mAttribs['rc_comment'],
816 $rc->getTitle(),
817 // Whether section links should refer to local page (using default false)
818 false,
819 // wikid to generate links for (using default null) */
820 null,
821 // whether parentheses should be rendered as part of the message
822 false
823 );
824 }
825 }
826
832 protected function numberofWatchingusers( $count ) {
833 if ( $count <= 0 ) {
834 return '';
835 }
836
837 return $this->watchMsgCache->getWithSetCallback(
838 $this->watchMsgCache->makeKey(
839 'watching-users-msg',
840 strval( $count ),
841 $this->getUser()->getName(),
842 $this->getLanguage()->getCode()
843 ),
844 function () use ( $count ) {
845 return $this->msg( 'number-of-watching-users-for-recent-changes' )
846 ->numParams( $count )->escaped();
847 }
848 );
849 }
850
857 public static function isDeleted( $rc, $field ) {
858 return ( $rc->mAttribs['rc_deleted'] & $field ) == $field;
859 }
860
870 public static function userCan( $rc, $field, ?Authority $performer = null ) {
871 $performer ??= RequestContext::getMain()->getAuthority();
872
873 if ( $rc->mAttribs['rc_source'] === RecentChange::SRC_LOG ) {
874 return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $performer );
875 }
876
877 return RevisionRecord::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $performer );
878 }
879
885 protected function maybeWatchedLink( $link, $watched = false ) {
886 if ( $watched ) {
887 return '<strong class="mw-watched">' . $link . '</strong>';
888 } else {
889 return '<span class="mw-rc-unwatched">' . $link . '</span>';
890 }
891 }
892
899 public function insertRollback( &$s, &$rc ) {
900 $this->insertPageTools( $s, $rc );
901 }
902
910 private function insertPageTools( &$s, &$rc ) {
911 // FIXME Some page tools (e.g. thanks) might make sense for log entries.
912 if ( !in_array( $rc->mAttribs['rc_source'], [ RecentChange::SRC_EDIT, RecentChange::SRC_NEW ] )
913 // FIXME When would either of these not exist when type is RC_EDIT? Document.
914 || !$rc->mAttribs['rc_this_oldid']
915 || !$rc->mAttribs['rc_cur_id']
916 ) {
917 return;
918 }
919
920 // Construct a fake revision for PagerTools. FIXME can't we just obtain the real one?
921 $title = $rc->getTitle();
922 $revRecord = new MutableRevisionRecord( $title );
923 $revRecord->setId( (int)$rc->mAttribs['rc_this_oldid'] );
924 $revRecord->setVisibility( (int)$rc->mAttribs['rc_deleted'] );
925 $user = new UserIdentityValue(
926 (int)$rc->mAttribs['rc_user'],
927 $rc->mAttribs['rc_user_text']
928 );
929 $revRecord->setUser( $user );
930
931 $tools = new PagerTools(
932 $revRecord,
933 null,
934 // only show a rollback link on the top-most revision
935 $rc->getAttribute( 'page_latest' ) == $rc->mAttribs['rc_this_oldid']
936 && $rc->mAttribs['rc_source'] !== RecentChange::SRC_NEW,
937 $this->getHookRunner(),
938 $title,
939 $this->getContext(),
940 // @todo: Inject
941 MediaWikiServices::getInstance()->getLinkRenderer()
942 );
943
944 $s .= $tools->toHTML();
945 }
946
952 public function getRollback( RecentChange $rc ) {
953 $s = '';
954 $this->insertRollback( $s, $rc );
955 return $s;
956 }
957
963 public function insertTags( &$s, &$rc, &$classes ) {
964 if ( empty( $rc->mAttribs['ts_tags'] ) ) {
965 return;
966 }
967
973 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
974 $this->tagsCache->makeKey(
975 $rc->mAttribs['ts_tags'],
976 $this->getUser()->getName(),
977 $this->getLanguage()->getCode()
978 ),
979 fn () => ChangeTags::formatSummaryRow(
980 $rc->mAttribs['ts_tags'],
981 'changeslist',
982 $this->getContext()
983 )
984 );
985 $classes = array_merge( $classes, $newClasses );
986 $s .= $this->message['word-separator'] . $tagSummary;
987 }
988
995 public function getTags( RecentChange $rc, array &$classes ) {
996 $s = '';
997 $this->insertTags( $s, $rc, $classes );
998 return $s;
999 }
1000
1006 public function insertExtra( &$s, &$rc, &$classes ) {
1007 // Empty, used for subclasses to add anything special.
1008 }
1009
1013 protected function showAsUnpatrolled( RecentChange $rc ) {
1014 return self::isUnpatrolled( $rc, $this->getUser() );
1015 }
1016
1022 public static function isUnpatrolled( $rc, User $user ) {
1023 if ( $rc instanceof RecentChange ) {
1024 $isPatrolled = $rc->mAttribs['rc_patrolled'];
1025 $rcSource = $rc->mAttribs['rc_source'];
1026 $rcLogType = $rc->mAttribs['rc_log_type'];
1027 } else {
1028 $isPatrolled = $rc->rc_patrolled;
1029 $rcSource = $rc->rc_source;
1030 $rcLogType = $rc->rc_log_type;
1031 }
1032
1033 if ( $isPatrolled ) {
1034 return false;
1035 }
1036
1037 return $user->useRCPatrol() ||
1038 ( $rcSource === RecentChange::SRC_NEW && $user->useNPPatrol() ) ||
1039 ( $rcLogType === 'upload' && $user->useFilePatrol() );
1040 }
1041
1051 protected function isCategorizationWithoutRevision( $rcObj ) {
1052 return $rcObj->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE
1053 && intval( $rcObj->getAttribute( 'rc_this_oldid' ) ) === 0;
1054 }
1055
1061 protected function getDataAttributes( RecentChange $rc ) {
1062 $attrs = [];
1063
1064 $source = $rc->getAttribute( 'rc_source' );
1065 switch ( $source ) {
1069 $attrs['data-mw-revid'] = $rc->mAttribs['rc_this_oldid'];
1070 break;
1072 $attrs['data-mw-logid'] = $rc->mAttribs['rc_logid'];
1073 $attrs['data-mw-logaction'] =
1074 $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'];
1075 break;
1076 }
1077
1078 $attrs[ 'data-mw-ts' ] = $rc->getAttribute( 'rc_timestamp' );
1079
1080 return $attrs;
1081 }
1082
1090 public function setChangeLinePrefixer( callable $prefixer ) {
1091 $this->changeLinePrefixer = $prefixer;
1092 }
1093}
1094
1096class_alias( ChangesList::class, 'ChangesList' );
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Recent changes tagging.
This is basically a CommentFormatter with a CommentStore dependency, allowing it to retrieve comment ...
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
setContext(IContextSource $context)
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getContext()
Get the base IContextSource object.
Group all the pieces relevant to the context of a request into one instance.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
Base class for language-specific code.
Definition Language.php:70
userTimeAndDate( $ts, UserIdentity $user, array $options=[])
Get the formatted date and time for the given timestamp and formatted for the given user.
getDir()
Return the correct HTML 'dir' attribute value for this language.
userTime( $ts, UserIdentity $user, array $options=[])
Get the formatted time for the given timestamp and formatted for the given user.
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:47
Service class that renders HTML for user-related links.
A value class to process existing log entries.
Class to simplify the use of log pages.
Definition LogPage.php:35
A class containing constants representing the names of configuration variables.
const RecentChangesFlags
Name constant for the RecentChangesFlags setting, for use with Config::get()
const RCChangedSizeThreshold
Name constant for the RCChangedSizeThreshold setting, for use with Config::get()
const MiserMode
Name constant for the MiserMode setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:32
Base class for lists of recent changes shown on special pages.
numberofWatchingusers( $count)
Returns the string which indicates the number of watching users.
getTimestamp( $rc)
Get the timestamp from $rc formatted with current user's settings and a separator.
static isDeleted( $rc, $field)
Determine if said field of a revision is hidden.
getHTMLClasses( $rc, $watched)
Get an array of default HTML class attributes for the change.
setChangeLinePrefixer(callable $prefixer)
Sets the callable that generates a change line prefix added to the beginning of each line.
getTags(RecentChange $rc, array &$classes)
static showCharacterDifference( $old, $new, ?IContextSource $context=null)
Show formatted char difference.
static newFromContext(IContextSource $context, ?ChangesListFilterGroupContainer $groups=null)
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
beginRecentChangesList()
Returns text for the start of the tabular part of RC.
endRecentChangesList()
Returns text for the end of RC.
getHighlightsContainerDiv()
Get the container for highlights that are used in the new StructuredFilters system.
recentChangesLine(&$rc, $watched=false, $linenumber=null)
Format a line.
insertLogEntry( $rc)
Insert a formatted action.
maybeWatchedLink( $link, $watched=false)
getArticleLink(&$rc, $unpatrolled, $watched)
Get the HTML link to the changed page, possibly with a prefix from hook handlers, and a suffix for te...
static flag( $flag, ?IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
static revDateLink(RevisionRecord $rev, Authority $performer, Language $lang, $title=null, $className='')
Render the date and time of a revision in the current user language based on whether the user is able...
insertLog(&$s, $title, $logtype, $useParentheses=true)
recentChangesFlags( $flags, $nothing="\u{00A0}")
Returns the appropriate flags for new page, minor change and patrolling.
static userCan( $rc, $field, ?Authority $performer=null)
Determine if the current user is allowed to view a particular field of this revision,...
__construct( $context, ?ChangesListFilterGroupContainer $filterGroups=null)
RowCommentFormatter $commentFormatter
insertComment( $rc)
Insert a formatted comment.
ChangesListFilterGroupContainer $filterGroups
insertUserRelatedLinks(&$s, &$rc)
Insert links to user page, user talk page and eventually a blocking link.
string[] $formattedComments
Comments indexed by rc_id.
setWatchlistDivs( $value=true)
Sets the list to use a "<li class='watchlist-(namespace)-(page)'>" tag.
insertDiffHist(&$s, &$rc, $unpatrolled=null)
insertRollback(&$s, &$rc)
Insert a rollback link.
isCategorizationWithoutRevision( $rcObj)
Determines whether a revision is linked to this change; this may not be the case when the categorizat...
getWatchlistExpiry(RecentChange $recentChange)
Get HTML to display the clock icon for watched items that have a watchlist expiry time.
getHTMLClassesForFilters( $rc)
Get an array of CSS classes attributed to filters for this row.
static isUnpatrolled( $rc, User $user)
insertTimestamp(&$s, $rc)
Insert time timestamp string from $rc into $s.
formatCharacterDifference(RecentChange $old, ?RecentChange $new=null)
Format the character difference of one or several changes.
getDataAttributes(RecentChange $rc)
Get recommended data attributes for a change line.
Generate a list of changes using an Enhanced system (uses javascript).
Generate a list of changes using the good old system (no javascript).
Utility class for creating and reading rows in the recentchanges table.
getAttribute( $name)
Get an attribute value.
Page revision base class.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
userCan( $field, Authority $performer)
Determine if the give authority is allowed to view a particular field of this revision,...
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
getId( $wikiId=self::LOCAL)
Get revision ID.
Represents a title within MediaWiki.
Definition Title.php:70
getBoolOption(UserIdentity $user, string $oname, int $queryFlags=IDBAccessObject::READ_NORMAL)
Get the user's current setting for a given option, as a boolean value.
Value object representing a user's identity.
User class for the MediaWiki software.
Definition User.php:110
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2138
useFilePatrol()
Check whether to enable new files patrol features for this user.
Definition User.php:2163
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2148
Representation of a pair of user and title for watchlist entries.
getExpiryInDaysText(MessageLocalizer $msgLocalizer, $isDropdownOption=false)
Get days remaining until a watched item expires as a text.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
Store key-value entries in a size-limited in-memory LRU cache.
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
getUser()
Returns the performer of the actions associated with this authority.
Result wrapper for grabbing data queried from an IDatabase object.
$source
element(SerializerNode $parent, SerializerNode $node, $contents)