62 use DeprecationHelper;
70 private const DIFF_VERSION =
'1.12';
98 private $mOldRevisionRecord;
108 private $mNewRevisionRecord;
141 private $mOldContent;
148 private $mNewContent;
154 private $mRevisionsIdsLoaded =
false;
157 protected $mRevisionsLoaded =
false;
160 protected $mTextLoaded = 0;
170 protected $isContentOverridden =
false;
173 protected $mCacheHit =
false;
181 public $enableDebugComment =
false;
186 protected $mReducedLineNumbers =
false;
189 protected $mMarkPatrolledLink =
null;
192 protected $unhide =
false;
195 protected $mRefreshCache =
false;
206 protected $isSlotDiffRenderer =
false;
212 private $slotDiffOptions = [];
222 private $contentHandlerFactory;
227 private $revisionStore;
233 private $wikiPageFactory;
236 private $userOptionsLookup;
248 public function __construct( $context =
null, $old = 0, $new = 0, $rcid = 0,
249 $refreshCache =
false, $unhide =
false
265 wfDebug(
"DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
267 $this->mOldid = $old;
268 $this->mNewid = $new;
269 $this->mRefreshCache = $refreshCache;
270 $this->unhide = $unhide;
272 $services = MediaWikiServices::getInstance();
273 $this->linkRenderer = $services->getLinkRenderer();
274 $this->contentHandlerFactory = $services->getContentHandlerFactory();
275 $this->revisionStore = $services->getRevisionStore();
276 $this->hookRunner =
new HookRunner( $services->getHookContainer() );
277 $this->wikiPageFactory = $services->getWikiPageFactory();
278 $this->userOptionsLookup = $services->getUserOptionsLookup();
286 if ( $this->isSlotDiffRenderer ) {
287 throw new LogicException( __METHOD__ .
' called in slot diff renderer mode' );
290 if ( $this->slotDiffRenderers ===
null ) {
296 $this->slotDiffRenderers = array_map(
function ( array $contents ) {
298 $content = $contents[
'new'] ?: $contents[
'old'];
299 return $content->getContentHandler()->getSlotDiffRenderer(
301 $this->slotDiffOptions
306 return $this->slotDiffRenderers;
316 $this->isSlotDiffRenderer =
true;
325 if ( $this->isContentOverridden ) {
327 SlotRecord::MAIN => [
'old' => $this->mOldContent,
'new' => $this->mNewContent ]
333 $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
334 $oldSlots = $this->mOldRevisionRecord ?
335 $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
341 $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
344 foreach ( $roles as $role ) {
346 'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() :
null,
347 'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() :
null,
351 if ( isset( $slots[SlotRecord::MAIN] ) ) {
352 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
360 return parent::getTitle() ?: Title::makeTitle(
NS_SPECIAL,
'BadTitle/DifferenceEngine' );
370 $this->mReducedLineNumbers = $value;
379 if ( $this->mDiffLang ===
null ) {
380 # Default language in which the diff text is written.
381 $this->mDiffLang = $this->
getTitle()->getPageLanguage();
384 return $this->mDiffLang;
391 return $this->mCacheHit;
402 $this->loadRevisionIds();
404 return $this->mOldid;
414 $this->loadRevisionIds();
416 return $this->mNewid;
426 return $this->mOldRevisionRecord ?:
null;
435 return $this->mNewRevisionRecord;
447 if ( $this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
449 $arQuery = $this->revisionStore->getArchiveQueryInfo();
450 $row =
$dbr->selectRow(
452 array_merge( $arQuery[
'fields'], [
'ar_namespace',
'ar_title' ] ),
453 [
'ar_rev_id' => $id ],
459 $revRecord = $this->revisionStore->newRevisionFromArchiveRow( $row );
460 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
463 'target' =>
$title->getPrefixedText(),
482 return "[$link $id]";
488 private function showMissingRevision() {
489 $out = $this->getOutput();
492 if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
493 $missing[] = $this->deletedIdMarker( $this->mOldid );
495 if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
499 $out->setPageTitle( $this->
msg(
'errorpagetitle' ) );
500 $msg = $this->
msg(
'difference-missing-revision' )
501 ->params( $this->
getLanguage()->listToText( $missing ) )
502 ->numParams( count( $missing ) )
504 $out->addHTML( $msg );
515 $this->mNewRevisionRecord &&
516 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
519 $this->mOldRevisionRecord &&
520 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
532 $permStatus = PermissionStatus::newEmpty();
533 if ( $this->mNewPage ) {
534 $performer->
authorizeRead(
'read', $this->mNewPage, $permStatus );
536 if ( $this->mOldPage ) {
537 $performer->
authorizeRead(
'read', $this->mOldPage, $permStatus );
539 return $permStatus->toLegacyErrorArray();
549 ( $this->mOldRevisionRecord &&
550 $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
551 ( $this->mNewRevisionRecord &&
552 $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
570 if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
571 RevisionRecord::DELETED_TEXT,
579 return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
580 RevisionRecord::DELETED_TEXT,
601 # Allow frames except in certain special cases
603 $out->setPreventClickjacking(
false );
604 $out->setRobotPolicy(
'noindex,nofollow' );
607 $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
610 if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
611 $this->showMissingRevision();
624 $query = $this->slotDiffOptions;
625 # Carry over 'diffonly' param via navigation links
626 if ( $diffOnly != MediaWikiServices::getInstance()
627 ->getUserOptionsLookup()->getBoolOption( $user,
'diffonly' )
629 $query[
'diffonly'] = $diffOnly;
631 # Cascade unhide param in links for easy deletion browsing
632 if ( $this->unhide ) {
633 $query[
'unhide'] = 1;
636 # Check if one of the revisions is deleted/suppressed
643 # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
644 # a diff between a version V and its previous version V' AND the version V
645 # is the first version of that article. In that case, V' does not exist.
646 if ( $this->mOldRevisionRecord ===
false ) {
647 if ( $this->mNewPage ) {
648 $out->setPageTitle( $this->
msg(
'difference-title', $this->mNewPage->getPrefixedText() ) );
653 $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
655 $this->hookRunner->onDifferenceEngineViewHeader( $this );
657 if ( !$this->mOldPage || !$this->mNewPage ) {
660 } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
661 $out->setPageTitle( $this->
msg(
'difference-title', $this->mNewPage->getPrefixedText() ) );
664 $out->setPageTitle( $this->
msg(
'difference-title-multipage',
665 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
666 $out->addSubtitle( $this->
msg(
'difference-multipage' ) );
670 if ( $samePage && $this->mNewPage &&
671 $this->
getAuthority()->probablyCan(
'edit', $this->mNewPage )
673 if ( $this->mNewRevisionRecord->isCurrent() &&
674 $this->getAuthority()->probablyCan(
'rollback', $this->mNewPage )
677 $this->mNewRevisionRecord,
681 if ( $rollbackLink ) {
682 $out->setPreventClickjacking(
true );
683 $rollback =
"\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
687 if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
688 $this->userCanEdit( $this->mNewRevisionRecord )
690 $undoLink = $this->linkRenderer->makeKnownLink(
692 $this->
msg(
'editundo' )->text(),
696 'undoafter' => $this->mOldid,
697 'undo' => $this->mNewid
700 $revisionTools[
'mw-diff-undo'] = $undoLink;
703 # Make "previous revision link"
704 $hasPrevious = $samePage && $this->mOldPage &&
705 $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
706 if ( $hasPrevious ) {
707 $prevlink = $this->linkRenderer->makeKnownLink(
709 $this->
msg(
'previousdiff' )->text(),
710 [
'id' =>
'differences-prevlink' ],
711 [
'diff' =>
'prev',
'oldid' => $this->mOldid ] + $query
714 $prevlink =
"\u{00A0}";
717 if ( $this->mOldRevisionRecord->isMinor() ) {
718 $oldminor = ChangesList::flag(
'minor' );
723 $oldRevRecord = $this->mOldRevisionRecord;
725 $ldel = $this->revisionDeleteLink( $oldRevRecord );
730 if ( $oldRevComment ===
'' ) {
731 $defaultComment = $this->
msg(
'changeslist-nocomment' )->escaped();
732 $oldRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
735 $oldHeader =
'<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader .
'</strong></div>' .
736 '<div id="mw-diff-otitle2">' .
738 '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel .
'</div>' .
739 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] .
'</div>' .
740 '<div id="mw-diff-otitle4">' . $prevlink .
'</div>';
743 $this->hookRunner->onDifferenceEngineOldHeader(
744 $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
747 $out->addJsConfigVars( [
748 'wgDiffOldId' => $this->mOldid,
749 'wgDiffNewId' => $this->mNewid,
752 # Make "next revision link"
753 # Skip next link on the top revision
754 if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
755 $nextlink = $this->linkRenderer->makeKnownLink(
757 $this->
msg(
'nextdiff' )->text(),
758 [
'id' =>
'differences-nextlink' ],
759 [
'diff' =>
'next',
'oldid' => $this->mNewid ] + $query
762 $nextlink =
"\u{00A0}";
765 if ( $this->mNewRevisionRecord->isMinor() ) {
766 $newminor = ChangesList::flag(
'minor' );
771 # Handle RevisionDelete links...
772 $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
774 # Allow extensions to define their own revision tools
775 $this->hookRunner->onDiffTools(
776 $this->mNewRevisionRecord,
778 $this->mOldRevisionRecord ?:
null,
782 $formattedRevisionTools = [];
784 foreach ( $revisionTools as $key => $tool ) {
785 $toolClass = is_string( $key ) ? $key :
'mw-diff-tool';
786 $element = Html::rawElement(
788 [
'class' => $toolClass ],
789 $this->
msg(
'parentheses' )->rawParams( $tool )->escaped()
791 $formattedRevisionTools[] = $element;
794 $newRevRecord = $this->mNewRevisionRecord;
797 ' ' . implode(
' ', $formattedRevisionTools );
801 if ( $newRevComment ===
'' ) {
802 $defaultComment = $this->
msg(
'changeslist-nocomment' )->escaped();
803 $newRevComment =
"<span class=\"comment mw-comment-none\">$defaultComment</span>";
806 $newHeader =
'<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader .
'</strong></div>' .
809 '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel .
'</div>' .
810 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] .
'</div>' .
811 '<div id="mw-diff-ntitle4">' . $nextlink . $this->
markPatrolledLink() .
'</div>';
814 $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
815 $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
816 $rdel, $this->unhide );
818 # If the diff cannot be shown due to a deleted revision, then output
819 # the diff header and links to unhide (if available)...
823 $out->addHTML( $this->
addHeader(
'', $oldHeader, $newHeader, $multi ) );
825 # Give explanation for why revision is not visible
826 $msg = [ $suppressed ?
'rev-suppressed-no-diff' :
'rev-deleted-no-diff' ];
828 # Give explanation and add a link to view the diff...
829 $query = $this->
getRequest()->appendQueryValue(
'unhide',
'1' );
831 $suppressed ?
'rev-suppressed-unhide-diff' :
'rev-deleted-unhide-diff',
832 $this->
getTitle()->getFullURL( $query )
835 $out->addHtml( Html::warningBox( $this->
msg( ...$msg )->parse(),
'plainlinks' ) );
836 # Otherwise, output a regular diff...
838 # Add deletion notice if the user is viewing deleted content
841 $msg = $suppressed ?
'rev-suppressed-diff-view' :
'rev-deleted-diff-view';
842 $notice = Html::warningBox( $this->
msg( $msg )->parse(),
'plainlinks' );
844 $this->
showDiff( $oldHeader, $newHeader, $notice );
862 if ( $this->mMarkPatrolledLink ===
null ) {
865 if ( !$linkInfo || !$this->mNewPage ) {
866 $this->mMarkPatrolledLink =
'';
868 $this->mMarkPatrolledLink =
' <span class="patrollink" data-mw="interface">[' .
869 $this->linkRenderer->makeKnownLink(
871 $this->
msg(
'markaspatrolleddiff' )->text(),
874 'action' =>
'markpatrolled',
875 'rcid' => $linkInfo[
'rcid'],
879 $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
880 $this->mMarkPatrolledLink, $linkInfo[
'rcid'] );
883 return $this->mMarkPatrolledLink;
900 $config->get( MainConfigNames::UseRCPatrol ) &&
902 $this->getAuthority()->probablyCan(
'patrol', $this->mNewPage ) &&
905 RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
908 $change = RecentChange::newFromConds(
910 'rc_this_oldid' => $this->mNewid,
911 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
916 if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
917 $rcid = $change->getAttribute(
'rc_id' );
928 $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
932 $this->
getOutput()->setPreventClickjacking(
true );
934 $this->
getOutput()->addModules(
'mediawiki.misc-authed-curate' );
937 return [
'rcid' => $rcid ];
950 private function revisionDeleteLink(
RevisionRecord $revRecord ) {
956 if ( $link !==
'' ) {
957 $link =
"\u{00A0}\u{00A0}\u{00A0}" . $link .
' ';
969 if ( $this->isContentOverridden ) {
973 throw new LogicException(
975 .
' is not supported after calling setContent(). Use setRevisions() instead.'
981 # Add "current version as of X" title
982 $out->addHTML(
"<hr class='diff-hr' id='mw-oldid' />
983 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
984 # Page content may be handled by a hooked call instead...
985 if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
987 if ( !$this->mNewPage ) {
994 $out->setRevisionId( $this->mNewid );
995 $out->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
996 $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
997 $out->setArticleFlag(
true );
999 if ( !$this->hookRunner->onArticleRevisionViewCustom(
1000 $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1006 if ( $this->
getTitle()->equals( $this->mNewPage ) ) {
1013 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1016 $parserOutput = $this->
getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1018 # WikiPage::getParserOutput() should not return false, but just in case
1019 if ( $parserOutput ) {
1021 if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1022 $this, $out, $parserOutput, $wikiPage )
1024 $skinOptions = $this->
getSkin()->getOptions();
1025 $out->setSections( $parserOutput->getSections() );
1026 $out->addParserOutput( $parserOutput, [
1028 'injectTOC' => $skinOptions[
'toc'],
1029 'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1030 && $this->getAuthority()->probablyCan(
1032 $this->mNewRevisionRecord->getPage()
1034 'absoluteURLs' => $this->slotDiffOptions[
'expand-url'] ??
false
1042 if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1043 # Add redundant patrol link on bottom...
1055 if ( !$revRecord->
getId() ) {
1076 public function showDiff( $otitle, $ntitle, $notice =
'' ) {
1078 $this->hookRunner->onDifferenceEngineShowDiff( $this );
1080 $diff = $this->
getDiff( $otitle, $ntitle, $notice );
1081 if ( $diff ===
false ) {
1082 $this->showMissingRevision();
1087 if ( $this->slotDiffOptions[
'expand-url'] ??
false ) {
1098 if ( !$this->isSlotDiffRenderer ) {
1099 $this->
getOutput()->addModules(
'mediawiki.diff' );
1101 'mediawiki.interface.helpers.styles',
1102 'mediawiki.diff.styles'
1105 $slotDiffRenderer->addModules( $this->
getOutput() );
1119 public function getDiff( $otitle, $ntitle, $notice =
'' ) {
1121 if ( $body ===
false ) {
1127 if ( $body ===
'' ) {
1128 $notice .=
'<div class="mw-diff-empty">' .
1129 $this->
msg(
'diff-empty' )->parse() .
1133 return $this->
addHeader( $body, $otitle, $ntitle, $multi, $notice );
1142 $this->mCacheHit =
true;
1144 if ( !$this->isContentOverridden ) {
1147 } elseif ( $this->mOldRevisionRecord &&
1148 !$this->mOldRevisionRecord->userCan(
1149 RevisionRecord::DELETED_TEXT,
1150 $this->getAuthority()
1154 } elseif ( $this->mNewRevisionRecord &&
1155 !$this->mNewRevisionRecord->userCan(
1156 RevisionRecord::DELETED_TEXT,
1157 $this->getAuthority()
1162 if ( $this->mOldRevisionRecord ===
false || (
1163 $this->mOldRevisionRecord &&
1164 $this->mNewRevisionRecord &&
1165 $this->mOldRevisionRecord->getId() &&
1166 $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1168 if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1176 $services = MediaWikiServices::getInstance();
1177 $cache = $services->getMainWANObjectCache();
1178 $stats = $services->getStatsdDataFactory();
1179 if ( $this->mOldid && $this->mNewid ) {
1182 $detected = MWDebug::detectDeprecatedOverride(
1185 'getDiffBodyCacheKey',
1192 if ( $key ===
null ) {
1197 if ( !$this->mRefreshCache ) {
1198 $difftext =
$cache->get( $key );
1199 if ( is_string( $difftext ) ) {
1200 $stats->updateCount(
'diff_cache.hit', 1 );
1201 $difftext = $this->localiseDiff( $difftext );
1202 $difftext .=
"\n<!-- diff cache key $key -->\n";
1208 $this->mCacheHit =
false;
1220 $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role][
'old'],
1221 $slotContents[$role][
'new'] );
1222 if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1227 $difftext .= $slotDiff;
1231 if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1232 $stats->updateCount(
'diff_cache.uncacheable', 1 );
1233 } elseif ( $key !==
false ) {
1234 $stats->updateCount(
'diff_cache.miss', 1 );
1235 $cache->set( $key, $difftext, 7 * 86400 );
1237 $stats->updateCount(
'diff_cache.uncacheable', 1 );
1240 $difftext = $this->localiseDiff( $difftext );
1253 if ( !isset( $diffRenderers[$role] ) ) {
1258 $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role][
'old'],
1259 $slotContents[$role][
'new'] );
1264 if ( $role !== SlotRecord::MAIN ) {
1270 return $this->localiseDiff( $slotDiff );
1282 $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1284 return Html::rawElement(
'tr', [
'class' =>
'mw-diff-slot-header',
'lang' => $userLang ],
1285 Html::element(
'th', [
'colspan' => $columnCount ], $headerText ) );
1318 if ( !$this->mOldid || !$this->mNewid ) {
1319 throw new MWException(
'mOldid and mNewid must be set to get diff cache key.' );
1325 $engine ===
'php' ? false : $engine,
1327 "old-{$this->mOldid}",
1328 "rev-{$this->mNewid}"
1331 if ( $engine ===
'wikidiff2' ) {
1332 $params[] = phpversion(
'wikidiff2' );
1335 if ( !$this->isSlotDiffRenderer ) {
1337 $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1355 $this->mOldid = 123456789;
1356 $this->mNewid = 987654321;
1359 $detected = MWDebug::detectDeprecatedOverride(
1362 'getDiffBodyCacheKey',
1367 if ( $cacheString ) {
1368 return [ $cacheString ];
1382 if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1383 $params = array_slice( $params, count( $standardParams ) );
1394 $this->slotDiffOptions = $options;
1414 && $this->isSlotDiffRenderer
1420 throw new Exception( get_class( $this ) .
': could not maintain backwards compatibility. '
1421 .
'Please use a SlotDiffRenderer.' );
1423 return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1439 $slotDiffRenderer = $this->contentHandlerFactory
1441 ->getSlotDiffRenderer( $this->
getContext() );
1445 throw new Exception(
'The slot diff renderer for text content should be a '
1446 .
'TextSlotDiffRenderer subclass' );
1448 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1458 $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1459 ->get( MainConfigNames::DiffEngine );
1460 $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1461 ->get( MainConfigNames::ExternalDiffEngine );
1463 if ( $diffEngine ===
null ) {
1464 $engines = [
'external',
'wikidiff2',
'php' ];
1466 $engines = [ $diffEngine ];
1469 $failureReason =
null;
1470 foreach ( $engines as $engine ) {
1471 switch ( $engine ) {
1473 if ( is_string( $externalDiffEngine ) ) {
1474 if ( is_executable( $externalDiffEngine ) ) {
1475 return $externalDiffEngine;
1477 $failureReason =
'ExternalDiffEngine config points to a non-executable';
1478 if ( $diffEngine ===
null ) {
1479 wfDebug(
"$failureReason, ignoring" );
1482 $failureReason =
'ExternalDiffEngine config is set to a non-string value';
1483 if ( $diffEngine ===
null && $externalDiffEngine ) {
1484 wfWarn(
"$failureReason, ignoring" );
1490 if ( function_exists(
'wikidiff2_do_diff' ) ) {
1493 $failureReason =
'wikidiff2 is not available';
1501 throw new DomainException(
'Invalid value for $wgDiffEngine: ' . $engine );
1504 throw new UnexpectedValueException(
"Cannot use diff engine '$engine': $failureReason" );
1522 $slotDiffRenderer = $this->contentHandlerFactory
1524 ->getSlotDiffRenderer( $this->
getContext() );
1528 throw new Exception(
'The slot diff renderer for text content should be a '
1529 .
'TextSlotDiffRenderer subclass' );
1531 return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1543 if ( !$this->enableDebugComment ) {
1547 if ( $this->
getConfig()->
get( MainConfigNames::ShowHostnames ) ) {
1552 return "<!-- diff generator: " .
1553 implode(
" ", array_map(
"htmlspecialchars", $data ) ) .
1560 private function getDebugString() {
1561 $engine = self::getEngine();
1562 if ( $engine ===
'wikidiff2' ) {
1563 return $this->debug(
'wikidiff2' );
1564 } elseif ( $engine ===
'php' ) {
1565 return $this->
debug(
'native PHP' );
1567 return $this->
debug(
"external $engine" );
1577 private function localiseDiff( $text ) {
1579 if ( $this->
getEngine() ===
'wikidiff2' &&
1580 version_compare( phpversion(
'wikidiff2' ),
'1.5.1',
'>=' )
1582 $text = $this->addLocalisedTitleTooltips( $text );
1595 return preg_replace_callback(
1596 '/<!--LINE (\d+)-->/',
1598 if (
$matches[1] ===
'1' && $this->mReducedLineNumbers ) {
1601 return $this->
msg(
'lineno' )->numParams(
$matches[1] )->escaped();
1613 private function addLocalisedTitleTooltips( $text ) {
1614 return preg_replace_callback(
1615 '/class="mw-diff-movedpara-(left|right)"/',
1618 'diff-paragraph-moved-toold' :
1619 'diff-paragraph-moved-tonew';
1620 return $matches[0] .
' title="' . $this->msg( $key )->escaped() .
'"';
1634 !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1635 || !$this->mOldPage || !$this->mNewPage
1636 || !$this->mOldPage->equals( $this->mNewPage )
1637 || $this->mOldRevisionRecord->getId() ===
null
1638 || $this->mNewRevisionRecord->getId() ===
null
1640 || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1641 || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1646 if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1647 $oldRevRecord = $this->mNewRevisionRecord;
1648 $newRevRecord = $this->mOldRevisionRecord;
1650 $oldRevRecord = $this->mOldRevisionRecord;
1651 $newRevRecord = $this->mNewRevisionRecord;
1657 $revisionIdList = $this->revisionStore->getRevisionIdsBetween(
1658 $this->mNewPage->getArticleID(),
1664 if ( count( $revisionIdList ) > 0 ) {
1665 foreach ( $revisionIdList as $revisionId ) {
1666 $revision = $this->revisionStore->getRevisionById( $revisionId );
1667 if ( $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ) ) {
1672 if ( $nEdits > 0 && $nEdits <= 1000 ) {
1675 $users = $this->revisionStore->getAuthorsBetween(
1676 $this->mNewPage->getArticleID(),
1682 $numUsers = count( $users );
1684 $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1685 $newRevUserText = $newRevUser ? $newRevUser->getName() :
'';
1686 if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1689 }
catch ( InvalidArgumentException $e ) {
1693 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1709 if ( $numUsers === 0 ) {
1710 $msg =
'diff-multi-sameuser';
1711 } elseif ( $numUsers > $limit ) {
1712 $msg =
'diff-multi-manyusers';
1715 $msg =
'diff-multi-otherusers';
1718 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1726 if ( !$revRecord->
userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1746 $timestamp =
$lang->userTimeAndDate( $revtimestamp, $user );
1747 $dateofrev =
$lang->userDate( $revtimestamp, $user );
1748 $timeofrev =
$lang->userTime( $revtimestamp, $user );
1751 $rev->
isCurrent() ?
'currentrev-asof' :
'revisionasof',
1757 if ( $complete !==
'complete' ) {
1764 [
'oldid' => $rev->
getId() ] );
1766 if ( $this->userCanEdit( $rev ) ) {
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();
1776 $header .=
' ' . Html::rawElement(
1778 [
'class' =>
'mw-diff-edit' ],
1781 if ( $rev->
isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1789 $header = Html::rawElement(
'span', [
'class' =>
'history-deleted' ],
$header );
1807 public function addHeader( $diff, $otitle, $ntitle, $multi =
'', $notice =
'' ) {
1810 $header = Html::openElement(
'table', [
1813 'diff-contentalign-' . $this->
getDiffLang()->alignStart(),
1814 'diff-editfont-' . $this->userOptionsLookup->getOption(
1819 'data-mw' =>
'interface',
1821 $userLang = htmlspecialchars( $this->
getLanguage()->getHtmlCode() );
1823 if ( !$diff && !$otitle ) {
1825 <tr class=\"diff-title\" lang=\"{$userLang}\">
1826 <td class=\"diff-ntitle\">{$ntitle}</td>
1832 <col class=\"diff-marker\" />
1833 <col class=\"diff-content\" />
1834 <col class=\"diff-marker\" />
1835 <col class=\"diff-content\" />";
1842 if ( $otitle || $ntitle ) {
1844 $deletedClass =
'diff-side-deleted';
1845 $addedClass =
'diff-side-added';
1847 <tr class=\"diff-title\" lang=\"{$userLang}\">
1848 <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
1849 <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
1854 if ( $multi !=
'' ) {
1855 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
1856 "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1858 if ( $notice !=
'' ) {
1859 $header .=
"<tr><td colspan=\"{$multiColspan}\" " .
1860 "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1863 return $header . $diff .
"</table>";
1874 $this->mOldContent = $oldContent;
1875 $this->mNewContent = $newContent;
1877 $this->mTextLoaded = 2;
1878 $this->mRevisionsLoaded =
true;
1879 $this->isContentOverridden =
true;
1880 $this->slotDiffRenderers =
null;
1891 if ( $oldRevision ) {
1892 $this->mOldRevisionRecord = $oldRevision;
1893 $this->mOldid = $oldRevision->
getId();
1897 $this->mOldContent = $oldRevision->
getContent( SlotRecord::MAIN,
1898 RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
1900 $this->mOldPage =
null;
1901 $this->mOldRevisionRecord = $this->mOldid =
false;
1903 $this->mNewRevisionRecord = $newRevision;
1904 $this->mNewid = $newRevision->
getId();
1906 $this->mNewContent = $newRevision->
getContent( SlotRecord::MAIN,
1907 RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
1909 $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded =
true;
1910 $this->mTextLoaded = $oldRevision ? 2 : 1;
1911 $this->isContentOverridden =
false;
1912 $this->slotDiffRenderers =
null;
1922 $this->mDiffLang =
$lang;
1938 if ( $new ===
'prev' ) {
1940 $newid = intval( $old );
1942 $newRev = $this->revisionStore->getRevisionById( $newid );
1944 $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
1946 $oldid = $oldRev->getId();
1949 } elseif ( $new ===
'next' ) {
1951 $oldid = intval( $old );
1953 $oldRev = $this->revisionStore->getRevisionById( $oldid );
1955 $newRev = $this->revisionStore->getNextRevision( $oldRev );
1957 $newid = $newRev->getId();
1961 $oldid = intval( $old );
1962 $newid = intval( $new );
1966 return [ $oldid, $newid ];
1969 private function loadRevisionIds() {
1970 if ( $this->mRevisionsIdsLoaded ) {
1974 $this->mRevisionsIdsLoaded =
true;
1976 $old = $this->mOldid;
1977 $new = $this->mNewid;
1979 list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1980 if ( $new ===
'next' && $this->mNewid ===
false ) {
1981 # if no result, NewId points to the newest old revision. The only newer
1982 # revision is cur, which is "0".
1986 $this->hookRunner->onNewDifferenceEngine(
1988 $this->
getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2005 if ( $this->mRevisionsLoaded ) {
2006 return $this->isContentOverridden ||
2007 ( $this->mOldRevisionRecord !==
null && $this->mNewRevisionRecord !== null );
2011 $this->mRevisionsLoaded =
true;
2013 $this->loadRevisionIds();
2016 if ( $this->mNewid ) {
2017 $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2019 $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->
getTitle() );
2027 $this->mNewid = $this->mNewRevisionRecord->getId();
2028 $this->mNewPage = $this->mNewid ?
2029 Title::newFromLinkTarget( $this->mNewRevisionRecord->getPageAsLinkTarget() ) :
2033 $this->mOldRevisionRecord =
false;
2034 if ( $this->mOldid ) {
2035 $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2036 } elseif ( $this->mOldid === 0 ) {
2037 $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2039 $this->mOldid = $revRecord ? $revRecord->
getId() :
false;
2040 $this->mOldRevisionRecord = $revRecord ??
false;
2043 if ( $this->mOldRevisionRecord ===
null ) {
2047 if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2048 $this->mOldPage = Title::newFromLinkTarget(
2049 $this->mOldRevisionRecord->getPageAsLinkTarget()
2052 $this->mOldPage =
null;
2057 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2058 if ( $this->mOldid !==
false ) {
2059 $tagIds =
$dbr->selectFieldValues(
2062 [
'ct_rev_id' => $this->mOldid ],
2066 foreach ( $tagIds as $tagId ) {
2068 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2073 $this->mOldTags = implode(
',', $tags );
2075 $this->mOldTags =
false;
2078 $tagIds =
$dbr->selectFieldValues(
2081 [
'ct_rev_id' => $this->mNewid ],
2085 foreach ( $tagIds as $tagId ) {
2087 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
2092 $this->mNewTags = implode(
',', $tags );
2106 if ( $this->mTextLoaded == 2 ) {
2108 ( $this->mOldRevisionRecord ===
false || $this->mOldContent )
2109 && $this->mNewContent;
2113 $this->mTextLoaded = 2;
2119 if ( $this->mOldRevisionRecord ) {
2120 $this->mOldContent = $this->mOldRevisionRecord->getContent(
2122 RevisionRecord::FOR_THIS_USER,
2125 if ( $this->mOldContent ===
null ) {
2130 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2132 RevisionRecord::FOR_THIS_USER,
2135 $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2136 if ( $this->mNewContent ===
null ) {
2149 if ( $this->mTextLoaded >= 1 ) {
2153 $this->mTextLoaded = 1;
2159 $this->mNewContent = $this->mNewRevisionRecord->getContent(
2161 RevisionRecord::FOR_THIS_USER,
2165 $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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
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.
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
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.
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
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.
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
getOldRevision()
Get the left side of the diff.
Base class for language-specific code.
static expandLocalLinks(string $html)
Helper function to expand local links.
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
static revComment(RevisionRecord $revRecord, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
static getRevDeleteLink(Authority $performer, RevisionRecord $revRecord, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
static titleAttrib( $name, $options=null, array $msgParams=[], $localizer=null)
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
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.
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.
Represents a title within MediaWiki.
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 content objects.
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