MediaWiki master
ChangesList.php
Go to the documentation of this file.
1<?php
8
14use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
38use OOUI\IconWidget;
39use RuntimeException;
40use stdClass;
44
54 use ProtectedHookAccessorTrait;
55
56 public const CSS_CLASS_PREFIX = 'mw-changeslist-';
57
59 protected $watchlist = false;
61 protected $lastdate;
63 protected $message;
65 protected $rc_cache;
67 protected $rcCacheIndex;
69 protected $rclistOpen;
71 protected $rcMoveIndex;
72
75
77 protected $watchMsgCache;
78
82 protected $linkRenderer;
83
88
93
97 protected $filterGroups;
98
102 protected $tagsCache;
103
107 protected $userLinkCache;
108
109 private LogFormatterFactory $logFormatterFactory;
110
112
113 private ChangeToolsFactory $changeToolsFactory;
114 private ChangeTagsFormatter $changeTagsFormatter;
115
116 protected array $userLabels;
117
124 public function __construct(
125 $context,
127 ?ChangeToolsFactory $changeToolsFactory = null,
128 ?ChangeTagsFormatter $changeTagsFormatter = null,
129 ) {
130 $this->setContext( $context );
131 $this->preCacheMessages();
132 $this->watchMsgCache = new MapCacheLRU( 50 );
133 $this->filterGroups = $filterGroups ?? new ChangesListFilterGroupContainer();
134
135 $services = MediaWikiServices::getInstance();
136 $this->linkRenderer = $services->getLinkRenderer();
137 $this->commentFormatter = $services->getRowCommentFormatter();
138 $this->logFormatterFactory = $services->getLogFormatterFactory();
139 $this->userLinkRenderer = $services->getUserLinkRenderer();
140 $this->changeToolsFactory = $changeToolsFactory ?? $services->getChangeToolsFactory();
141 $this->changeTagsFormatter = $changeTagsFormatter ?? $services->getChangeTagsFormatter();
142 $this->tagsCache = new MapCacheLRU( 50 );
143 $this->userLinkCache = new MapCacheLRU( 50 );
144 }
145
154 public static function newFromContext(
155 IContextSource $context,
156 ?ChangesListFilterGroupContainer $groups = null
157 ) {
158 $user = $context->getUser();
159 $sk = $context->getSkin();
160 $services = MediaWikiServices::getInstance();
161 $list = null;
162 $groups ??= new ChangesListFilterGroupContainer();
163 if ( ( new HookRunner( $services->getHookContainer() ) )->onFetchChangesList( $user, $sk, $list, $groups ) ) {
164 $userOptionsLookup = $services->getUserOptionsLookup();
165 $new = $context->getRequest()->getBool(
166 'enhanced',
167 $userOptionsLookup->getBoolOption( $user, 'usenewrc' )
168 );
169
170 return $new ?
171 new EnhancedChangesList( $context, $groups ) :
172 new OldChangesList( $context, $groups );
173 } else {
174 return $list;
175 }
176 }
177
189 public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
190 throw new RuntimeException( 'recentChangesLine should be implemented' );
191 }
192
199 protected function getHighlightsContainerDiv() {
200 $highlightColorDivs = '';
201 foreach ( [ 'none', 'c1', 'c2', 'c3', 'c4', 'c5' ] as $color ) {
202 $highlightColorDivs .= Html::rawElement(
203 'div',
204 [
205 'class' => 'mw-rcfilters-ui-highlights-color-' . $color,
206 'data-color' => $color
207 ]
208 );
209 }
210
211 return Html::rawElement(
212 'div',
213 [ 'class' => 'mw-rcfilters-ui-highlights' ],
214 $highlightColorDivs
215 );
216 }
217
222 public function setWatchlistDivs( $value = true ) {
223 $this->watchlist = $value;
224 }
225
230 public function isWatchlist() {
231 return (bool)$this->watchlist;
232 }
233
238 private function preCacheMessages() {
239 // @phan-suppress-next-line MediaWikiNoIssetIfDefined False positives when documented as nullable
240 if ( !isset( $this->message ) ) {
241 $this->message = [];
242 foreach ( [
243 'cur', 'diff', 'hist', 'enhancedrc-history', 'last', 'blocklink', 'history',
244 'semicolon-separator', 'pipe-separator', 'word-separator' ] as $msg
245 ) {
246 $this->message[$msg] = $this->msg( $msg )->escaped();
247 }
248 }
249 }
250
257 public function recentChangesFlags( $flags, $nothing = "\u{00A0}" ) {
258 $f = '';
259 foreach (
260 $this->getConfig()->get( MainConfigNames::RecentChangesFlags ) as $flag => $_
261 ) {
262 $f .= isset( $flags[$flag] ) && $flags[$flag]
263 ? self::flag( $flag, $this->getContext() )
264 : $nothing;
265 }
266
267 return $f;
268 }
269
278 protected function getHTMLClasses( $rc, $watched ) {
279 $classes = [ self::CSS_CLASS_PREFIX . 'line' ];
280 $logType = $rc->mAttribs['rc_log_type'];
281
282 if ( $logType ) {
283 $classes[] = self::CSS_CLASS_PREFIX . 'log';
284 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'log-' . $logType );
285 } else {
286 $classes[] = self::CSS_CLASS_PREFIX . 'edit';
287 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns' .
288 $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] );
289 }
290
291 // Indicate watched status on the line to allow for more
292 // comprehensive styling.
293 $classes[] = $watched && $rc->mAttribs['rc_timestamp'] >= $watched
294 ? self::CSS_CLASS_PREFIX . 'line-watched'
295 : self::CSS_CLASS_PREFIX . 'line-not-watched';
296
297 $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rc ) );
298
299 return $classes;
300 }
301
309 protected function getHTMLClassesForFilters( $rc ) {
310 $classes = [];
311
312 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns-' .
313 $rc->mAttribs['rc_namespace'] );
314
315 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
316 $classes[] = Sanitizer::escapeClass(
317 self::CSS_CLASS_PREFIX .
318 'ns-' .
319 ( $nsInfo->isTalk( $rc->mAttribs['rc_namespace'] ) ? 'talk' : 'subject' )
320 );
321
322 $this->filterGroups->applyCssClassIfNeeded( $this->getContext(), $rc, $classes );
323
324 return $classes;
325 }
326
337 public static function flag( $flag, IContextSource $context ) {
338 static $map = [ 'minoredit' => 'minor', 'botedit' => 'bot' ];
339 static $flagInfos = null;
340
341 if ( $flagInfos === null ) {
342 $recentChangesFlags = MediaWikiServices::getInstance()->getMainConfig()
344 $flagInfos = [];
345 foreach ( $recentChangesFlags as $key => $value ) {
346 $flagInfos[$key]['letter'] = $value['letter'];
347 $flagInfos[$key]['title'] = $value['title'];
348 // Allow customized class name, fall back to flag name
349 $flagInfos[$key]['class'] = $value['class'] ?? $key;
350 }
351 }
352
353 // Inconsistent naming, kept for b/c
354 if ( isset( $map[$flag] ) ) {
355 $flag = $map[$flag];
356 }
357
358 $info = $flagInfos[$flag];
359 return Html::element( 'abbr', [
360 'class' => $info['class'],
361 'title' => wfMessage( $info['title'] )->setContext( $context )->text(),
362 ], wfMessage( $info['letter'] )->setContext( $context )->text() );
363 }
364
369 public function beginRecentChangesList() {
370 $this->rc_cache = [];
371 $this->rcMoveIndex = 0;
372 $this->rcCacheIndex = 0;
373 $this->lastdate = '';
374 $this->rclistOpen = false;
375 $this->getOutput()->addModuleStyles( [
376 'mediawiki.interface.helpers.styles',
377 'mediawiki.special.changeslist'
378 ] );
379
380 return '<div class="mw-changeslist">';
381 }
382
386 public function initChangesListRows( $rows ) {
387 $this->getHookRunner()->onChangesListInitRows( $this, $rows );
388 $this->formattedComments = $this->commentFormatter->createBatch()
389 ->comments(
390 $this->commentFormatter->rows( $rows )
391 ->commentKey( 'rc_comment' )
392 ->namespaceField( 'rc_namespace' )
393 ->titleField( 'rc_title' )
394 ->indexField( 'rc_id' )
395 )
396 ->useBlock()
397 ->execute();
398 }
399
410 public static function showCharacterDifference( $old, $new, IContextSource $context ) {
411 $new = (int)$new;
412 $old = (int)$old;
413 $szdiff = $new - $old;
414
415 $lang = $context->getLanguage();
416 $config = $context->getConfig();
417 $code = $lang->getCode();
418 static $fastCharDiff = [];
419 if ( !isset( $fastCharDiff[$code] ) ) {
420 $fastCharDiff[$code] = $config->get( MainConfigNames::MiserMode )
421 || $context->msg( 'rc-change-size' )->plain() === '$1';
422 }
423
424 $formattedSize = $lang->formatNum( $szdiff );
425
426 if ( !$fastCharDiff[$code] ) {
427 $formattedSize = $context->msg( 'rc-change-size', $formattedSize )->text();
428 }
429
430 if ( abs( $szdiff ) > abs( $config->get( MainConfigNames::RCChangedSizeThreshold ) ) ) {
431 $tag = 'strong';
432 } else {
433 $tag = 'span';
434 }
435
436 if ( $szdiff === 0 ) {
437 $formattedSizeClass = 'mw-plusminus-null';
438 } elseif ( $szdiff > 0 ) {
439 $formattedSize = '+' . $formattedSize;
440 $formattedSizeClass = 'mw-plusminus-pos';
441 } else {
442 $formattedSizeClass = 'mw-plusminus-neg';
443 }
444 $formattedSizeClass .= ' mw-diff-bytes';
445
446 $formattedTotalSize = $context->msg( 'rc-change-size-new' )->numParams( $new )->text();
447
448 return Html::element( $tag,
449 [ 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ],
450 $formattedSize );
451 }
452
460 public function formatCharacterDifference( RecentChange $old, ?RecentChange $new = null ) {
461 $oldlen = $old->mAttribs['rc_old_len'];
462
463 if ( $new ) {
464 $newlen = $new->mAttribs['rc_new_len'];
465 } else {
466 $newlen = $old->mAttribs['rc_new_len'];
467 }
468
469 if ( $oldlen === null || $newlen === null ) {
470 return '';
471 }
472
473 return self::showCharacterDifference( $oldlen, $newlen, $this->getContext() );
474 }
475
480 public function endRecentChangesList() {
481 $out = $this->rclistOpen ? "</ul>\n" : '';
482 $out .= '</div>';
483
484 return $out;
485 }
486
500 public static function revDateLink(
501 RevisionRecord $rev,
502 Authority $performer,
503 Language $lang,
504 $title = null,
505 $className = ''
506 ) {
507 $ts = $rev->getTimestamp();
508 $time = $lang->userTime( $ts, $performer->getUser() );
509 $date = $lang->userTimeAndDate( $ts, $performer->getUser() );
510 $class = trim( 'mw-changeslist-date ' . $className );
511 if ( $rev->userCan( RevisionRecord::DELETED_TEXT, $performer ) ) {
512 $link = Html::rawElement( 'bdi', [ 'dir' => $lang->getDir() ],
513 MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
514 $title ?? $rev->getPageAsLinkTarget(),
515 $date,
516 [ 'class' => $class ],
517 [ 'oldid' => $rev->getId() ]
518 )
519 );
520 } else {
521 $link = htmlspecialchars( $date );
522 }
523 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
524 $class = Linker::getRevisionDeletedClass( $rev ) . " $class";
525 $link = "<span class=\"$class\">$link</span>";
526 }
527 return Html::element( 'span', [
528 'class' => 'mw-changeslist-time'
529 ], $time ) . $link;
530 }
531
536 public function insertDateHeader( &$s, $rc_timestamp ) {
537 # Make date header if necessary
538 $date = $this->getLanguage()->userDate( $rc_timestamp, $this->getUser() );
539 if ( $date != $this->lastdate ) {
540 if ( $this->lastdate != '' ) {
541 $s .= "</ul>\n";
542 }
543 $s .= Html::element( 'h4', [], $date ) . "\n<ul class=\"special\">";
544 $this->lastdate = $date;
545 $this->rclistOpen = true;
546 }
547 }
548
555 public function insertLog( &$s, $title, $logtype, $useParentheses = true ) {
556 $page = new LogPage( $logtype );
557 $logname = $page->getName()->setContext( $this->getContext() )->text();
558 $link = $this->linkRenderer->makeKnownLink( $title, $logname, [
559 'class' => $useParentheses ? '' : 'mw-changeslist-links'
560 ] );
561 if ( $useParentheses ) {
562 $s .= $this->msg( 'parentheses' )->rawParams(
563 $link
564 )->escaped();
565 } else {
566 $s .= $link;
567 }
568 }
569
575 public function insertDiffHist( &$s, &$rc, $unpatrolled = null ) {
576 # Diff link
577 if (
578 $rc->mAttribs['rc_source'] === RecentChange::SRC_NEW ||
579 $rc->mAttribs['rc_source'] === RecentChange::SRC_LOG
580 ) {
581 $diffLink = $this->message['diff'];
582 } elseif ( !self::userCan( $rc, RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
583 $diffLink = $this->message['diff'];
584 } else {
585 $query = [
586 'curid' => $rc->mAttribs['rc_cur_id'],
587 'diff' => $rc->mAttribs['rc_this_oldid'],
588 'oldid' => $rc->mAttribs['rc_last_oldid']
589 ];
590
591 $diffLink = $this->linkRenderer->makeKnownLink(
592 $rc->getTitle(),
593 new HtmlArmor( $this->message['diff'] ),
594 [ 'class' => 'mw-changeslist-diff' ],
595 $query
596 );
597 }
598 $histLink = $this->linkRenderer->makeKnownLink(
599 $rc->getTitle(),
600 new HtmlArmor( $this->message['hist'] ),
601 [ 'class' => 'mw-changeslist-history' ],
602 [
603 'curid' => $rc->mAttribs['rc_cur_id'],
604 'action' => 'history'
605 ]
606 );
607
608 $s .= Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ],
609 Html::rawElement( 'span', [], $diffLink ) .
610 Html::rawElement( 'span', [], $histLink )
611 ) .
612 ' <span class="mw-changeslist-separator"></span> ';
613 }
614
625 public function getArticleLink( &$rc, $unpatrolled, $watched ) {
626 $params = [];
627 if ( $rc->getTitle()->isRedirect() ) {
628 $params = [ 'redirect' => 'no' ];
629 }
630
631 $articlelink = $this->linkRenderer->makeLink(
632 $rc->getTitle(),
633 null,
634 [ 'class' => 'mw-changeslist-title' ],
635 $params
636 );
637 if ( static::isDeleted( $rc, RevisionRecord::DELETED_TEXT ) ) {
638 $class = 'history-deleted';
639 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
640 $class .= ' mw-history-suppressed';
641 }
642 $articlelink = '<span class="' . $class . '">' . $articlelink . '</span>';
643 }
644 $dir = $this->getLanguage()->getDir();
645 $articlelink = Html::rawElement( 'bdi', [ 'dir' => $dir ], $articlelink );
646 # To allow for boldening pages watched by this user
647 # Don't wrap result of this with another tag, see T376814
648 $articlelink = "<span class=\"mw-title\">{$articlelink}</span>";
649
650 # TODO: Deprecate the $s argument, it seems happily unused.
651 $s = '';
652 $this->getHookRunner()->onChangesListInsertArticleLink( $this, $articlelink,
653 $s, $rc, $unpatrolled, $watched );
654
655 // Watchlist expiry icon.
656 $watchlistExpiry = '';
657 // @phan-suppress-next-line MediaWikiNoIssetIfDefined
658 if ( isset( $rc->watchlistExpiry ) && $rc->watchlistExpiry ) {
659 $watchlistExpiry = $this->getWatchlistExpiry( $rc );
660 }
661
662 return "{$s} {$articlelink}{$watchlistExpiry}";
663 }
664
671 public function getWatchlistExpiry( RecentChange $recentChange ): string {
672 $item = WatchedItem::newFromRecentChange( $recentChange, $this->getUser() );
673 // Guard against expired items, even though they shouldn't come here.
674 if ( $item->isExpired() ) {
675 return '';
676 }
677 $daysLeftText = $item->getExpiryInDaysText( $this->getContext() );
678 // Matching widget is also created in ChangesListSpecialPage, for the legend.
679 $widget = new IconWidget( [
680 'icon' => 'clock',
681 'title' => $daysLeftText,
682 'classes' => [ 'mw-changesList-watchlistExpiry' ],
683 ] );
684 $widget->setAttributes( [
685 // Add labels for assistive technologies.
686 'role' => 'img',
687 'aria-label' => $this->msg( 'watchlist-expires-in-aria-label' )->text(),
688 // Days-left is used in resources/src/mediawiki.special.changeslist.watchlistexpiry/watchlistexpiry.js
689 'data-days-left' => $item->getExpiryInDays(),
690 ] );
691 // Add spaces around the widget (the page title is to one side,
692 // and a semicolon or opening-parenthesis to the other).
693 return " $widget ";
694 }
695
704 public function getTimestamp( $rc ) {
705 // This uses the semi-colon separator unless there's a watchlist expiry date for the entry,
706 // because in that case the timestamp is preceded by a clock icon.
707 // A space is important after `.mw-changeslist-separator--semicolon` to make sure
708 // that whatever comes before it is distinguishable.
709 // (Otherwise your have the text of titles pushing up against the timestamp)
710 // A specific element is used for this purpose rather than styling `.mw-changeslist-date`
711 // as the `.mw-changeslist-date` class is used in a variety
712 // of other places with a different position and the information proceeding getTimestamp can vary.
713 // The `.mw-changeslist-time` class allows us to distinguish from `.mw-changeslist-date` elements that
714 // contain the full date (month, year) and adds consistency with Special:Contributions
715 // and other pages.
716 $separatorClass = $rc->watchlistExpiry ? 'mw-changeslist-separator' : 'mw-changeslist-separator--semicolon';
717 return Html::element( 'span', [ 'class' => $separatorClass ] ) . $this->message['word-separator'] .
718 '<span class="mw-changeslist-date mw-changeslist-time">' .
719 htmlspecialchars( $this->getLanguage()->userTime(
720 $rc->mAttribs['rc_timestamp'],
721 $this->getUser()
722 ) ) . '</span> <span class="mw-changeslist-separator"></span> ';
723 }
724
731 public function insertTimestamp( &$s, $rc ) {
732 $s .= $this->getTimestamp( $rc );
733 }
734
741 public function insertUserRelatedLinks( &$s, &$rc ) {
742 if ( static::isDeleted( $rc, RevisionRecord::DELETED_USER ) ) {
743 $deletedClass = 'history-deleted';
744 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
745 $deletedClass .= ' mw-history-suppressed';
746 }
747 $s .= ' <span class="' . $deletedClass . '">' .
748 $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
749 } else {
750 $s .= $this->linkRenderer->makeUserLink(
751 $rc->getPerformerIdentity(),
752 $this
753 );
754 # Don't wrap result of this with another tag, see T376814
755 $s .= $this->userLinkCache->getWithSetCallback(
756 $this->userLinkCache->makeKey(
757 $rc->mAttribs['rc_user_text'],
758 $this->getUser()->getName(),
759 $this->getLanguage()->getCode()
760 ),
761 // The text content of tools is not wrapped with parentheses or "piped".
762 // This will be handled in CSS (T205581).
763 static fn () => Linker::userToolLinks(
764 $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'],
765 false, 0, null,
766 false
767 )
768 );
769 }
770 }
771
778 public function insertLogEntry( $rc ) {
779 $entry = DatabaseLogEntry::newFromRow( $rc->mAttribs );
780 $formatter = $this->logFormatterFactory->newFromEntry( $entry );
781 $formatter->setContext( $this->getContext() );
782 $formatter->setShowUserToolLinks( true );
783
784 $comment = $formatter->getComment();
785 if ( $comment !== '' ) {
786 $dir = $this->getLanguage()->getDir();
787 $comment = Html::rawElement( 'bdi', [ 'dir' => $dir ], $comment );
788 }
789
790 $html = $formatter->getActionText() . $this->message['word-separator'] . $comment .
791 $this->message['word-separator'] . $formatter->getActionLinks();
792 $classes = [ 'mw-changeslist-log-entry' ];
793 $attribs = [];
794
795 // Let extensions add data to the outputted log entry in a similar way to the LogEventsListLineEnding hook
796 $this->getHookRunner()->onChangesListInsertLogEntry( $entry, $this->getContext(), $html, $classes, $attribs );
797 $attribs = array_filter( $attribs,
798 Sanitizer::isReservedDataAttribute( ... ),
799 ARRAY_FILTER_USE_KEY
800 );
801 $attribs['class'] = $classes;
802
803 return Html::rawElement( 'span', $attribs, $html );
804 }
805
811 public function insertComment( $rc ) {
812 if ( static::isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) {
813 $deletedClass = 'history-deleted';
814 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
815 $deletedClass .= ' mw-history-suppressed';
816 }
817 return ' <span class="' . $deletedClass . ' comment">' .
818 $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
819 } elseif ( isset( $rc->mAttribs['rc_id'] )
820 && isset( $this->formattedComments[$rc->mAttribs['rc_id']] )
821 ) {
822 return $this->formattedComments[$rc->mAttribs['rc_id']];
823 } else {
824 return $this->commentFormatter->formatBlock(
825 $rc->mAttribs['rc_comment'],
826 $rc->getTitle(),
827 // Whether section links should refer to local page (using default false)
828 false,
829 // wikid to generate links for (using default null) */
830 null,
831 // whether parentheses should be rendered as part of the message
832 false
833 );
834 }
835 }
836
842 protected function numberofWatchingusers( $count ) {
843 if ( $count <= 0 ) {
844 return '';
845 }
846
847 return $this->watchMsgCache->getWithSetCallback(
848 $this->watchMsgCache->makeKey(
849 'watching-users-msg',
850 strval( $count ),
851 $this->getUser()->getName(),
852 $this->getLanguage()->getCode()
853 ),
854 function () use ( $count ) {
855 return $this->msg( 'number-of-watching-users-for-recent-changes' )
856 ->numParams( $count )->escaped();
857 }
858 );
859 }
860
867 public static function isDeleted( $rc, $field ) {
868 return ( $rc->mAttribs['rc_deleted'] & $field ) == $field;
869 }
870
879 public static function userCan( $rc, $field, Authority $performer ) {
880 if ( $rc->mAttribs['rc_source'] === RecentChange::SRC_LOG ) {
881 return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $performer );
882 }
883
884 return RevisionRecord::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $performer );
885 }
886
892 protected function maybeWatchedLink( $link, $watched = false ) {
893 if ( $watched ) {
894 return '<strong class="mw-watched">' . $link . '</strong>';
895 } else {
896 return '<span class="mw-rc-unwatched">' . $link . '</span>';
897 }
898 }
899
906 public function insertRollback( &$s, &$rc ) {
907 $this->insertPageTools( $s, $rc );
908 }
909
917 private function insertPageTools( &$s, &$rc ) {
918 // FIXME Some page tools (e.g. thanks) might make sense for log entries.
919 if ( !in_array( $rc->mAttribs['rc_source'], [ RecentChange::SRC_EDIT, RecentChange::SRC_NEW ] )
920 // FIXME When would either of these not exist when type is RC_EDIT? Document.
921 || !$rc->mAttribs['rc_this_oldid']
922 || !$rc->mAttribs['rc_cur_id']
923 ) {
924 return;
925 }
926
927 // Construct a fake revision for ChangeTools. FIXME can't we just obtain the real one?
928 $title = $rc->getTitle();
929 $revRecord = new MutableRevisionRecord( $title );
930 $revRecord->setId( (int)$rc->mAttribs['rc_this_oldid'] );
931 $revRecord->setVisibility( (int)$rc->mAttribs['rc_deleted'] );
932 $user = new UserIdentityValue(
933 (int)$rc->mAttribs['rc_user'],
934 $rc->mAttribs['rc_user_text']
935 );
936 $revRecord->setUser( $user );
937
938 $tools = $this->changeToolsFactory->buildChangeTools(
939 $revRecord,
940 null,
941 // only show a rollback link on the top-most revision
942 $rc->getAttribute( 'page_latest' ) == $rc->mAttribs['rc_this_oldid']
943 && $rc->mAttribs['rc_source'] !== RecentChange::SRC_NEW,
944 $this->getContext(),
945 );
946
947 $s .= $tools->toHTML();
948 }
949
955 public function getRollback( RecentChange $rc ) {
956 $s = '';
957 $this->insertRollback( $s, $rc );
958 return $s;
959 }
960
966 public function insertTags( &$s, &$rc, &$classes ) {
967 if ( empty( $rc->mAttribs['ts_tags'] ) ) {
968 return;
969 }
970
976 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
977 $this->tagsCache->makeKey(
978 $rc->mAttribs['ts_tags'],
979 $this->getUser()->getName(),
980 $this->getLanguage()->getCode()
981 ),
982 fn () => $this->changeTagsFormatter->formatTagsAsSummaryList(
983 $rc->mAttribs['ts_tags'],
984 $this->getContext(),
985 $this->getAuthority()
986 )
987 );
988 $classes = array_merge( $classes, $newClasses );
989 $s .= $this->message['word-separator'] . $tagSummary;
990 }
991
998 public function getTags( RecentChange $rc, array &$classes ) {
999 $s = '';
1000 $this->insertTags( $s, $rc, $classes );
1001 return $s;
1002 }
1003
1010 public function getLabels( RecentChange $rc, &$classes ): string {
1011 if ( !$this->getConfig()->get( MainConfigNames::EnableWatchlistLabels ) ) {
1012 return '';
1013 }
1014 if ( empty( $rc->mAttribs[ WatchlistLabelCondition::LABEL_IDS ] ) ) {
1015 return '';
1016 }
1017 $labelIds = explode( ',', $rc->mAttribs[ WatchlistLabelCondition::LABEL_IDS ] );
1018 $labelStrings = [];
1019 foreach ( $labelIds as $labelId ) {
1020 $classes[] = SpecialWatchlist::WATCHLIST_LABEL_CSS_CLASS_PREFIX . $labelId;
1021 $labelStrings[] = wfEscapeWikiText( $this->userLabels[ $labelId ]->getName() );
1022 }
1023 $labelsList = $this->msg( 'watchlistlabels-list-wrapper' )->params(
1024 $this->getLanguage()->commaList( $labelStrings )
1025 )->parse();
1026 return $this->message['word-separator'] .
1027 Html::rawElement(
1028 'span',
1029 [ 'class' => 'mw-changeslist-watchlistlabels' ],
1030 $this->msg( 'parentheses' )->rawParams( $labelsList )->escaped()
1031 );
1032 }
1033
1039 public function insertLabels( &$s, &$rc, &$classes ) {
1040 $s .= $this->getLabels( $rc, $classes );
1041 }
1042
1048 public function insertExtra( &$s, &$rc, &$classes ) {
1049 // Empty, used for subclasses to add anything special.
1050 }
1051
1055 protected function showAsUnpatrolled( RecentChange $rc ) {
1056 return self::isUnpatrolled( $rc, $this->getUser() );
1057 }
1058
1064 public static function isUnpatrolled( $rc, User $user ) {
1065 if ( $rc instanceof RecentChange ) {
1066 $isPatrolled = $rc->mAttribs['rc_patrolled'];
1067 $rcSource = $rc->mAttribs['rc_source'];
1068 $rcLogType = $rc->mAttribs['rc_log_type'];
1069 } else {
1070 $isPatrolled = $rc->rc_patrolled;
1071 $rcSource = $rc->rc_source;
1072 $rcLogType = $rc->rc_log_type;
1073 }
1074
1075 if ( $isPatrolled ) {
1076 return false;
1077 }
1078
1079 return $user->useRCPatrol() ||
1080 ( $rcSource === RecentChange::SRC_NEW && $user->useNPPatrol() ) ||
1081 ( $rcLogType === 'upload' && $user->useFilePatrol() );
1082 }
1083
1093 protected function isCategorizationWithoutRevision( $rcObj ) {
1094 return $rcObj->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE
1095 && intval( $rcObj->getAttribute( 'rc_this_oldid' ) ) === 0;
1096 }
1097
1103 protected function getDataAttributes( RecentChange $rc ) {
1104 $attrs = [];
1105
1106 $source = $rc->getAttribute( 'rc_source' );
1107 switch ( $source ) {
1108 case RecentChange::SRC_EDIT:
1109 case RecentChange::SRC_CATEGORIZE:
1110 case RecentChange::SRC_NEW:
1111 $attrs['data-mw-revid'] = $rc->mAttribs['rc_this_oldid'];
1112 break;
1113 case RecentChange::SRC_LOG:
1114 $attrs['data-mw-logid'] = $rc->mAttribs['rc_logid'];
1115 $attrs['data-mw-logaction'] =
1116 $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'];
1117 break;
1118 }
1119
1120 $attrs[ 'data-mw-ts' ] = $rc->getAttribute( 'rc_timestamp' );
1121
1122 return $attrs;
1123 }
1124
1132 public function setChangeLinePrefixer( callable $prefixer ) {
1133 $this->changeLinePrefixer = $prefixer;
1134 }
1135
1140 public function setUserLabels( array $userLabels ): void {
1141 $this->userLabels = $userLabels;
1142 }
1143}
1144
1146class_alias( ChangesList::class, 'ChangesList' );
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
Formats change tags for display in HTML and use filter dropdown menus.
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.
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:44
Base class for language-specific code.
Definition Language.php:65
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:48
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:34
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:34
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)
__construct( $context, ?ChangesListFilterGroupContainer $filterGroups=null, ?ChangeToolsFactory $changeToolsFactory=null, ?ChangeTagsFormatter $changeTagsFormatter=null,)
getLabels(RecentChange $rc, &$classes)
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 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.
RowCommentFormatter $commentFormatter
insertComment( $rc)
Insert a formatted comment.
ChangesListFilterGroupContainer $filterGroups
static showCharacterDifference( $old, $new, IContextSource $context)
Show formatted char difference.
insertUserRelatedLinks(&$s, &$rc)
Insert links to user page, user talk page and eventually a blocking link.
string[] $formattedComments
Comments indexed by rc_id.
static userCan( $rc, $field, Authority $performer)
Determine if the given user is allowed to view a particular field of this revision,...
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.
static flag( $flag, IContextSource $context)
Make an "<abbr>" element for a given change flag.
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.
userCan(int $field, Authority $performer)
Determine if the given authority is allowed to view a particular field of this revision,...
isDeleted(int $field)
MCR migration note: this replaced Revision::isDeleted.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
getId( $wikiId=self::LOCAL)
Get revision ID.
A special page that lists last changes made to the wiki, limited to user-defined list of titles.
Represents a title within MediaWiki.
Definition Title.php:69
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:129
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2154
useFilePatrol()
Check whether to enable new files patrol features for this user.
Definition User.php:2179
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2164
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.
getConfig()
Get the site configuration.
msg( $key,... $params)
This is the method for getting translated interface messages.
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)