13use BadMethodCallException;
15use InvalidArgumentException;
23use MediaWiki\Debug\DeprecationHelper;
57use Wikimedia\Timestamp\ConvertibleTimestamp;
58use Wikimedia\Timestamp\TimestampFormat as TS;
84 use DeprecationHelper;
92 private const DIFF_VERSION =
'1.41';
120 private $mOldRevisionRecord;
130 private $mNewRevisionRecord;
161 private $mOldContent;
168 private $mNewContent;
174 private $mRevisionsIdsLoaded =
false;
196 private $cacheHitKey =
null;
235 private $slotDiffOptions = [];
241 private $extraQueryParams = [];
262 private $revisionLoadErrors = [];
272 public function __construct( $context =
null, $old = 0, $new = 0, $rcid = 0,
273 $refreshCache =
false,
$unhide =
false
279 wfDebug(
"DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
281 $this->mOldid = $old;
282 $this->mNewid = $new;
283 $this->mRefreshCache = $refreshCache;
287 $this->linkRenderer = $services->getLinkRenderer();
288 $this->contentHandlerFactory = $services->getContentHandlerFactory();
289 $this->revisionStore = $services->getRevisionStore();
290 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
291 $this->hookRunner =
new HookRunner( $services->getHookContainer() );
292 $this->wikiPageFactory = $services->getWikiPageFactory();
293 $this->userOptionsLookup = $services->getUserOptionsLookup();
294 $this->commentFormatter = $services->getCommentFormatter();
295 $this->dbProvider = $services->getConnectionProvider();
296 $this->userGroupManager = $services->getUserGroupManager();
297 $this->userEditTracker = $services->getUserEditTracker();
298 $this->userIdentityUtils = $services->getUserIdentityUtils();
299 $this->recentChangeLookup = $services->getRecentChangeLookup();
300 $this->changeTagsFormatter = $services->getChangeTagsFormatter();
309 if ( $this->isSlotDiffRenderer ) {
310 throw new LogicException( __METHOD__ .
' called in slot diff renderer mode' );
313 if ( $this->slotDiffRenderers ===
null ) {
319 $this->slotDiffRenderers = [];
320 foreach ( $slotContents as $role => $contents ) {
321 if ( $contents[
'new'] && $contents[
'old']
322 && $contents[
'new']->equals( $contents[
'old'] )
327 if ( !$contents[
'new'] && !$contents[
'old'] ) {
331 $handler = ( $contents[
'new'] ?: $contents[
'old'] )->getContentHandler();
332 $this->slotDiffRenderers[$role] = $handler->getSlotDiffRenderer(
334 $this->slotDiffOptions + [
335 'contentLanguage' => $this->
getDiffLang()->getCode(),
352 $this->isSlotDiffRenderer =
true;
361 if ( $this->isContentOverridden ) {
363 SlotRecord::MAIN => [
'old' => $this->mOldContent,
'new' => $this->mNewContent ]
369 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
370 $oldSlots = $this->mOldRevisionRecord ?
371 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
377 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
380 foreach ( $roles as $role ) {
382 'old' => $this->loadSingleSlot(
383 $oldSlots[$role] ??
null,
386 'new' => $this->loadSingleSlot(
387 $newSlots[$role] ??
null,
393 if ( isset( $slots[SlotRecord::MAIN] ) ) {
394 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
406 private function loadSingleSlot( ?
SlotRecord $slot,
string $which ) {
412 }
catch ( BadRevisionException ) {
413 $this->addRevisionLoadError( $which );
423 private function addRevisionLoadError( $which ) {
424 $this->revisionLoadErrors[] = $this->
msg( $which ===
'new'
425 ?
'difference-bad-new-revision' :
'difference-bad-old-revision'
436 return $this->revisionLoadErrors;
443 private function hasNewRevisionLoadError() {
444 foreach ( $this->revisionLoadErrors as $error ) {
445 if ( $error->getKey() ===
'difference-bad-new-revision' ) {
465 $this->mReducedLineNumbers = $value;
474 # Default language in which the diff text is written.
486 return $this->
getTitle()->getPageLanguage();
504 $this->loadRevisionIds();
516 $this->loadRevisionIds();
528 return $this->mOldRevisionRecord ?:
null;
537 return $this->mNewRevisionRecord;
549 if ( $this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
550 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord(
null, $id );
552 $title = Title::newFromPageIdentity( $revRecord->
getPage() );
555 'target' => $title->getPrefixedText(),
574 return "[$link $id]";
580 private function showMissingRevision() {
584 if ( $this->mOldid && !$this->mOldRevisionRecord ) {
587 if ( $this->mNewid && !$this->mNewRevisionRecord ) {
591 $out->setPageTitleMsg( $this->
msg(
'errorpagetitle' ) );
594 $key = $this->
getTitle()->equals( Title::newMainPage() ) ?
595 'difference-missing-revision-nolog' :
596 'difference-missing-revision';
598 $msg = $this->
msg( $key )
600 ->numParams( count( $missing ) )
602 $out->addHTML( $msg );
613 $this->mNewRevisionRecord &&
614 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
617 $this->mOldRevisionRecord &&
618 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
631 $permStatus = PermissionStatus::newEmpty();
632 if ( $this->mNewPage ) {
633 $performer->
authorizeRead(
'read', $this->mNewPage, $permStatus );
635 if ( $this->mOldPage ) {
636 $performer->
authorizeRead(
'read', $this->mOldPage, $permStatus );
647 return $this->hasDeletedRevision() && (
648 ( $this->mOldRevisionRecord &&
649 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
650 ( $this->mNewRevisionRecord &&
651 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
661 private function getUserEditCount( $user ): string {
662 $editCount = $this->userEditTracker->getUserEditCount( $user );
663 if ( $editCount ===
null ) {
667 return Html::rawElement(
'div', [
668 'class' =>
'mw-diff-usereditcount',
672 $this->getLanguage()->formatNum( $editCount )
683 private function getUserRoles( UserIdentity $user ) {
684 if ( !$this->userIdentityUtils->isNamed( $user ) ) {
687 $userGroups = $this->userGroupManager->getUserGroups( $user );
688 $userGroupLinks = [];
689 foreach ( $userGroups as $group ) {
690 $userGroupLinks[] = UserGroupMembership::getLinkHTML( $group, $this->
getContext() );
692 return Html::rawElement(
'div', [
693 'class' =>
'mw-diff-userroles',
694 ], $this->
getLanguage()->commaList( $userGroupLinks ) );
703 private function getUserMetaData( ?UserIdentity $user ) {
707 return Html::rawElement(
'div', [
708 'class' =>
'mw-diff-usermetadata',
709 ], $this->getUserRoles( $user ) . $this->getUserEditCount( $user ) );
724 $this->loadRevisionData();
726 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
727 RevisionRecord::DELETED_TEXT,
735 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
736 RevisionRecord::DELETED_TEXT,
749 return $this->hasDeletedRevision() && ( !$this->unhide ||
750 !$this->isUserAllowedToSeeRevisions( $performer ) );
757 # Allow frames except in certain special cases
758 $out = $this->getOutput();
759 $out->getMetadata()->setPreventClickjacking(
false );
760 $out->setRobotPolicy(
'noindex,nofollow' );
763 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
765 if ( !$this->loadRevisionData() ) {
766 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
767 $this->showMissingRevision();
772 $user = $this->getUser();
773 $permStatus = $this->authorizeView( $this->getAuthority() );
774 if ( !$permStatus->isGood() ) {
780 $query = $this->extraQueryParams;
781 # Carry over 'diffonly' param via navigation links
782 if ( $diffOnly != MediaWikiServices::getInstance()
783 ->getUserOptionsLookup()->getBoolOption( $user,
'diffonly' )
785 $query[
'diffonly'] = $diffOnly;
787 # Cascade unhide param in links for easy deletion browsing
788 if ( $this->unhide ) {
789 $query[
'unhide'] = 1;
792 # Check if one of the revisions is deleted/suppressed
793 $deleted = $this->hasDeletedRevision();
794 $suppressed = $this->hasSuppressedRevision();
795 $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
800 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
801 # a diff between a version V and its previous version V' AND the version V
802 # is the first version of that article. In that case, V' does not exist.
803 if ( $this->mOldRevisionRecord ===
false ) {
804 if ( $this->mNewPage ) {
805 $out->setPageTitleMsg(
806 $this->msg(
'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
812 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
814 $this->hookRunner->onDifferenceEngineViewHeader( $this );
816 if ( !$this->mOldPage || !$this->mNewPage ) {
819 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
820 $out->setPageTitleMsg(
821 $this->msg(
'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
825 $out->setPageTitleMsg( $this->msg(
'difference-title-multipage' )->plaintextParams(
826 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
827 $out->addSubtitle( $this->msg(
'difference-multipage' ) );
831 if ( $samePage && $this->mNewPage &&
832 $this->getAuthority()->probablyCan(
'edit', $this->mNewPage )
834 if ( $this->mNewRevisionRecord->isCurrent() &&
835 $this->getAuthority()->probablyCan(
'rollback', $this->mNewPage )
837 $rollbackLink = Linker::generateRollback(
838 $this->mNewRevisionRecord,
842 if ( $rollbackLink ) {
843 $out->getMetadata()->setPreventClickjacking(
true );
844 $rollback =
"\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
848 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
849 $this->userCanEdit( $this->mNewRevisionRecord )
851 $undoLink = $this->linkRenderer->makeKnownLink(
853 $this->msg(
'editundo' )->text(),
854 [
'title' => Linker::titleAttrib(
'undo' ) ],
857 'undoafter' => $this->mOldid,
858 'undo' => $this->mNewid
861 $revisionTools[
'mw-diff-undo'] = $undoLink;
864 # Make "previous revision link"
865 $hasPrevious = $samePage && $this->mOldPage &&
866 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
867 if ( $hasPrevious ) {
868 $prevlinkQuery = [
'diff' =>
'prev',
'oldid' => $this->mOldid ] + $query;
869 $prevlink = $this->linkRenderer->makeKnownLink(
871 $this->msg(
'previousdiff' )->text(),
872 [
'id' =>
'differences-prevlink' ],
875 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
877 $this->msg(
'previousdiff' )->text(),
879 'class' =>
'mw-diff-revision-history-link-previous'
884 $prevlink =
"\u{00A0}";
887 if ( $this->mOldRevisionRecord->isMinor() ) {
888 $oldminor = ChangesList::flag(
'minor', $this->getContext() );
893 $oldRevRecord = $this->mOldRevisionRecord;
895 $ldel = $this->revisionDeleteLink( $oldRevRecord );
896 $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord,
'complete' );
897 $oldChangeTags = $this->changeTagsFormatter->formatTagsAsSummaryList(
900 $this->getAuthority()
902 $oldRevComment = $this->commentFormatter
904 $oldRevRecord, $user, !$diffOnly, !$this->unhide,
false
907 if ( $oldRevComment ===
'' ) {
908 $defaultComment = $this->msg(
'changeslist-nocomment' )->escaped();
909 $oldRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
912 $oldHeader =
'<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader .
'</strong></div>' .
913 '<div id="mw-diff-otitle2">' .
914 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
915 $this->getUserMetaData( $oldRevRecord->getUser() ) .
917 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel .
'</div>' .
918 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] .
'</div>' .
919 '<div id="mw-diff-otitle4">' . $prevlink .
'</div>';
922 $this->hookRunner->onDifferenceEngineOldHeader(
923 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
926 $out->addJsConfigVars( [
927 'wgDiffOldId' => $this->mOldid,
928 'wgDiffNewId' => $this->mNewid,
931 # Make "next revision link"
932 # Skip next link on the top revision
933 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
934 $nextlinkQuery = [
'diff' =>
'next',
'oldid' => $this->mNewid ] + $query;
935 $nextlink = $this->linkRenderer->makeKnownLink(
937 $this->msg(
'nextdiff' )->text(),
938 [
'id' =>
'differences-nextlink' ],
941 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
943 $this->msg(
'nextdiff' )->text(),
945 'class' =>
'mw-diff-revision-history-link-next'
950 $nextlink =
"\u{00A0}";
953 if ( $this->mNewRevisionRecord->isMinor() ) {
954 $newminor = ChangesList::flag(
'minor', $this->getContext() );
959 # Handle RevisionDelete links...
960 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
962 # Allow extensions to define their own revision tools
963 $this->hookRunner->onDiffTools(
964 $this->mNewRevisionRecord,
966 $this->mOldRevisionRecord ?:
null,
970 $formattedRevisionTools = [];
972 foreach ( $revisionTools as $key => $tool ) {
973 $toolClass = is_string( $key ) ? $key :
'mw-diff-tool';
974 $element = Html::rawElement(
976 [
'class' => $toolClass ],
979 $formattedRevisionTools[] = $element;
982 $newRevRecord = $this->mNewRevisionRecord;
984 $newRevisionHeader = $this->getRevisionHeader( $newRevRecord,
'complete' ) .
985 ' ' . implode(
' ', $formattedRevisionTools );
986 $newChangeTags = $this->changeTagsFormatter->formatTagsAsSummaryList(
989 $this->getAuthority()
991 $newRevComment = $this->commentFormatter->formatRevision(
992 $newRevRecord, $user, !$diffOnly, !$this->unhide,
false
995 if ( $newRevComment ===
'' ) {
996 $defaultComment = $this->msg(
'changeslist-nocomment' )->escaped();
997 $newRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
1000 $newMobileFooter = $this->getMobileFooter( $newRevRecord, $formattedRevisionTools );
1002 $newHeader =
'<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader .
'</strong></div>' .
1003 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
1005 $this->getUserMetaData( $newRevRecord->
getUser() ) .
1007 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel .
'</div>' .
1008 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] .
'</div>' .
1009 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() .
'</div>';
1012 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
1013 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
1014 $rdel, $this->unhide );
1017 Html::rawElement(
'div', [
1018 'class' =>
'mw-diff-revision-history-links'
1023 Html::rawElement(
'div', [
1024 'class' =>
'mw-diff-mobile-footer'
1025 ], $newMobileFooter )
1027 $addMessageBoxStyles =
false;
1028 # If the diff cannot be shown due to a deleted revision, then output
1029 # the diff header and links to unhide (if available)...
1030 if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
1031 $this->showDiffStyle();
1032 $multi = $this->getMultiNotice();
1033 $out->addHTML( $this->addHeader(
'', $oldHeader, $newHeader, $multi ) );
1035 # Give explanation for why revision is not visible
1036 $msg = [ $suppressed ?
'rev-suppressed-no-diff' :
'rev-deleted-no-diff' ];
1038 # Give explanation and add a link to view the diff...
1039 $query = $this->getRequest()->appendQueryValue(
'unhide',
'1' );
1041 $suppressed ?
'rev-suppressed-unhide-diff' :
'rev-deleted-unhide-diff',
1042 $this->getTitle()->getFullURL( $query )
1045 $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(),
'plainlinks' ) );
1046 $addMessageBoxStyles =
true;
1047 # Otherwise, output a regular diff...
1049 # Add deletion notice if the user is viewing deleted content
1052 $msg = $suppressed ?
'rev-suppressed-diff-view' :
'rev-deleted-diff-view';
1053 $notice = Html::warningBox( $this->msg( $msg )->parse(),
'plainlinks' );
1054 $addMessageBoxStyles =
true;
1057 # Add an error if the content can't be loaded
1058 $this->getSlotContents();
1059 foreach ( $this->getRevisionLoadErrors() as $msg ) {
1060 $notice .= Html::warningBox( $msg->parse() );
1061 $addMessageBoxStyles =
true;
1065 if ( $this->getTextDiffer()->hasFormat(
'inline' ) ) {
1069 $this->showTablePrefixes();
1070 $this->showDiff( $oldHeader, $newHeader, $notice );
1072 $this->renderNewRevision();
1076 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1077 # Add redundant patrol link on bottom...
1078 $out->addHTML( $this->markPatrolledLink() );
1081 if ( $addMessageBoxStyles ) {
1082 $out->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
1089 private function showTablePrefixes() {
1091 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1092 $parts += $slotDiffRenderer->getTablePrefix( $this->getContext(), $this->mNewPage );
1095 $nonEmptyParts = array_values( array_filter( $parts ) );
1096 if ( $nonEmptyParts ) {
1097 $language = $this->getLanguage();
1099 'class' =>
'mw-diff-table-prefix',
1100 'dir' => $language->getDir(),
1101 'lang' => $language->getCode(),
1103 $this->getOutput()->addHTML(
1104 Html::rawElement(
'div', $attrs, implode(
'', $nonEmptyParts ) ) );
1120 if ( $this->mMarkPatrolledLink ===
null ) {
1121 $linkInfo = $this->getMarkPatrolledLinkInfo();
1123 if ( !$linkInfo || !$this->mNewPage ) {
1124 $this->mMarkPatrolledLink =
'';
1126 $patrolLinkClass =
'patrollink';
1127 $this->mMarkPatrolledLink =
' <span class="' . $patrolLinkClass .
'"' .
1128 ' data-mw-interface>[' .
1129 $this->linkRenderer->makeKnownLink(
1131 $this->msg(
'markaspatrolleddiff' )->text(),
1134 'action' =>
'markpatrolled',
1135 'rcid' => $linkInfo[
'rcid'],
1139 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
1140 $this->mMarkPatrolledLink, $linkInfo[
'rcid'] );
1143 return $this->mMarkPatrolledLink;
1154 $user = $this->getUser();
1155 $config = $this->getConfig();
1160 $config->get( MainConfigNames::UseRCPatrol ) &&
1162 $this->getAuthority()->probablyCan(
'patrol', $this->mNewPage ) &&
1165 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
1168 $change = $this->recentChangeLookup->getRecentChangeByConds(
1170 'rc_this_oldid' => $this->mNewid,
1171 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
1176 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
1177 $rcid = $change->getAttribute(
'rc_id' );
1188 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1192 $this->getOutput()->getMetadata()->setPreventClickjacking(
true );
1193 $this->getOutput()->addModules(
'mediawiki.misc-authed-curate' );
1195 return [
'rcid' => $rcid ];
1208 private function revisionDeleteLink(
RevisionRecord $revRecord ) {
1209 $link = Linker::getRevDeleteLink(
1210 $this->getAuthority(),
1214 if ( $link !==
'' ) {
1215 $link =
"\u{00A0}\u{00A0}\u{00A0}" . $link .
' ';
1227 if ( $this->isContentOverridden ) {
1231 throw new LogicException(
1233 .
' is not supported after calling setContent(). Use setRevisions() instead.'
1237 $out = $this->getOutput();
1238 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1239 # Add "current version as of X" title
1240 $out->addHTML(
"<hr class='diff-hr' id='mw-oldid' />
1241 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1242 # Page content may be handled by a hooked call instead...
1243 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1244 $this->loadNewText();
1245 if ( !$this->mNewPage ) {
1251 if ( $this->hasNewRevisionLoadError() ) {
1256 $out->setRevisionId( $this->mNewid );
1257 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1258 $out->getMetadata()->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1259 $out->setArticleFlag(
true );
1261 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1262 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1268 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1272 $wikiPage = $this->getWikiPage();
1275 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1278 # Use appropriate parser options for the Article.
1279 $article = Article::newFromWikiPage( $wikiPage, $this->getContext() );
1280 $parserOptions = $article->getParserOptions();
1282 $parserOptions->setRenderReason(
'diff-page' );
1285 $parserOutput = $article->getPage()->getParserOutput(
1286 $parserOptions, $this->mNewRevisionRecord,
1289 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK =>
true,
1291 ParserOutputAccess::OPT_LINKS_UPDATE =>
true,
1296 if ( $parserOutput !==
false ) {
1298 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1299 $this, $out, $parserOutput, $wikiPage )
1301 $editLinks = $this->mNewRevisionRecord->isCurrent()
1302 && $this->getAuthority()->probablyCan(
1304 $this->mNewRevisionRecord->getPage() );
1305 if ( !$editLinks ) {
1306 $parserOptions->setSuppressSectionEditLinks();
1308 $out->addParserOutput( $parserOutput, $parserOptions, [
1309 'absoluteURLs' => $this->slotDiffOptions[
'expand-url'] ??
false
1313 $out->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
1315 foreach ( $errors as $msg ) {
1316 $out->addHTML( Html::errorBox(
1317 $this->msg( $msg )->parse()
1335 public function showDiff( $otitle, $ntitle, $notice =
'' ) {
1337 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1339 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1340 if ( $diff ===
false ) {
1341 $this->showMissingRevision();
1345 $this->showDiffStyle();
1346 if ( $this->slotDiffOptions[
'expand-url'] ??
false ) {
1347 $diff = Linker::expandLocalLinks( $diff );
1349 $this->getOutput()->addHTML( $diff );
1357 if ( !$this->isSlotDiffRenderer ) {
1358 $this->getOutput()->addModules(
'mediawiki.diff' );
1359 $this->getOutput()->addModuleStyles( [
1360 'mediawiki.interface.helpers.styles',
1361 'mediawiki.diff.styles'
1363 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1364 $slotDiffRenderer->addModules( $this->getOutput() );
1378 public function getDiff( $otitle, $ntitle, $notice =
'' ) {
1379 $body = $this->getDiffBody();
1380 if ( $body ===
false ) {
1384 $multi = $this->getMultiNotice();
1386 if ( $body ===
'' ) {
1387 $notice .=
'<div class="mw-diff-empty">' .
1388 $this->msg(
'diff-empty' )->parse() .
1392 if ( $this->cacheHitKey !==
null ) {
1393 $body .=
"\n<!-- diff cache key " . htmlspecialchars( $this->cacheHitKey ) .
" -->\n";
1396 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1399 private function incrementStats(
string $cacheStatus ): void {
1401 $stats->getCounter(
'diff_cache_total' )
1402 ->setLabel(
'status', $cacheStatus )
1412 $this->mCacheHit =
true;
1414 if ( !$this->isContentOverridden ) {
1415 if ( !$this->loadRevisionData() ) {
1417 } elseif ( $this->mOldRevisionRecord &&
1418 !$this->mOldRevisionRecord->userCan(
1419 RevisionRecord::DELETED_TEXT,
1420 $this->getAuthority()
1424 } elseif ( $this->mNewRevisionRecord &&
1425 !$this->mNewRevisionRecord->userCan(
1426 RevisionRecord::DELETED_TEXT,
1427 $this->getAuthority()
1432 if ( $this->mOldRevisionRecord ===
false || (
1433 $this->mOldRevisionRecord &&
1434 $this->mNewRevisionRecord &&
1435 $this->mOldRevisionRecord->getId() &&
1436 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1438 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1446 $services = MediaWikiServices::getInstance();
1447 $cache = $services->getMainWANObjectCache();
1448 $stats = $services->getStatsdDataFactory();
1449 if ( $this->mOldid && $this->mNewid ) {
1450 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1453 if ( !$this->mRefreshCache ) {
1454 $difftext = $cache->get( $key );
1455 if ( is_string( $difftext ) ) {
1456 $this->incrementStats(
'hit' );
1457 $difftext = $this->localiseDiff( $difftext );
1458 $this->cacheHitKey = $key;
1463 $this->mCacheHit =
false;
1464 $this->cacheHitKey =
null;
1467 if ( !$this->loadText() ) {
1474 $slotContents = $this->getSlotContents();
1475 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1477 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role][
'old'],
1478 $slotContents[$role][
'new'] );
1482 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1485 $difftext .= $this->getSlotHeader( $slotTitle );
1487 $difftext .= $slotDiff;
1491 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1492 $this->incrementStats(
'uncacheable' );
1493 } elseif ( $key !==
false ) {
1494 $this->incrementStats(
'miss' );
1495 $cache->set( $key, $difftext, 7 * 86400 );
1497 $this->incrementStats(
'uncacheable' );
1500 $difftext = $this->localiseDiff( $difftext );
1512 $diffRenderers = $this->getSlotDiffRenderers();
1513 if ( !isset( $diffRenderers[$role] ) ) {
1517 $slotContents = $this->getSlotContents();
1519 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role][
'old'],
1520 $slotContents[$role][
'new'] );
1524 if ( $slotDiff ===
'' ) {
1528 if ( $role !== SlotRecord::MAIN ) {
1531 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1534 return $this->localiseDiff( $slotDiff );
1545 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1546 $userLang = $this->getLanguage()->getHtmlCode();
1547 return Html::rawElement(
'tr', [
'class' =>
'mw-diff-slot-header',
'lang' => $userLang ],
1548 Html::element(
'th', [
'colspan' => $columnCount ], $headerText ) );
1559 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1560 $userLang = $this->getLanguage()->getHtmlCode();
1561 return Html::rawElement(
'tr', [
'class' =>
'mw-diff-slot-error',
'lang' => $userLang ],
1562 Html::rawElement(
'td', [
'colspan' => $columnCount ], $errorText ) );
1579 if ( !$this->mOldid || !$this->mNewid ) {
1580 throw new BadMethodCallException(
'mOldid and mNewid must be set to get diff cache key.' );
1586 "old-{$this->mOldid}",
1587 "rev-{$this->mNewid}"
1591 if ( !$this->isSlotDiffRenderer ) {
1592 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1593 $extraKeys = array_merge( $extraKeys, $slotDiffRenderer->getExtraCacheKeys() );
1596 ksort( $extraKeys );
1597 return array_merge( $params, array_values( $extraKeys ) );
1611 $this->mOldid = 123456789;
1612 $this->mNewid = 987654321;
1615 $params = $this->getDiffBodyCacheKeyParams();
1624 $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1625 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1626 $params = array_slice( $params, count( $standardParams ) );
1643 $validatedOptions = [];
1644 if ( isset( $options[
'diff-type'] )
1645 && $this->getTextDiffer()->hasFormat( $options[
'diff-type'] )
1647 $validatedOptions[
'diff-type'] = $options[
'diff-type'];
1649 if ( !empty( $options[
'expand-url'] ) ) {
1650 $validatedOptions[
'expand-url'] =
true;
1652 if ( !empty( $options[
'inline-toggle'] ) ) {
1653 $validatedOptions[
'inline-toggle'] =
true;
1655 $this->slotDiffOptions = $validatedOptions;
1666 $this->extraQueryParams = $params;
1683 $slotDiffRenderer = $new->
getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1686 && $this->isSlotDiffRenderer
1692 throw new LogicException( get_class( $this ) .
': could not maintain backwards compatibility. '
1693 .
'Please use a SlotDiffRenderer.' );
1695 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1711 $slotDiffRenderer = $this->contentHandlerFactory
1713 ->getSlotDiffRenderer( $this->getContext() );
1717 throw new LogicException(
'The slot diff renderer for text content should be a '
1718 .
'TextSlotDiffRenderer subclass' );
1720 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1730 $differenceEngine =
new self;
1731 $engine = $differenceEngine->getTextDiffer()->getEngineForFormat(
'table' );
1732 if ( $engine ===
'external' ) {
1733 return MediaWikiServices::getInstance()->getMainConfig()
1734 ->get( MainConfigNames::ExternalDiffEngine );
1748 protected function debug( $generator =
"internal" ) {
1749 if ( !$this->enableDebugComment ) {
1752 $data = [ $generator ];
1753 if ( $this->getConfig()->
get( MainConfigNames::ShowHostnames ) ) {
1756 $data[] = ConvertibleTimestamp::now( TS::DB );
1758 return "<!-- diff generator: " .
1759 implode(
" ", array_map(
"htmlspecialchars", $data ) ) .
1766 private function getDebugString() {
1767 $engine = self::getEngine();
1768 if ( $engine ===
'wikidiff2' ) {
1769 return $this->debug(
'wikidiff2' );
1770 } elseif ( $engine ===
'php' ) {
1771 return $this->debug(
'native PHP' );
1773 return $this->debug(
"external $engine" );
1783 private function localiseDiff( $text ) {
1784 return $this->getTextDiffer()->localize( $this->getTextDiffFormat(), $text );
1796 return preg_replace_callback(
'/<!--LINE (\d+)-->/',
1798 if (
$matches[1] ===
'1' && $this->mReducedLineNumbers ) {
1801 return $this->msg(
'lineno' )->numParams(
$matches[1] )->escaped();
1813 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1814 || !$this->mOldPage || !$this->mNewPage
1815 || !$this->mOldPage->equals( $this->mNewPage )
1816 || $this->mOldRevisionRecord->getId() ===
null
1817 || $this->mNewRevisionRecord->getId() ===
null
1819 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1820 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1825 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1826 $oldRevRecord = $this->mNewRevisionRecord;
1827 $newRevRecord = $this->mOldRevisionRecord;
1829 $oldRevRecord = $this->mOldRevisionRecord;
1830 $newRevRecord = $this->mNewRevisionRecord;
1836 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1837 $this->mNewPage->getArticleID(),
1843 if ( count( $revisionIdList ) > 0 ) {
1844 foreach ( $revisionIdList as $revisionId ) {
1845 $revision = $this->revisionStore->getRevisionById( $revisionId );
1846 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1851 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1853 $newRevUserForGender =
'[HIDDEN]';
1856 $users = $this->revisionStore->getAuthorsBetween(
1857 $this->mNewPage->getArticleID(),
1863 $numUsers = count( $users );
1865 $newRevUser = $newRevRecord->
getUser( RevisionRecord::RAW );
1866 $newRevUserText = $newRevUser ? $newRevUser->getName() :
'';
1867 $newRevUserSafe = $newRevRecord->
getUser(
1868 RevisionRecord::FOR_THIS_USER,
1869 $this->getAuthority()
1871 $newRevUserForGender = $newRevUserSafe ? $newRevUserSafe->getName() :
'[HIDDEN]';
1872 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1875 }
catch ( InvalidArgumentException ) {
1879 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit, $newRevUserForGender );
1896 if ( $numUsers === 0 ) {
1897 $msg =
'diff-multi-sameuser';
1899 ->numParams( $numEdits, $numUsers )
1900 ->params( $lastUser )
1902 } elseif ( $numUsers > $limit ) {
1903 $msg =
'diff-multi-manyusers';
1906 $msg =
'diff-multi-otherusers';
1909 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1917 if ( !$revRecord->
userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1934 $lang = $this->getLanguage();
1935 $user = $this->getUser();
1937 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1938 $dateofrev = $lang->userDate( $revtimestamp, $user );
1939 $timeofrev = $lang->userTime( $revtimestamp, $user );
1941 $header = $this->msg(
1942 $rev->
isCurrent() ?
'currentrev-asof' :
'revisionasof',
1948 if ( $complete !==
'complete' ) {
1949 return $header->escaped();
1954 if ( $this->userCanEdit( $rev ) ) {
1955 $header = $this->linkRenderer->makeKnownLink(
1959 [
'oldid' => $rev->
getId() ]
1961 $editQuery = [
'action' =>
'edit' ];
1963 $editQuery[
'oldid'] = $rev->
getId();
1966 $key = $this->getAuthority()->probablyCan(
'edit', $rev->
getPage() ) ?
'editold' :
'viewsourceold';
1967 $msg = $this->msg( $key )->text();
1968 $editLink = $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery );
1969 $header .=
' ' . Html::rawElement(
1971 [
'class' =>
'mw-diff-edit' ],
1975 $header = $header->escaped();
1979 $header .= Html::element(
'span',
1981 'class' =>
'mw-diff-timestamp',
1982 'data-timestamp' =>
wfTimestamp( TS::ISO_8601, $revtimestamp ),
1986 if ( $rev->
isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1987 return Html::rawElement(
1989 [
'class' => Linker::getRevisionDeletedClass( $rev ) ],
2009 public function addHeader( $diff, $otitle, $ntitle, $multi =
'', $notice =
'' ) {
2012 $header = Html::openElement(
'table', [
2018 'diff-type-' . $this->getTextDiffFormat(),
2022 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
2027 'diff-editfont-' . $this->userOptionsLookup->getOption(
2032 'data-mw-interface' =>
'',
2034 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
2036 if ( !$diff && !$otitle ) {
2038 <tr class=\"diff-title\" lang=\"{$userLang}\">
2039 <td class=\"diff-ntitle\">{$ntitle}</td>
2045 <col class=\"diff-marker\" />
2046 <col class=\"diff-content\" />
2047 <col class=\"diff-marker\" />
2048 <col class=\"diff-content\" />";
2055 if ( $otitle || $ntitle ) {
2057 $deletedClass =
'diff-side-deleted';
2058 $addedClass =
'diff-side-added';
2060 <tr class=\"diff-title\" lang=\"{$userLang}\">
2061 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
2062 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
2067 if ( $multi !=
'' ) {
2068 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
2069 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
2071 if ( $notice !=
'' ) {
2072 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
2073 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
2076 return $header . $diff .
"</table>";
2087 $this->mOldContent = $oldContent;
2088 $this->mNewContent = $newContent;
2090 $this->mTextLoaded = 2;
2091 $this->mRevisionsLoaded =
true;
2092 $this->isContentOverridden =
true;
2093 $this->slotDiffRenderers =
null;
2104 if ( $oldRevision ) {
2105 $this->mOldRevisionRecord = $oldRevision;
2106 $this->mOldid = $oldRevision->
getId();
2107 $this->mOldPage = Title::newFromPageIdentity( $oldRevision->
getPage() );
2110 $this->mOldContent = $oldRevision->
getContent( SlotRecord::MAIN,
2111 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2112 if ( !$this->mOldContent ) {
2113 $this->addRevisionLoadError(
'old' );
2116 $this->mOldPage =
null;
2117 $this->mOldRevisionRecord = $this->mOldid =
false;
2119 $this->mNewRevisionRecord = $newRevision;
2120 $this->mNewid = $newRevision->
getId();
2121 $this->mNewPage = Title::newFromPageIdentity( $newRevision->
getPage() );
2122 $this->mNewContent = $newRevision->
getContent( SlotRecord::MAIN,
2123 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2124 if ( !$this->mNewContent ) {
2125 $this->addRevisionLoadError(
'new' );
2128 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded =
true;
2129 $this->mTextLoaded = $oldRevision ? 2 : 1;
2130 $this->isContentOverridden =
false;
2131 $this->slotDiffRenderers =
null;
2141 $this->mDiffLang = $lang;
2157 if ( $new ===
'prev' ) {
2159 $newid = intval( $old );
2161 $newRev = $this->revisionStore->getRevisionById( $newid );
2163 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
2165 $oldid = $oldRev->getId();
2168 } elseif ( $new ===
'next' ) {
2170 $oldid = intval( $old );
2172 $oldRev = $this->revisionStore->getRevisionById( $oldid );
2174 $newRev = $this->revisionStore->getNextRevision( $oldRev );
2176 $newid = $newRev->getId();
2180 $oldid = intval( $old );
2181 $newid = intval( $new );
2185 return [ $oldid, $newid ];
2188 private function loadRevisionIds() {
2189 if ( $this->mRevisionsIdsLoaded ) {
2193 $this->mRevisionsIdsLoaded =
true;
2195 $old = $this->mOldid;
2196 $new = $this->mNewid;
2198 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2199 if ( $new ===
'next' && $this->mNewid ===
false ) {
2200 # if no result, NewId points to the newest old revision. The only newer
2201 # revision is cur, which is "0".
2205 $this->hookRunner->onNewDifferenceEngine(
2207 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2224 if ( $this->mRevisionsLoaded ) {
2225 return $this->isContentOverridden ||
2226 ( $this->mOldRevisionRecord !==
null && $this->mNewRevisionRecord !== null );
2230 $this->mRevisionsLoaded =
true;
2232 $this->loadRevisionIds();
2235 if ( $this->mNewid ) {
2236 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2238 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2242 $this->mOldRevisionRecord =
false;
2243 if ( $this->mOldid ) {
2244 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2245 } elseif ( $this->mOldid === 0 && $this->mNewRevisionRecord instanceof
RevisionRecord ) {
2246 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2248 $this->mOldid = $revRecord ? $revRecord->
getId() :
false;
2249 $this->mOldRevisionRecord = $revRecord ??
false;
2252 if ( $this->mOldRevisionRecord ===
null || $this->mNewRevisionRecord ===
null ) {
2257 $this->mNewid = $this->mNewRevisionRecord->
getId();
2258 $this->mNewPage = $this->mNewid ?
2259 Title::newFromPageIdentity( $this->mNewRevisionRecord->getPage() ) :
2262 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2263 $this->mOldPage = Title::newFromPageIdentity( $this->mOldRevisionRecord->getPage() );
2265 $this->mOldPage =
null;
2269 $dbr = $this->dbProvider->getReplicaDatabase();
2270 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2271 if ( $this->mOldid !==
false ) {
2272 $tagIds = $dbr->newSelectQueryBuilder()
2273 ->select(
'ct_tag_id' )
2274 ->from(
'change_tag' )
2275 ->where( [
'ct_rev_id' => $this->mOldid ] )
2276 ->caller( __METHOD__ )->fetchFieldValues();
2278 foreach ( $tagIds as $tagId ) {
2280 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2285 $this->mOldTags = implode(
',', $tags );
2287 $this->mOldTags =
false;
2290 $tagIds = $dbr->newSelectQueryBuilder()
2291 ->select(
'ct_tag_id' )
2292 ->from(
'change_tag' )
2293 ->where( [
'ct_rev_id' => $this->mNewid ] )
2294 ->caller( __METHOD__ )->fetchFieldValues();
2296 foreach ( $tagIds as $tagId ) {
2298 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2303 $this->mNewTags = implode(
',', $tags );
2316 if ( $this->mTextLoaded == 2 ) {
2317 return $this->loadRevisionData() &&
2318 ( $this->mOldRevisionRecord ===
false || $this->mOldContent )
2319 && $this->mNewContent;
2323 $this->mTextLoaded = 2;
2325 if ( !$this->loadRevisionData() ) {
2329 if ( $this->mOldRevisionRecord ) {
2330 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2332 RevisionRecord::FOR_THIS_USER,
2333 $this->getAuthority()
2335 if ( $this->mOldContent ===
null ) {
2340 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2342 RevisionRecord::FOR_THIS_USER,
2343 $this->getAuthority()
2345 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2346 if ( $this->mNewContent ===
null ) {
2359 if ( $this->mTextLoaded >= 1 ) {
2360 return $this->loadRevisionData();
2363 $this->mTextLoaded = 1;
2365 if ( !$this->loadRevisionData() ) {
2369 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2371 RevisionRecord::FOR_THIS_USER,
2372 $this->getAuthority()
2375 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2386 if ( $this->textDiffer ===
null ) {
2388 $this->getContext(),
2389 $this->getDiffLang(),
2390 $this->getConfig()->
get( MainConfigNames::DiffEngine ),
2391 $this->getConfig()->
get( MainConfigNames::ExternalDiffEngine ),
2392 $this->getConfig()->
get( MainConfigNames::Wikidiff2Options )
2395 return $this->textDiffer;
2405 return $this->getTextDiffer()->getFormats();
2415 return $this->slotDiffOptions[
'diff-type'] ??
'table';
2423 private function getMobileFooter( ?
RevisionRecord $newRevRecord, array $formattedRevisionTools ): string {
2424 $this->getOutput()->addModuleStyles( [
'codex-styles' ] );
2425 $summary = Html::rawElement(
2427 [
"class" =>
"cdx-accordion--has-icon" ],
2430 [
"class" =>
"cdx-accordion__header" ],
2433 [
'class' =>
'cdx-accordion__header__title' ],
2434 Linker::revUserTools( $newRevRecord, !$this->unhide )
2439 if ( $this->mNewRevisionRecord->isCurrent() &&
2440 $this->getAuthority()->probablyCan(
'rollback', $this->mNewPage )
2442 $rollbackLink = Linker::generateRollback(
2443 $this->mNewRevisionRecord,
2444 $this->getContext(),
2448 $user = $newRevRecord->
getUser();
2450 if ( $user !==
null ) {
2451 $userGroups = $this->userGroupManager->getUserGroups( $user );
2453 $userGroupCount = count( $userGroups );
2454 $userEditCount = $user ===
null ?
'' : $this->getUserEditCount( $user );
2455 $userGroupList = [];
2456 foreach ( $userGroups as $userGroup ) {
2457 $userGroupList[] = $this->msg(
"group-$userGroup" )->escaped();
2459 if ( $userGroupCount == 0 ) {
2460 $userGroupsPopover =
'';
2462 $popover = Html::rawElement(
2464 [
'class' =>
'cdx-popover mw-diff-usergroups-popover',
'role' =>
'tooltip' ],
2467 [
'class' =>
'cdx-popover__body' ],
2468 $this->
msg(
'diff-usergroups-list', $this->
getLanguage()->commaList( $userGroupList ) )->escaped()
2469 ) . Html::rawElement(
'div', [
'class' =>
'cdx-popover__arrow' ] )
2471 $popoverTrigger = Html::element(
2473 [
'class' =>
'cdx-popover-trigger cdx-button__icon cdx-icon cdx-icon--info ',
'tabindex' =>
'0' ],
2475 $userGroupsPopover = Html::rawElement(
2477 [
'class' =>
'mw-diff-usermetadata' ],
2479 Html::element(
'span',
2480 [
'class' =>
'mw-diff-usergroups-popover-text' ],
2481 $this->
msg(
'diff-usergroups', $userGroupCount )->text()
2485 [
'class' =>
'mw-diff-usergroups-popover-wrapper' ],
2486 $popoverTrigger . $popover
2491 $content = Html::rawElement(
2493 [
"class" =>
"cdx-accordion__content" ],
2497 . implode(
'', $formattedRevisionTools )
2499 return Html::rawElement(
2501 [
"class" =>
"mw-diff-new-mobile-footer-accordion cdx-accordion cdx-accordion--separation-minimal" ],
2509class_alias( DifferenceEngine::class,
'DifferenceEngine' );
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
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.
A TextDiffer which acts as a container for other TextDiffers, and dispatches requests to them.
Renders a slot diff by doing a text diff on the native representation.
A class containing constants representing the names of configuration variables.
Legacy class representing an editable page and handling UI for some page actions.
Service for getting rendered output of a given page.
Service for creating WikiPage objects.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Content objects represent page content, e.g.
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
Interface for objects which can provide a MediaWiki context on request.