68 use DeprecationHelper;
76 private const DIFF_VERSION =
'1.12';
104 private $mOldRevisionRecord;
114 private $mNewRevisionRecord;
147 private $mOldContent;
154 private $mNewContent;
160 private $mRevisionsIdsLoaded =
false;
163 protected $mRevisionsLoaded =
false;
166 protected $mTextLoaded = 0;
176 protected $isContentOverridden =
false;
179 protected $mCacheHit =
false;
187 public $enableDebugComment =
false;
192 protected $mReducedLineNumbers =
false;
195 protected $mMarkPatrolledLink =
null;
198 protected $unhide =
false;
201 protected $mRefreshCache =
false;
212 protected $isSlotDiffRenderer =
false;
218 private $slotDiffOptions = [];
228 private $contentHandlerFactory;
233 private $revisionStore;
239 private $wikiPageFactory;
242 private $userOptionsLookup;
245 private $commentFormatter;
248 private $revisionLoadErrors = [];
260 public function __construct( $context =
null, $old = 0, $new = 0, $rcid = 0,
261 $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;
282 $this->unhide = $unhide;
284 $services = MediaWikiServices::getInstance();
285 $this->linkRenderer = $services->getLinkRenderer();
286 $this->contentHandlerFactory = $services->getContentHandlerFactory();
287 $this->revisionStore = $services->getRevisionStore();
288 $this->hookRunner =
new HookRunner( $services->getHookContainer() );
289 $this->wikiPageFactory = $services->getWikiPageFactory();
290 $this->userOptionsLookup = $services->getUserOptionsLookup();
291 $this->commentFormatter = $services->getCommentFormatter();
299 if ( $this->isSlotDiffRenderer ) {
300 throw new LogicException( __METHOD__ .
' called in slot diff renderer mode' );
303 if ( $this->slotDiffRenderers ===
null ) {
309 $this->slotDiffRenderers = array_map(
function ( array $contents ) {
311 $content = $contents[
'new'] ?: $contents[
'old'];
312 return $content->getContentHandler()->getSlotDiffRenderer(
314 $this->slotDiffOptions
319 return $this->slotDiffRenderers;
329 $this->isSlotDiffRenderer =
true;
338 if ( $this->isContentOverridden ) {
340 SlotRecord::MAIN => [
'old' => $this->mOldContent,
'new' => $this->mNewContent ]
346 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
347 $oldSlots = $this->mOldRevisionRecord ?
348 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
354 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
357 foreach ( $roles as $role ) {
359 'old' => $this->loadSingleSlot(
360 $oldSlots[$role] ??
null,
363 'new' => $this->loadSingleSlot(
364 $newSlots[$role] ??
null,
370 if ( isset( $slots[SlotRecord::MAIN] ) ) {
371 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
383 private function loadSingleSlot( ?
SlotRecord $slot,
string $which ) {
390 $this->addRevisionLoadError( $which );
400 private function addRevisionLoadError( $which ) {
401 $this->revisionLoadErrors[] = $this->
msg( $which ===
'new'
402 ?
'difference-bad-new-revision' :
'difference-bad-old-revision'
413 return $this->revisionLoadErrors;
420 private function hasNewRevisionLoadError() {
421 foreach ( $this->revisionLoadErrors as $error ) {
422 if ( $error->getKey() ===
'difference-bad-new-revision' ) {
432 return parent::getTitle() ?: Title::makeTitle(
NS_SPECIAL,
'BadTitle/DifferenceEngine' );
442 $this->mReducedLineNumbers = $value;
451 # Default language in which the diff text is written.
452 $this->mDiffLang ??= $this->
getTitle()->getPageLanguage();
454 return $this->mDiffLang;
461 return $this->mCacheHit;
472 $this->loadRevisionIds();
474 return $this->mOldid;
484 $this->loadRevisionIds();
486 return $this->mNewid;
496 return $this->mOldRevisionRecord ?:
null;
505 return $this->mNewRevisionRecord;
517 if ( $this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
519 $arQuery = $this->revisionStore->getArchiveQueryInfo();
520 $row =
$dbr->selectRow(
522 array_merge( $arQuery[
'fields'], [
'ar_namespace',
'ar_title' ] ),
523 [
'ar_rev_id' => $id ],
529 $revRecord = $this->revisionStore->newRevisionFromArchiveRow( $row );
530 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
533 'target' =>
$title->getPrefixedText(),
552 return "[$link $id]";
558 private function showMissingRevision() {
559 $out = $this->getOutput();
562 if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
563 $missing[] = $this->deletedIdMarker( $this->mOldid );
565 if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
569 $out->setPageTitle( $this->
msg(
'errorpagetitle' ) );
570 $msg = $this->
msg(
'difference-missing-revision' )
571 ->params( $this->
getLanguage()->listToText( $missing ) )
572 ->numParams( count( $missing ) )
574 $out->addHTML( $msg );
585 $this->mNewRevisionRecord &&
586 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
589 $this->mOldRevisionRecord &&
590 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
602 $permStatus = PermissionStatus::newEmpty();
603 if ( $this->mNewPage ) {
604 $performer->
authorizeRead(
'read', $this->mNewPage, $permStatus );
606 if ( $this->mOldPage ) {
607 $performer->
authorizeRead(
'read', $this->mOldPage, $permStatus );
609 return $permStatus->toLegacyErrorArray();
619 ( $this->mOldRevisionRecord &&
620 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
621 ( $this->mNewRevisionRecord &&
622 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
640 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
641 RevisionRecord::DELETED_TEXT,
649 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
650 RevisionRecord::DELETED_TEXT,
671 # Allow frames except in certain special cases
673 $out->setPreventClickjacking(
false );
674 $out->setRobotPolicy(
'noindex,nofollow' );
677 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
680 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
681 $this->showMissingRevision();
694 $query = $this->slotDiffOptions;
695 # Carry over 'diffonly' param via navigation links
696 if ( $diffOnly != MediaWikiServices::getInstance()
697 ->getUserOptionsLookup()->getBoolOption( $user,
'diffonly' )
699 $query[
'diffonly'] = $diffOnly;
701 # Cascade unhide param in links for easy deletion browsing
702 if ( $this->unhide ) {
703 $query[
'unhide'] = 1;
706 # Check if one of the revisions is deleted/suppressed
713 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
714 # a diff between a version V and its previous version V' AND the version V
715 # is the first version of that article. In that case, V' does not exist.
716 if ( $this->mOldRevisionRecord ===
false ) {
717 if ( $this->mNewPage ) {
718 $out->setPageTitle( $this->
msg(
'difference-title', $this->mNewPage->getPrefixedText() ) );
723 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
725 $this->hookRunner->onDifferenceEngineViewHeader( $this );
727 if ( !$this->mOldPage || !$this->mNewPage ) {
730 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
731 $out->setPageTitle( $this->
msg(
'difference-title', $this->mNewPage->getPrefixedText() ) );
734 $out->setPageTitle( $this->
msg(
'difference-title-multipage',
735 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
736 $out->addSubtitle( $this->
msg(
'difference-multipage' ) );
740 if ( $samePage && $this->mNewPage &&
741 $this->
getAuthority()->probablyCan(
'edit', $this->mNewPage )
743 if ( $this->mNewRevisionRecord->isCurrent() &&
744 $this->getAuthority()->probablyCan(
'rollback', $this->mNewPage )
746 $rollbackLink = Linker::generateRollback(
747 $this->mNewRevisionRecord,
751 if ( $rollbackLink ) {
752 $out->setPreventClickjacking(
true );
753 $rollback =
"\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
757 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
758 $this->userCanEdit( $this->mNewRevisionRecord )
760 $undoLink = $this->linkRenderer->makeKnownLink(
762 $this->
msg(
'editundo' )->text(),
763 [
'title' => Linker::titleAttrib(
'undo' ) ],
766 'undoafter' => $this->mOldid,
767 'undo' => $this->mNewid
770 $revisionTools[
'mw-diff-undo'] = $undoLink;
773 # Make "previous revision link"
774 $hasPrevious = $samePage && $this->mOldPage &&
775 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
776 if ( $hasPrevious ) {
777 $prevlink = $this->linkRenderer->makeKnownLink(
779 $this->
msg(
'previousdiff' )->text(),
780 [
'id' =>
'differences-prevlink' ],
781 [
'diff' =>
'prev',
'oldid' => $this->mOldid ] + $query
784 $prevlink =
"\u{00A0}";
787 if ( $this->mOldRevisionRecord->isMinor() ) {
788 $oldminor = ChangesList::flag(
'minor' );
793 $oldRevRecord = $this->mOldRevisionRecord;
795 $ldel = $this->revisionDeleteLink( $oldRevRecord );
798 $oldRevComment = $this->commentFormatter
799 ->formatRevision( $oldRevRecord, $user, !$diffOnly, !$this->unhide );
801 if ( $oldRevComment ===
'' ) {
802 $defaultComment = $this->
msg(
'changeslist-nocomment' )->escaped();
803 $oldRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
806 $oldHeader =
'<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader .
'</strong></div>' .
807 '<div id="mw-diff-otitle2">' .
808 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
'</div>' .
809 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel .
'</div>' .
810 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] .
'</div>' .
811 '<div id="mw-diff-otitle4">' . $prevlink .
'</div>';
814 $this->hookRunner->onDifferenceEngineOldHeader(
815 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
818 $out->addJsConfigVars( [
819 'wgDiffOldId' => $this->mOldid,
820 'wgDiffNewId' => $this->mNewid,
823 # Make "next revision link"
824 # Skip next link on the top revision
825 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
826 $nextlink = $this->linkRenderer->makeKnownLink(
828 $this->
msg(
'nextdiff' )->text(),
829 [
'id' =>
'differences-nextlink' ],
830 [
'diff' =>
'next',
'oldid' => $this->mNewid ] + $query
833 $nextlink =
"\u{00A0}";
836 if ( $this->mNewRevisionRecord->isMinor() ) {
837 $newminor = ChangesList::flag(
'minor' );
842 # Handle RevisionDelete links...
843 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
845 # Allow extensions to define their own revision tools
846 $this->hookRunner->onDiffTools(
847 $this->mNewRevisionRecord,
849 $this->mOldRevisionRecord ?:
null,
853 $formattedRevisionTools = [];
855 foreach ( $revisionTools as $key => $tool ) {
856 $toolClass = is_string( $key ) ? $key :
'mw-diff-tool';
857 $element = Html::rawElement(
859 [
'class' => $toolClass ],
860 $this->
msg(
'parentheses' )->rawParams( $tool )->escaped()
862 $formattedRevisionTools[] = $element;
865 $newRevRecord = $this->mNewRevisionRecord;
868 ' ' . implode(
' ', $formattedRevisionTools );
870 $newRevComment = $this->commentFormatter->formatRevision( $newRevRecord, $user, !$diffOnly, !$this->unhide );
872 if ( $newRevComment ===
'' ) {
873 $defaultComment = $this->
msg(
'changeslist-nocomment' )->escaped();
874 $newRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
877 $newHeader =
'<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader .
'</strong></div>' .
878 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
880 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel .
'</div>' .
881 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] .
'</div>' .
882 '<div id="mw-diff-ntitle4">' . $nextlink . $this->
markPatrolledLink() .
'</div>';
885 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
886 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
887 $rdel, $this->unhide );
889 # If the diff cannot be shown due to a deleted revision, then output
890 # the diff header and links to unhide (if available)...
894 $out->addHTML( $this->
addHeader(
'', $oldHeader, $newHeader, $multi ) );
896 # Give explanation for why revision is not visible
897 $msg = [ $suppressed ?
'rev-suppressed-no-diff' :
'rev-deleted-no-diff' ];
899 # Give explanation and add a link to view the diff...
900 $query = $this->
getRequest()->appendQueryValue(
'unhide',
'1' );
902 $suppressed ?
'rev-suppressed-unhide-diff' :
'rev-deleted-unhide-diff',
903 $this->
getTitle()->getFullURL( $query )
906 $out->addHTML( Html::warningBox( $this->
msg( ...$msg )->parse(),
'plainlinks' ) );
907 # Otherwise, output a regular diff...
909 # Add deletion notice if the user is viewing deleted content
912 $msg = $suppressed ?
'rev-suppressed-diff-view' :
'rev-deleted-diff-view';
913 $notice = Html::warningBox( $this->
msg( $msg )->parse(),
'plainlinks' );
916 # Add an error if the content can't be loaded
919 $notice .= Html::warningBox( $msg->parse() );
922 $this->
showDiff( $oldHeader, $newHeader, $notice );
940 if ( $this->mMarkPatrolledLink ===
null ) {
943 if ( !$linkInfo || !$this->mNewPage ) {
944 $this->mMarkPatrolledLink =
'';
946 $this->mMarkPatrolledLink =
' <span class="patrollink" data-mw="interface">[' .
947 $this->linkRenderer->makeKnownLink(
949 $this->
msg(
'markaspatrolleddiff' )->text(),
952 'action' =>
'markpatrolled',
953 'rcid' => $linkInfo[
'rcid'],
957 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
958 $this->mMarkPatrolledLink, $linkInfo[
'rcid'] );
961 return $this->mMarkPatrolledLink;
978 $config->get( MainConfigNames::UseRCPatrol ) &&
980 $this->getAuthority()->probablyCan(
'patrol', $this->mNewPage ) &&
983 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
986 $change = RecentChange::newFromConds(
988 'rc_this_oldid' => $this->mNewid,
989 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
994 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
995 $rcid = $change->getAttribute(
'rc_id' );
1006 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1010 $this->
getOutput()->setPreventClickjacking(
true );
1011 if ( $this->
getAuthority()->isAllowed(
'writeapi' ) ) {
1012 $this->
getOutput()->addModules(
'mediawiki.misc-authed-curate' );
1015 return [
'rcid' => $rcid ];
1028 private function revisionDeleteLink(
RevisionRecord $revRecord ) {
1029 $link = Linker::getRevDeleteLink(
1034 if ( $link !==
'' ) {
1035 $link =
"\u{00A0}\u{00A0}\u{00A0}" . $link .
' ';
1047 if ( $this->isContentOverridden ) {
1051 throw new LogicException(
1053 .
' is not supported after calling setContent(). Use setRevisions() instead.'
1059 # Add "current version as of X" title
1060 $out->addHTML(
"<hr class='diff-hr' id='mw-oldid' />
1061 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1062 # Page content may be handled by a hooked call instead...
1063 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1065 if ( !$this->mNewPage ) {
1071 if ( $this->hasNewRevisionLoadError() ) {
1076 $out->setRevisionId( $this->mNewid );
1077 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1078 $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1079 $out->setArticleFlag(
true );
1081 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1082 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1088 if ( $this->
getTitle()->equals( $this->mNewPage ) ) {
1095 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1098 $parserOutput = $this->
getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1100 # WikiPage::getParserOutput() should not return false, but just in case
1101 if ( $parserOutput ) {
1103 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1104 $this, $out, $parserOutput, $wikiPage )
1106 $out->addParserOutput( $parserOutput, [
1107 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1108 && $this->getAuthority()->probablyCan(
1110 $this->mNewRevisionRecord->getPage()
1112 'absoluteURLs' => $this->slotDiffOptions[
'expand-url'] ??
false
1120 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1121 # Add redundant patrol link on bottom...
1133 if ( !$revRecord->
getId() ) {
1141 $parserOptions->setRenderReason(
'diff-page' );
1155 public function showDiff( $otitle, $ntitle, $notice =
'' ) {
1157 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1159 $diff = $this->
getDiff( $otitle, $ntitle, $notice );
1160 if ( $diff ===
false ) {
1161 $this->showMissingRevision();
1166 if ( $this->slotDiffOptions[
'expand-url'] ??
false ) {
1167 $diff = Linker::expandLocalLinks( $diff );
1177 if ( !$this->isSlotDiffRenderer ) {
1178 $this->
getOutput()->addModules(
'mediawiki.diff' );
1180 'mediawiki.interface.helpers.styles',
1181 'mediawiki.diff.styles'
1184 $slotDiffRenderer->addModules( $this->
getOutput() );
1198 public function getDiff( $otitle, $ntitle, $notice =
'' ) {
1200 if ( $body ===
false ) {
1206 if ( $body ===
'' ) {
1207 $notice .=
'<div class="mw-diff-empty">' .
1208 $this->
msg(
'diff-empty' )->parse() .
1212 return $this->
addHeader( $body, $otitle, $ntitle, $multi, $notice );
1221 $this->mCacheHit =
true;
1223 if ( !$this->isContentOverridden ) {
1226 } elseif ( $this->mOldRevisionRecord &&
1227 !$this->mOldRevisionRecord->userCan(
1228 RevisionRecord::DELETED_TEXT,
1229 $this->getAuthority()
1233 } elseif ( $this->mNewRevisionRecord &&
1234 !$this->mNewRevisionRecord->userCan(
1235 RevisionRecord::DELETED_TEXT,
1236 $this->getAuthority()
1241 if ( $this->mOldRevisionRecord ===
false || (
1242 $this->mOldRevisionRecord &&
1243 $this->mNewRevisionRecord &&
1244 $this->mOldRevisionRecord->getId() &&
1245 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1247 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1255 $services = MediaWikiServices::getInstance();
1256 $cache = $services->getMainWANObjectCache();
1257 $stats = $services->getStatsdDataFactory();
1258 if ( $this->mOldid && $this->mNewid ) {
1262 if ( !$this->mRefreshCache ) {
1263 $difftext = $cache->get( $key );
1264 if ( is_string( $difftext ) ) {
1265 $stats->updateCount(
'diff_cache.hit', 1 );
1266 $difftext = $this->localiseDiff( $difftext );
1267 $difftext .=
"\n<!-- diff cache key $key -->\n";
1273 $this->mCacheHit =
false;
1285 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role][
'old'],
1286 $slotContents[$role][
'new'] );
1287 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1292 $difftext .= $slotDiff;
1296 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1297 $stats->updateCount(
'diff_cache.uncacheable', 1 );
1298 } elseif ( $key !==
false ) {
1299 $stats->updateCount(
'diff_cache.miss', 1 );
1300 $cache->set( $key, $difftext, 7 * 86400 );
1302 $stats->updateCount(
'diff_cache.uncacheable', 1 );
1305 $difftext = $this->localiseDiff( $difftext );
1318 if ( !isset( $diffRenderers[$role] ) ) {
1323 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role][
'old'],
1324 $slotContents[$role][
'new'] );
1329 if ( $role !== SlotRecord::MAIN ) {
1335 return $this->localiseDiff( $slotDiff );
1347 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1349 return Html::rawElement(
'tr', [
'class' =>
'mw-diff-slot-header',
'lang' => $userLang ],
1350 Html::element(
'th', [
'colspan' => $columnCount ], $headerText ) );
1368 if ( !$this->mOldid || !$this->mNewid ) {
1369 throw new MWException(
'mOldid and mNewid must be set to get diff cache key.' );
1375 $engine ===
'php' ? false : $engine,
1377 "old-{$this->mOldid}",
1378 "rev-{$this->mNewid}"
1381 if ( $engine ===
'wikidiff2' ) {
1382 $params[] = phpversion(
'wikidiff2' );
1385 if ( !$this->isSlotDiffRenderer ) {
1387 $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1405 $this->mOldid = 123456789;
1406 $this->mNewid = 987654321;
1419 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1420 $params = array_slice( $params, count( $standardParams ) );
1431 $this->slotDiffOptions = $options;
1451 && $this->isSlotDiffRenderer
1457 throw new Exception( get_class( $this ) .
': could not maintain backwards compatibility. '
1458 .
'Please use a SlotDiffRenderer.' );
1460 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1476 $slotDiffRenderer = $this->contentHandlerFactory
1478 ->getSlotDiffRenderer( $this->
getContext() );
1482 throw new Exception(
'The slot diff renderer for text content should be a '
1483 .
'TextSlotDiffRenderer subclass' );
1485 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1495 $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1496 ->get( MainConfigNames::DiffEngine );
1497 $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1498 ->get( MainConfigNames::ExternalDiffEngine );
1500 if ( $diffEngine ===
null ) {
1501 $engines = [
'external',
'wikidiff2',
'php' ];
1503 $engines = [ $diffEngine ];
1506 $failureReason =
null;
1507 foreach ( $engines as $engine ) {
1508 switch ( $engine ) {
1510 if ( is_string( $externalDiffEngine ) ) {
1511 if ( is_executable( $externalDiffEngine ) ) {
1512 return $externalDiffEngine;
1514 $failureReason =
'ExternalDiffEngine config points to a non-executable';
1515 if ( $diffEngine ===
null ) {
1516 wfDebug(
"$failureReason, ignoring" );
1519 $failureReason =
'ExternalDiffEngine config is set to a non-string value';
1520 if ( $diffEngine ===
null && $externalDiffEngine ) {
1521 wfWarn(
"$failureReason, ignoring" );
1527 if ( function_exists(
'wikidiff2_do_diff' ) ) {
1530 $failureReason =
'wikidiff2 is not available';
1538 throw new DomainException(
'Invalid value for $wgDiffEngine: ' . $engine );
1541 throw new UnexpectedValueException(
"Cannot use diff engine '$engine': $failureReason" );
1553 if ( !$this->enableDebugComment ) {
1557 if ( $this->
getConfig()->
get( MainConfigNames::ShowHostnames ) ) {
1562 return "<!-- diff generator: " .
1563 implode(
" ", array_map(
"htmlspecialchars", $data ) ) .
1570 private function getDebugString() {
1571 $engine = self::getEngine();
1572 if ( $engine ===
'wikidiff2' ) {
1573 return $this->debug(
'wikidiff2' );
1574 } elseif ( $engine ===
'php' ) {
1575 return $this->
debug(
'native PHP' );
1577 return $this->
debug(
"external $engine" );
1587 private function localiseDiff( $text ) {
1589 if ( $this->
getEngine() ===
'wikidiff2' &&
1590 version_compare( phpversion(
'wikidiff2' ),
'1.5.1',
'>=' )
1592 $text = $this->addLocalisedTitleTooltips( $text );
1605 return preg_replace_callback(
1606 '/<!--LINE (\d+)-->/',
1608 if (
$matches[1] ===
'1' && $this->mReducedLineNumbers ) {
1611 return $this->
msg(
'lineno' )->numParams(
$matches[1] )->escaped();
1623 private function addLocalisedTitleTooltips( $text ) {
1624 return preg_replace_callback(
1625 '/class="mw-diff-movedpara-(left|right)"/',
1628 'diff-paragraph-moved-toold' :
1629 'diff-paragraph-moved-tonew';
1630 return $matches[0] .
' title="' . $this->msg( $key )->escaped() .
'"';
1644 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1645 || !$this->mOldPage || !$this->mNewPage
1646 || !$this->mOldPage->equals( $this->mNewPage )
1647 || $this->mOldRevisionRecord->getId() ===
null
1648 || $this->mNewRevisionRecord->getId() ===
null
1650 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1651 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1656 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1657 $oldRevRecord = $this->mNewRevisionRecord;
1658 $newRevRecord = $this->mOldRevisionRecord;
1660 $oldRevRecord = $this->mOldRevisionRecord;
1661 $newRevRecord = $this->mNewRevisionRecord;
1667 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1668 $this->mNewPage->getArticleID(),
1674 if ( count( $revisionIdList ) > 0 ) {
1675 foreach ( $revisionIdList as $revisionId ) {
1676 $revision = $this->revisionStore->getRevisionById( $revisionId );
1677 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1682 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1685 $users = $this->revisionStore->getAuthorsBetween(
1686 $this->mNewPage->getArticleID(),
1692 $numUsers = count( $users );
1694 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1695 $newRevUserText = $newRevUser ? $newRevUser->getName() :
'';
1696 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1699 }
catch ( InvalidArgumentException $e ) {
1703 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1719 if ( $numUsers === 0 ) {
1720 $msg =
'diff-multi-sameuser';
1721 } elseif ( $numUsers > $limit ) {
1722 $msg =
'diff-multi-manyusers';
1725 $msg =
'diff-multi-otherusers';
1728 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1736 if ( !$revRecord->
userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1756 $timestamp =
$lang->userTimeAndDate( $revtimestamp, $user );
1757 $dateofrev =
$lang->userDate( $revtimestamp, $user );
1758 $timeofrev =
$lang->userTime( $revtimestamp, $user );
1761 $rev->
isCurrent() ?
'currentrev-asof' :
'revisionasof',
1767 if ( $complete !==
'complete' ) {
1773 if ( $this->userCanEdit( $rev ) ) {
1774 $header = $this->linkRenderer->makeKnownLink(
1778 [
'oldid' => $rev->
getId() ]
1780 $editQuery = [
'action' =>
'edit' ];
1782 $editQuery[
'oldid'] = $rev->
getId();
1785 $key = $this->
getAuthority()->probablyCan(
'edit', $rev->
getPage() ) ?
'editold' :
'viewsourceold';
1786 $msg = $this->
msg( $key )->text();
1787 $editLink = $this->
msg(
'parentheses' )->rawParams(
1788 $this->linkRenderer->makeKnownLink(
$title, $msg, [], $editQuery ) )->escaped();
1789 $header .=
' ' . Html::rawElement(
1791 [
'class' =>
'mw-diff-edit' ],
1798 if ( $rev->
isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1799 return Html::rawElement(
1801 [
'class' => Linker::getRevisionDeletedClass( $rev ) ],
1821 public function addHeader( $diff, $otitle, $ntitle, $multi =
'', $notice =
'' ) {
1824 $header = Html::openElement(
'table', [
1827 'diff-contentalign-' . $this->
getDiffLang()->alignStart(),
1828 'diff-editfont-' . $this->userOptionsLookup->getOption(
1833 'data-mw' =>
'interface',
1835 $userLang = htmlspecialchars( $this->
getLanguage()->getHtmlCode() );
1837 if ( !$diff && !$otitle ) {
1839 <tr class=\"diff-title\" lang=\"{$userLang}\">
1840 <td class=\"diff-ntitle\">{$ntitle}</td>
1846 <col class=\"diff-marker\" />
1847 <col class=\"diff-content\" />
1848 <col class=\"diff-marker\" />
1849 <col class=\"diff-content\" />";
1856 if ( $otitle || $ntitle ) {
1858 $deletedClass =
'diff-side-deleted';
1859 $addedClass =
'diff-side-added';
1861 <tr class=\"diff-title\" lang=\"{$userLang}\">
1862 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
1863 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
1868 if ( $multi !=
'' ) {
1869 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
1870 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1872 if ( $notice !=
'' ) {
1873 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
1874 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1877 return $header . $diff .
"</table>";
1888 $this->mOldContent = $oldContent;
1889 $this->mNewContent = $newContent;
1891 $this->mTextLoaded = 2;
1892 $this->mRevisionsLoaded =
true;
1893 $this->isContentOverridden =
true;
1894 $this->slotDiffRenderers =
null;
1905 if ( $oldRevision ) {
1906 $this->mOldRevisionRecord = $oldRevision;
1907 $this->mOldid = $oldRevision->
getId();
1911 $this->mOldContent = $oldRevision->
getContent( SlotRecord::MAIN,
1912 RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
1913 if ( !$this->mOldContent ) {
1914 $this->addRevisionLoadError(
'old' );
1917 $this->mOldPage =
null;
1918 $this->mOldRevisionRecord = $this->mOldid =
false;
1920 $this->mNewRevisionRecord = $newRevision;
1921 $this->mNewid = $newRevision->
getId();
1923 $this->mNewContent = $newRevision->
getContent( SlotRecord::MAIN,
1924 RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
1925 if ( !$this->mNewContent ) {
1926 $this->addRevisionLoadError(
'new' );
1929 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded =
true;
1930 $this->mTextLoaded = $oldRevision ? 2 : 1;
1931 $this->isContentOverridden =
false;
1932 $this->slotDiffRenderers =
null;
1942 $this->mDiffLang =
$lang;
1958 if ( $new ===
'prev' ) {
1960 $newid = intval( $old );
1962 $newRev = $this->revisionStore->getRevisionById( $newid );
1964 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
1966 $oldid = $oldRev->getId();
1969 } elseif ( $new ===
'next' ) {
1971 $oldid = intval( $old );
1973 $oldRev = $this->revisionStore->getRevisionById( $oldid );
1975 $newRev = $this->revisionStore->getNextRevision( $oldRev );
1977 $newid = $newRev->getId();
1981 $oldid = intval( $old );
1982 $newid = intval( $new );
1986 return [ $oldid, $newid ];
1989 private function loadRevisionIds() {
1990 if ( $this->mRevisionsIdsLoaded ) {
1994 $this->mRevisionsIdsLoaded =
true;
1996 $old = $this->mOldid;
1997 $new = $this->mNewid;
1999 [ $this->mOldid, $this->mNewid ] = self::mapDiffPrevNext( $old, $new );
2000 if ( $new ===
'next' && $this->mNewid ===
false ) {
2001 # if no result, NewId points to the newest old revision. The only newer
2002 # revision is cur, which is "0".
2006 $this->hookRunner->onNewDifferenceEngine(
2008 $this->
getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2025 if ( $this->mRevisionsLoaded ) {
2026 return $this->isContentOverridden ||
2027 ( $this->mOldRevisionRecord !==
null && $this->mNewRevisionRecord !== null );
2031 $this->mRevisionsLoaded =
true;
2033 $this->loadRevisionIds();
2036 if ( $this->mNewid ) {
2037 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2039 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->
getTitle() );
2047 $this->mNewid = $this->mNewRevisionRecord->getId();
2048 $this->mNewPage = $this->mNewid ?
2049 Title::newFromLinkTarget( $this->mNewRevisionRecord->getPageAsLinkTarget() ) :
2053 $this->mOldRevisionRecord =
false;
2054 if ( $this->mOldid ) {
2055 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2056 } elseif ( $this->mOldid === 0 ) {
2057 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2059 $this->mOldid = $revRecord ? $revRecord->
getId() :
false;
2060 $this->mOldRevisionRecord = $revRecord ??
false;
2063 if ( $this->mOldRevisionRecord ===
null ) {
2067 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2068 $this->mOldPage = Title::newFromLinkTarget(
2069 $this->mOldRevisionRecord->getPageAsLinkTarget()
2072 $this->mOldPage =
null;
2077 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2078 if ( $this->mOldid !==
false ) {
2079 $tagIds =
$dbr->selectFieldValues(
2082 [
'ct_rev_id' => $this->mOldid ],
2086 foreach ( $tagIds as $tagId ) {
2088 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2093 $this->mOldTags = implode(
',', $tags );
2095 $this->mOldTags =
false;
2098 $tagIds =
$dbr->selectFieldValues(
2101 [
'ct_rev_id' => $this->mNewid ],
2105 foreach ( $tagIds as $tagId ) {
2107 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2112 $this->mNewTags = implode(
',', $tags );
2126 if ( $this->mTextLoaded == 2 ) {
2128 ( $this->mOldRevisionRecord ===
false || $this->mOldContent )
2129 && $this->mNewContent;
2133 $this->mTextLoaded = 2;
2139 if ( $this->mOldRevisionRecord ) {
2140 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2142 RevisionRecord::FOR_THIS_USER,
2145 if ( $this->mOldContent ===
null ) {
2150 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2152 RevisionRecord::FOR_THIS_USER,
2155 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2156 if ( $this->mNewContent ===
null ) {
2169 if ( $this->mTextLoaded >= 1 ) {
2173 $this->mTextLoaded = 1;
2179 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2181 RevisionRecord::FOR_THIS_USER,
2185 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
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 ...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getWikiPage()
Get the WikiPage object.
setContext(IContextSource $context)
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
Language StubUserLang $mDiffLang
bool $enableDebugComment
Set this to true to add debug info to the HTML output.
bool $unhide
Show rev_deleted content if allowed.
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
setSlotDiffOptions( $options)
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
SlotDiffRenderer[] null $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
getOldid()
Get the ID of old revision (left pane) of the diff.
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated.
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
loadNewText()
Load the text of the new revision, not the old one.
showDiffPage( $diffOnly=false)
loadText()
Load the text of the revisions, as well as revision data.
int string false null $mNewid
Revision ID for the new revision.
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
getPermissionErrors(Authority $performer)
Get the permission errors associated with the revisions for the current diff.
getDiffBody()
Get the diff table body, without header.
getTitle()
1.18 to override Title|null
getParserOutput(WikiPage $page, RevisionRecord $revRecord)
loadRevisionData()
Load revision metadata for the specified revisions.
static getEngine()
Process DiffEngine config and get a sensible, usable engine.
bool $mRevisionsLoaded
Have the revisions been loaded.
getNewRevision()
Get the right side of the diff.
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated,...
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
getSlotContents()
Get the old and new content objects for all slots.
string $mMarkPatrolledLink
Link to action=markpatrolled.
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
Title null $mNewPage
Title of new revision or null if the new revision does not exist or does not belong to a page.
bool $mCacheHit
Was the diff fetched from cache?
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
isUserAllowedToSeeRevisions(Authority $performer)
Checks whether the current user has permission for accessing the revisions of the diff.
int false null $mOldid
Revision ID for the old revision.
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
bool $mRefreshCache
Refresh the diff cache.
LinkRenderer $linkRenderer
getDiffBodyCacheKeyParams()
Get the cache key parameters.
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
getNewid()
Get the ID of new revision (right pane) of the diff.
renderNewRevision()
Show the new revision of the page.
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
shouldBeHiddenFromUser(Authority $performer)
Checks whether the diff should be hidden from the current user This is based on whether the user is a...
getRevisionHeader(RevisionRecord $rev, $complete='')
Get a header for a specified revision.
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds a JS module to the ...
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
Title null $mOldPage
Title of old revision or null if the old revision does not exist or does not belong to a page.
getDiffLang()
Get the language of the difference engine, defaults to page content language.
showDiffStyle()
Add style sheets for diff display.
markPatrolledLink()
Build a link to mark a change as patrolled.
getRevisionLoadErrors()
If errors were encountered while loading the revision contents, this will return an array of Messages...
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
getOldRevision()
Get the left side of the diff.
Base class for language-specific code.
A class containing constants representing the names of configuration variables.
Service for creating WikiPage objects.
The Message class deals with fetching and processing of interface message into a variety of formats.
Show an error when a user tries to do something they do not have the necessary permissions for.
Renders a diff for a single slot (that is, a diff between two content objects).
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,...
Renders a slot diff by doing a text diff on the native representation.
Base representation for an editable wiki page.
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
getParserOutput(?ParserOptions $parserOptions=null, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Base interface for representing page content.
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.
if(!isset( $args[0])) $lang