MediaWiki master
ChangesList.php
Go to the documentation of this file.
1<?php
22
29use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
50use OOUI\IconWidget;
51use RuntimeException;
52use stdClass;
56
66 use ProtectedHookAccessorTrait;
67
68 public const CSS_CLASS_PREFIX = 'mw-changeslist-';
69
71 protected $watchlist = false;
73 protected $lastdate;
75 protected $message;
77 protected $rc_cache;
79 protected $rcCacheIndex;
81 protected $rclistOpen;
83 protected $rcMoveIndex;
84
87
89 protected $watchMsgCache;
90
94 protected $linkRenderer;
95
100
105
109 protected $filterGroups;
110
114 protected $tagsCache;
115
119 protected $userLinkCache;
120
121 private LogFormatterFactory $logFormatterFactory;
122
124
129 public function __construct( $context, array $filterGroups = [] ) {
130 $this->setContext( $context );
131 $this->preCacheMessages();
132 $this->watchMsgCache = new MapCacheLRU( 50 );
133 $this->filterGroups = $filterGroups;
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->tagsCache = new MapCacheLRU( 50 );
141 $this->userLinkCache = new MapCacheLRU( 50 );
142 }
143
152 public static function newFromContext( IContextSource $context, array $groups = [] ) {
153 $user = $context->getUser();
154 $sk = $context->getSkin();
155 $services = MediaWikiServices::getInstance();
156 $list = null;
157 if ( ( new HookRunner( $services->getHookContainer() ) )->onFetchChangesList( $user, $sk, $list, $groups ) ) {
158 $userOptionsLookup = $services->getUserOptionsLookup();
159 $new = $context->getRequest()->getBool(
160 'enhanced',
161 $userOptionsLookup->getBoolOption( $user, 'usenewrc' )
162 );
163
164 return $new ?
165 new EnhancedChangesList( $context, $groups ) :
166 new OldChangesList( $context, $groups );
167 } else {
168 return $list;
169 }
170 }
171
183 public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
184 throw new RuntimeException( 'recentChangesLine should be implemented' );
185 }
186
193 protected function getHighlightsContainerDiv() {
194 $highlightColorDivs = '';
195 foreach ( [ 'none', 'c1', 'c2', 'c3', 'c4', 'c5' ] as $color ) {
196 $highlightColorDivs .= Html::rawElement(
197 'div',
198 [
199 'class' => 'mw-rcfilters-ui-highlights-color-' . $color,
200 'data-color' => $color
201 ]
202 );
203 }
204
205 return Html::rawElement(
206 'div',
207 [ 'class' => 'mw-rcfilters-ui-highlights' ],
208 $highlightColorDivs
209 );
210 }
211
216 public function setWatchlistDivs( $value = true ) {
217 $this->watchlist = $value;
218 }
219
224 public function isWatchlist() {
225 return (bool)$this->watchlist;
226 }
227
232 private function preCacheMessages() {
233 // @phan-suppress-next-line MediaWikiNoIssetIfDefined False positives when documented as nullable
234 if ( !isset( $this->message ) ) {
235 $this->message = [];
236 foreach ( [
237 'cur', 'diff', 'hist', 'enhancedrc-history', 'last', 'blocklink', 'history',
238 'semicolon-separator', 'pipe-separator', 'word-separator' ] as $msg
239 ) {
240 $this->message[$msg] = $this->msg( $msg )->escaped();
241 }
242 }
243 }
244
251 public function recentChangesFlags( $flags, $nothing = "\u{00A0}" ) {
252 $f = '';
253 foreach (
254 $this->getConfig()->get( MainConfigNames::RecentChangesFlags ) as $flag => $_
255 ) {
256 $f .= isset( $flags[$flag] ) && $flags[$flag]
257 ? self::flag( $flag, $this->getContext() )
258 : $nothing;
259 }
260
261 return $f;
262 }
263
272 protected function getHTMLClasses( $rc, $watched ) {
273 $classes = [ self::CSS_CLASS_PREFIX . 'line' ];
274 $logType = $rc->mAttribs['rc_log_type'];
275
276 if ( $logType ) {
277 $classes[] = self::CSS_CLASS_PREFIX . 'log';
278 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'log-' . $logType );
279 } else {
280 $classes[] = self::CSS_CLASS_PREFIX . 'edit';
281 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns' .
282 $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] );
283 }
284
285 // Indicate watched status on the line to allow for more
286 // comprehensive styling.
287 $classes[] = $watched && $rc->mAttribs['rc_timestamp'] >= $watched
288 ? self::CSS_CLASS_PREFIX . 'line-watched'
289 : self::CSS_CLASS_PREFIX . 'line-not-watched';
290
291 $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rc ) );
292
293 return $classes;
294 }
295
303 protected function getHTMLClassesForFilters( $rc ) {
304 $classes = [];
305
306 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns-' .
307 $rc->mAttribs['rc_namespace'] );
308
309 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
310 $classes[] = Sanitizer::escapeClass(
311 self::CSS_CLASS_PREFIX .
312 'ns-' .
313 ( $nsInfo->isTalk( $rc->mAttribs['rc_namespace'] ) ? 'talk' : 'subject' )
314 );
315
316 foreach ( $this->filterGroups as $filterGroup ) {
317 foreach ( $filterGroup->getFilters() as $filter ) {
318 $filter->applyCssClassIfNeeded( $this, $rc, $classes );
319 }
320 }
321
322 return $classes;
323 }
324
335 public static function flag( $flag, ?IContextSource $context = null ) {
336 static $map = [ 'minoredit' => 'minor', 'botedit' => 'bot' ];
337 static $flagInfos = null;
338
339 if ( $flagInfos === null ) {
340 $recentChangesFlags = MediaWikiServices::getInstance()->getMainConfig()
342 $flagInfos = [];
343 foreach ( $recentChangesFlags as $key => $value ) {
344 $flagInfos[$key]['letter'] = $value['letter'];
345 $flagInfos[$key]['title'] = $value['title'];
346 // Allow customized class name, fall back to flag name
347 $flagInfos[$key]['class'] = $value['class'] ?? $key;
348 }
349 }
350
351 $context = $context ?: RequestContext::getMain();
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 = null ) {
411 if ( !$context ) {
412 $context = RequestContext::getMain();
413 }
414
415 $new = (int)$new;
416 $old = (int)$old;
417 $szdiff = $new - $old;
418
419 $lang = $context->getLanguage();
420 $config = $context->getConfig();
421 $code = $lang->getCode();
422 static $fastCharDiff = [];
423 if ( !isset( $fastCharDiff[$code] ) ) {
424 $fastCharDiff[$code] = $config->get( MainConfigNames::MiserMode )
425 || $context->msg( 'rc-change-size' )->plain() === '$1';
426 }
427
428 $formattedSize = $lang->formatNum( $szdiff );
429
430 if ( !$fastCharDiff[$code] ) {
431 $formattedSize = $context->msg( 'rc-change-size', $formattedSize )->text();
432 }
433
434 if ( abs( $szdiff ) > abs( $config->get( MainConfigNames::RCChangedSizeThreshold ) ) ) {
435 $tag = 'strong';
436 } else {
437 $tag = 'span';
438 }
439
440 if ( $szdiff === 0 ) {
441 $formattedSizeClass = 'mw-plusminus-null';
442 } elseif ( $szdiff > 0 ) {
443 $formattedSize = '+' . $formattedSize;
444 $formattedSizeClass = 'mw-plusminus-pos';
445 } else {
446 $formattedSizeClass = 'mw-plusminus-neg';
447 }
448 $formattedSizeClass .= ' mw-diff-bytes';
449
450 $formattedTotalSize = $context->msg( 'rc-change-size-new' )->numParams( $new )->text();
451
452 return Html::element( $tag,
453 [ 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ],
454 $formattedSize );
455 }
456
464 public function formatCharacterDifference( RecentChange $old, ?RecentChange $new = null ) {
465 $oldlen = $old->mAttribs['rc_old_len'];
466
467 if ( $new ) {
468 $newlen = $new->mAttribs['rc_new_len'];
469 } else {
470 $newlen = $old->mAttribs['rc_new_len'];
471 }
472
473 if ( $oldlen === null || $newlen === null ) {
474 return '';
475 }
476
477 return self::showCharacterDifference( $oldlen, $newlen, $this->getContext() );
478 }
479
484 public function endRecentChangesList() {
485 $out = $this->rclistOpen ? "</ul>\n" : '';
486 $out .= '</div>';
487
488 return $out;
489 }
490
504 public static function revDateLink(
505 RevisionRecord $rev,
506 Authority $performer,
507 Language $lang,
508 $title = null,
509 $className = ''
510 ) {
511 $ts = $rev->getTimestamp();
512 $time = $lang->userTime( $ts, $performer->getUser() );
513 $date = $lang->userTimeAndDate( $ts, $performer->getUser() );
514 $class = trim( 'mw-changeslist-date ' . $className );
515 if ( $rev->userCan( RevisionRecord::DELETED_TEXT, $performer ) ) {
516 $link = Html::rawElement( 'bdi', [ 'dir' => $lang->getDir() ],
517 MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
518 $title ?? $rev->getPageAsLinkTarget(),
519 $date,
520 [ 'class' => $class ],
521 [ 'oldid' => $rev->getId() ]
522 )
523 );
524 } else {
525 $link = htmlspecialchars( $date );
526 }
527 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
528 $class = Linker::getRevisionDeletedClass( $rev ) . " $class";
529 $link = "<span class=\"$class\">$link</span>";
530 }
531 return Html::element( 'span', [
532 'class' => 'mw-changeslist-time'
533 ], $time ) . $link;
534 }
535
540 public function insertDateHeader( &$s, $rc_timestamp ) {
541 # Make date header if necessary
542 $date = $this->getLanguage()->userDate( $rc_timestamp, $this->getUser() );
543 if ( $date != $this->lastdate ) {
544 if ( $this->lastdate != '' ) {
545 $s .= "</ul>\n";
546 }
547 $s .= Html::element( 'h4', [], $date ) . "\n<ul class=\"special\">";
548 $this->lastdate = $date;
549 $this->rclistOpen = true;
550 }
551 }
552
559 public function insertLog( &$s, $title, $logtype, $useParentheses = true ) {
560 $page = new LogPage( $logtype );
561 $logname = $page->getName()->setContext( $this->getContext() )->text();
562 $link = $this->linkRenderer->makeKnownLink( $title, $logname, [
563 'class' => $useParentheses ? '' : 'mw-changeslist-links'
564 ] );
565 if ( $useParentheses ) {
566 $s .= $this->msg( 'parentheses' )->rawParams(
567 $link
568 )->escaped();
569 } else {
570 $s .= $link;
571 }
572 }
573
579 public function insertDiffHist( &$s, &$rc, $unpatrolled = null ) {
580 # Diff link
581 if (
582 $rc->mAttribs['rc_type'] == RC_NEW ||
583 $rc->mAttribs['rc_type'] == RC_LOG
584 ) {
585 $diffLink = $this->message['diff'];
586 } elseif ( !self::userCan( $rc, RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
587 $diffLink = $this->message['diff'];
588 } else {
589 $query = [
590 'curid' => $rc->mAttribs['rc_cur_id'],
591 'diff' => $rc->mAttribs['rc_this_oldid'],
592 'oldid' => $rc->mAttribs['rc_last_oldid']
593 ];
594
595 $diffLink = $this->linkRenderer->makeKnownLink(
596 $rc->getTitle(),
597 new HtmlArmor( $this->message['diff'] ),
598 [ 'class' => 'mw-changeslist-diff' ],
599 $query
600 );
601 }
602 $histLink = $this->linkRenderer->makeKnownLink(
603 $rc->getTitle(),
604 new HtmlArmor( $this->message['hist'] ),
605 [ 'class' => 'mw-changeslist-history' ],
606 [
607 'curid' => $rc->mAttribs['rc_cur_id'],
608 'action' => 'history'
609 ]
610 );
611
612 $s .= Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ],
613 Html::rawElement( 'span', [], $diffLink ) .
614 Html::rawElement( 'span', [], $histLink )
615 ) .
616 ' <span class="mw-changeslist-separator"></span> ';
617 }
618
629 public function getArticleLink( &$rc, $unpatrolled, $watched ) {
630 $params = [];
631 if ( $rc->getTitle()->isRedirect() ) {
632 $params = [ 'redirect' => 'no' ];
633 }
634
635 $articlelink = $this->linkRenderer->makeLink(
636 $rc->getTitle(),
637 null,
638 [ 'class' => 'mw-changeslist-title' ],
639 $params
640 );
641 if ( static::isDeleted( $rc, RevisionRecord::DELETED_TEXT ) ) {
642 $class = 'history-deleted';
643 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
644 $class .= ' mw-history-suppressed';
645 }
646 $articlelink = '<span class="' . $class . '">' . $articlelink . '</span>';
647 }
648 $dir = $this->getLanguage()->getDir();
649 $articlelink = Html::rawElement( 'bdi', [ 'dir' => $dir ], $articlelink );
650 # To allow for boldening pages watched by this user
651 # Don't wrap result of this with another tag, see T376814
652 $articlelink = "<span class=\"mw-title\">{$articlelink}</span>";
653
654 # TODO: Deprecate the $s argument, it seems happily unused.
655 $s = '';
656 $this->getHookRunner()->onChangesListInsertArticleLink( $this, $articlelink,
657 $s, $rc, $unpatrolled, $watched );
658
659 // Watchlist expiry icon.
660 $watchlistExpiry = '';
661 // @phan-suppress-next-line MediaWikiNoIssetIfDefined
662 if ( isset( $rc->watchlistExpiry ) && $rc->watchlistExpiry ) {
663 $watchlistExpiry = $this->getWatchlistExpiry( $rc );
664 }
665
666 return "{$s} {$articlelink}{$watchlistExpiry}";
667 }
668
675 public function getWatchlistExpiry( RecentChange $recentChange ): string {
676 $item = WatchedItem::newFromRecentChange( $recentChange, $this->getUser() );
677 // Guard against expired items, even though they shouldn't come here.
678 if ( $item->isExpired() ) {
679 return '';
680 }
681 $daysLeftText = $item->getExpiryInDaysText( $this->getContext() );
682 // Matching widget is also created in ChangesListSpecialPage, for the legend.
683 $widget = new IconWidget( [
684 'icon' => 'clock',
685 'title' => $daysLeftText,
686 'classes' => [ 'mw-changesList-watchlistExpiry' ],
687 ] );
688 $widget->setAttributes( [
689 // Add labels for assistive technologies.
690 'role' => 'img',
691 'aria-label' => $this->msg( 'watchlist-expires-in-aria-label' )->text(),
692 // Days-left is used in resources/src/mediawiki.special.changeslist.watchlistexpiry/watchlistexpiry.js
693 'data-days-left' => $item->getExpiryInDays(),
694 ] );
695 // Add spaces around the widget (the page title is to one side,
696 // and a semicolon or opening-parenthesis to the other).
697 return " $widget ";
698 }
699
708 public function getTimestamp( $rc ) {
709 // This uses the semi-colon separator unless there's a watchlist expiry date for the entry,
710 // because in that case the timestamp is preceded by a clock icon.
711 // A space is important after `.mw-changeslist-separator--semicolon` to make sure
712 // that whatever comes before it is distinguishable.
713 // (Otherwise your have the text of titles pushing up against the timestamp)
714 // A specific element is used for this purpose rather than styling `.mw-changeslist-date`
715 // as the `.mw-changeslist-date` class is used in a variety
716 // of other places with a different position and the information proceeding getTimestamp can vary.
717 // The `.mw-changeslist-time` class allows us to distinguish from `.mw-changeslist-date` elements that
718 // contain the full date (month, year) and adds consistency with Special:Contributions
719 // and other pages.
720 $separatorClass = $rc->watchlistExpiry ? 'mw-changeslist-separator' : 'mw-changeslist-separator--semicolon';
721 return Html::element( 'span', [ 'class' => $separatorClass ] ) . $this->message['word-separator'] .
722 '<span class="mw-changeslist-date mw-changeslist-time">' .
723 htmlspecialchars( $this->getLanguage()->userTime(
724 $rc->mAttribs['rc_timestamp'],
725 $this->getUser()
726 ) ) . '</span> <span class="mw-changeslist-separator"></span> ';
727 }
728
735 public function insertTimestamp( &$s, $rc ) {
736 $s .= $this->getTimestamp( $rc );
737 }
738
745 public function insertUserRelatedLinks( &$s, &$rc ) {
746 if ( static::isDeleted( $rc, RevisionRecord::DELETED_USER ) ) {
747 $deletedClass = 'history-deleted';
748 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
749 $deletedClass .= ' mw-history-suppressed';
750 }
751 $s .= ' <span class="' . $deletedClass . '">' .
752 $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
753 } else {
754 $s .= $this->userLinkRenderer->userLink(
755 $rc->getPerformerIdentity(),
756 $this
757 );
758 # Don't wrap result of this with another tag, see T376814
759 $s .= $this->userLinkCache->getWithSetCallback(
760 $this->userLinkCache->makeKey(
761 $rc->mAttribs['rc_user_text'],
762 $this->getUser()->getName(),
763 $this->getLanguage()->getCode()
764 ),
765 // The text content of tools is not wrapped with parentheses or "piped".
766 // This will be handled in CSS (T205581).
767 static fn () => Linker::userToolLinks(
768 $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'],
769 false, 0, null,
770 false
771 )
772 );
773 }
774 }
775
782 public function insertLogEntry( $rc ) {
783 $entry = DatabaseLogEntry::newFromRow( $rc->mAttribs );
784 $formatter = $this->logFormatterFactory->newFromEntry( $entry );
785 $formatter->setContext( $this->getContext() );
786 $formatter->setShowUserToolLinks( true );
787
788 $comment = $formatter->getComment();
789 if ( $comment !== '' ) {
790 $dir = $this->getLanguage()->getDir();
791 $comment = Html::rawElement( 'bdi', [ 'dir' => $dir ], $comment );
792 }
793
794 $html = $formatter->getActionText() . $this->message['word-separator'] . $comment .
795 $this->message['word-separator'] . $formatter->getActionLinks();
796 $classes = [ 'mw-changeslist-log-entry' ];
797 $attribs = [];
798
799 // Let extensions add data to the outputted log entry in a similar way to the LogEventsListLineEnding hook
800 $this->getHookRunner()->onChangesListInsertLogEntry( $entry, $this->getContext(), $html, $classes, $attribs );
801 $attribs = array_filter( $attribs,
802 [ Sanitizer::class, 'isReservedDataAttribute' ],
803 ARRAY_FILTER_USE_KEY
804 );
805 $attribs['class'] = $classes;
806
807 return Html::openElement( 'span', $attribs ) . $html . Html::closeElement( 'span' );
808 }
809
815 public function insertComment( $rc ) {
816 if ( static::isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) {
817 $deletedClass = 'history-deleted';
818 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
819 $deletedClass .= ' mw-history-suppressed';
820 }
821 return ' <span class="' . $deletedClass . ' comment">' .
822 $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
823 } elseif ( isset( $rc->mAttribs['rc_id'] )
824 && isset( $this->formattedComments[$rc->mAttribs['rc_id']] )
825 ) {
826 return $this->formattedComments[$rc->mAttribs['rc_id']];
827 } else {
828 return $this->commentFormatter->formatBlock(
829 $rc->mAttribs['rc_comment'],
830 $rc->getTitle(),
831 // Whether section links should refer to local page (using default false)
832 false,
833 // wikid to generate links for (using default null) */
834 null,
835 // whether parentheses should be rendered as part of the message
836 false
837 );
838 }
839 }
840
846 protected function numberofWatchingusers( $count ) {
847 if ( $count <= 0 ) {
848 return '';
849 }
850
851 return $this->watchMsgCache->getWithSetCallback(
852 $this->watchMsgCache->makeKey(
853 'watching-users-msg',
854 strval( $count ),
855 $this->getUser()->getName(),
856 $this->getLanguage()->getCode()
857 ),
858 function () use ( $count ) {
859 return $this->msg( 'number-of-watching-users-for-recent-changes' )
860 ->numParams( $count )->escaped();
861 }
862 );
863 }
864
871 public static function isDeleted( $rc, $field ) {
872 return ( $rc->mAttribs['rc_deleted'] & $field ) == $field;
873 }
874
884 public static function userCan( $rc, $field, ?Authority $performer = null ) {
885 $performer ??= RequestContext::getMain()->getAuthority();
886
887 if ( $rc->mAttribs['rc_type'] == RC_LOG ) {
888 return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $performer );
889 }
890
891 return RevisionRecord::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $performer );
892 }
893
899 protected function maybeWatchedLink( $link, $watched = false ) {
900 if ( $watched ) {
901 return '<strong class="mw-watched">' . $link . '</strong>';
902 } else {
903 return '<span class="mw-rc-unwatched">' . $link . '</span>';
904 }
905 }
906
913 public function insertRollback( &$s, &$rc ) {
914 $this->insertPageTools( $s, $rc );
915 }
916
925 private function insertPageTools( &$s, &$rc ) {
926 // FIXME Some page tools (e.g. thanks) might make sense for log entries.
927 if ( !in_array( $rc->mAttribs['rc_type'], [ RC_EDIT, RC_NEW ] )
928 // FIXME When would either of these not exist when type is RC_EDIT? Document.
929 || !$rc->mAttribs['rc_this_oldid']
930 || !$rc->mAttribs['rc_cur_id']
931 ) {
932 return;
933 }
934
935 // Construct a fake revision for PagerTools. FIXME can't we just obtain the real one?
936 $title = $rc->getTitle();
937 $revRecord = new MutableRevisionRecord( $title );
938 $revRecord->setId( (int)$rc->mAttribs['rc_this_oldid'] );
939 $revRecord->setVisibility( (int)$rc->mAttribs['rc_deleted'] );
940 $user = new UserIdentityValue(
941 (int)$rc->mAttribs['rc_user'],
942 $rc->mAttribs['rc_user_text']
943 );
944 $revRecord->setUser( $user );
945
946 $tools = new PagerTools(
947 $revRecord,
948 null,
949 // only show a rollback link on the top-most revision
950 $rc->getAttribute( 'page_latest' ) == $rc->mAttribs['rc_this_oldid']
951 && $rc->mAttribs['rc_type'] != RC_NEW,
952 $this->getHookRunner(),
953 $title,
954 $this->getContext(),
955 // @todo: Inject
956 MediaWikiServices::getInstance()->getLinkRenderer()
957 );
958
959 $s .= $tools->toHTML();
960 }
961
967 public function getRollback( RecentChange $rc ) {
968 $s = '';
969 $this->insertRollback( $s, $rc );
970 return $s;
971 }
972
978 public function insertTags( &$s, &$rc, &$classes ) {
979 if ( empty( $rc->mAttribs['ts_tags'] ) ) {
980 return;
981 }
982
988 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
989 $this->tagsCache->makeKey(
990 $rc->mAttribs['ts_tags'],
991 $this->getUser()->getName(),
992 $this->getLanguage()->getCode()
993 ),
994 fn () => ChangeTags::formatSummaryRow(
995 $rc->mAttribs['ts_tags'],
996 'changeslist',
997 $this->getContext()
998 )
999 );
1000 $classes = array_merge( $classes, $newClasses );
1001 $s .= $this->message['word-separator'] . $tagSummary;
1002 }
1003
1010 public function getTags( RecentChange $rc, array &$classes ) {
1011 $s = '';
1012 $this->insertTags( $s, $rc, $classes );
1013 return $s;
1014 }
1015
1016 public function insertExtra( &$s, &$rc, &$classes ) {
1017 // Empty, used for subclasses to add anything special.
1018 }
1019
1020 protected function showAsUnpatrolled( RecentChange $rc ) {
1021 return self::isUnpatrolled( $rc, $this->getUser() );
1022 }
1023
1029 public static function isUnpatrolled( $rc, User $user ) {
1030 if ( $rc instanceof RecentChange ) {
1031 $isPatrolled = $rc->mAttribs['rc_patrolled'];
1032 $rcType = $rc->mAttribs['rc_type'];
1033 $rcLogType = $rc->mAttribs['rc_log_type'];
1034 } else {
1035 $isPatrolled = $rc->rc_patrolled;
1036 $rcType = $rc->rc_type;
1037 $rcLogType = $rc->rc_log_type;
1038 }
1039
1040 if ( $isPatrolled ) {
1041 return false;
1042 }
1043
1044 return $user->useRCPatrol() ||
1045 ( $rcType == RC_NEW && $user->useNPPatrol() ) ||
1046 ( $rcLogType === 'upload' && $user->useFilePatrol() );
1047 }
1048
1058 protected function isCategorizationWithoutRevision( $rcObj ) {
1059 return intval( $rcObj->getAttribute( 'rc_type' ) ) === RC_CATEGORIZE
1060 && intval( $rcObj->getAttribute( 'rc_this_oldid' ) ) === 0;
1061 }
1062
1068 protected function getDataAttributes( RecentChange $rc ) {
1069 $attrs = [];
1070
1071 $type = $rc->getAttribute( 'rc_source' );
1072 switch ( $type ) {
1076 $attrs['data-mw-revid'] = $rc->mAttribs['rc_this_oldid'];
1077 break;
1079 $attrs['data-mw-logid'] = $rc->mAttribs['rc_logid'];
1080 $attrs['data-mw-logaction'] =
1081 $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'];
1082 break;
1083 }
1084
1085 $attrs[ 'data-mw-ts' ] = $rc->getAttribute( 'rc_timestamp' );
1086
1087 return $attrs;
1088 }
1089
1097 public function setChangeLinePrefixer( callable $prefixer ) {
1098 $this->changeLinePrefixer = $prefixer;
1099 }
1100}
1101
1103class_alias( ChangesList::class, 'ChangesList' );
const RC_NEW
Definition Defines.php:118
const RC_LOG
Definition Defines.php:119
const RC_EDIT
Definition Defines.php:117
const RC_CATEGORIZE
Definition Defines.php:121
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:57
Base class for language-specific code.
Definition Language.php:81
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:61
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:50
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.
Generate a set of tools for a revision.
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Represents a filter group (used on ChangesListSpecialPage and descendants)
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.
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...
ChangesListFilterGroup[] $filterGroups
insertLog(&$s, $title, $logtype, $useParentheses=true)
__construct( $context, array $filterGroups=[])
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,...
RowCommentFormatter $commentFormatter
insertComment( $rc)
Insert a formatted comment.
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.
static newFromContext(IContextSource $context, array $groups=[])
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
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:78
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:123
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2198
useFilePatrol()
Check whether to enable new files patrol features for this user.
Definition User.php:2223
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2208
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:32
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:37
getUser()
Returns the performer of the actions associated with this authority.
Result wrapper for grabbing data queried from an IDatabase object.
element(SerializerNode $parent, SerializerNode $node, $contents)