26use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
41 use ProtectedHookAccessorTrait;
83 public function __construct( $context, array $filterGroups = [] ) {
85 $this->preCacheMessages();
87 $this->filterGroups = $filterGroups;
89 $services = MediaWikiServices::getInstance();
90 $this->linkRenderer = $services->getLinkRenderer();
91 $this->commentFormatter = $services->getRowCommentFormatter();
106 if ( Hooks::runner()->onFetchChangesList( $user, $sk, $list, $groups ) ) {
107 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
110 $userOptionsLookup->getBoolOption( $user,
'usenewrc' )
133 throw new RuntimeException(
'recentChangesLine should be implemented' );
143 $highlightColorDivs =
'';
144 foreach ( [
'none',
'c1',
'c2',
'c3',
'c4',
'c5' ] as $color ) {
145 $highlightColorDivs .= Html::rawElement(
148 'class' =>
'mw-rcfilters-ui-highlights-color-' . $color,
149 'data-color' => $color
154 return Html::rawElement(
156 [
'class' =>
'mw-rcfilters-ui-highlights' ],
166 $this->watchlist = $value;
174 return (
bool)$this->watchlist;
181 private function preCacheMessages() {
182 if ( !isset( $this->message ) ) {
185 'cur',
'diff',
'hist',
'enhancedrc-history',
'last',
'blocklink',
'history',
186 'semicolon-separator',
'pipe-separator' ] as $msg
188 $this->message[$msg] = $this->msg( $msg )->escaped();
202 array_keys( $this->
getConfig()->
get( MainConfigNames::RecentChangesFlags ) ) as $flag
204 $f .= isset( $flags[$flag] ) && $flags[$flag]
221 $classes = [ self::CSS_CLASS_PREFIX .
'line' ];
222 $logType = $rc->mAttribs[
'rc_log_type'];
225 $classes[] = self::CSS_CLASS_PREFIX .
'log';
226 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX .
'log-' . $logType );
228 $classes[] = self::CSS_CLASS_PREFIX .
'edit';
229 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX .
'ns' .
230 $rc->mAttribs[
'rc_namespace'] .
'-' . $rc->mAttribs[
'rc_title'] );
235 $classes[] = $watched && $rc->mAttribs[
'rc_timestamp'] >= $watched
236 ? self::CSS_CLASS_PREFIX .
'line-watched'
237 : self::CSS_CLASS_PREFIX .
'line-not-watched';
254 $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX .
'ns-' .
255 $rc->mAttribs[
'rc_namespace'] );
257 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
258 $classes[] = Sanitizer::escapeClass(
259 self::CSS_CLASS_PREFIX .
261 ( $nsInfo->isTalk( $rc->mAttribs[
'rc_namespace'] ) ?
'talk' :
'subject' )
264 foreach ( $this->filterGroups as $filterGroup ) {
265 foreach ( $filterGroup->getFilters() as $filter ) {
266 $filter->applyCssClassIfNeeded( $this, $rc, $classes );
284 static $map = [
'minoredit' =>
'minor',
'botedit' =>
'bot' ];
285 static $flagInfos =
null;
287 if ( $flagInfos ===
null ) {
288 $recentChangesFlags = MediaWikiServices::getInstance()->getMainConfig()
289 ->get( MainConfigNames::RecentChangesFlags );
291 foreach ( $recentChangesFlags as $key => $value ) {
292 $flagInfos[$key][
'letter'] = $value[
'letter'];
293 $flagInfos[$key][
'title'] = $value[
'title'];
295 $flagInfos[$key][
'class'] = $value[
'class'] ?? $key;
299 $context = $context ?: RequestContext::getMain();
302 if ( isset( $map[$flag] ) ) {
306 $info = $flagInfos[$flag];
307 return Html::element(
'abbr', [
308 'class' => $info[
'class'],
318 $this->rc_cache = [];
319 $this->rcMoveIndex = 0;
320 $this->rcCacheIndex = 0;
321 $this->lastdate =
'';
322 $this->rclistOpen =
false;
324 'mediawiki.interface.helpers.styles',
325 'mediawiki.special.changeslist'
328 return '<div class="mw-changeslist">';
335 $this->getHookRunner()->onChangesListInitRows( $this, $rows );
336 $this->formattedComments = $this->commentFormatter->createBatch()
338 $this->commentFormatter->rows( $rows )
339 ->commentKey(
'rc_comment' )
340 ->namespaceField(
'rc_namespace' )
341 ->titleField(
'rc_title' )
342 ->indexField(
'rc_id' )
360 $context = RequestContext::getMain();
365 $szdiff = $new - $old;
367 $lang = $context->getLanguage();
368 $config = $context->getConfig();
369 $code =
$lang->getCode();
370 static $fastCharDiff = [];
371 if ( !isset( $fastCharDiff[$code] ) ) {
372 $fastCharDiff[$code] = $config->get( MainConfigNames::MiserMode )
373 || $context->msg(
'rc-change-size' )->plain() ===
'$1';
376 $formattedSize =
$lang->formatNum( $szdiff );
378 if ( !$fastCharDiff[$code] ) {
379 $formattedSize = $context->msg(
'rc-change-size', $formattedSize )->text();
382 if ( abs( $szdiff ) > abs( $config->get( MainConfigNames::RCChangedSizeThreshold ) ) ) {
388 if ( $szdiff === 0 ) {
389 $formattedSizeClass =
'mw-plusminus-null';
390 } elseif ( $szdiff > 0 ) {
391 $formattedSize =
'+' . $formattedSize;
392 $formattedSizeClass =
'mw-plusminus-pos';
394 $formattedSizeClass =
'mw-plusminus-neg';
396 $formattedSizeClass .=
' mw-diff-bytes';
398 $formattedTotalSize = $context->msg(
'rc-change-size-new' )->numParams( $new )->text();
400 return Html::element( $tag,
401 [
'dir' =>
'ltr',
'class' => $formattedSizeClass,
'title' => $formattedTotalSize ],
402 $formattedSize ) .
$lang->getDirMark();
413 $oldlen = $old->mAttribs[
'rc_old_len'];
416 $newlen = $new->mAttribs[
'rc_new_len'];
418 $newlen = $old->mAttribs[
'rc_new_len'];
421 if ( $oldlen ===
null || $newlen ===
null ) {
425 return self::showCharacterDifference( $oldlen, $newlen, $this->
getContext() );
433 $out = $this->rclistOpen ?
"</ul>\n" :
'';
458 $date =
$lang->userTimeAndDate( $ts, $performer->
getUser() );
459 if ( $rev->
userCan( RevisionRecord::DELETED_TEXT, $performer ) ) {
460 $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
463 [
'class' =>
'mw-changeslist-date' ],
464 [
'oldid' => $rev->
getId() ]
467 $link = htmlspecialchars( $date );
469 if ( $rev->
isDeleted( RevisionRecord::DELETED_TEXT ) ) {
470 $deletedClass = Linker::getRevisionDeletedClass( $rev );
471 $link =
"<span class=\"$deletedClass mw-changeslist-date\">$link</span>";
473 return Html::element(
'span', [
474 'class' =>
'mw-changeslist-time'
483 # Make date header if necessary
485 if ( $date != $this->lastdate ) {
486 if ( $this->lastdate !=
'' ) {
489 $s .= Xml::element(
'h4',
null, $date ) .
"\n<ul class=\"special\">";
490 $this->lastdate = $date;
491 $this->rclistOpen =
true;
502 $page =
new LogPage( $logtype );
503 $logname = $page->getName()->setContext( $this->
getContext() )->text();
504 $link = $this->linkRenderer->makeKnownLink(
$title, $logname, [
505 'class' => $useParentheses ?
'' :
'mw-changeslist-links'
507 if ( $useParentheses ) {
508 $s .= $this->
msg(
'parentheses' )->rawParams(
524 $rc->mAttribs[
'rc_type'] ==
RC_NEW ||
525 $rc->mAttribs[
'rc_type'] ==
RC_LOG ||
528 $diffLink = $this->message[
'diff'];
529 } elseif ( !self::userCan( $rc, RevisionRecord::DELETED_TEXT, $this->
getAuthority() ) ) {
530 $diffLink = $this->message[
'diff'];
533 'curid' => $rc->mAttribs[
'rc_cur_id'],
534 'diff' => $rc->mAttribs[
'rc_this_oldid'],
535 'oldid' => $rc->mAttribs[
'rc_last_oldid']
538 $diffLink = $this->linkRenderer->makeKnownLink(
541 [
'class' =>
'mw-changeslist-diff' ],
546 $histLink = $this->message[
'hist'];
548 $histLink = $this->linkRenderer->makeKnownLink(
551 [
'class' =>
'mw-changeslist-history' ],
553 'curid' => $rc->mAttribs[
'rc_cur_id'],
554 'action' =>
'history'
559 $s .= Html::rawElement(
'div', [
'class' =>
'mw-changeslist-links' ],
560 Html::rawElement(
'span', [], $diffLink ) .
561 Html::rawElement(
'span', [], $histLink )
563 ' <span class="mw-changeslist-separator"></span> ';
578 if ( $rc->getTitle()->isRedirect() ) {
579 $params = [
'redirect' =>
'no' ];
582 $articlelink = $this->linkRenderer->makeLink(
585 [
'class' =>
'mw-changeslist-title' ],
588 if ( static::isDeleted( $rc, RevisionRecord::DELETED_TEXT ) ) {
589 $class =
'history-deleted';
590 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
591 $class .=
' mw-history-suppressed';
593 $articlelink =
'<span class="' . $class .
'">' . $articlelink .
'</span>';
595 # To allow for boldening pages watched by this user
596 $articlelink =
"<span class=\"mw-title\">{$articlelink}</span>";
598 $articlelink .= $this->
getLanguage()->getDirMark();
600 # TODO: Deprecate the $s argument, it seems happily unused.
602 $this->getHookRunner()->onChangesListInsertArticleLink( $this, $articlelink,
603 $s, $rc, $unpatrolled, $watched );
606 $watchlistExpiry =
'';
607 if ( isset( $rc->watchlistExpiry ) && $rc->watchlistExpiry ) {
611 return "{$s} {$articlelink}{$watchlistExpiry}";
623 if ( $item->isExpired() ) {
628 $widget =
new IconWidget( [
630 'title' => $daysLeftText,
631 'classes' => [
'mw-changesList-watchlistExpiry' ],
633 $widget->setAttributes( [
636 'aria-label' => $this->msg(
'watchlist-expires-in-aria-label' )->text(),
638 'data-days-left' => $item->getExpiryInDays(),
665 $separatorClass = $rc->watchlistExpiry ?
'mw-changeslist-separator' :
'mw-changeslist-separator--semicolon';
666 return Html::element(
'span', [
'class' => $separatorClass ] ) .
' ' .
667 '<span class="mw-changeslist-date mw-changeslist-time">' .
668 htmlspecialchars( $this->getLanguage()->userTime(
669 $rc->mAttribs[
'rc_timestamp'],
671 ) ) .
'</span> <span class="mw-changeslist-separator"></span> ';
691 if ( static::isDeleted( $rc, RevisionRecord::DELETED_USER ) ) {
692 $deletedClass =
'history-deleted';
693 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
694 $deletedClass .=
' mw-history-suppressed';
696 $s .=
' <span class="' . $deletedClass .
'">' .
697 $this->msg(
'rev-deleted-user' )->escaped() .
'</span>';
699 $s .= $this->getLanguage()->getDirMark() . Linker::userLink(
700 $rc->mAttribs[
'rc_user'],
701 $rc->mAttribs[
'rc_user_text'],
703 [
'data-mw-revid' => $rc->mAttribs[
'rc_this_oldid'] ]
705 $s .= Linker::userToolLinks(
706 $rc->mAttribs[
'rc_user'], $rc->mAttribs[
'rc_user_text'],
722 $formatter = LogFormatter::newFromRow( $rc->mAttribs );
723 $formatter->setContext( $this->
getContext() );
724 $formatter->setShowUserToolLinks(
true );
725 $mark = $this->getLanguage()->getDirMark();
727 return Html::openElement(
'span', [
'class' =>
'mw-changeslist-log-entry' ] )
728 . $formatter->getActionText()
730 . $formatter->getComment()
731 . $this->msg(
'word-separator' )->escaped()
732 . $formatter->getActionLinks()
733 . Html::closeElement(
'span' );
742 if ( static::isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) {
743 $deletedClass =
'history-deleted';
744 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
745 $deletedClass .=
' mw-history-suppressed';
747 return ' <span class="' . $deletedClass .
' comment">' .
748 $this->msg(
'rev-deleted-comment' )->escaped() .
'</span>';
749 } elseif ( isset( $rc->mAttribs[
'rc_id'] )
750 && isset( $this->formattedComments[$rc->mAttribs[
'rc_id']] )
752 return $this->formattedComments[$rc->mAttribs[
'rc_id']];
754 return $this->commentFormatter->formatBlock(
755 $rc->mAttribs[
'rc_comment'],
777 return $this->watchMsgCache->getWithSetCallback(
778 "watching-users-msg:$count",
779 function () use ( $count ) {
780 return $this->msg(
'number-of-watching-users-for-recent-changes' )
781 ->numParams( $count )->escaped();
793 return ( $rc->mAttribs[
'rc_deleted'] & $field ) == $field;
806 $performer ??= RequestContext::getMain()->getAuthority();
808 if ( $rc->mAttribs[
'rc_type'] ==
RC_LOG ) {
809 return LogEventsList::userCanBitfield( $rc->mAttribs[
'rc_deleted'], $field, $performer );
812 return RevisionRecord::userCanBitfield( $rc->mAttribs[
'rc_deleted'], $field, $performer );
822 return '<strong class="mw-watched">' . $link .
'</strong>';
824 return '<span class="mw-rc-unwatched">' . $link .
'</span>';
835 $this->insertPageTools( $s, $rc );
846 private function insertPageTools( &$s, &$rc ) {
848 if ( !in_array( $rc->mAttribs[
'rc_type'], [
RC_EDIT,
RC_NEW ] )
850 || !$rc->mAttribs[
'rc_this_oldid']
851 || !$rc->mAttribs[
'rc_cur_id']
859 $revRecord->setId( (
int)$rc->mAttribs[
'rc_this_oldid'] );
860 $revRecord->setVisibility( (
int)$rc->mAttribs[
'rc_deleted'] );
862 (
int)$rc->mAttribs[
'rc_user'],
863 $rc->mAttribs[
'rc_user_text']
865 $revRecord->setUser( $user );
871 $rc->getAttribute(
'page_latest' ) == $rc->mAttribs[
'rc_this_oldid']
872 && $rc->mAttribs[
'rc_type'] !=
RC_NEW,
873 $this->getHookRunner(),
877 MediaWikiServices::getInstance()->getLinkRenderer()
880 $s .= $tools->toHTML();
890 $this->insertRollback( $s, $rc );
900 if ( empty( $rc->mAttribs[
'ts_tags'] ) ) {
905 $rc->mAttribs[
'ts_tags'],
909 $classes = array_merge( $classes, $newClasses );
910 $s .=
' ' . $tagSummary;
921 $this->insertTags( $s, $rc, $classes );
930 return self::isUnpatrolled( $rc, $this->
getUser() );
940 $isPatrolled = $rc->mAttribs[
'rc_patrolled'];
941 $rcType = $rc->mAttribs[
'rc_type'];
942 $rcLogType = $rc->mAttribs[
'rc_log_type'];
944 $isPatrolled = $rc->rc_patrolled;
945 $rcType = $rc->rc_type;
946 $rcLogType = $rc->rc_log_type;
949 if ( !$isPatrolled ) {
974 return intval( $rcObj->getAttribute(
'rc_type' ) ) ===
RC_CATEGORIZE
975 && intval( $rcObj->getAttribute(
'rc_this_oldid' ) ) === 0;
988 case RecentChange::SRC_EDIT:
989 case RecentChange::SRC_NEW:
990 $attrs[
'data-mw-revid'] = $rc->mAttribs[
'rc_this_oldid'];
992 case RecentChange::SRC_LOG:
993 $attrs[
'data-mw-logid'] = $rc->mAttribs[
'rc_logid'];
994 $attrs[
'data-mw-logaction'] =
995 $rc->mAttribs[
'rc_log_type'] .
'/' . $rc->mAttribs[
'rc_log_action'];
997 case RecentChange::SRC_CATEGORIZE:
998 $attrs[
'data-mw-revid'] = $rc->mAttribs[
'rc_this_oldid'];
1002 $attrs[
'data-mw-ts' ] = $rc->
getAttribute(
'rc_timestamp' );
1015 $this->changeLinePrefixer = $prefixer;
getWatchlistExpiry(WatchedItemStoreInterface $store, Title $title, UserIdentity $user)
Get existing expiry from the database.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Represents a filter group (used on ChangesListSpecialPage and descendants)
static newFromContext(IContextSource $context, array $groups=[])
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
maybeWatchedLink( $link, $watched=false)
setWatchlistDivs( $value=true)
Sets the list to use a "<li class='watchlist-(namespace)-(page)'>" tag.
formatCharacterDifference(RecentChange $old, RecentChange $new=null)
Format the character difference of one or several changes.
insertDateHeader(&$s, $rc_timestamp)
insertRollback(&$s, &$rc)
Insert a rollback link.
showAsUnpatrolled(RecentChange $rc)
RowCommentFormatter $commentFormatter
static isUnpatrolled( $rc, User $user)
getHighlightsContainerDiv()
Get the container for highlights that are used in the new StructuredFilters system.
static revDateLink(RevisionRecord $rev, Authority $performer, Language $lang, $title=null)
Render the date and time of a revision in the current user language based on whether the user is able...
recentChangesLine(&$rc, $watched=false, $linenumber=null)
Format a line.
__construct( $context, array $filterGroups=[])
recentChangesFlags( $flags, $nothing="\u{00A0}")
Returns the appropriate flags for new page, minor change and patrolling.
getDataAttributes(RecentChange $rc)
Get recommended data attributes for a change line.
numberofWatchingusers( $count)
Returns the string which indicates the number of watching users.
getHTMLClasses( $rc, $watched)
Get an array of default HTML class attributes for the change.
getWatchlistExpiry(RecentChange $recentChange)
Get HTML to display the clock icon for watched items that have a watchlist expiry time.
callable $changeLinePrefixer
getTags(RecentChange $rc, array &$classes)
getArticleLink(&$rc, $unpatrolled, $watched)
Get the HTML link to the changed page, possibly with a prefix from hook handlers, and a suffix for te...
insertUserRelatedLinks(&$s, &$rc)
Insert links to user page, user talk page and eventually a blocking link.
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
getRollback(RecentChange $rc)
MapCacheLRU $watchMsgCache
endRecentChangesList()
Returns text for the end of RC.
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
LinkRenderer $linkRenderer
insertLogEntry( $rc)
Insert a formatted action.
setChangeLinePrefixer(callable $prefixer)
Sets the callable that generates a change line prefix added to the beginning of each line.
static isDeleted( $rc, $field)
Determine if said field of a revision is hidden.
insertLog(&$s, $title, $logtype, $useParentheses=true)
insertTags(&$s, &$rc, &$classes)
ChangesListFilterGroup[] $filterGroups
string[] $formattedComments
Comments indexed by rc_id.
insertComment( $rc)
Insert a formatted comment.
static userCan( $rc, $field, Authority $performer=null)
Determine if the current user is allowed to view a particular field of this revision,...
insertExtra(&$s, &$rc, &$classes)
initChangesListRows( $rows)
getTimestamp( $rc)
Get the timestamp from $rc formatted with current user's settings and a separator.
isCategorizationWithoutRevision( $rcObj)
Determines whether a revision is linked to this change; this may not be the case when the categorizat...
getHTMLClassesForFilters( $rc)
Get an array of CSS classes attributed to filters for this row.
insertDiffHist(&$s, &$rc, $unpatrolled=null)
insertTimestamp(&$s, $rc)
Insert time timestamp string from $rc into $s.
beginRecentChangesList()
Returns text for the start of the tabular part of RC.
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
setContext(IContextSource $context)
Marks HTML that shouldn't be escaped.
Base class for language-specific code.
Class to simplify the use of log pages.
Handles a simple LRU key/value map with a maximum number of entries.
A class containing constants representing the names of configuration variables.
Utility class for creating new RC entries.
getAttribute( $name)
Get an attribute value.
useFilePatrol()
Check whether to enable new files patrol features for this user.
useNPPatrol()
Check whether to enable new pages patrol features for this user.
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
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.
Interface for objects which can provide a MediaWiki context on request.
if(!isset( $args[0])) $lang