73 private const DIFF_VERSION =
'1.12';
101 private $mOldRevisionRecord;
111 private $mNewRevisionRecord;
144 private $mOldContent;
151 private $mNewContent;
157 private $mRevisionsIdsLoaded =
false;
215 private $slotDiffOptions = [];
225 private $contentHandlerFactory;
230 private $revisionStore;
236 private $wikiPageFactory;
239 private $userOptionsLookup;
242 private $commentFormatter;
245 private $revisionLoadErrors = [];
257 public function __construct( $context =
null, $old = 0, $new = 0, $rcid = 0,
258 $refreshCache =
false,
$unhide =
false
274 wfDebug(
"DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
276 $this->mOldid = $old;
277 $this->mNewid = $new;
278 $this->mRefreshCache = $refreshCache;
281 $services = MediaWikiServices::getInstance();
282 $this->linkRenderer = $services->getLinkRenderer();
283 $this->contentHandlerFactory = $services->getContentHandlerFactory();
284 $this->revisionStore = $services->getRevisionStore();
285 $this->hookRunner =
new HookRunner( $services->getHookContainer() );
286 $this->wikiPageFactory = $services->getWikiPageFactory();
287 $this->userOptionsLookup = $services->getUserOptionsLookup();
288 $this->commentFormatter = $services->getCommentFormatter();
296 if ( $this->isSlotDiffRenderer ) {
297 throw new LogicException( __METHOD__ .
' called in slot diff renderer mode' );
300 if ( $this->slotDiffRenderers ===
null ) {
306 $this->slotDiffRenderers = array_map(
function ( array $contents ) {
308 $content = $contents[
'new'] ?: $contents[
'old'];
309 return $content->getContentHandler()->getSlotDiffRenderer(
311 $this->slotDiffOptions
326 $this->isSlotDiffRenderer =
true;
335 if ( $this->isContentOverridden ) {
337 SlotRecord::MAIN => [
'old' => $this->mOldContent,
'new' => $this->mNewContent ]
343 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
344 $oldSlots = $this->mOldRevisionRecord ?
345 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
351 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
354 foreach ( $roles as $role ) {
356 'old' => $this->loadSingleSlot(
357 $oldSlots[$role] ??
null,
360 'new' => $this->loadSingleSlot(
361 $newSlots[$role] ??
null,
367 if ( isset( $slots[SlotRecord::MAIN] ) ) {
368 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
380 private function loadSingleSlot( ?
SlotRecord $slot,
string $which ) {
387 $this->addRevisionLoadError( $which );
397 private function addRevisionLoadError( $which ) {
398 $this->revisionLoadErrors[] = $this->
msg( $which ===
'new'
399 ?
'difference-bad-new-revision' :
'difference-bad-old-revision'
410 return $this->revisionLoadErrors;
417 private function hasNewRevisionLoadError() {
418 foreach ( $this->revisionLoadErrors as $error ) {
419 if ( $error->getKey() ===
'difference-bad-new-revision' ) {
439 $this->mReducedLineNumbers = $value;
448 # Default language in which the diff text is written.
449 $this->mDiffLang ??= $this->
getTitle()->getPageLanguage();
469 $this->loadRevisionIds();
481 $this->loadRevisionIds();
493 return $this->mOldRevisionRecord ?:
null;
502 return $this->mNewRevisionRecord;
514 if ( $this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
516 $arQuery = $this->revisionStore->getArchiveQueryInfo();
517 $row =
$dbr->selectRow(
519 array_merge( $arQuery[
'fields'], [
'ar_namespace',
'ar_title' ] ),
520 [
'ar_rev_id' => $id ],
526 $revRecord = $this->revisionStore->newRevisionFromArchiveRow( $row );
530 'target' =>
$title->getPrefixedText(),
549 return "[$link $id]";
555 private function showMissingRevision() {
559 if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
562 if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
566 $out->setPageTitle( $this->
msg(
'errorpagetitle' ) );
567 $msg = $this->
msg(
'difference-missing-revision' )
568 ->params( $this->
getLanguage()->listToText( $missing ) )
569 ->numParams( count( $missing ) )
571 $out->addHTML( $msg );
582 $this->mNewRevisionRecord &&
583 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
586 $this->mOldRevisionRecord &&
587 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
599 $permStatus = PermissionStatus::newEmpty();
600 if ( $this->mNewPage ) {
601 $performer->
authorizeRead(
'read', $this->mNewPage, $permStatus );
603 if ( $this->mOldPage ) {
604 $performer->
authorizeRead(
'read', $this->mOldPage, $permStatus );
606 return $permStatus->toLegacyErrorArray();
616 ( $this->mOldRevisionRecord &&
617 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
618 ( $this->mNewRevisionRecord &&
619 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
637 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
638 RevisionRecord::DELETED_TEXT,
646 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
647 RevisionRecord::DELETED_TEXT,
668 # Allow frames except in certain special cases
670 $out->setPreventClickjacking(
false );
671 $out->setRobotPolicy(
'noindex,nofollow' );
674 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
677 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
678 $this->showMissingRevision();
691 $query = $this->slotDiffOptions;
692 # Carry over 'diffonly' param via navigation links
693 if ( $diffOnly != MediaWikiServices::getInstance()
694 ->getUserOptionsLookup()->getBoolOption( $user,
'diffonly' )
696 $query[
'diffonly'] = $diffOnly;
698 # Cascade unhide param in links for easy deletion browsing
699 if ( $this->unhide ) {
700 $query[
'unhide'] = 1;
703 # Check if one of the revisions is deleted/suppressed
710 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
711 # a diff between a version V and its previous version V' AND the version V
712 # is the first version of that article. In that case, V' does not exist.
713 if ( $this->mOldRevisionRecord ===
false ) {
714 if ( $this->mNewPage ) {
715 $out->setPageTitle( $this->
msg(
'difference-title', $this->mNewPage->getPrefixedText() ) );
720 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
722 $this->hookRunner->onDifferenceEngineViewHeader( $this );
724 if ( !$this->mOldPage || !$this->mNewPage ) {
727 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
728 $out->setPageTitle( $this->
msg(
'difference-title', $this->mNewPage->getPrefixedText() ) );
731 $out->setPageTitle( $this->
msg(
'difference-title-multipage',
732 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
733 $out->addSubtitle( $this->
msg(
'difference-multipage' ) );
737 if ( $samePage && $this->mNewPage &&
738 $this->
getAuthority()->probablyCan(
'edit', $this->mNewPage )
740 if ( $this->mNewRevisionRecord->isCurrent() &&
741 $this->getAuthority()->probablyCan(
'rollback', $this->mNewPage )
743 $rollbackLink = Linker::generateRollback(
744 $this->mNewRevisionRecord,
748 if ( $rollbackLink ) {
749 $out->setPreventClickjacking(
true );
750 $rollback =
"\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
754 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
755 $this->userCanEdit( $this->mNewRevisionRecord )
757 $undoLink = $this->linkRenderer->makeKnownLink(
759 $this->
msg(
'editundo' )->text(),
760 [
'title' => Linker::titleAttrib(
'undo' ) ],
763 'undoafter' => $this->mOldid,
764 'undo' => $this->mNewid
767 $revisionTools[
'mw-diff-undo'] = $undoLink;
770 # Make "previous revision link"
771 $hasPrevious = $samePage && $this->mOldPage &&
772 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
773 if ( $hasPrevious ) {
774 $prevlink = $this->linkRenderer->makeKnownLink(
776 $this->
msg(
'previousdiff' )->text(),
777 [
'id' =>
'differences-prevlink' ],
778 [
'diff' =>
'prev',
'oldid' => $this->mOldid ] + $query
781 $prevlink =
"\u{00A0}";
784 if ( $this->mOldRevisionRecord->isMinor() ) {
790 $oldRevRecord = $this->mOldRevisionRecord;
792 $ldel = $this->revisionDeleteLink( $oldRevRecord );
795 $oldRevComment = $this->commentFormatter
796 ->formatRevision( $oldRevRecord, $user, !$diffOnly, !$this->unhide );
798 if ( $oldRevComment ===
'' ) {
799 $defaultComment = $this->
msg(
'changeslist-nocomment' )->escaped();
800 $oldRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
803 $oldHeader =
'<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader .
'</strong></div>' .
804 '<div id="mw-diff-otitle2">' .
805 Linker::revUserTools( $oldRevRecord, !$this->unhide ) .
'</div>' .
806 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel .
'</div>' .
807 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] .
'</div>' .
808 '<div id="mw-diff-otitle4">' . $prevlink .
'</div>';
811 $this->hookRunner->onDifferenceEngineOldHeader(
812 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
815 $out->addJsConfigVars( [
816 'wgDiffOldId' => $this->mOldid,
817 'wgDiffNewId' => $this->mNewid,
820 # Make "next revision link"
821 # Skip next link on the top revision
822 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
823 $nextlink = $this->linkRenderer->makeKnownLink(
825 $this->
msg(
'nextdiff' )->text(),
826 [
'id' =>
'differences-nextlink' ],
827 [
'diff' =>
'next',
'oldid' => $this->mNewid ] + $query
830 $nextlink =
"\u{00A0}";
833 if ( $this->mNewRevisionRecord->isMinor() ) {
839 # Handle RevisionDelete links...
840 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
842 # Allow extensions to define their own revision tools
843 $this->hookRunner->onDiffTools(
844 $this->mNewRevisionRecord,
846 $this->mOldRevisionRecord ?:
null,
850 $formattedRevisionTools = [];
852 foreach ( $revisionTools as $key => $tool ) {
853 $toolClass = is_string( $key ) ? $key :
'mw-diff-tool';
856 [
'class' => $toolClass ],
857 $this->
msg(
'parentheses' )->rawParams( $tool )->escaped()
859 $formattedRevisionTools[] = $element;
862 $newRevRecord = $this->mNewRevisionRecord;
865 ' ' . implode(
' ', $formattedRevisionTools );
867 $newRevComment = $this->commentFormatter->formatRevision( $newRevRecord, $user, !$diffOnly, !$this->unhide );
869 if ( $newRevComment ===
'' ) {
870 $defaultComment = $this->
msg(
'changeslist-nocomment' )->escaped();
871 $newRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
874 $newHeader =
'<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader .
'</strong></div>' .
875 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
877 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel .
'</div>' .
878 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] .
'</div>' .
879 '<div id="mw-diff-ntitle4">' . $nextlink . $this->
markPatrolledLink() .
'</div>';
882 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
883 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
884 $rdel, $this->unhide );
886 # If the diff cannot be shown due to a deleted revision, then output
887 # the diff header and links to unhide (if available)...
891 $out->addHTML( $this->
addHeader(
'', $oldHeader, $newHeader, $multi ) );
893 # Give explanation for why revision is not visible
894 $msg = [ $suppressed ?
'rev-suppressed-no-diff' :
'rev-deleted-no-diff' ];
896 # Give explanation and add a link to view the diff...
897 $query = $this->
getRequest()->appendQueryValue(
'unhide',
'1' );
899 $suppressed ?
'rev-suppressed-unhide-diff' :
'rev-deleted-unhide-diff',
900 $this->
getTitle()->getFullURL( $query )
904 # Otherwise, output a regular diff...
906 # Add deletion notice if the user is viewing deleted content
909 $msg = $suppressed ?
'rev-suppressed-diff-view' :
'rev-deleted-diff-view';
913 # Add an error if the content can't be loaded
919 $this->
showDiff( $oldHeader, $newHeader, $notice );
937 if ( $this->mMarkPatrolledLink ===
null ) {
940 if ( !$linkInfo || !$this->mNewPage ) {
941 $this->mMarkPatrolledLink =
'';
943 $this->mMarkPatrolledLink =
' <span class="patrollink" data-mw="interface">[' .
944 $this->linkRenderer->makeKnownLink(
946 $this->
msg(
'markaspatrolleddiff' )->text(),
949 'action' =>
'markpatrolled',
950 'rcid' => $linkInfo[
'rcid'],
954 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
955 $this->mMarkPatrolledLink, $linkInfo[
'rcid'] );
975 $config->get( MainConfigNames::UseRCPatrol ) &&
977 $this->getAuthority()->probablyCan(
'patrol', $this->mNewPage ) &&
985 'rc_this_oldid' => $this->mNewid,
991 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
992 $rcid = $change->getAttribute(
'rc_id' );
1003 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
1007 $this->
getOutput()->setPreventClickjacking(
true );
1008 if ( $this->
getAuthority()->isAllowed(
'writeapi' ) ) {
1009 $this->
getOutput()->addModules(
'mediawiki.misc-authed-curate' );
1012 return [
'rcid' => $rcid ];
1025 private function revisionDeleteLink(
RevisionRecord $revRecord ) {
1026 $link = Linker::getRevDeleteLink(
1031 if ( $link !==
'' ) {
1032 $link =
"\u{00A0}\u{00A0}\u{00A0}" . $link .
' ';
1044 if ( $this->isContentOverridden ) {
1048 throw new LogicException(
1050 .
' is not supported after calling setContent(). Use setRevisions() instead.'
1056 # Add "current version as of X" title
1057 $out->addHTML(
"<hr class='diff-hr' id='mw-oldid' />
1058 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1059 # Page content may be handled by a hooked call instead...
1060 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1062 if ( !$this->mNewPage ) {
1068 if ( $this->hasNewRevisionLoadError() ) {
1073 $out->setRevisionId( $this->mNewid );
1074 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
1075 $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1076 $out->setArticleFlag(
true );
1078 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1079 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1085 if ( $this->
getTitle()->equals( $this->mNewPage ) ) {
1092 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1095 $parserOutput = $this->
getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1097 # WikiPage::getParserOutput() should not return false, but just in case
1098 if ( $parserOutput ) {
1100 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1101 $this, $out, $parserOutput, $wikiPage )
1103 $out->addParserOutput( $parserOutput, [
1104 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1105 && $this->getAuthority()->probablyCan(
1107 $this->mNewRevisionRecord->getPage()
1109 'absoluteURLs' => $this->slotDiffOptions[
'expand-url'] ??
false
1117 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1118 # Add redundant patrol link on bottom...
1130 if ( !$revRecord->
getId() ) {
1138 $parserOptions->setRenderReason(
'diff-page' );
1152 public function showDiff( $otitle, $ntitle, $notice =
'' ) {
1154 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1156 $diff = $this->
getDiff( $otitle, $ntitle, $notice );
1157 if ( $diff ===
false ) {
1158 $this->showMissingRevision();
1163 if ( $this->slotDiffOptions[
'expand-url'] ??
false ) {
1164 $diff = Linker::expandLocalLinks( $diff );
1174 if ( !$this->isSlotDiffRenderer ) {
1175 $this->
getOutput()->addModules(
'mediawiki.diff' );
1177 'mediawiki.interface.helpers.styles',
1178 'mediawiki.diff.styles'
1181 $slotDiffRenderer->addModules( $this->
getOutput() );
1195 public function getDiff( $otitle, $ntitle, $notice =
'' ) {
1197 if ( $body ===
false ) {
1203 if ( $body ===
'' ) {
1204 $notice .=
'<div class="mw-diff-empty">' .
1205 $this->
msg(
'diff-empty' )->parse() .
1209 return $this->
addHeader( $body, $otitle, $ntitle, $multi, $notice );
1218 $this->mCacheHit =
true;
1220 if ( !$this->isContentOverridden ) {
1223 } elseif ( $this->mOldRevisionRecord &&
1224 !$this->mOldRevisionRecord->userCan(
1225 RevisionRecord::DELETED_TEXT,
1226 $this->getAuthority()
1230 } elseif ( $this->mNewRevisionRecord &&
1231 !$this->mNewRevisionRecord->userCan(
1232 RevisionRecord::DELETED_TEXT,
1233 $this->getAuthority()
1238 if ( $this->mOldRevisionRecord ===
false || (
1239 $this->mOldRevisionRecord &&
1240 $this->mNewRevisionRecord &&
1241 $this->mOldRevisionRecord->getId() &&
1242 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1244 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1252 $services = MediaWikiServices::getInstance();
1253 $cache = $services->getMainWANObjectCache();
1254 $stats = $services->getStatsdDataFactory();
1255 if ( $this->mOldid && $this->mNewid ) {
1259 if ( !$this->mRefreshCache ) {
1260 $difftext = $cache->get( $key );
1261 if ( is_string( $difftext ) ) {
1262 $stats->updateCount(
'diff_cache.hit', 1 );
1263 $difftext = $this->localiseDiff( $difftext );
1264 $difftext .=
"\n<!-- diff cache key $key -->\n";
1270 $this->mCacheHit =
false;
1282 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role][
'old'],
1283 $slotContents[$role][
'new'] );
1284 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1289 $difftext .= $slotDiff;
1293 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1294 $stats->updateCount(
'diff_cache.uncacheable', 1 );
1295 } elseif ( $key !==
false ) {
1296 $stats->updateCount(
'diff_cache.miss', 1 );
1297 $cache->set( $key, $difftext, 7 * 86400 );
1299 $stats->updateCount(
'diff_cache.uncacheable', 1 );
1302 $difftext = $this->localiseDiff( $difftext );
1315 if ( !isset( $diffRenderers[$role] ) ) {
1320 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role][
'old'],
1321 $slotContents[$role][
'new'] );
1326 if ( $role !== SlotRecord::MAIN ) {
1332 return $this->localiseDiff( $slotDiff );
1344 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1346 return Html::rawElement(
'tr', [
'class' =>
'mw-diff-slot-header',
'lang' => $userLang ],
1347 Html::element(
'th', [
'colspan' => $columnCount ], $headerText ) );
1365 if ( !$this->mOldid || !$this->mNewid ) {
1366 throw new MWException(
'mOldid and mNewid must be set to get diff cache key.' );
1372 $engine ===
'php' ? false : $engine,
1374 "old-{$this->mOldid}",
1375 "rev-{$this->mNewid}"
1378 if ( $engine ===
'wikidiff2' ) {
1379 $params[] = phpversion(
'wikidiff2' );
1382 if ( !$this->isSlotDiffRenderer ) {
1384 $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1402 $this->mOldid = 123456789;
1403 $this->mNewid = 987654321;
1416 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1417 $params = array_slice( $params, count( $standardParams ) );
1428 $this->slotDiffOptions = $options;
1448 && $this->isSlotDiffRenderer
1454 throw new Exception( get_class( $this ) .
': could not maintain backwards compatibility. '
1455 .
'Please use a SlotDiffRenderer.' );
1457 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1473 $slotDiffRenderer = $this->contentHandlerFactory
1475 ->getSlotDiffRenderer( $this->
getContext() );
1479 throw new Exception(
'The slot diff renderer for text content should be a '
1480 .
'TextSlotDiffRenderer subclass' );
1482 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1492 $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1493 ->get( MainConfigNames::DiffEngine );
1494 $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1495 ->get( MainConfigNames::ExternalDiffEngine );
1497 if ( $diffEngine ===
null ) {
1498 $engines = [
'external',
'wikidiff2',
'php' ];
1500 $engines = [ $diffEngine ];
1503 $failureReason =
null;
1504 foreach ( $engines as $engine ) {
1505 switch ( $engine ) {
1507 if ( is_string( $externalDiffEngine ) ) {
1508 if ( is_executable( $externalDiffEngine ) ) {
1509 return $externalDiffEngine;
1511 $failureReason =
'ExternalDiffEngine config points to a non-executable';
1512 if ( $diffEngine ===
null ) {
1513 wfDebug(
"$failureReason, ignoring" );
1516 $failureReason =
'ExternalDiffEngine config is set to a non-string value';
1517 if ( $diffEngine ===
null && $externalDiffEngine ) {
1518 wfWarn(
"$failureReason, ignoring" );
1524 if ( function_exists(
'wikidiff2_do_diff' ) ) {
1527 $failureReason =
'wikidiff2 is not available';
1535 throw new DomainException(
'Invalid value for $wgDiffEngine: ' . $engine );
1538 throw new UnexpectedValueException(
"Cannot use diff engine '$engine': $failureReason" );
1550 if ( !$this->enableDebugComment ) {
1554 if ( $this->
getConfig()->
get( MainConfigNames::ShowHostnames ) ) {
1559 return "<!-- diff generator: " .
1560 implode(
" ", array_map(
"htmlspecialchars", $data ) ) .
1567 private function getDebugString() {
1569 if ( $engine ===
'wikidiff2' ) {
1570 return $this->
debug(
'wikidiff2' );
1571 } elseif ( $engine ===
'php' ) {
1572 return $this->
debug(
'native PHP' );
1574 return $this->
debug(
"external $engine" );
1584 private function localiseDiff( $text ) {
1586 if ( $this->
getEngine() ===
'wikidiff2' &&
1587 version_compare( phpversion(
'wikidiff2' ),
'1.5.1',
'>=' )
1589 $text = $this->addLocalisedTitleTooltips( $text );
1602 return preg_replace_callback(
1603 '/<!--LINE (\d+)-->/',
1605 if (
$matches[1] ===
'1' && $this->mReducedLineNumbers ) {
1608 return $this->
msg(
'lineno' )->numParams( $matches[1] )->escaped();
1620 private function addLocalisedTitleTooltips( $text ) {
1621 return preg_replace_callback(
1622 '/class="mw-diff-movedpara-(left|right)"/',
1625 'diff-paragraph-moved-toold' :
1626 'diff-paragraph-moved-tonew';
1627 return $matches[0] .
' title="' . $this->
msg( $key )->escaped() .
'"';
1641 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1642 || !$this->mOldPage || !$this->mNewPage
1643 || !$this->mOldPage->equals( $this->mNewPage )
1644 || $this->mOldRevisionRecord->getId() ===
null
1645 || $this->mNewRevisionRecord->getId() ===
null
1647 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1648 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1653 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1654 $oldRevRecord = $this->mNewRevisionRecord;
1655 $newRevRecord = $this->mOldRevisionRecord;
1657 $oldRevRecord = $this->mOldRevisionRecord;
1658 $newRevRecord = $this->mNewRevisionRecord;
1663 $nEdits = $this->revisionStore->countRevisionsBetween(
1664 $this->mNewPage->getArticleID(),
1669 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1672 $users = $this->revisionStore->getAuthorsBetween(
1673 $this->mNewPage->getArticleID(),
1679 $numUsers = count( $users );
1681 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1682 $newRevUserText = $newRevUser ? $newRevUser->getName() :
'';
1683 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1686 }
catch ( InvalidArgumentException $e ) {
1706 if ( $numUsers === 0 ) {
1707 $msg =
'diff-multi-sameuser';
1708 } elseif ( $numUsers > $limit ) {
1709 $msg =
'diff-multi-manyusers';
1712 $msg =
'diff-multi-otherusers';
1715 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1723 if ( !$revRecord->
userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1743 $timestamp =
$lang->userTimeAndDate( $revtimestamp, $user );
1744 $dateofrev =
$lang->userDate( $revtimestamp, $user );
1745 $timeofrev =
$lang->userTime( $revtimestamp, $user );
1748 $rev->
isCurrent() ?
'currentrev-asof' :
'revisionasof',
1754 if ( $complete !==
'complete' ) {
1760 if ( $this->userCanEdit( $rev ) ) {
1761 $header = $this->linkRenderer->makeKnownLink(
1765 [
'oldid' => $rev->
getId() ]
1767 $editQuery = [
'action' =>
'edit' ];
1769 $editQuery[
'oldid'] = $rev->
getId();
1772 $key = $this->
getAuthority()->probablyCan(
'edit', $rev->
getPage() ) ?
'editold' :
'viewsourceold';
1773 $msg = $this->
msg( $key )->text();
1774 $editLink = $this->
msg(
'parentheses' )->rawParams(
1775 $this->linkRenderer->makeKnownLink(
$title, $msg, [], $editQuery ) )->escaped();
1778 [
'class' =>
'mw-diff-edit' ],
1785 if ( $rev->
isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1788 [
'class' => Linker::getRevisionDeletedClass( $rev ) ],
1808 public function addHeader( $diff, $otitle, $ntitle, $multi =
'', $notice =
'' ) {
1814 'diff-contentalign-' . $this->
getDiffLang()->alignStart(),
1815 'diff-editfont-' . $this->userOptionsLookup->getOption(
1820 'data-mw' =>
'interface',
1822 $userLang = htmlspecialchars( $this->
getLanguage()->getHtmlCode() );
1824 if ( !$diff && !$otitle ) {
1826 <tr class=\"diff-title\" lang=\"{$userLang}\">
1827 <td class=\"diff-ntitle\">{$ntitle}</td>
1833 <col class=\"diff-marker\" />
1834 <col class=\"diff-content\" />
1835 <col class=\"diff-marker\" />
1836 <col class=\"diff-content\" />";
1843 if ( $otitle || $ntitle ) {
1845 $deletedClass =
'diff-side-deleted';
1846 $addedClass =
'diff-side-added';
1848 <tr class=\"diff-title\" lang=\"{$userLang}\">
1849 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
1850 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
1855 if ( $multi !=
'' ) {
1856 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
1857 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1859 if ( $notice !=
'' ) {
1860 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
1861 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1864 return $header . $diff .
"</table>";
1875 $this->mOldContent = $oldContent;
1876 $this->mNewContent = $newContent;
1878 $this->mTextLoaded = 2;
1879 $this->mRevisionsLoaded =
true;
1880 $this->isContentOverridden =
true;
1881 $this->slotDiffRenderers =
null;
1892 if ( $oldRevision ) {
1893 $this->mOldRevisionRecord = $oldRevision;
1894 $this->mOldid = $oldRevision->
getId();
1898 $this->mOldContent = $oldRevision->
getContent( SlotRecord::MAIN,
1899 RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
1900 if ( !$this->mOldContent ) {
1901 $this->addRevisionLoadError(
'old' );
1904 $this->mOldPage =
null;
1905 $this->mOldRevisionRecord = $this->mOldid =
false;
1907 $this->mNewRevisionRecord = $newRevision;
1908 $this->mNewid = $newRevision->
getId();
1910 $this->mNewContent = $newRevision->
getContent( SlotRecord::MAIN,
1911 RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
1912 if ( !$this->mNewContent ) {
1913 $this->addRevisionLoadError(
'new' );
1916 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded =
true;
1917 $this->mTextLoaded = $oldRevision ? 2 : 1;
1918 $this->isContentOverridden =
false;
1919 $this->slotDiffRenderers =
null;
1929 $this->mDiffLang =
$lang;
1945 if ( $new ===
'prev' ) {
1947 $newid = intval( $old );
1949 $newRev = $this->revisionStore->getRevisionById( $newid );
1951 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
1953 $oldid = $oldRev->getId();
1956 } elseif ( $new ===
'next' ) {
1958 $oldid = intval( $old );
1960 $oldRev = $this->revisionStore->getRevisionById( $oldid );
1962 $newRev = $this->revisionStore->getNextRevision( $oldRev );
1964 $newid = $newRev->getId();
1968 $oldid = intval( $old );
1969 $newid = intval( $new );
1973 return [ $oldid, $newid ];
1976 private function loadRevisionIds() {
1977 if ( $this->mRevisionsIdsLoaded ) {
1981 $this->mRevisionsIdsLoaded =
true;
1987 if ( $new ===
'next' && $this->mNewid ===
false ) {
1988 # if no result, NewId points to the newest old revision. The only newer
1989 # revision is cur, which is "0".
1993 $this->hookRunner->onNewDifferenceEngine(
1995 $this->
getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2012 if ( $this->mRevisionsLoaded ) {
2013 return $this->isContentOverridden ||
2014 ( $this->mOldRevisionRecord !==
null && $this->mNewRevisionRecord !== null );
2018 $this->mRevisionsLoaded =
true;
2020 $this->loadRevisionIds();
2023 if ( $this->mNewid ) {
2024 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2026 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->
getTitle() );
2034 $this->mNewid = $this->mNewRevisionRecord->getId();
2035 $this->mNewPage = $this->mNewid ?
2040 $this->mOldRevisionRecord =
false;
2041 if ( $this->mOldid ) {
2042 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2043 } elseif ( $this->mOldid === 0 ) {
2044 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2046 $this->mOldid = $revRecord ? $revRecord->
getId() :
false;
2047 $this->mOldRevisionRecord = $revRecord ??
false;
2050 if ( $this->mOldRevisionRecord ===
null ) {
2054 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2056 $this->mOldRevisionRecord->getPageAsLinkTarget()
2059 $this->mOldPage =
null;
2064 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2065 if ( $this->mOldid !==
false ) {
2066 $tagIds =
$dbr->selectFieldValues(
2069 [
'ct_rev_id' => $this->mOldid ],
2073 foreach ( $tagIds as $tagId ) {
2075 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2080 $this->mOldTags = implode(
',', $tags );
2082 $this->mOldTags =
false;
2085 $tagIds =
$dbr->selectFieldValues(
2088 [
'ct_rev_id' => $this->mNewid ],
2092 foreach ( $tagIds as $tagId ) {
2094 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2099 $this->mNewTags = implode(
',', $tags );
2113 if ( $this->mTextLoaded == 2 ) {
2115 ( $this->mOldRevisionRecord ===
false || $this->mOldContent )
2116 && $this->mNewContent;
2120 $this->mTextLoaded = 2;
2126 if ( $this->mOldRevisionRecord ) {
2127 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2129 RevisionRecord::FOR_THIS_USER,
2132 if ( $this->mOldContent ===
null ) {
2137 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2139 RevisionRecord::FOR_THIS_USER,
2142 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2143 if ( $this->mNewContent ===
null ) {
2156 if ( $this->mTextLoaded >= 1 ) {
2160 $this->mTextLoaded = 1;
2166 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2168 RevisionRecord::FOR_THIS_USER,
2172 $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated or implementati...
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.
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
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.
getContext()
Get the base IContextSource 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.
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 Stability: stableto 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.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
static warningBox( $html, $className='')
Return a warning box.
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Base class for language-specific code.
A class containing constants representing the names of configuration variables.
Service for creating WikiPage objects.
Show an error when a user tries to do something they do not have the necessary permissions for.
static isInRCLifespan( $timestamp, $tolerance=0)
Check whether the given timestamp is new enough to have a RC row with a given tolerance as the recent...
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
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.
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
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