MediaWiki master
ChangesList.php
Go to the documentation of this file.
1<?php
8
15use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
27use MediaWiki\Pager\PagerTools;
39use OOUI\IconWidget;
40use RuntimeException;
41use stdClass;
43use Wikimedia\MapCacheLRU\MapCacheLRU;
45
55 use ProtectedHookAccessorTrait;
56
57 public const CSS_CLASS_PREFIX = 'mw-changeslist-';
58
60 protected $watchlist = false;
62 protected $lastdate;
64 protected $message;
66 protected $rc_cache;
68 protected $rcCacheIndex;
70 protected $rclistOpen;
72 protected $rcMoveIndex;
73
76
78 protected $watchMsgCache;
79
83 protected $linkRenderer;
84
89
94
98 protected $filterGroups;
99
103 protected $tagsCache;
104
108 protected $userLinkCache;
109
110 private LogFormatterFactory $logFormatterFactory;
111
113
114 protected array $userLabels;
115
120 public function __construct( $context, ?ChangesListFilterGroupContainer $filterGroups = null ) {
121 $this->setContext( $context );
122 $this->preCacheMessages();
123 $this->watchMsgCache = new MapCacheLRU( 50 );
124 $this->filterGroups = $filterGroups ?? new ChangesListFilterGroupContainer();
125
126 $services = MediaWikiServices::getInstance();
127 $this->linkRenderer = $services->getLinkRenderer();
128 $this->commentFormatter = $services->getRowCommentFormatter();
129 $this->logFormatterFactory = $services->getLogFormatterFactory();
130 $this->userLinkRenderer = $services->getUserLinkRenderer();
131 $this->tagsCache = new MapCacheLRU( 50 );
132 $this->userLinkCache = new MapCacheLRU( 50 );
133 }
134
143 public static function newFromContext(
144 IContextSource $context,
145 ?ChangesListFilterGroupContainer $groups = null
146 ) {
147 $user = $context->getUser();
148 $sk = $context->getSkin();
149 $services = MediaWikiServices::getInstance();
150 $list = null;
151 $groups ??= new ChangesListFilterGroupContainer();
152 if ( ( new HookRunner( $services->getHookContainer() ) )->onFetchChangesList( $user, $sk, $list, $groups ) ) {
153 $userOptionsLookup = $services->getUserOptionsLookup();
154 $new = $context->getRequest()->getBool(
155 'enhanced',
156 $userOptionsLookup->getBoolOption( $user, 'usenewrc' )
157 );
158
159 return $new ?
160 new EnhancedChangesList( $context, $groups ) :
161 new OldChangesList( $context, $groups );
162 } else {
163 return $list;
164 }
165 }
166
178 public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
179 throw new RuntimeException( 'recentChangesLine should be implemented' );
180 }
181
188 protected function getHighlightsContainerDiv() {
189 $highlightColorDivs = '';
190 foreach ( [ 'none', 'c1', 'c2', 'c3', 'c4', 'c5' ] as $color ) {
191 $highlightColorDivs .= Html::rawElement(
192 'div',
193 [
194 'class' => 'mw-rcfilters-ui-highlights-color-' . $color,
195 'data-color' => $color
196 ]
197 );
198 }
199
200 return Html::rawElement(
201 'div',
202 [ 'class' => 'mw-rcfilters-ui-highlights' ],
203 $highlightColorDivs
204 );
205 }
206
211 public function setWatchlistDivs( $value = true ) {
212 $this->watchlist = $value;
213 }
214
219 public function isWatchlist() {
220 return (bool)$this->watchlist;
221 }
222
227 private function preCacheMessages() {
228 // @phan-suppress-next-line MediaWikiNoIssetIfDefined False positives when documented as nullable
229 if ( !isset( $this->message ) ) {
230 $this->message = [];
231 foreach ( [
232 'cur', 'diff', 'hist', 'enhancedrc-history', 'last', 'blocklink', 'history',
233 'semicolon-separator', 'pipe-separator', 'word-separator' ] as $msg
234 ) {
235 $this->message[$msg] = $this->msg( $msg )->escaped();
236 }
237 }
238 }
239
246 public function recentChangesFlags( $flags, $nothing = "\u{00A0}" ) {
247 $f = '';
248 foreach (
249 $this->getConfig()->get( MainConfigNames::RecentChangesFlags ) as $flag => $_
250 ) {
251 $f .= isset( $flags[$flag] ) && $flags[$flag]
252 ? self::flag( $flag, $this->getContext() )
253 : $nothing;
254 }
255
256 return $f;
257 }
258
267 protected function getHTMLClasses( $rc, $watched ) {
268 $classes = [ self::CSS_CLASS_PREFIX . 'line' ];
269 $logType = $rc->mAttribs['rc_log_type'];
270
271 if ( $logType ) {
272 $classes[] = self::CSS_CLASS_PREFIX . 'log';
273 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'log-' . $logType );
274 } else {
275 $classes[] = self::CSS_CLASS_PREFIX . 'edit';
276 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns' .
277 $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] );
278 }
279
280 // Indicate watched status on the line to allow for more
281 // comprehensive styling.
282 $classes[] = $watched && $rc->mAttribs['rc_timestamp'] >= $watched
283 ? self::CSS_CLASS_PREFIX . 'line-watched'
284 : self::CSS_CLASS_PREFIX . 'line-not-watched';
285
286 $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rc ) );
287
288 return $classes;
289 }
290
298 protected function getHTMLClassesForFilters( $rc ) {
299 $classes = [];
300
301 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns-' .
302 $rc->mAttribs['rc_namespace'] );
303
304 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
305 $classes[] = Sanitizer::escapeClass(
306 self::CSS_CLASS_PREFIX .
307 'ns-' .
308 ( $nsInfo->isTalk( $rc->mAttribs['rc_namespace'] ) ? 'talk' : 'subject' )
309 );
310
311 $this->filterGroups->applyCssClassIfNeeded( $this->getContext(), $rc, $classes );
312
313 return $classes;
314 }
315
326 public static function flag( $flag, ?IContextSource $context = null ) {
327 static $map = [ 'minoredit' => 'minor', 'botedit' => 'bot' ];
328 static $flagInfos = null;
329
330 if ( $flagInfos === null ) {
331 $recentChangesFlags = MediaWikiServices::getInstance()->getMainConfig()
333 $flagInfos = [];
334 foreach ( $recentChangesFlags as $key => $value ) {
335 $flagInfos[$key]['letter'] = $value['letter'];
336 $flagInfos[$key]['title'] = $value['title'];
337 // Allow customized class name, fall back to flag name
338 $flagInfos[$key]['class'] = $value['class'] ?? $key;
339 }
340 }
341
342 if ( !$context ) {
344 'Calling ChangesList::flag without specifying a context is deprecated since 1.46',
345 '1.46'
346 );
347 $context = RequestContext::getMain();
348 }
349
350 // Inconsistent naming, kept for b/c
351 if ( isset( $map[$flag] ) ) {
352 $flag = $map[$flag];
353 }
354
355 $info = $flagInfos[$flag];
356 return Html::element( 'abbr', [
357 'class' => $info['class'],
358 'title' => wfMessage( $info['title'] )->setContext( $context )->text(),
359 ], wfMessage( $info['letter'] )->setContext( $context )->text() );
360 }
361
366 public function beginRecentChangesList() {
367 $this->rc_cache = [];
368 $this->rcMoveIndex = 0;
369 $this->rcCacheIndex = 0;
370 $this->lastdate = '';
371 $this->rclistOpen = false;
372 $this->getOutput()->addModuleStyles( [
373 'mediawiki.interface.helpers.styles',
374 'mediawiki.special.changeslist'
375 ] );
376
377 return '<div class="mw-changeslist">';
378 }
379
383 public function initChangesListRows( $rows ) {
384 $this->getHookRunner()->onChangesListInitRows( $this, $rows );
385 $this->formattedComments = $this->commentFormatter->createBatch()
386 ->comments(
387 $this->commentFormatter->rows( $rows )
388 ->commentKey( 'rc_comment' )
389 ->namespaceField( 'rc_namespace' )
390 ->titleField( 'rc_title' )
391 ->indexField( 'rc_id' )
392 )
393 ->useBlock()
394 ->execute();
395 }
396
407 public static function showCharacterDifference( $old, $new, ?IContextSource $context = null ) {
408 if ( !$context ) {
410 'Calling ChangesList::showCharacterDifference without specifying a context is deprecated since 1.46',
411 '1.46'
412 );
413 $context = RequestContext::getMain();
414 }
415
416 $new = (int)$new;
417 $old = (int)$old;
418 $szdiff = $new - $old;
419
420 $lang = $context->getLanguage();
421 $config = $context->getConfig();
422 $code = $lang->getCode();
423 static $fastCharDiff = [];
424 if ( !isset( $fastCharDiff[$code] ) ) {
425 $fastCharDiff[$code] = $config->get( MainConfigNames::MiserMode )
426 || $context->msg( 'rc-change-size' )->plain() === '$1';
427 }
428
429 $formattedSize = $lang->formatNum( $szdiff );
430
431 if ( !$fastCharDiff[$code] ) {
432 $formattedSize = $context->msg( 'rc-change-size', $formattedSize )->text();
433 }
434
435 if ( abs( $szdiff ) > abs( $config->get( MainConfigNames::RCChangedSizeThreshold ) ) ) {
436 $tag = 'strong';
437 } else {
438 $tag = 'span';
439 }
440
441 if ( $szdiff === 0 ) {
442 $formattedSizeClass = 'mw-plusminus-null';
443 } elseif ( $szdiff > 0 ) {
444 $formattedSize = '+' . $formattedSize;
445 $formattedSizeClass = 'mw-plusminus-pos';
446 } else {
447 $formattedSizeClass = 'mw-plusminus-neg';
448 }
449 $formattedSizeClass .= ' mw-diff-bytes';
450
451 $formattedTotalSize = $context->msg( 'rc-change-size-new' )->numParams( $new )->text();
452
453 return Html::element( $tag,
454 [ 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ],
455 $formattedSize );
456 }
457
465 public function formatCharacterDifference( RecentChange $old, ?RecentChange $new = null ) {
466 $oldlen = $old->mAttribs['rc_old_len'];
467
468 if ( $new ) {
469 $newlen = $new->mAttribs['rc_new_len'];
470 } else {
471 $newlen = $old->mAttribs['rc_new_len'];
472 }
473
474 if ( $oldlen === null || $newlen === null ) {
475 return '';
476 }
477
478 return self::showCharacterDifference( $oldlen, $newlen, $this->getContext() );
479 }
480
485 public function endRecentChangesList() {
486 $out = $this->rclistOpen ? "</ul>\n" : '';
487 $out .= '</div>';
488
489 return $out;
490 }
491
505 public static function revDateLink(
506 RevisionRecord $rev,
507 Authority $performer,
508 Language $lang,
509 $title = null,
510 $className = ''
511 ) {
512 $ts = $rev->getTimestamp();
513 $time = $lang->userTime( $ts, $performer->getUser() );
514 $date = $lang->userTimeAndDate( $ts, $performer->getUser() );
515 $class = trim( 'mw-changeslist-date ' . $className );
516 if ( $rev->userCan( RevisionRecord::DELETED_TEXT, $performer ) ) {
517 $link = Html::rawElement( 'bdi', [ 'dir' => $lang->getDir() ],
518 MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
519 $title ?? $rev->getPageAsLinkTarget(),
520 $date,
521 [ 'class' => $class ],
522 [ 'oldid' => $rev->getId() ]
523 )
524 );
525 } else {
526 $link = htmlspecialchars( $date );
527 }
528 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
529 $class = Linker::getRevisionDeletedClass( $rev ) . " $class";
530 $link = "<span class=\"$class\">$link</span>";
531 }
532 return Html::element( 'span', [
533 'class' => 'mw-changeslist-time'
534 ], $time ) . $link;
535 }
536
541 public function insertDateHeader( &$s, $rc_timestamp ) {
542 # Make date header if necessary
543 $date = $this->getLanguage()->userDate( $rc_timestamp, $this->getUser() );
544 if ( $date != $this->lastdate ) {
545 if ( $this->lastdate != '' ) {
546 $s .= "</ul>\n";
547 }
548 $s .= Html::element( 'h4', [], $date ) . "\n<ul class=\"special\">";
549 $this->lastdate = $date;
550 $this->rclistOpen = true;
551 }
552 }
553
560 public function insertLog( &$s, $title, $logtype, $useParentheses = true ) {
561 $page = new LogPage( $logtype );
562 $logname = $page->getName()->setContext( $this->getContext() )->text();
563 $link = $this->linkRenderer->makeKnownLink( $title, $logname, [
564 'class' => $useParentheses ? '' : 'mw-changeslist-links'
565 ] );
566 if ( $useParentheses ) {
567 $s .= $this->msg( 'parentheses' )->rawParams(
568 $link
569 )->escaped();
570 } else {
571 $s .= $link;
572 }
573 }
574
580 public function insertDiffHist( &$s, &$rc, $unpatrolled = null ) {
581 # Diff link
582 if (
583 $rc->mAttribs['rc_source'] === RecentChange::SRC_NEW ||
584 $rc->mAttribs['rc_source'] === RecentChange::SRC_LOG
585 ) {
586 $diffLink = $this->message['diff'];
587 } elseif ( !self::userCan( $rc, RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
588 $diffLink = $this->message['diff'];
589 } else {
590 $query = [
591 'curid' => $rc->mAttribs['rc_cur_id'],
592 'diff' => $rc->mAttribs['rc_this_oldid'],
593 'oldid' => $rc->mAttribs['rc_last_oldid']
594 ];
595
596 $diffLink = $this->linkRenderer->makeKnownLink(
597 $rc->getTitle(),
598 new HtmlArmor( $this->message['diff'] ),
599 [ 'class' => 'mw-changeslist-diff' ],
600 $query
601 );
602 }
603 $histLink = $this->linkRenderer->makeKnownLink(
604 $rc->getTitle(),
605 new HtmlArmor( $this->message['hist'] ),
606 [ 'class' => 'mw-changeslist-history' ],
607 [
608 'curid' => $rc->mAttribs['rc_cur_id'],
609 'action' => 'history'
610 ]
611 );
612
613 $s .= Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ],
614 Html::rawElement( 'span', [], $diffLink ) .
615 Html::rawElement( 'span', [], $histLink )
616 ) .
617 ' <span class="mw-changeslist-separator"></span> ';
618 }
619
630 public function getArticleLink( &$rc, $unpatrolled, $watched ) {
631 $params = [];
632 if ( $rc->getTitle()->isRedirect() ) {
633 $params = [ 'redirect' => 'no' ];
634 }
635
636 $articlelink = $this->linkRenderer->makeLink(
637 $rc->getTitle(),
638 null,
639 [ 'class' => 'mw-changeslist-title' ],
640 $params
641 );
642 if ( static::isDeleted( $rc, RevisionRecord::DELETED_TEXT ) ) {
643 $class = 'history-deleted';
644 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
645 $class .= ' mw-history-suppressed';
646 }
647 $articlelink = '<span class="' . $class . '">' . $articlelink . '</span>';
648 }
649 $dir = $this->getLanguage()->getDir();
650 $articlelink = Html::rawElement( 'bdi', [ 'dir' => $dir ], $articlelink );
651 # To allow for boldening pages watched by this user
652 # Don't wrap result of this with another tag, see T376814
653 $articlelink = "<span class=\"mw-title\">{$articlelink}</span>";
654
655 # TODO: Deprecate the $s argument, it seems happily unused.
656 $s = '';
657 $this->getHookRunner()->onChangesListInsertArticleLink( $this, $articlelink,
658 $s, $rc, $unpatrolled, $watched );
659
660 // Watchlist expiry icon.
661 $watchlistExpiry = '';
662 // @phan-suppress-next-line MediaWikiNoIssetIfDefined
663 if ( isset( $rc->watchlistExpiry ) && $rc->watchlistExpiry ) {
664 $watchlistExpiry = $this->getWatchlistExpiry( $rc );
665 }
666
667 return "{$s} {$articlelink}{$watchlistExpiry}";
668 }
669
676 public function getWatchlistExpiry( RecentChange $recentChange ): string {
677 $item = WatchedItem::newFromRecentChange( $recentChange, $this->getUser() );
678 // Guard against expired items, even though they shouldn't come here.
679 if ( $item->isExpired() ) {
680 return '';
681 }
682 $daysLeftText = $item->getExpiryInDaysText( $this->getContext() );
683 // Matching widget is also created in ChangesListSpecialPage, for the legend.
684 $widget = new IconWidget( [
685 'icon' => 'clock',
686 'title' => $daysLeftText,
687 'classes' => [ 'mw-changesList-watchlistExpiry' ],
688 ] );
689 $widget->setAttributes( [
690 // Add labels for assistive technologies.
691 'role' => 'img',
692 'aria-label' => $this->msg( 'watchlist-expires-in-aria-label' )->text(),
693 // Days-left is used in resources/src/mediawiki.special.changeslist.watchlistexpiry/watchlistexpiry.js
694 'data-days-left' => $item->getExpiryInDays(),
695 ] );
696 // Add spaces around the widget (the page title is to one side,
697 // and a semicolon or opening-parenthesis to the other).
698 return " $widget ";
699 }
700
709 public function getTimestamp( $rc ) {
710 // This uses the semi-colon separator unless there's a watchlist expiry date for the entry,
711 // because in that case the timestamp is preceded by a clock icon.
712 // A space is important after `.mw-changeslist-separator--semicolon` to make sure
713 // that whatever comes before it is distinguishable.
714 // (Otherwise your have the text of titles pushing up against the timestamp)
715 // A specific element is used for this purpose rather than styling `.mw-changeslist-date`
716 // as the `.mw-changeslist-date` class is used in a variety
717 // of other places with a different position and the information proceeding getTimestamp can vary.
718 // The `.mw-changeslist-time` class allows us to distinguish from `.mw-changeslist-date` elements that
719 // contain the full date (month, year) and adds consistency with Special:Contributions
720 // and other pages.
721 $separatorClass = $rc->watchlistExpiry ? 'mw-changeslist-separator' : 'mw-changeslist-separator--semicolon';
722 return Html::element( 'span', [ 'class' => $separatorClass ] ) . $this->message['word-separator'] .
723 '<span class="mw-changeslist-date mw-changeslist-time">' .
724 htmlspecialchars( $this->getLanguage()->userTime(
725 $rc->mAttribs['rc_timestamp'],
726 $this->getUser()
727 ) ) . '</span> <span class="mw-changeslist-separator"></span> ';
728 }
729
736 public function insertTimestamp( &$s, $rc ) {
737 $s .= $this->getTimestamp( $rc );
738 }
739
746 public function insertUserRelatedLinks( &$s, &$rc ) {
747 if ( static::isDeleted( $rc, RevisionRecord::DELETED_USER ) ) {
748 $deletedClass = 'history-deleted';
749 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
750 $deletedClass .= ' mw-history-suppressed';
751 }
752 $s .= ' <span class="' . $deletedClass . '">' .
753 $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
754 } else {
755 $s .= $this->linkRenderer->makeUserLink(
756 $rc->getPerformerIdentity(),
757 $this
758 );
759 # Don't wrap result of this with another tag, see T376814
760 $s .= $this->userLinkCache->getWithSetCallback(
761 $this->userLinkCache->makeKey(
762 $rc->mAttribs['rc_user_text'],
763 $this->getUser()->getName(),
764 $this->getLanguage()->getCode()
765 ),
766 // The text content of tools is not wrapped with parentheses or "piped".
767 // This will be handled in CSS (T205581).
768 static fn () => Linker::userToolLinks(
769 $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'],
770 false, 0, null,
771 false
772 )
773 );
774 }
775 }
776
783 public function insertLogEntry( $rc ) {
784 $entry = DatabaseLogEntry::newFromRow( $rc->mAttribs );
785 $formatter = $this->logFormatterFactory->newFromEntry( $entry );
786 $formatter->setContext( $this->getContext() );
787 $formatter->setShowUserToolLinks( true );
788
789 $comment = $formatter->getComment();
790 if ( $comment !== '' ) {
791 $dir = $this->getLanguage()->getDir();
792 $comment = Html::rawElement( 'bdi', [ 'dir' => $dir ], $comment );
793 }
794
795 $html = $formatter->getActionText() . $this->message['word-separator'] . $comment .
796 $this->message['word-separator'] . $formatter->getActionLinks();
797 $classes = [ 'mw-changeslist-log-entry' ];
798 $attribs = [];
799
800 // Let extensions add data to the outputted log entry in a similar way to the LogEventsListLineEnding hook
801 $this->getHookRunner()->onChangesListInsertLogEntry( $entry, $this->getContext(), $html, $classes, $attribs );
802 $attribs = array_filter( $attribs,
803 Sanitizer::isReservedDataAttribute( ... ),
804 ARRAY_FILTER_USE_KEY
805 );
806 $attribs['class'] = $classes;
807
808 return Html::rawElement( 'span', $attribs, $html );
809 }
810
816 public function insertComment( $rc ) {
817 if ( static::isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) {
818 $deletedClass = 'history-deleted';
819 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
820 $deletedClass .= ' mw-history-suppressed';
821 }
822 return ' <span class="' . $deletedClass . ' comment">' .
823 $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
824 } elseif ( isset( $rc->mAttribs['rc_id'] )
825 && isset( $this->formattedComments[$rc->mAttribs['rc_id']] )
826 ) {
827 return $this->formattedComments[$rc->mAttribs['rc_id']];
828 } else {
829 return $this->commentFormatter->formatBlock(
830 $rc->mAttribs['rc_comment'],
831 $rc->getTitle(),
832 // Whether section links should refer to local page (using default false)
833 false,
834 // wikid to generate links for (using default null) */
835 null,
836 // whether parentheses should be rendered as part of the message
837 false
838 );
839 }
840 }
841
847 protected function numberofWatchingusers( $count ) {
848 if ( $count <= 0 ) {
849 return '';
850 }
851
852 return $this->watchMsgCache->getWithSetCallback(
853 $this->watchMsgCache->makeKey(
854 'watching-users-msg',
855 strval( $count ),
856 $this->getUser()->getName(),
857 $this->getLanguage()->getCode()
858 ),
859 function () use ( $count ) {
860 return $this->msg( 'number-of-watching-users-for-recent-changes' )
861 ->numParams( $count )->escaped();
862 }
863 );
864 }
865
872 public static function isDeleted( $rc, $field ) {
873 return ( $rc->mAttribs['rc_deleted'] & $field ) == $field;
874 }
875
885 public static function userCan( $rc, $field, ?Authority $performer = null ) {
886 if ( !$performer ) {
888 'Calling ChangesList::userCan without specifying a performer is deprecated since 1.46',
889 '1.46'
890 );
891 $performer = RequestContext::getMain()->getAuthority();
892 }
893
894 if ( $rc->mAttribs['rc_source'] === RecentChange::SRC_LOG ) {
895 return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $performer );
896 }
897
898 return RevisionRecord::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $performer );
899 }
900
906 protected function maybeWatchedLink( $link, $watched = false ) {
907 if ( $watched ) {
908 return '<strong class="mw-watched">' . $link . '</strong>';
909 } else {
910 return '<span class="mw-rc-unwatched">' . $link . '</span>';
911 }
912 }
913
920 public function insertRollback( &$s, &$rc ) {
921 $this->insertPageTools( $s, $rc );
922 }
923
931 private function insertPageTools( &$s, &$rc ) {
932 // FIXME Some page tools (e.g. thanks) might make sense for log entries.
933 if ( !in_array( $rc->mAttribs['rc_source'], [ RecentChange::SRC_EDIT, RecentChange::SRC_NEW ] )
934 // FIXME When would either of these not exist when type is RC_EDIT? Document.
935 || !$rc->mAttribs['rc_this_oldid']
936 || !$rc->mAttribs['rc_cur_id']
937 ) {
938 return;
939 }
940
941 // Construct a fake revision for PagerTools. FIXME can't we just obtain the real one?
942 $title = $rc->getTitle();
943 $revRecord = new MutableRevisionRecord( $title );
944 $revRecord->setId( (int)$rc->mAttribs['rc_this_oldid'] );
945 $revRecord->setVisibility( (int)$rc->mAttribs['rc_deleted'] );
946 $user = new UserIdentityValue(
947 (int)$rc->mAttribs['rc_user'],
948 $rc->mAttribs['rc_user_text']
949 );
950 $revRecord->setUser( $user );
951
952 $tools = new PagerTools(
953 $revRecord,
954 null,
955 // only show a rollback link on the top-most revision
956 $rc->getAttribute( 'page_latest' ) == $rc->mAttribs['rc_this_oldid']
957 && $rc->mAttribs['rc_source'] !== RecentChange::SRC_NEW,
958 $this->getHookRunner(),
959 $title,
960 $this->getContext(),
961 // @todo: Inject
962 MediaWikiServices::getInstance()->getLinkRenderer()
963 );
964
965 $s .= $tools->toHTML();
966 }
967
973 public function getRollback( RecentChange $rc ) {
974 $s = '';
975 $this->insertRollback( $s, $rc );
976 return $s;
977 }
978
984 public function insertTags( &$s, &$rc, &$classes ) {
985 if ( empty( $rc->mAttribs['ts_tags'] ) ) {
986 return;
987 }
988
994 [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback(
995 $this->tagsCache->makeKey(
996 $rc->mAttribs['ts_tags'],
997 $this->getUser()->getName(),
998 $this->getLanguage()->getCode()
999 ),
1000 fn () => ChangeTags::formatSummaryRow(
1001 $rc->mAttribs['ts_tags'],
1002 'changeslist',
1003 $this->getContext()
1004 )
1005 );
1006 $classes = array_merge( $classes, $newClasses );
1007 $s .= $this->message['word-separator'] . $tagSummary;
1008 }
1009
1016 public function getTags( RecentChange $rc, array &$classes ) {
1017 $s = '';
1018 $this->insertTags( $s, $rc, $classes );
1019 return $s;
1020 }
1021
1028 public function getLabels( RecentChange $rc, &$classes ): string {
1029 if ( !$this->getConfig()->get( MainConfigNames::EnableWatchlistLabels ) ) {
1030 return '';
1031 }
1032 if ( empty( $rc->mAttribs[ WatchlistLabelCondition::LABEL_IDS ] ) ) {
1033 return '';
1034 }
1035 $labelIds = explode( ',', $rc->mAttribs[ WatchlistLabelCondition::LABEL_IDS ] );
1036 $labelStrings = [];
1037 foreach ( $labelIds as $labelId ) {
1038 $classes[] = SpecialWatchlist::WATCHLIST_LABEL_CSS_CLASS_PREFIX . $labelId;
1039 $labelStrings[] = wfEscapeWikiText( $this->userLabels[ $labelId ]->getName() );
1040 }
1041 $labelsList = $this->msg( 'watchlistlabels-list-wrapper' )->params(
1042 $this->getLanguage()->commaList( $labelStrings )
1043 )->parse();
1044 return $this->message['word-separator'] .
1045 Html::rawElement(
1046 'span',
1047 [ 'class' => 'mw-changeslist-watchlistlabels' ],
1048 $this->msg( 'parentheses' )->rawParams( $labelsList )->escaped()
1049 );
1050 }
1051
1057 public function insertLabels( &$s, &$rc, &$classes ) {
1058 $s .= $this->getLabels( $rc, $classes );
1059 }
1060
1066 public function insertExtra( &$s, &$rc, &$classes ) {
1067 // Empty, used for subclasses to add anything special.
1068 }
1069
1073 protected function showAsUnpatrolled( RecentChange $rc ) {
1074 return self::isUnpatrolled( $rc, $this->getUser() );
1075 }
1076
1082 public static function isUnpatrolled( $rc, User $user ) {
1083 if ( $rc instanceof RecentChange ) {
1084 $isPatrolled = $rc->mAttribs['rc_patrolled'];
1085 $rcSource = $rc->mAttribs['rc_source'];
1086 $rcLogType = $rc->mAttribs['rc_log_type'];
1087 } else {
1088 $isPatrolled = $rc->rc_patrolled;
1089 $rcSource = $rc->rc_source;
1090 $rcLogType = $rc->rc_log_type;
1091 }
1092
1093 if ( $isPatrolled ) {
1094 return false;
1095 }
1096
1097 return $user->useRCPatrol() ||
1098 ( $rcSource === RecentChange::SRC_NEW && $user->useNPPatrol() ) ||
1099 ( $rcLogType === 'upload' && $user->useFilePatrol() );
1100 }
1101
1111 protected function isCategorizationWithoutRevision( $rcObj ) {
1112 return $rcObj->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE
1113 && intval( $rcObj->getAttribute( 'rc_this_oldid' ) ) === 0;
1114 }
1115
1121 protected function getDataAttributes( RecentChange $rc ) {
1122 $attrs = [];
1123
1124 $source = $rc->getAttribute( 'rc_source' );
1125 switch ( $source ) {
1126 case RecentChange::SRC_EDIT:
1127 case RecentChange::SRC_CATEGORIZE:
1128 case RecentChange::SRC_NEW:
1129 $attrs['data-mw-revid'] = $rc->mAttribs['rc_this_oldid'];
1130 break;
1131 case RecentChange::SRC_LOG:
1132 $attrs['data-mw-logid'] = $rc->mAttribs['rc_logid'];
1133 $attrs['data-mw-logaction'] =
1134 $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'];
1135 break;
1136 }
1137
1138 $attrs[ 'data-mw-ts' ] = $rc->getAttribute( 'rc_timestamp' );
1139
1140 return $attrs;
1141 }
1142
1150 public function setChangeLinePrefixer( callable $prefixer ) {
1151 $this->changeLinePrefixer = $prefixer;
1152 }
1153
1158 public function setUserLabels( array $userLabels ): void {
1159 $this->userLabels = $userLabels;
1160 }
1161}
1162
1164class_alias( ChangesList::class, 'ChangesList' );
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
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: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: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.
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 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.
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:130
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2158
useFilePatrol()
Check whether to enable new files patrol features for this user.
Definition User.php:2183
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2168
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
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)