13use BadMethodCallException;
15use InvalidArgumentException;
23use MediaWiki\Debug\DeprecationHelper;
56use Wikimedia\Timestamp\ConvertibleTimestamp;
57use Wikimedia\Timestamp\TimestampFormat as TS;
83 use DeprecationHelper;
91 private const DIFF_VERSION =
'1.41';
119 private $mOldRevisionRecord;
129 private $mNewRevisionRecord;
160 private $mOldContent;
167 private $mNewContent;
173 private $mRevisionsIdsLoaded =
false;
195 private $cacheHitKey =
null;
234 private $slotDiffOptions = [];
240 private $extraQueryParams = [];
260 private $revisionLoadErrors = [];
270 public function __construct( $context =
null, $old = 0, $new = 0, $rcid = 0,
271 $refreshCache =
false,
$unhide =
false
277 wfDebug(
"DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
279 $this->mOldid = $old;
280 $this->mNewid = $new;
281 $this->mRefreshCache = $refreshCache;
285 $this->linkRenderer = $services->getLinkRenderer();
286 $this->contentHandlerFactory = $services->getContentHandlerFactory();
287 $this->revisionStore = $services->getRevisionStore();
288 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
289 $this->hookRunner =
new HookRunner( $services->getHookContainer() );
290 $this->wikiPageFactory = $services->getWikiPageFactory();
291 $this->userOptionsLookup = $services->getUserOptionsLookup();
292 $this->commentFormatter = $services->getCommentFormatter();
293 $this->dbProvider = $services->getConnectionProvider();
294 $this->userGroupManager = $services->getUserGroupManager();
295 $this->userEditTracker = $services->getUserEditTracker();
296 $this->userIdentityUtils = $services->getUserIdentityUtils();
297 $this->recentChangeLookup = $services->getRecentChangeLookup();
306 if ( $this->isSlotDiffRenderer ) {
307 throw new LogicException( __METHOD__ .
' called in slot diff renderer mode' );
310 if ( $this->slotDiffRenderers ===
null ) {
316 $this->slotDiffRenderers = [];
317 foreach ( $slotContents as $role => $contents ) {
318 if ( $contents[
'new'] && $contents[
'old']
319 && $contents[
'new']->equals( $contents[
'old'] )
324 if ( !$contents[
'new'] && !$contents[
'old'] ) {
328 $handler = ( $contents[
'new'] ?: $contents[
'old'] )->getContentHandler();
329 $this->slotDiffRenderers[$role] = $handler->getSlotDiffRenderer(
331 $this->slotDiffOptions + [
332 'contentLanguage' => $this->
getDiffLang()->getCode(),
349 $this->isSlotDiffRenderer =
true;
358 if ( $this->isContentOverridden ) {
360 SlotRecord::MAIN => [
'old' => $this->mOldContent,
'new' => $this->mNewContent ]
366 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
367 $oldSlots = $this->mOldRevisionRecord ?
368 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
374 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
377 foreach ( $roles as $role ) {
379 'old' => $this->loadSingleSlot(
380 $oldSlots[$role] ??
null,
383 'new' => $this->loadSingleSlot(
384 $newSlots[$role] ??
null,
390 if ( isset( $slots[SlotRecord::MAIN] ) ) {
391 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
403 private function loadSingleSlot( ?
SlotRecord $slot,
string $which ) {
409 }
catch ( BadRevisionException ) {
410 $this->addRevisionLoadError( $which );
420 private function addRevisionLoadError( $which ) {
421 $this->revisionLoadErrors[] = $this->
msg( $which ===
'new'
422 ?
'difference-bad-new-revision' :
'difference-bad-old-revision'
433 return $this->revisionLoadErrors;
440 private function hasNewRevisionLoadError() {
441 foreach ( $this->revisionLoadErrors as $error ) {
442 if ( $error->getKey() ===
'difference-bad-new-revision' ) {
462 $this->mReducedLineNumbers = $value;
471 # Default language in which the diff text is written.
483 return $this->
getTitle()->getPageLanguage();
501 $this->loadRevisionIds();
513 $this->loadRevisionIds();
525 return $this->mOldRevisionRecord ?:
null;
534 return $this->mNewRevisionRecord;
546 if ( $this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
547 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord(
null, $id );
549 $title = Title::newFromPageIdentity( $revRecord->
getPage() );
552 'target' => $title->getPrefixedText(),
571 return "[$link $id]";
577 private function showMissingRevision() {
581 if ( $this->mOldid && !$this->mOldRevisionRecord ) {
584 if ( !$this->mNewRevisionRecord ) {
588 $out->setPageTitleMsg( $this->
msg(
'errorpagetitle' ) );
591 $key = $this->
getTitle()->equals( Title::newMainPage() ) ?
592 'difference-missing-revision-nolog' :
593 'difference-missing-revision';
595 $msg = $this->
msg( $key )
596 ->params( $this->
getLanguage()->listToText( $missing ) )
597 ->numParams( count( $missing ) )
599 $out->addHTML( $msg );
610 $this->mNewRevisionRecord &&
611 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
614 $this->mOldRevisionRecord &&
615 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
628 $permStatus = PermissionStatus::newEmpty();
629 if ( $this->mNewPage ) {
630 $performer->
authorizeRead(
'read', $this->mNewPage, $permStatus );
632 if ( $this->mOldPage ) {
633 $performer->
authorizeRead(
'read', $this->mOldPage, $permStatus );
644 return $this->hasDeletedRevision() && (
645 ( $this->mOldRevisionRecord &&
646 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
647 ( $this->mNewRevisionRecord &&
648 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
658 private function getUserEditCount( $user ): string {
659 $editCount = $this->userEditTracker->getUserEditCount( $user );
660 if ( $editCount ===
null ) {
664 return Html::rawElement(
'div', [
665 'class' =>
'mw-diff-usereditcount',
669 $this->getLanguage()->formatNum( $editCount )
680 private function getUserRoles( UserIdentity $user ) {
681 if ( !$this->userIdentityUtils->isNamed( $user ) ) {
684 $userGroups = $this->userGroupManager->getUserGroups( $user );
685 $userGroupLinks = [];
686 foreach ( $userGroups as $group ) {
687 $userGroupLinks[] = UserGroupMembership::getLinkHTML( $group, $this->
getContext() );
689 return Html::rawElement(
'div', [
690 'class' =>
'mw-diff-userroles',
691 ], $this->getLanguage()->commaList( $userGroupLinks ) );
700 private function getUserMetaData( ?UserIdentity $user ) {
704 return Html::rawElement(
'div', [
705 'class' =>
'mw-diff-usermetadata',
706 ], $this->getUserRoles( $user ) . $this->getUserEditCount( $user ) );
721 $this->loadRevisionData();
723 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
724 RevisionRecord::DELETED_TEXT,
732 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
733 RevisionRecord::DELETED_TEXT,
746 return $this->hasDeletedRevision() && ( !$this->unhide ||
747 !$this->isUserAllowedToSeeRevisions( $performer ) );
754 # Allow frames except in certain special cases
755 $out = $this->getOutput();
756 $out->getMetadata()->setPreventClickjacking(
false );
757 $out->setRobotPolicy(
'noindex,nofollow' );
760 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
762 if ( !$this->loadRevisionData() ) {
763 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
764 $this->showMissingRevision();
769 $user = $this->getUser();
770 $permStatus = $this->authorizeView( $this->getAuthority() );
771 if ( !$permStatus->isGood() ) {
777 $query = $this->extraQueryParams;
778 # Carry over 'diffonly' param via navigation links
779 if ( $diffOnly != MediaWikiServices::getInstance()
780 ->getUserOptionsLookup()->getBoolOption( $user,
'diffonly' )
782 $query[
'diffonly'] = $diffOnly;
784 # Cascade unhide param in links for easy deletion browsing
785 if ( $this->unhide ) {
786 $query[
'unhide'] = 1;
789 # Check if one of the revisions is deleted/suppressed
790 $deleted = $this->hasDeletedRevision();
791 $suppressed = $this->hasSuppressedRevision();
792 $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
797 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
798 # a diff between a version V and its previous version V' AND the version V
799 # is the first version of that article. In that case, V' does not exist.
800 if ( $this->mOldRevisionRecord ===
false ) {
801 if ( $this->mNewPage ) {
802 $out->setPageTitleMsg(
803 $this->msg(
'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
809 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
811 $this->hookRunner->onDifferenceEngineViewHeader( $this );
813 if ( !$this->mOldPage || !$this->mNewPage ) {
816 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
817 $out->setPageTitleMsg(
818 $this->msg(
'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() )
822 $out->setPageTitleMsg( $this->msg(
'difference-title-multipage' )->plaintextParams(
823 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
824 $out->addSubtitle( $this->msg(
'difference-multipage' ) );
828 if ( $samePage && $this->mNewPage &&
829 $this->getAuthority()->probablyCan(
'edit', $this->mNewPage )
831 if ( $this->mNewRevisionRecord->isCurrent() &&
832 $this->getAuthority()->probablyCan(
'rollback', $this->mNewPage )
834 $rollbackLink = Linker::generateRollback(
835 $this->mNewRevisionRecord,
839 if ( $rollbackLink ) {
840 $out->getMetadata()->setPreventClickjacking(
true );
841 $rollback =
"\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
845 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
846 $this->userCanEdit( $this->mNewRevisionRecord )
848 $undoLink = $this->linkRenderer->makeKnownLink(
850 $this->msg(
'editundo' )->text(),
851 [
'title' => Linker::titleAttrib(
'undo' ) ],
854 'undoafter' => $this->mOldid,
855 'undo' => $this->mNewid
858 $revisionTools[
'mw-diff-undo'] = $undoLink;
861 # Make "previous revision link"
862 $hasPrevious = $samePage && $this->mOldPage &&
863 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
864 if ( $hasPrevious ) {
865 $prevlinkQuery = [
'diff' =>
'prev',
'oldid' => $this->mOldid ] + $query;
866 $prevlink = $this->linkRenderer->makeKnownLink(
868 $this->msg(
'previousdiff' )->text(),
869 [
'id' =>
'differences-prevlink' ],
872 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
874 $this->msg(
'previousdiff' )->text(),
876 'class' =>
'mw-diff-revision-history-link-previous'
881 $prevlink =
"\u{00A0}";
884 if ( $this->mOldRevisionRecord->isMinor() ) {
885 $oldminor = ChangesList::flag(
'minor', $this->getContext() );
890 $oldRevRecord = $this->mOldRevisionRecord;
892 $ldel = $this->revisionDeleteLink( $oldRevRecord );
893 $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord,
'complete' );
894 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags,
'diff', $this->getContext() );
895 $oldRevComment = $this->commentFormatter
897 $oldRevRecord, $user, !$diffOnly, !$this->unhide,
false
900 if ( $oldRevComment ===
'' ) {
901 $defaultComment = $this->msg(
'changeslist-nocomment' )->escaped();
902 $oldRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
905 $oldHeader =
'<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader .
'</strong></div>' .
906 '<div id="mw-diff-otitle2">' .
907 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
908 $this->getUserMetaData( $oldRevRecord->getUser() ) .
910 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel .
'</div>' .
911 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] .
'</div>' .
912 '<div id="mw-diff-otitle4">' . $prevlink .
'</div>';
915 $this->hookRunner->onDifferenceEngineOldHeader(
916 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
919 $out->addJsConfigVars( [
920 'wgDiffOldId' => $this->mOldid,
921 'wgDiffNewId' => $this->mNewid,
924 # Make "next revision link"
925 # Skip next link on the top revision
926 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
927 $nextlinkQuery = [
'diff' =>
'next',
'oldid' => $this->mNewid ] + $query;
928 $nextlink = $this->linkRenderer->makeKnownLink(
930 $this->msg(
'nextdiff' )->text(),
931 [
'id' =>
'differences-nextlink' ],
934 $breadCrumbs .= $this->linkRenderer->makeKnownLink(
936 $this->msg(
'nextdiff' )->text(),
938 'class' =>
'mw-diff-revision-history-link-next'
943 $nextlink =
"\u{00A0}";
946 if ( $this->mNewRevisionRecord->isMinor() ) {
947 $newminor = ChangesList::flag(
'minor', $this->getContext() );
952 # Handle RevisionDelete links...
953 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
955 # Allow extensions to define their own revision tools
956 $this->hookRunner->onDiffTools(
957 $this->mNewRevisionRecord,
959 $this->mOldRevisionRecord ?:
null,
963 $formattedRevisionTools = [];
965 foreach ( $revisionTools as $key => $tool ) {
966 $toolClass = is_string( $key ) ? $key :
'mw-diff-tool';
967 $element = Html::rawElement(
969 [
'class' => $toolClass ],
972 $formattedRevisionTools[] = $element;
975 $newRevRecord = $this->mNewRevisionRecord;
977 $newRevisionHeader = $this->getRevisionHeader( $newRevRecord,
'complete' ) .
978 ' ' . implode(
' ', $formattedRevisionTools );
979 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags,
'diff', $this->getContext() );
980 $newRevComment = $this->commentFormatter->formatRevision(
981 $newRevRecord, $user, !$diffOnly, !$this->unhide,
false
984 if ( $newRevComment ===
'' ) {
985 $defaultComment = $this->msg(
'changeslist-nocomment' )->escaped();
986 $newRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
989 $newMobileFooter = $this->getMobileFooter( $newRevRecord, $formattedRevisionTools );
991 $newHeader =
'<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader .
'</strong></div>' .
992 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
994 $this->getUserMetaData( $newRevRecord->
getUser() ) .
996 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel .
'</div>' .
997 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] .
'</div>' .
998 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() .
'</div>';
1001 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
1002 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
1003 $rdel, $this->unhide );
1006 Html::rawElement(
'div', [
1007 'class' =>
'mw-diff-revision-history-links'
1012 Html::rawElement(
'div', [
1013 'class' =>
'mw-diff-mobile-footer'
1014 ], $newMobileFooter )
1016 $addMessageBoxStyles =
false;
1017 # If the diff cannot be shown due to a deleted revision, then output
1018 # the diff header and links to unhide (if available)...
1019 if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
1020 $this->showDiffStyle();
1021 $multi = $this->getMultiNotice();
1022 $out->addHTML( $this->addHeader(
'', $oldHeader, $newHeader, $multi ) );
1024 # Give explanation for why revision is not visible
1025 $msg = [ $suppressed ?
'rev-suppressed-no-diff' :
'rev-deleted-no-diff' ];
1027 # Give explanation and add a link to view the diff...
1028 $query = $this->getRequest()->appendQueryValue(
'unhide',
'1' );
1030 $suppressed ?
'rev-suppressed-unhide-diff' :
'rev-deleted-unhide-diff',
1031 $this->getTitle()->getFullURL( $query )
1034 $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(),
'plainlinks' ) );
1035 $addMessageBoxStyles =
true;
1036 # Otherwise, output a regular diff...
1038 # Add deletion notice if the user is viewing deleted content
1041 $msg = $suppressed ?
'rev-suppressed-diff-view' :
'rev-deleted-diff-view';
1042 $notice = Html::warningBox( $this->msg( $msg )->parse(),
'plainlinks' );
1043 $addMessageBoxStyles =
true;
1046 # Add an error if the content can't be loaded
1047 $this->getSlotContents();
1048 foreach ( $this->getRevisionLoadErrors() as $msg ) {
1049 $notice .= Html::warningBox( $msg->parse() );
1050 $addMessageBoxStyles =
true;
1054 if ( $this->getTextDiffer()->hasFormat(
'inline' ) ) {
1058 $this->showTablePrefixes();
1059 $this->showDiff( $oldHeader, $newHeader, $notice );
1061 $this->renderNewRevision();
1065 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1066 # Add redundant patrol link on bottom...
1067 $out->addHTML( $this->markPatrolledLink() );
1070 if ( $addMessageBoxStyles ) {
1071 $out->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
1078 private function showTablePrefixes() {
1080 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1081 $parts += $slotDiffRenderer->getTablePrefix( $this->getContext(), $this->mNewPage );
1084 $nonEmptyParts = array_values( array_filter( $parts ) );
1085 if ( $nonEmptyParts ) {
1086 $language = $this->getLanguage();
1088 'class' =>
'mw-diff-table-prefix',
1089 'dir' => $language->getDir(),
1090 'lang' => $language->getCode(),
1092 $this->getOutput()->addHTML(
1093 Html::rawElement(
'div', $attrs, implode(
'', $nonEmptyParts ) ) );
1109 if ( $this->mMarkPatrolledLink ===
null ) {
1110 $linkInfo = $this->getMarkPatrolledLinkInfo();
1112 if ( !$linkInfo || !$this->mNewPage ) {
1113 $this->mMarkPatrolledLink =
'';
1115 $patrolLinkClass =
'patrollink';
1116 $this->mMarkPatrolledLink =
' <span class="' . $patrolLinkClass .
'"' .
1117 ' data-mw-interface>[' .
1118 $this->linkRenderer->makeKnownLink(
1120 $this->msg(
'markaspatrolleddiff' )->text(),
1123 'action' =>
'markpatrolled',
1124 'rcid' => $linkInfo[
'rcid'],
1128 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
1129 $this->mMarkPatrolledLink, $linkInfo[
'rcid'] );
1132 return $this->mMarkPatrolledLink;
1143 $user = $this->getUser();
1144 $config = $this->getConfig();
1149 $config->get( MainConfigNames::UseRCPatrol ) &&
1151 $this->getAuthority()->probablyCan(
'patrol', $this->mNewPage ) &&
1154 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
1157 $change = $this->recentChangeLookup->getRecentChangeByConds(
1159 'rc_this_oldid' => $this->mNewid,
1160 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
1165 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
1166 $rcid = $change->getAttribute(
'rc_id' );
1177 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1181 $this->getOutput()->getMetadata()->setPreventClickjacking(
true );
1182 $this->getOutput()->addModules(
'mediawiki.misc-authed-curate' );
1184 return [
'rcid' => $rcid ];
1197 private function revisionDeleteLink(
RevisionRecord $revRecord ) {
1198 $link = Linker::getRevDeleteLink(
1199 $this->getAuthority(),
1203 if ( $link !==
'' ) {
1204 $link =
"\u{00A0}\u{00A0}\u{00A0}" . $link .
' ';
1216 if ( $this->isContentOverridden ) {
1220 throw new LogicException(
1222 .
' is not supported after calling setContent(). Use setRevisions() instead.'
1226 $out = $this->getOutput();
1227 $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1228 # Add "current version as of X" title
1229 $out->addHTML(
"<hr class='diff-hr' id='mw-oldid' />
1230 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1231 # Page content may be handled by a hooked call instead...
1232 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1233 $this->loadNewText();
1234 if ( !$this->mNewPage ) {
1240 if ( $this->hasNewRevisionLoadError() ) {
1245 $out->setRevisionId( $this->mNewid );
1246 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1247 $out->getMetadata()->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1248 $out->setArticleFlag(
true );
1250 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1251 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1257 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1261 $wikiPage = $this->getWikiPage();
1264 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1267 $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
1268 $parserOptions->setRenderReason(
'diff-page' );
1270 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
1271 $status = $parserOutputAccess->getParserOutput(
1274 $this->mNewRevisionRecord,
1277 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK =>
true,
1279 ParserOutputAccess::OPT_LINKS_UPDATE =>
true,
1282 if ( $status->isOK() ) {
1283 $parserOutput = $status->getValue();
1285 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1286 $this, $out, $parserOutput, $wikiPage )
1288 $editLinks = $this->mNewRevisionRecord->isCurrent()
1289 && $this->getAuthority()->probablyCan(
1291 $this->mNewRevisionRecord->getPage() );
1292 if ( !$editLinks ) {
1293 $parserOptions->setSuppressSectionEditLinks();
1295 $out->addParserOutput( $parserOutput, $parserOptions, [
1296 'absoluteURLs' => $this->slotDiffOptions[
'expand-url'] ??
false
1300 $out->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
1301 foreach ( $status->getMessages() as $msg ) {
1302 $out->addHTML( Html::errorBox(
1303 $this->msg( $msg )->parse()
1321 public function showDiff( $otitle, $ntitle, $notice =
'' ) {
1323 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1325 $diff = $this->getDiff( $otitle, $ntitle, $notice );
1326 if ( $diff ===
false ) {
1327 $this->showMissingRevision();
1331 $this->showDiffStyle();
1332 if ( $this->slotDiffOptions[
'expand-url'] ??
false ) {
1333 $diff = Linker::expandLocalLinks( $diff );
1335 $this->getOutput()->addHTML( $diff );
1343 if ( !$this->isSlotDiffRenderer ) {
1344 $this->getOutput()->addModules(
'mediawiki.diff' );
1345 $this->getOutput()->addModuleStyles( [
1346 'mediawiki.interface.helpers.styles',
1347 'mediawiki.diff.styles'
1349 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1350 $slotDiffRenderer->addModules( $this->getOutput() );
1364 public function getDiff( $otitle, $ntitle, $notice =
'' ) {
1365 $body = $this->getDiffBody();
1366 if ( $body ===
false ) {
1370 $multi = $this->getMultiNotice();
1372 if ( $body ===
'' ) {
1373 $notice .=
'<div class="mw-diff-empty">' .
1374 $this->msg(
'diff-empty' )->parse() .
1378 if ( $this->cacheHitKey !==
null ) {
1379 $body .=
"\n<!-- diff cache key " . htmlspecialchars( $this->cacheHitKey ) .
" -->\n";
1382 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1385 private function incrementStats(
string $cacheStatus ): void {
1387 $stats->getCounter(
'diff_cache_total' )
1388 ->setLabel(
'status', $cacheStatus )
1398 $this->mCacheHit =
true;
1400 if ( !$this->isContentOverridden ) {
1401 if ( !$this->loadRevisionData() ) {
1403 } elseif ( $this->mOldRevisionRecord &&
1404 !$this->mOldRevisionRecord->userCan(
1405 RevisionRecord::DELETED_TEXT,
1406 $this->getAuthority()
1410 } elseif ( $this->mNewRevisionRecord &&
1411 !$this->mNewRevisionRecord->userCan(
1412 RevisionRecord::DELETED_TEXT,
1413 $this->getAuthority()
1418 if ( $this->mOldRevisionRecord ===
false || (
1419 $this->mOldRevisionRecord &&
1420 $this->mNewRevisionRecord &&
1421 $this->mOldRevisionRecord->getId() &&
1422 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1424 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1432 $services = MediaWikiServices::getInstance();
1433 $cache = $services->getMainWANObjectCache();
1434 $stats = $services->getStatsdDataFactory();
1435 if ( $this->mOldid && $this->mNewid ) {
1436 $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1439 if ( !$this->mRefreshCache ) {
1440 $difftext = $cache->get( $key );
1441 if ( is_string( $difftext ) ) {
1442 $this->incrementStats(
'hit' );
1443 $difftext = $this->localiseDiff( $difftext );
1444 $this->cacheHitKey = $key;
1449 $this->mCacheHit =
false;
1450 $this->cacheHitKey =
null;
1453 if ( !$this->loadText() ) {
1460 $slotContents = $this->getSlotContents();
1461 foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1463 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role][
'old'],
1464 $slotContents[$role][
'new'] );
1468 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1471 $difftext .= $this->getSlotHeader( $slotTitle );
1473 $difftext .= $slotDiff;
1477 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1478 $this->incrementStats(
'uncacheable' );
1479 } elseif ( $key !==
false ) {
1480 $this->incrementStats(
'miss' );
1481 $cache->set( $key, $difftext, 7 * 86400 );
1483 $this->incrementStats(
'uncacheable' );
1486 $difftext = $this->localiseDiff( $difftext );
1498 $diffRenderers = $this->getSlotDiffRenderers();
1499 if ( !isset( $diffRenderers[$role] ) ) {
1503 $slotContents = $this->getSlotContents();
1505 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role][
'old'],
1506 $slotContents[$role][
'new'] );
1510 if ( $slotDiff ===
'' ) {
1514 if ( $role !== SlotRecord::MAIN ) {
1517 $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1520 return $this->localiseDiff( $slotDiff );
1531 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1532 $userLang = $this->getLanguage()->getHtmlCode();
1533 return Html::rawElement(
'tr', [
'class' =>
'mw-diff-slot-header',
'lang' => $userLang ],
1534 Html::element(
'th', [
'colspan' => $columnCount ], $headerText ) );
1545 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1546 $userLang = $this->getLanguage()->getHtmlCode();
1547 return Html::rawElement(
'tr', [
'class' =>
'mw-diff-slot-error',
'lang' => $userLang ],
1548 Html::rawElement(
'td', [
'colspan' => $columnCount ], $errorText ) );
1565 if ( !$this->mOldid || !$this->mNewid ) {
1566 throw new BadMethodCallException(
'mOldid and mNewid must be set to get diff cache key.' );
1572 "old-{$this->mOldid}",
1573 "rev-{$this->mNewid}"
1577 if ( !$this->isSlotDiffRenderer ) {
1578 foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1579 $extraKeys = array_merge( $extraKeys, $slotDiffRenderer->getExtraCacheKeys() );
1582 ksort( $extraKeys );
1583 return array_merge( $params, array_values( $extraKeys ) );
1597 $this->mOldid = 123456789;
1598 $this->mNewid = 987654321;
1601 $params = $this->getDiffBodyCacheKeyParams();
1610 $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1611 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1612 $params = array_slice( $params, count( $standardParams ) );
1629 $validatedOptions = [];
1630 if ( isset( $options[
'diff-type'] )
1631 && $this->getTextDiffer()->hasFormat( $options[
'diff-type'] )
1633 $validatedOptions[
'diff-type'] = $options[
'diff-type'];
1635 if ( !empty( $options[
'expand-url'] ) ) {
1636 $validatedOptions[
'expand-url'] =
true;
1638 if ( !empty( $options[
'inline-toggle'] ) ) {
1639 $validatedOptions[
'inline-toggle'] =
true;
1641 $this->slotDiffOptions = $validatedOptions;
1652 $this->extraQueryParams = $params;
1669 $slotDiffRenderer = $new->
getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1672 && $this->isSlotDiffRenderer
1678 throw new LogicException( get_class( $this ) .
': could not maintain backwards compatibility. '
1679 .
'Please use a SlotDiffRenderer.' );
1681 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1697 $slotDiffRenderer = $this->contentHandlerFactory
1699 ->getSlotDiffRenderer( $this->getContext() );
1703 throw new LogicException(
'The slot diff renderer for text content should be a '
1704 .
'TextSlotDiffRenderer subclass' );
1706 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1716 $differenceEngine =
new self;
1717 $engine = $differenceEngine->getTextDiffer()->getEngineForFormat(
'table' );
1718 if ( $engine ===
'external' ) {
1719 return MediaWikiServices::getInstance()->getMainConfig()
1720 ->get( MainConfigNames::ExternalDiffEngine );
1734 protected function debug( $generator =
"internal" ) {
1735 if ( !$this->enableDebugComment ) {
1738 $data = [ $generator ];
1739 if ( $this->getConfig()->
get( MainConfigNames::ShowHostnames ) ) {
1742 $data[] = ConvertibleTimestamp::now( TS::DB );
1744 return "<!-- diff generator: " .
1745 implode(
" ", array_map(
"htmlspecialchars", $data ) ) .
1752 private function getDebugString() {
1753 $engine = self::getEngine();
1754 if ( $engine ===
'wikidiff2' ) {
1755 return $this->debug(
'wikidiff2' );
1756 } elseif ( $engine ===
'php' ) {
1757 return $this->debug(
'native PHP' );
1759 return $this->debug(
"external $engine" );
1769 private function localiseDiff( $text ) {
1770 return $this->getTextDiffer()->localize( $this->getTextDiffFormat(), $text );
1782 return preg_replace_callback(
'/<!--LINE (\d+)-->/',
1784 if (
$matches[1] ===
'1' && $this->mReducedLineNumbers ) {
1787 return $this->msg(
'lineno' )->numParams(
$matches[1] )->escaped();
1799 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1800 || !$this->mOldPage || !$this->mNewPage
1801 || !$this->mOldPage->equals( $this->mNewPage )
1802 || $this->mOldRevisionRecord->getId() ===
null
1803 || $this->mNewRevisionRecord->getId() ===
null
1805 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1806 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1811 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1812 $oldRevRecord = $this->mNewRevisionRecord;
1813 $newRevRecord = $this->mOldRevisionRecord;
1815 $oldRevRecord = $this->mOldRevisionRecord;
1816 $newRevRecord = $this->mNewRevisionRecord;
1822 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1823 $this->mNewPage->getArticleID(),
1829 if ( count( $revisionIdList ) > 0 ) {
1830 foreach ( $revisionIdList as $revisionId ) {
1831 $revision = $this->revisionStore->getRevisionById( $revisionId );
1832 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1837 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1839 $newRevUserForGender =
'[HIDDEN]';
1842 $users = $this->revisionStore->getAuthorsBetween(
1843 $this->mNewPage->getArticleID(),
1849 $numUsers = count( $users );
1851 $newRevUser = $newRevRecord->
getUser( RevisionRecord::RAW );
1852 $newRevUserText = $newRevUser ? $newRevUser->getName() :
'';
1853 $newRevUserSafe = $newRevRecord->
getUser(
1854 RevisionRecord::FOR_THIS_USER,
1855 $this->getAuthority()
1857 $newRevUserForGender = $newRevUserSafe ? $newRevUserSafe->getName() :
'[HIDDEN]';
1858 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1861 }
catch ( InvalidArgumentException ) {
1865 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit, $newRevUserForGender );
1882 if ( $numUsers === 0 ) {
1883 $msg =
'diff-multi-sameuser';
1885 ->numParams( $numEdits, $numUsers )
1886 ->params( $lastUser )
1888 } elseif ( $numUsers > $limit ) {
1889 $msg =
'diff-multi-manyusers';
1892 $msg =
'diff-multi-otherusers';
1895 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1903 if ( !$revRecord->
userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1920 $lang = $this->getLanguage();
1921 $user = $this->getUser();
1923 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1924 $dateofrev = $lang->userDate( $revtimestamp, $user );
1925 $timeofrev = $lang->userTime( $revtimestamp, $user );
1927 $header = $this->msg(
1928 $rev->
isCurrent() ?
'currentrev-asof' :
'revisionasof',
1934 if ( $complete !==
'complete' ) {
1935 return $header->escaped();
1940 if ( $this->userCanEdit( $rev ) ) {
1941 $header = $this->linkRenderer->makeKnownLink(
1945 [
'oldid' => $rev->
getId() ]
1947 $editQuery = [
'action' =>
'edit' ];
1949 $editQuery[
'oldid'] = $rev->
getId();
1952 $key = $this->getAuthority()->probablyCan(
'edit', $rev->
getPage() ) ?
'editold' :
'viewsourceold';
1953 $msg = $this->msg( $key )->text();
1954 $editLink = $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery );
1955 $header .=
' ' . Html::rawElement(
1957 [
'class' =>
'mw-diff-edit' ],
1961 $header = $header->escaped();
1965 $header .= Html::element(
'span',
1967 'class' =>
'mw-diff-timestamp',
1968 'data-timestamp' =>
wfTimestamp( TS::ISO_8601, $revtimestamp ),
1972 if ( $rev->
isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1973 return Html::rawElement(
1975 [
'class' => Linker::getRevisionDeletedClass( $rev ) ],
1995 public function addHeader( $diff, $otitle, $ntitle, $multi =
'', $notice =
'' ) {
1998 $header = Html::openElement(
'table', [
2004 'diff-type-' . $this->getTextDiffFormat(),
2008 'diff-contentalign-' . $this->getDiffLang()->alignStart(),
2013 'diff-editfont-' . $this->userOptionsLookup->getOption(
2018 'data-mw-interface' =>
'',
2020 $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
2022 if ( !$diff && !$otitle ) {
2024 <tr class=\"diff-title\" lang=\"{$userLang}\">
2025 <td class=\"diff-ntitle\">{$ntitle}</td>
2031 <col class=\"diff-marker\" />
2032 <col class=\"diff-content\" />
2033 <col class=\"diff-marker\" />
2034 <col class=\"diff-content\" />";
2041 if ( $otitle || $ntitle ) {
2043 $deletedClass =
'diff-side-deleted';
2044 $addedClass =
'diff-side-added';
2046 <tr class=\"diff-title\" lang=\"{$userLang}\">
2047 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
2048 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
2053 if ( $multi !=
'' ) {
2054 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
2055 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
2057 if ( $notice !=
'' ) {
2058 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
2059 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
2062 return $header . $diff .
"</table>";
2073 $this->mOldContent = $oldContent;
2074 $this->mNewContent = $newContent;
2076 $this->mTextLoaded = 2;
2077 $this->mRevisionsLoaded =
true;
2078 $this->isContentOverridden =
true;
2079 $this->slotDiffRenderers =
null;
2090 if ( $oldRevision ) {
2091 $this->mOldRevisionRecord = $oldRevision;
2092 $this->mOldid = $oldRevision->
getId();
2093 $this->mOldPage = Title::newFromPageIdentity( $oldRevision->
getPage() );
2096 $this->mOldContent = $oldRevision->
getContent( SlotRecord::MAIN,
2097 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2098 if ( !$this->mOldContent ) {
2099 $this->addRevisionLoadError(
'old' );
2102 $this->mOldPage =
null;
2103 $this->mOldRevisionRecord = $this->mOldid =
false;
2105 $this->mNewRevisionRecord = $newRevision;
2106 $this->mNewid = $newRevision->
getId();
2107 $this->mNewPage = Title::newFromPageIdentity( $newRevision->
getPage() );
2108 $this->mNewContent = $newRevision->
getContent( SlotRecord::MAIN,
2109 RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
2110 if ( !$this->mNewContent ) {
2111 $this->addRevisionLoadError(
'new' );
2114 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded =
true;
2115 $this->mTextLoaded = $oldRevision ? 2 : 1;
2116 $this->isContentOverridden =
false;
2117 $this->slotDiffRenderers =
null;
2127 $this->mDiffLang = $lang;
2143 if ( $new ===
'prev' ) {
2145 $newid = intval( $old );
2147 $newRev = $this->revisionStore->getRevisionById( $newid );
2149 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
2151 $oldid = $oldRev->getId();
2154 } elseif ( $new ===
'next' ) {
2156 $oldid = intval( $old );
2158 $oldRev = $this->revisionStore->getRevisionById( $oldid );
2160 $newRev = $this->revisionStore->getNextRevision( $oldRev );
2162 $newid = $newRev->getId();
2166 $oldid = intval( $old );
2167 $newid = intval( $new );
2171 return [ $oldid, $newid ];
2174 private function loadRevisionIds() {
2175 if ( $this->mRevisionsIdsLoaded ) {
2179 $this->mRevisionsIdsLoaded =
true;
2181 $old = $this->mOldid;
2182 $new = $this->mNewid;
2184 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2185 if ( $new ===
'next' && $this->mNewid ===
false ) {
2186 # if no result, NewId points to the newest old revision. The only newer
2187 # revision is cur, which is "0".
2191 $this->hookRunner->onNewDifferenceEngine(
2193 $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2210 if ( $this->mRevisionsLoaded ) {
2211 return $this->isContentOverridden ||
2212 ( $this->mOldRevisionRecord !==
null && $this->mNewRevisionRecord !== null );
2216 $this->mRevisionsLoaded =
true;
2218 $this->loadRevisionIds();
2221 if ( $this->mNewid ) {
2222 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2224 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2228 $this->mOldRevisionRecord =
false;
2229 if ( $this->mOldid ) {
2230 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2231 } elseif ( $this->mOldid === 0 && $this->mNewRevisionRecord instanceof
RevisionRecord ) {
2232 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2234 $this->mOldid = $revRecord ? $revRecord->
getId() :
false;
2235 $this->mOldRevisionRecord = $revRecord ??
false;
2238 if ( $this->mOldRevisionRecord ===
null || $this->mNewRevisionRecord ===
null ) {
2243 $this->mNewid = $this->mNewRevisionRecord->
getId();
2244 $this->mNewPage = $this->mNewid ?
2245 Title::newFromPageIdentity( $this->mNewRevisionRecord->getPage() ) :
2248 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2249 $this->mOldPage = Title::newFromPageIdentity( $this->mOldRevisionRecord->getPage() );
2251 $this->mOldPage =
null;
2255 $dbr = $this->dbProvider->getReplicaDatabase();
2256 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2257 if ( $this->mOldid !==
false ) {
2258 $tagIds = $dbr->newSelectQueryBuilder()
2259 ->select(
'ct_tag_id' )
2260 ->from(
'change_tag' )
2261 ->where( [
'ct_rev_id' => $this->mOldid ] )
2262 ->caller( __METHOD__ )->fetchFieldValues();
2264 foreach ( $tagIds as $tagId ) {
2266 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2271 $this->mOldTags = implode(
',', $tags );
2273 $this->mOldTags =
false;
2276 $tagIds = $dbr->newSelectQueryBuilder()
2277 ->select(
'ct_tag_id' )
2278 ->from(
'change_tag' )
2279 ->where( [
'ct_rev_id' => $this->mNewid ] )
2280 ->caller( __METHOD__ )->fetchFieldValues();
2282 foreach ( $tagIds as $tagId ) {
2284 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2289 $this->mNewTags = implode(
',', $tags );
2302 if ( $this->mTextLoaded == 2 ) {
2303 return $this->loadRevisionData() &&
2304 ( $this->mOldRevisionRecord ===
false || $this->mOldContent )
2305 && $this->mNewContent;
2309 $this->mTextLoaded = 2;
2311 if ( !$this->loadRevisionData() ) {
2315 if ( $this->mOldRevisionRecord ) {
2316 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2318 RevisionRecord::FOR_THIS_USER,
2319 $this->getAuthority()
2321 if ( $this->mOldContent ===
null ) {
2326 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2328 RevisionRecord::FOR_THIS_USER,
2329 $this->getAuthority()
2331 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2332 if ( $this->mNewContent ===
null ) {
2345 if ( $this->mTextLoaded >= 1 ) {
2346 return $this->loadRevisionData();
2349 $this->mTextLoaded = 1;
2351 if ( !$this->loadRevisionData() ) {
2355 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2357 RevisionRecord::FOR_THIS_USER,
2358 $this->getAuthority()
2361 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2372 if ( $this->textDiffer ===
null ) {
2374 $this->getContext(),
2375 $this->getDiffLang(),
2376 $this->getConfig()->
get( MainConfigNames::DiffEngine ),
2377 $this->getConfig()->
get( MainConfigNames::ExternalDiffEngine ),
2378 $this->getConfig()->
get( MainConfigNames::Wikidiff2Options )
2381 return $this->textDiffer;
2391 return $this->getTextDiffer()->getFormats();
2401 return $this->slotDiffOptions[
'diff-type'] ??
'table';
2409 private function getMobileFooter( ?
RevisionRecord $newRevRecord, array $formattedRevisionTools ): string {
2410 $this->getOutput()->addModuleStyles( [
'codex-styles' ] );
2411 $summary = Html::rawElement(
2413 [
"class" =>
"cdx-accordion--has-icon" ],
2416 [
"class" =>
"cdx-accordion__header" ],
2419 [
'class' =>
'cdx-accordion__header__title' ],
2420 Linker::revUserTools( $newRevRecord, !$this->unhide )
2425 if ( $this->mNewRevisionRecord->isCurrent() &&
2426 $this->getAuthority()->probablyCan(
'rollback', $this->mNewPage )
2428 $rollbackLink = Linker::generateRollback(
2429 $this->mNewRevisionRecord,
2430 $this->getContext(),
2434 $user = $newRevRecord->
getUser();
2436 if ( $user !==
null ) {
2437 $userGroups = $this->userGroupManager->getUserGroups( $user );
2439 $userGroupCount = count( $userGroups );
2440 $userEditCount = $user ===
null ?
'' : $this->getUserEditCount( $user );
2441 $userGroupList = [];
2442 foreach ( $userGroups as $userGroup ) {
2443 $userGroupList[] = $this->msg(
"group-$userGroup" )->escaped();
2445 if ( $userGroupCount == 0 ) {
2446 $userGroupsPopover =
'';
2448 $popover = Html::rawElement(
2450 [
'class' =>
'cdx-popover mw-diff-usergroups-popover',
'role' =>
'tooltip' ],
2453 [
'class' =>
'cdx-popover__body' ],
2454 $this->
msg(
'diff-usergroups-list', $this->getLanguage()->commaList( $userGroupList ) )->escaped()
2455 ) . Html::rawElement(
'div', [
'class' =>
'cdx-popover__arrow' ] )
2457 $popoverTrigger = Html::element(
2459 [
'class' =>
'cdx-popover-trigger cdx-button__icon cdx-icon cdx-icon--info ',
'tabindex' =>
'0' ],
2461 $userGroupsPopover = Html::rawElement(
2463 [
'class' =>
'mw-diff-usermetadata' ],
2465 Html::element(
'span',
2466 [
'class' =>
'mw-diff-usergroups-popover-text' ],
2467 $this->
msg(
'diff-usergroups', $userGroupCount )->text()
2471 [
'class' =>
'mw-diff-usergroups-popover-wrapper' ],
2472 $popoverTrigger . $popover
2477 $content = Html::rawElement(
2479 [
"class" =>
"cdx-accordion__content" ],
2483 . implode(
'', $formattedRevisionTools )
2485 return Html::rawElement(
2487 [
"class" =>
"mw-diff-new-mobile-footer-accordion cdx-accordion cdx-accordion--separation-minimal" ],
2495class_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.
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.