26 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
39 use ProtectedHookAccessorTrait;
83 $this->preCacheMessages();
87 $services = MediaWikiServices::getInstance();
88 $this->linkRenderer = $services->getLinkRenderer();
89 $this->commentFormatter = $services->getRowCommentFormatter();
104 if (
Hooks::runner()->onFetchChangesList( $user, $sk, $list, $groups ) ) {
105 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
108 $userOptionsLookup->getBoolOption( $user,
'usenewrc' )
131 throw new RuntimeException(
'recentChangesLine should be implemented' );
141 $highlightColorDivs =
'';
142 foreach ( [
'none',
'c1',
'c2',
'c3',
'c4',
'c5' ] as $color ) {
146 'class' =>
'mw-rcfilters-ui-highlights-color-' . $color,
147 'data-color' => $color
154 [
'class' =>
'mw-rcfilters-ui-highlights' ],
164 $this->watchlist = $value;
179 private function preCacheMessages() {
180 if ( !isset( $this->message ) ) {
183 'cur',
'diff',
'hist',
'enhancedrc-history',
'last',
'blocklink',
'history',
184 'semicolon-separator',
'pipe-separator' ] as $msg
186 $this->message[$msg] = $this->
msg( $msg )->escaped();
200 array_keys( $this->
getConfig()->
get( MainConfigNames::RecentChangesFlags ) ) as $flag
202 $f .= isset( $flags[$flag] ) && $flags[$flag]
219 $classes = [ self::CSS_CLASS_PREFIX .
'line' ];
220 $logType = $rc->mAttribs[
'rc_log_type'];
223 $classes[] = self::CSS_CLASS_PREFIX .
'log';
226 $classes[] = self::CSS_CLASS_PREFIX .
'edit';
228 $rc->mAttribs[
'rc_namespace'] .
'-' . $rc->mAttribs[
'rc_title'] );
233 $classes[] = $watched && $rc->mAttribs[
'rc_timestamp'] >= $watched
234 ? self::CSS_CLASS_PREFIX .
'line-watched'
235 : self::CSS_CLASS_PREFIX .
'line-not-watched';
253 $rc->mAttribs[
'rc_namespace'] );
255 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
257 self::CSS_CLASS_PREFIX .
259 ( $nsInfo->isTalk( $rc->mAttribs[
'rc_namespace'] ) ?
'talk' :
'subject' )
262 foreach ( $this->filterGroups as $filterGroup ) {
263 foreach ( $filterGroup->getFilters() as $filter ) {
264 $filter->applyCssClassIfNeeded( $this, $rc, $classes );
282 static $map = [
'minoredit' =>
'minor',
'botedit' =>
'bot' ];
283 static $flagInfos =
null;
285 if ( $flagInfos ===
null ) {
286 $recentChangesFlags = MediaWikiServices::getInstance()->getMainConfig()
287 ->get( MainConfigNames::RecentChangesFlags );
289 foreach ( $recentChangesFlags as $key => $value ) {
290 $flagInfos[$key][
'letter'] = $value[
'letter'];
291 $flagInfos[$key][
'title'] = $value[
'title'];
293 $flagInfos[$key][
'class'] = $value[
'class'] ?? $key;
300 if ( isset( $map[$flag] ) ) {
304 $info = $flagInfos[$flag];
306 'class' => $info[
'class'],
316 $this->rc_cache = [];
317 $this->rcMoveIndex = 0;
318 $this->rcCacheIndex = 0;
319 $this->lastdate =
'';
320 $this->rclistOpen =
false;
322 'mediawiki.interface.helpers.styles',
323 'mediawiki.special.changeslist'
326 return '<div class="mw-changeslist">';
333 $this->getHookRunner()->onChangesListInitRows( $this, $rows );
334 $this->formattedComments = $this->commentFormatter->createBatch()
336 $this->commentFormatter->rows( $rows )
337 ->commentKey(
'rc_comment' )
338 ->namespaceField(
'rc_namespace' )
339 ->titleField(
'rc_title' )
340 ->indexField(
'rc_id' )
363 $szdiff = $new - $old;
365 $lang = $context->getLanguage();
366 $config = $context->getConfig();
367 $code =
$lang->getCode();
368 static $fastCharDiff = [];
369 if ( !isset( $fastCharDiff[$code] ) ) {
370 $fastCharDiff[$code] = $config->get( MainConfigNames::MiserMode )
371 || $context->msg(
'rc-change-size' )->plain() ===
'$1';
374 $formattedSize =
$lang->formatNum( $szdiff );
376 if ( !$fastCharDiff[$code] ) {
377 $formattedSize = $context->msg(
'rc-change-size', $formattedSize )->text();
380 if ( abs( $szdiff ) > abs( $config->get( MainConfigNames::RCChangedSizeThreshold ) ) ) {
386 if ( $szdiff === 0 ) {
387 $formattedSizeClass =
'mw-plusminus-null';
388 } elseif ( $szdiff > 0 ) {
389 $formattedSize =
'+' . $formattedSize;
390 $formattedSizeClass =
'mw-plusminus-pos';
392 $formattedSizeClass =
'mw-plusminus-neg';
394 $formattedSizeClass .=
' mw-diff-bytes';
396 $formattedTotalSize = $context->msg(
'rc-change-size-new' )->numParams( $new )->text();
399 [
'dir' =>
'ltr',
'class' => $formattedSizeClass,
'title' => $formattedTotalSize ],
400 $formattedSize ) .
$lang->getDirMark();
411 $oldlen = $old->mAttribs[
'rc_old_len'];
414 $newlen = $new->mAttribs[
'rc_new_len'];
416 $newlen = $old->mAttribs[
'rc_new_len'];
419 if ( $oldlen ===
null || $newlen ===
null ) {
431 $out = $this->rclistOpen ?
"</ul>\n" :
'';
456 $date =
$lang->userTimeAndDate( $ts, $performer->
getUser() );
457 if ( $rev->
userCan( RevisionRecord::DELETED_TEXT, $performer ) ) {
458 $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
461 [
'class' =>
'mw-changeslist-date' ],
462 [
'oldid' => $rev->
getId() ]
465 $link = htmlspecialchars( $date );
467 if ( $rev->
isDeleted( RevisionRecord::DELETED_TEXT ) ) {
468 $deletedClass = Linker::getRevisionDeletedClass( $rev );
469 $link =
"<span class=\"$deletedClass mw-changeslist-date\">$link</span>";
472 'class' =>
'mw-changeslist-time'
481 # Make date header if necessary
483 if ( $date != $this->lastdate ) {
484 if ( $this->lastdate !=
'' ) {
487 $s .=
Xml::element(
'h4',
null, $date ) .
"\n<ul class=\"special\">";
488 $this->lastdate = $date;
489 $this->rclistOpen =
true;
500 $page =
new LogPage( $logtype );
501 $logname = $page->getName()->setContext( $this->
getContext() )->text();
502 $link = $this->linkRenderer->makeKnownLink(
$title, $logname, [
503 'class' => $useParentheses ?
'' :
'mw-changeslist-links'
505 if ( $useParentheses ) {
506 $s .= $this->
msg(
'parentheses' )->rawParams(
522 $rc->mAttribs[
'rc_type'] ==
RC_NEW ||
523 $rc->mAttribs[
'rc_type'] ==
RC_LOG ||
526 $diffLink = $this->message[
'diff'];
527 } elseif ( !self::userCan( $rc, RevisionRecord::DELETED_TEXT, $this->
getAuthority() ) ) {
528 $diffLink = $this->message[
'diff'];
531 'curid' => $rc->mAttribs[
'rc_cur_id'],
532 'diff' => $rc->mAttribs[
'rc_this_oldid'],
533 'oldid' => $rc->mAttribs[
'rc_last_oldid']
536 $diffLink = $this->linkRenderer->makeKnownLink(
539 [
'class' =>
'mw-changeslist-diff' ],
544 $histLink = $this->message[
'hist'];
546 $histLink = $this->linkRenderer->makeKnownLink(
549 [
'class' =>
'mw-changeslist-history' ],
551 'curid' => $rc->mAttribs[
'rc_cur_id'],
552 'action' =>
'history'
561 ' <span class="mw-changeslist-separator"></span> ';
576 if ( $rc->getTitle()->isRedirect() ) {
577 $params = [
'redirect' =>
'no' ];
580 $articlelink = $this->linkRenderer->makeLink(
583 [
'class' =>
'mw-changeslist-title' ],
586 if ( static::isDeleted( $rc, RevisionRecord::DELETED_TEXT ) ) {
587 $class =
'history-deleted';
588 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
589 $class .=
' mw-history-suppressed';
591 $articlelink =
'<span class="' . $class .
'">' . $articlelink .
'</span>';
593 # To allow for boldening pages watched by this user
594 $articlelink =
"<span class=\"mw-title\">{$articlelink}</span>";
596 $articlelink .= $this->
getLanguage()->getDirMark();
598 # TODO: Deprecate the $s argument, it seems happily unused.
600 $this->getHookRunner()->onChangesListInsertArticleLink( $this, $articlelink,
601 $s, $rc, $unpatrolled, $watched );
604 $watchlistExpiry =
'';
605 if ( isset( $rc->watchlistExpiry ) && $rc->watchlistExpiry ) {
609 return "{$s} {$articlelink}{$watchlistExpiry}";
621 if ( $item->isExpired() ) {
626 $widget =
new IconWidget( [
628 'title' => $daysLeftText,
629 'classes' => [
'mw-changesList-watchlistExpiry' ],
631 $widget->setAttributes( [
634 'aria-label' => $this->
msg(
'watchlist-expires-in-aria-label' )->text(),
636 'data-days-left' => $item->getExpiryInDays(),
663 $separatorClass = $rc->watchlistExpiry ?
'mw-changeslist-separator' :
'mw-changeslist-separator--semicolon';
664 return Html::element(
'span', [
'class' => $separatorClass ] ) .
' ' .
665 '<span class="mw-changeslist-date mw-changeslist-time">' .
666 htmlspecialchars( $this->getLanguage()->userTime(
667 $rc->mAttribs[
'rc_timestamp'],
669 ) ) .
'</span> <span class="mw-changeslist-separator"></span> ';
689 if ( static::isDeleted( $rc, RevisionRecord::DELETED_USER ) ) {
690 $deletedClass =
'history-deleted';
691 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
692 $deletedClass .=
' mw-history-suppressed';
694 $s .=
' <span class="' . $deletedClass .
'">' .
695 $this->msg(
'rev-deleted-user' )->escaped() .
'</span>';
697 $s .= $this->getLanguage()->getDirMark() . Linker::userLink( $rc->mAttribs[
'rc_user'],
698 $rc->mAttribs[
'rc_user_text'] );
699 $s .= Linker::userToolLinks(
700 $rc->mAttribs[
'rc_user'], $rc->mAttribs[
'rc_user_text'],
717 $formatter->setContext( $this->
getContext() );
718 $formatter->setShowUserToolLinks(
true );
719 $mark = $this->getLanguage()->getDirMark();
722 . $formatter->getActionText()
724 . $formatter->getComment()
725 . $this->msg(
'word-separator' )->escaped()
726 . $formatter->getActionLinks()
736 if ( static::isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) {
737 $deletedClass =
'history-deleted';
738 if ( static::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
739 $deletedClass .=
' mw-history-suppressed';
741 return ' <span class="' . $deletedClass .
' comment">' .
742 $this->msg(
'rev-deleted-comment' )->escaped() .
'</span>';
743 } elseif ( isset( $rc->mAttribs[
'rc_id'] )
744 && isset( $this->formattedComments[$rc->mAttribs[
'rc_id']] )
746 return $this->formattedComments[$rc->mAttribs[
'rc_id']];
748 return $this->commentFormatter->formatBlock(
749 $rc->mAttribs[
'rc_comment'],
771 return $this->watchMsgCache->getWithSetCallback(
772 "watching-users-msg:$count",
773 function () use ( $count ) {
774 return $this->msg(
'number-of-watching-users-for-recent-changes' )
775 ->numParams( $count )->escaped();
787 return ( $rc->mAttribs[
'rc_deleted'] & $field ) == $field;
802 if ( $rc->mAttribs[
'rc_type'] ==
RC_LOG ) {
806 return RevisionRecord::userCanBitfield( $rc->mAttribs[
'rc_deleted'], $field, $performer );
816 return '<strong class="mw-watched">' . $link .
'</strong>';
818 return '<span class="mw-rc-unwatched">' . $link .
'</span>';
829 $this->insertPageTools(
$s, $rc );
840 private function insertPageTools( &
$s, &$rc ) {
842 if ( !in_array( $rc->mAttribs[
'rc_type'], [
RC_EDIT,
RC_NEW ] )
844 || !$rc->mAttribs[
'rc_this_oldid']
845 || !$rc->mAttribs[
'rc_cur_id']
853 $revRecord->setId( (
int)$rc->mAttribs[
'rc_this_oldid'] );
854 $revRecord->setVisibility( (
int)$rc->mAttribs[
'rc_deleted'] );
856 (
int)$rc->mAttribs[
'rc_user'],
857 $rc->mAttribs[
'rc_user_text']
859 $revRecord->setUser( $user );
865 $rc->getAttribute(
'page_latest' ) == $rc->mAttribs[
'rc_this_oldid']
866 && $rc->mAttribs[
'rc_type'] !=
RC_NEW,
867 $this->getHookRunner(),
871 MediaWikiServices::getInstance()->getLinkRenderer()
874 $s .= $tools->toHTML();
884 $this->insertRollback(
$s, $rc );
894 if ( empty( $rc->mAttribs[
'ts_tags'] ) ) {
899 $rc->mAttribs[
'ts_tags'],
903 $classes = array_merge( $classes, $newClasses );
904 $s .=
' ' . $tagSummary;
915 $this->insertTags(
$s, $rc, $classes );
924 return self::isUnpatrolled( $rc, $this->
getUser() );
934 $isPatrolled = $rc->mAttribs[
'rc_patrolled'];
935 $rcType = $rc->mAttribs[
'rc_type'];
936 $rcLogType = $rc->mAttribs[
'rc_log_type'];
938 $isPatrolled = $rc->rc_patrolled;
939 $rcType = $rc->rc_type;
940 $rcLogType = $rc->rc_log_type;
943 if ( !$isPatrolled ) {
968 return intval( $rcObj->getAttribute(
'rc_type' ) ) ===
RC_CATEGORIZE
969 && intval( $rcObj->getAttribute(
'rc_this_oldid' ) ) === 0;
984 $attrs[
'data-mw-revid'] = $rc->mAttribs[
'rc_this_oldid'];
987 $attrs[
'data-mw-logid'] = $rc->mAttribs[
'rc_logid'];
988 $attrs[
'data-mw-logaction'] =
989 $rc->mAttribs[
'rc_log_type'] .
'/' . $rc->mAttribs[
'rc_log_action'];
992 $attrs[
'data-mw-revid'] = $rc->mAttribs[
'rc_this_oldid'];
996 $attrs[
'data-mw-ts' ] = $rc->
getAttribute(
'rc_timestamp' );
1009 $this->changeLinePrefixer = $prefixer;
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
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()
getContext()
Get the base IContextSource object.
setContext(IContextSource $context)
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Marks HTML that shouldn't be escaped.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
static closeElement( $element)
Returns "</$element>".
Base class for language-specific code.
static userCanBitfield( $bitfield, $field, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
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.
static getMain()
Get the RequestContext object associated with the main request.
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
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.
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Interface for objects which can provide a MediaWiki context on request.
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
if(!isset( $args[0])) $lang