<?php
/**
- * User interface for the difference engine
+ * User interface for the difference engine.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup DifferenceEngine
*/
+use MediaWiki\MediaWikiServices;
-/**
- * Constant to indicate diff cache compatibility.
- * Bump this when changing the diff formatting in a way that
- * fixes important bugs or such to force cached diff views to
- * clear.
- */
+/** @deprecated use class constant instead */
define( 'MW_DIFF_VERSION', '1.11a' );
/**
* @todo document
* @ingroup DifferenceEngine
*/
-class DifferenceEngine {
- /**#@+
- * @private
+class DifferenceEngine extends ContextSource {
+ /**
+ * Constant to indicate diff cache compatibility.
+ * Bump this when changing the diff formatting in a way that
+ * fixes important bugs or such to force cached diff views to
+ * clear.
*/
- var $mOldid, $mNewid, $mTitle;
- var $mOldtitle, $mNewtitle, $mPagetitle;
- var $mOldtext, $mNewtext;
- var $mOldPage, $mNewPage;
- var $mRcidMarkPatrolled;
- var $mOldRev, $mNewRev;
- var $mRevisionsLoaded = false; // Have the revisions been loaded
- var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
- var $mCacheHit = false; // Was the diff fetched from cache?
+ const DIFF_VERSION = MW_DIFF_VERSION;
+
+ /** @var int */
+ public $mOldid;
+
+ /** @var int */
+ public $mNewid;
+
+ private $mOldTags;
+ private $mNewTags;
+
+ /** @var Content */
+ public $mOldContent;
+
+ /** @var Content */
+ public $mNewContent;
+
+ /** @var Language */
+ protected $mDiffLang;
+
+ /** @var Title */
+ public $mOldPage;
+
+ /** @var Title */
+ public $mNewPage;
+
+ /** @var Revision */
+ public $mOldRev;
+
+ /** @var Revision */
+ public $mNewRev;
+
+ /** @var bool Have the revisions IDs been loaded */
+ private $mRevisionsIdsLoaded = false;
+
+ /** @var bool Have the revisions been loaded */
+ public $mRevisionsLoaded = false;
+
+ /** @var int How many text blobs have been loaded, 0, 1 or 2? */
+ public $mTextLoaded = 0;
+
+ /** @var bool Was the diff fetched from cache? */
+ public $mCacheHit = false;
/**
* Set this to true to add debug info to the HTML output.
* Warning: this may cause RSS readers to spuriously mark articles as "new"
- * (bug 20601)
+ * (T22601)
*/
- var $enableDebugComment = false;
+ public $enableDebugComment = false;
- // If true, line X is not displayed when X is 1, for example to increase
- // readability and conserve space with many small diffs.
+ /** @var bool If true, line X is not displayed when X is 1, for example
+ * to increase readability and conserve space with many small diffs.
+ */
protected $mReducedLineNumbers = false;
- protected $unhide = false; # show rev_deleted content if allowed
+ /** @var string Link to action=markpatrolled */
+ protected $mMarkPatrolledLink = null;
+
+ /** @var bool Show rev_deleted content if allowed */
+ protected $unhide = false;
+
+ /** @var bool Refresh the diff cache */
+ protected $mRefreshCache = false;
+
/**#@-*/
/**
- * Constructor
- * @param $titleObj Title object that the diff is associated with
- * @param $old Integer: old ID we want to show and diff with.
- * @param $new String: either 'prev' or 'next'.
- * @param $rcid Integer: ??? FIXME (default 0)
- * @param $refreshCache boolean If set, refreshes the diff cache
- * @param $unhide boolean If set, allow viewing deleted revs
+ * @param IContextSource $context Context to use, anything else will be ignored
+ * @param int $old Old ID we want to show and diff with.
+ * @param string|int $new Either revision ID or 'prev' or 'next'. Default: 0.
+ * @param int $rcid Deprecated, no longer used!
+ * @param bool $refreshCache If set, refreshes the diff cache
+ * @param bool $unhide If set, allow viewing deleted revs
*/
- function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0,
- $refreshCache = false, $unhide = false )
- {
- if ( $titleObj ) {
- $this->mTitle = $titleObj;
- } else {
- global $wgTitle;
- $this->mTitle = $wgTitle;
+ public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
+ $refreshCache = false, $unhide = false
+ ) {
+ if ( $context instanceof IContextSource ) {
+ $this->setContext( $context );
}
+
wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
- if ( 'prev' === $new ) {
- # Show diff between revision $old and the previous one.
- # Get previous one from DB.
- $this->mNewid = intval( $old );
- $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
- } elseif ( 'next' === $new ) {
- # Show diff between revision $old and the next one.
- # Get next one from DB.
- $this->mOldid = intval( $old );
- $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
- if ( false === $this->mNewid ) {
- # if no result, NewId points to the newest old revision. The only newer
- # revision is cur, which is "0".
- $this->mNewid = 0;
- }
- } else {
- $this->mOldid = intval( $old );
- $this->mNewid = intval( $new );
- wfRunHooks( 'NewDifferenceEngine', array( &$titleObj, &$this->mOldid, &$this->mNewid, $old, $new ) );
- }
- $this->mRcidMarkPatrolled = intval( $rcid ); # force it to be an integer
+ $this->mOldid = $old;
+ $this->mNewid = $new;
$this->mRefreshCache = $refreshCache;
$this->unhide = $unhide;
}
- function setReducedLineNumbers( $value = true ) {
+ /**
+ * @param bool $value
+ */
+ public function setReducedLineNumbers( $value = true ) {
$this->mReducedLineNumbers = $value;
}
- function getTitle() {
- return $this->mTitle;
+ /**
+ * @return Language
+ */
+ public function getDiffLang() {
+ if ( $this->mDiffLang === null ) {
+ # Default language in which the diff text is written.
+ $this->mDiffLang = $this->getTitle()->getPageLanguage();
+ }
+
+ return $this->mDiffLang;
}
- function wasCacheHit() {
+ /**
+ * @return bool
+ */
+ public function wasCacheHit() {
return $this->mCacheHit;
}
- function getOldid() {
+ /**
+ * @return int
+ */
+ public function getOldid() {
+ $this->loadRevisionIds();
+
return $this->mOldid;
}
- function getNewid() {
+ /**
+ * @return bool|int
+ */
+ public function getNewid() {
+ $this->loadRevisionIds();
+
return $this->mNewid;
}
- function showDiffPage( $diffOnly = false ) {
- global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
- wfProfileIn( __METHOD__ );
-
- # Allow frames except in certain special cases
- $wgOut->allowClickjacking();
-
- # If external diffs are enabled both globally and for the user,
- # we'll use the application/x-external-editor interface to call
- # an external diff tool like kompare, kdiff3, etc.
- if ( $wgUseExternalEditor && $wgUser->getOption( 'externaldiff' ) ) {
- global $wgInputEncoding, $wgServer, $wgScript, $wgLang;
- $wgOut->disable();
- header ( "Content-type: application/x-external-editor; charset=" . $wgInputEncoding );
- $url1 = $this->mTitle->getFullURL( array(
- 'action' => 'raw',
- 'oldid' => $this->mOldid
- ) );
- $url2 = $this->mTitle->getFullURL( array(
- 'action' => 'raw',
- 'oldid' => $this->mNewid
- ) );
- $special = $wgLang->getNsText( NS_SPECIAL );
- $control = <<<CONTROL
- [Process]
- Type=Diff text
- Engine=MediaWiki
- Script={$wgServer}{$wgScript}
- Special namespace={$special}
-
- [File]
- Extension=wiki
- URL=$url1
-
- [File 2]
- Extension=wiki
- URL=$url2
-CONTROL;
- echo( $control );
-
- wfProfileOut( __METHOD__ );
- return;
+ /**
+ * Look up a special:Undelete link to the given deleted revision id,
+ * as a workaround for being unable to load deleted diffs in currently.
+ *
+ * @param int $id Revision ID
+ *
+ * @return string|bool Link HTML or false
+ */
+ public function deletedLink( $id ) {
+ if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = $dbr->selectRow( 'archive',
+ array_merge(
+ Revision::selectArchiveFields(),
+ [ 'ar_namespace', 'ar_title' ]
+ ),
+ [ 'ar_rev_id' => $id ],
+ __METHOD__ );
+ if ( $row ) {
+ $rev = Revision::newFromArchiveRow( $row );
+ $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
+
+ return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
+ 'target' => $title->getPrefixedText(),
+ 'timestamp' => $rev->getTimestamp()
+ ] );
+ }
}
- $wgOut->setArticleFlag( false );
- if ( !$this->loadRevisionData() ) {
- $t = $this->mTitle->getPrefixedText();
- $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
- $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
- $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
- wfProfileOut( __METHOD__ );
- return;
+ return false;
+ }
+
+ /**
+ * Build a wikitext link toward a deleted revision, if viewable.
+ *
+ * @param int $id Revision ID
+ *
+ * @return string Wikitext fragment
+ */
+ public function deletedIdMarker( $id ) {
+ $link = $this->deletedLink( $id );
+ if ( $link ) {
+ return "[$link $id]";
+ } else {
+ return (string)$id;
}
+ }
- wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
+ private function showMissingRevision() {
+ $out = $this->getOutput();
- if ( $this->mNewRev->isCurrent() ) {
- $wgOut->setArticleFlag( true );
+ $missing = [];
+ if ( $this->mOldRev === null ||
+ ( $this->mOldRev && $this->mOldContent === null )
+ ) {
+ $missing[] = $this->deletedIdMarker( $this->mOldid );
+ }
+ if ( $this->mNewRev === null ||
+ ( $this->mNewRev && $this->mNewContent === null )
+ ) {
+ $missing[] = $this->deletedIdMarker( $this->mNewid );
}
- # mOldid is false if the difference engine is called with a "vague" query for
- # a diff between a version V and its previous version V' AND the version V
- # is the first version of that article. In that case, V' does not exist.
- if ( $this->mOldid === false ) {
- $this->showFirstRevision();
- $this->renderNewRevision(); // should we respect $diffOnly here or not?
- wfProfileOut( __METHOD__ );
+ $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
+ $msg = $this->msg( 'difference-missing-revision' )
+ ->params( $this->getLanguage()->listToText( $missing ) )
+ ->numParams( count( $missing ) )
+ ->parseAsBlock();
+ $out->addHTML( $msg );
+ }
+
+ public function showDiffPage( $diffOnly = false ) {
+ # Allow frames except in certain special cases
+ $out = $this->getOutput();
+ $out->allowClickjacking();
+ $out->setRobotPolicy( 'noindex,nofollow' );
+
+ // Allow extensions to add any extra output here
+ Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
+
+ if ( !$this->loadRevisionData() ) {
+ if ( Hooks::run( 'DifferenceEngineShowDiffPageMaybeShowMissingRevision', [ $this ] ) ) {
+ $this->showMissingRevision();
+ }
return;
}
- $wgOut->suppressQuickbar();
-
- $oldTitle = $this->mOldPage->getPrefixedText();
- $newTitle = $this->mNewPage->getPrefixedText();
- if ( $oldTitle == $newTitle ) {
- $wgOut->setPageTitle( $newTitle );
- } else {
- $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
+ $user = $this->getUser();
+ $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
+ if ( $this->mOldPage ) { # mOldPage might not be set, see below.
+ $permErrors = wfMergeErrorArrays( $permErrors,
+ $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
}
- if ( $this->mNewPage->equals( $this->mOldPage ) ) {
- $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
- } else {
- $wgOut->setSubtitle( wfMsgExt( 'difference-multipage', array( 'parseinline' ) ) );
+ if ( count( $permErrors ) ) {
+ throw new PermissionsError( 'read', $permErrors );
}
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
- $wgOut->loginToUse();
- $wgOut->output();
- $wgOut->disable();
- wfProfileOut( __METHOD__ );
- return;
+ $rollback = '';
+
+ $query = [];
+ # Carry over 'diffonly' param via navigation links
+ if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
+ $query['diffonly'] = $diffOnly;
+ }
+ # Cascade unhide param in links for easy deletion browsing
+ if ( $this->unhide ) {
+ $query['unhide'] = 1;
}
- $sk = $wgUser->getSkin();
+ # Check if one of the revisions is deleted/suppressed
+ $deleted = $suppressed = false;
+ $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
+
+ $revisionTools = [];
- // Check if page is editable
- $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
- if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
- $wgOut->preventClickjacking();
- $rollback = '   ' . $sk->generateRollback( $this->mNewRev );
+ # mOldRev is false if the difference engine is called with a "vague" query for
+ # a diff between a version V and its previous version V' AND the version V
+ # is the first version of that article. In that case, V' does not exist.
+ if ( $this->mOldRev === false ) {
+ $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
+ $samePage = true;
+ $oldHeader = '';
+ // Allow extensions to change the $oldHeader variable
+ Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
} else {
- $rollback = '';
- }
+ Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
- // Prepare a change patrol link, if applicable
- if ( $wgUseRCPatrol && $this->mTitle->userCan( 'patrol' ) ) {
- // If we've been given an explicit change identifier, use it; saves time
- if ( $this->mRcidMarkPatrolled ) {
- $rcid = $this->mRcidMarkPatrolled;
- $rc = RecentChange::newFromId( $rcid );
- // Already patrolled?
- $rcid = is_object( $rc ) && !$rc->getAttribute( 'rc_patrolled' ) ? $rcid : 0;
+ if ( $this->mNewPage->equals( $this->mOldPage ) ) {
+ $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
+ $samePage = true;
} else {
- // Look for an unpatrolled change corresponding to this diff
- $db = wfGetDB( DB_SLAVE );
- $change = RecentChange::newFromConds(
- array(
- // Redundant user,timestamp condition so we can use the existing index
- 'rc_user_text' => $this->mNewRev->getRawUserText(),
- 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
- 'rc_this_oldid' => $this->mNewid,
- 'rc_last_oldid' => $this->mOldid,
- 'rc_patrolled' => 0
- ),
- __METHOD__
- );
- if ( $change instanceof RecentChange ) {
- $rcid = $change->mAttribs['rc_id'];
- $this->mRcidMarkPatrolled = $rcid;
- } else {
- // None found
- $rcid = 0;
+ $out->setPageTitle( $this->msg( 'difference-title-multipage',
+ $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
+ $out->addSubtitle( $this->msg( 'difference-multipage' ) );
+ $samePage = false;
+ }
+
+ if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
+ if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
+ $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
+ if ( $rollbackLink ) {
+ $out->preventClickjacking();
+ $rollback = '   ' . $rollbackLink;
+ }
+ }
+
+ if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) &&
+ !$this->mNewRev->isDeleted( Revision::DELETED_TEXT )
+ ) {
+ $undoLink = Html::element( 'a', [
+ 'href' => $this->mNewPage->getLocalURL( [
+ 'action' => 'edit',
+ 'undoafter' => $this->mOldid,
+ 'undo' => $this->mNewid
+ ] ),
+ 'title' => Linker::titleAttrib( 'undo' ),
+ ],
+ $this->msg( 'editundo' )->text()
+ );
+ $revisionTools['mw-diff-undo'] = $undoLink;
}
}
- // Build the link
- if ( $rcid ) {
- $wgOut->preventClickjacking();
- $token = $wgUser->editToken( $rcid );
- $patrol = ' <span class="patrollink">[' . $sk->link(
- $this->mTitle,
- wfMsgHtml( 'markaspatrolleddiff' ),
- array(),
- array(
- 'action' => 'markpatrolled',
- 'rcid' => $rcid,
- 'token' => $token,
- ),
- array(
- 'known',
- 'noclasses'
- )
- ) . ']</span>';
+
+ # Make "previous revision link"
+ if ( $samePage && $this->mOldRev->getPrevious() ) {
+ $prevlink = Linker::linkKnown(
+ $this->mOldPage,
+ $this->msg( 'previousdiff' )->escaped(),
+ [ 'id' => 'differences-prevlink' ],
+ [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
+ );
} else {
- $patrol = '';
+ $prevlink = ' ';
}
- } else {
- $patrol = '';
- }
- # Carry over 'diffonly' param via navigation links
- if ( $diffOnly != $wgUser->getBoolOption( 'diffonly' ) ) {
- $query['diffonly'] = $diffOnly;
- }
+ if ( $this->mOldRev->isMinor() ) {
+ $oldminor = ChangesList::flag( 'minor' );
+ } else {
+ $oldminor = '';
+ }
- # Make "previous revision link"
- $query['diff'] = 'prev';
- $query['oldid'] = $this->mOldid;
- # Cascade unhide param in links for easy deletion browsing
- if ( $this->unhide ) {
- $query['unhide'] = 1;
- }
- if ( !$this->mOldRev->getPrevious() ) {
- $prevlink = ' ';
- } else {
- $prevlink = $sk->link(
- $this->mTitle,
- wfMsgHtml( 'previousdiff' ),
- array(
- 'id' => 'differences-prevlink'
- ),
- $query,
- array(
- 'known',
- 'noclasses'
- )
- );
+ $ldel = $this->revisionDeleteLink( $this->mOldRev );
+ $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
+ $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
+
+ $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
+ '<div id="mw-diff-otitle2">' .
+ Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
+ '<div id="mw-diff-otitle3">' . $oldminor .
+ Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
+ '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
+ '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
+
+ // Allow extensions to change the $oldHeader variable
+ Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
+ $diffOnly, $ldel, $this->unhide ] );
+
+ if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $deleted = true; // old revisions text is hidden
+ if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
+ $suppressed = true; // also suppressed
+ }
+ }
+
+ # Check if this user can see the revisions
+ if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
+ $allowed = false;
+ }
}
+ $out->addJsConfigVars( [
+ 'wgDiffOldId' => $this->mOldid,
+ 'wgDiffNewId' => $this->mNewid,
+ ] );
+
# Make "next revision link"
- $query['diff'] = 'next';
- $query['oldid'] = $this->mNewid;
# Skip next link on the top revision
- if ( $this->mNewRev->isCurrent() ) {
- $nextlink = ' ';
- } else {
- $nextlink = $sk->link(
- $this->mTitle,
- wfMsgHtml( 'nextdiff' ),
- array(
- 'id' => 'differences-nextlink'
- ),
- $query,
- array(
- 'known',
- 'noclasses'
- )
+ if ( $samePage && !$this->mNewRev->isCurrent() ) {
+ $nextlink = Linker::linkKnown(
+ $this->mNewPage,
+ $this->msg( 'nextdiff' )->escaped(),
+ [ 'id' => 'differences-nextlink' ],
+ [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
);
+ } else {
+ $nextlink = ' ';
}
- $oldminor = '';
- $newminor = '';
-
- if ( $this->mOldRev->isMinor() ) {
- $oldminor = ChangesList::flag( 'minor' );
- }
if ( $this->mNewRev->isMinor() ) {
$newminor = ChangesList::flag( 'minor' );
+ } else {
+ $newminor = '';
}
# Handle RevisionDelete links...
- $ldel = $this->revisionDeleteLink( $this->mOldRev );
$rdel = $this->revisionDeleteLink( $this->mNewRev );
- $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $this->mOldtitle . '</strong></div>' .
- '<div id="mw-diff-otitle2">' .
- $sk->revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
- '<div id="mw-diff-otitle3">' . $oldminor .
- $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
- '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
- $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $this->mNewtitle . '</strong></div>' .
- '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) .
- " $rollback</div>" .
+ # Allow extensions to define their own revision tools
+ Hooks::run( 'DiffRevisionTools',
+ [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
+ $formattedRevisionTools = [];
+ // Put each one in parentheses (poor man's button)
+ foreach ( $revisionTools as $key => $tool ) {
+ $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
+ $element = Html::rawElement(
+ 'span',
+ [ 'class' => $toolClass ],
+ $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
+ );
+ $formattedRevisionTools[] = $element;
+ }
+ $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
+ ' ' . implode( ' ', $formattedRevisionTools );
+ $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
+
+ $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
+ '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
+ " $rollback</div>" .
'<div id="mw-diff-ntitle3">' . $newminor .
- $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
- '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
+ Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
+ '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
+ '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
+
+ // Allow extensions to change the $newHeader variable
+ Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
+ $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
- # Check if this user can see the revisions
- $allowed = $this->mOldRev->userCan( Revision::DELETED_TEXT )
- && $this->mNewRev->userCan( Revision::DELETED_TEXT );
- # Check if one of the revisions is deleted/suppressed
- $deleted = $suppressed = false;
- if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $deleted = true; // old revisions text is hidden
- if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) )
- $suppressed = true; // also suppressed
- }
if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
$deleted = true; // new revisions text is hidden
- if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
+ if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
$suppressed = true; // also suppressed
+ }
}
+
# If the diff cannot be shown due to a deleted revision, then output
# the diff header and links to unhide (if available)...
if ( $deleted && ( !$this->unhide || !$allowed ) ) {
$this->showDiffStyle();
$multi = $this->getMultiNotice();
- $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
+ $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
if ( !$allowed ) {
$msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
# Give explanation for why revision is not visible
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
- array( $msg ) );
+ $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
+ [ $msg ] );
} else {
# Give explanation and add a link to view the diff...
- $link = $this->mTitle->getFullUrl( array(
- 'diff' => $this->mNewid,
- 'oldid' => $this->mOldid,
- 'unhide' => 1
- ) );
+ $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
+ $link = $this->getTitle()->getFullURL( $query );
$msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) );
+ $out->wrapWikiMsg(
+ "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
+ [ $msg, $link ]
+ );
}
# Otherwise, output a regular diff...
} else {
$notice = '';
if ( $deleted ) {
$msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
- $notice = "<div class='mw-warning plainlinks'>\n" . wfMsgExt( $msg, 'parseinline' ) . "</div>\n";
+ $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
+ $this->msg( $msg )->parse() .
+ "</div>\n";
}
$this->showDiff( $oldHeader, $newHeader, $notice );
if ( !$diffOnly ) {
$this->renderNewRevision();
}
}
- wfProfileOut( __METHOD__ );
}
- protected function revisionDeleteLink( $rev ) {
- global $wgUser;
- $link = '';
- $canHide = $wgUser->isAllowed( 'deleterevision' );
- // Show del/undel link if:
- // (a) the user can delete revisions, or
- // (b) the user can view deleted revision *and* this one is deleted
- if ( $canHide || ( $rev->getVisibility() && $wgUser->isAllowed( 'deletedhistory' ) ) ) {
- $sk = $wgUser->getSkin();
- if ( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
- $link = $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
+ /**
+ * Build a link to mark a change as patrolled.
+ *
+ * Returns empty string if there's either no revision to patrol or the user is not allowed to.
+ * Side effect: When the patrol link is build, this method will call
+ * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax.
+ *
+ * @return string HTML or empty string
+ */
+ public function markPatrolledLink() {
+ if ( $this->mMarkPatrolledLink === null ) {
+ $linkInfo = $this->getMarkPatrolledLinkInfo();
+ // If false, there is no patrol link needed/allowed
+ if ( !$linkInfo ) {
+ $this->mMarkPatrolledLink = '';
} else {
- $query = array(
- 'type' => 'revision',
- 'target' => $rev->mTitle->getPrefixedDbkey(),
- 'ids' => $rev->getId()
- );
- $link = $sk->revDeleteLink( $query,
- $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
+ $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
+ Linker::linkKnown(
+ $this->mNewPage,
+ $this->msg( 'markaspatrolleddiff' )->escaped(),
+ [],
+ [
+ 'action' => 'markpatrolled',
+ 'rcid' => $linkInfo['rcid'],
+ ]
+ ) . ']</span>';
+ // Allow extensions to change the markpatrolled link
+ Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
+ &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
}
- $link = '   ' . $link . ' ';
}
- return $link;
+ return $this->mMarkPatrolledLink;
}
/**
- * Show the new revision of the page.
+ * Returns an array of meta data needed to build a "mark as patrolled" link and
+ * adds the mediawiki.page.patrol.ajax to the output.
+ *
+ * @return array|false An array of meta data for a patrol link (rcid only)
+ * or false if no link is needed
*/
- function renderNewRevision() {
- global $wgOut, $wgUser;
- wfProfileIn( __METHOD__ );
-
- $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
- # Add deleted rev tag if needed
- if ( !$this->mNewRev->userCan( Revision::DELETED_TEXT ) ) {
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' );
- } else if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' );
- }
-
- $pCache = true;
- if ( !$this->mNewRev->isCurrent() ) {
- $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
- $pCache = false;
- }
-
- $this->loadNewText();
- if ( is_object( $this->mNewRev ) ) {
- $wgOut->setRevisionId( $this->mNewRev->getId() );
- }
-
- if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
- // Stolen from Article::view --AG 2007-10-11
- // Give hooks a chance to customise the output
- if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
- // Wrap the whole lot in a <pre> and don't parse
- $m = array();
- preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
- $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
- $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
- $wgOut->addHTML( "\n</pre>\n" );
+ protected function getMarkPatrolledLinkInfo() {
+ global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI;
+
+ $user = $this->getUser();
+
+ // Prepare a change patrol link, if applicable
+ if (
+ // Is patrolling enabled and the user allowed to?
+ $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
+ // Only do this if the revision isn't more than 6 hours older
+ // than the Max RC age (6h because the RC might not be cleaned out regularly)
+ RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
+ ) {
+ // Look for an unpatrolled change corresponding to this diff
+ $db = wfGetDB( DB_REPLICA );
+ $change = RecentChange::newFromConds(
+ [
+ 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
+ 'rc_this_oldid' => $this->mNewid,
+ 'rc_patrolled' => 0
+ ],
+ __METHOD__
+ );
+
+ if ( $change && !$change->getPerformer()->equals( $user ) ) {
+ $rcid = $change->getAttribute( 'rc_id' );
+ } else {
+ // None found or the page has been created by the current user.
+ // If the user could patrol this it already would be patrolled
+ $rcid = 0;
}
- } elseif ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $wgOut ) ) ) {
- if ( $pCache ) {
- $article = new Article( $this->mTitle, 0 );
- $pOutput = ParserCache::singleton()->get( $article, $wgOut->parserOptions() );
- if ( $pOutput ) {
- $wgOut->addParserOutput( $pOutput );
- } else {
- $article->doViewParse();
+
+ // Allow extensions to possibly change the rcid here
+ // For example the rcid might be set to zero due to the user
+ // being the same as the performer of the change but an extension
+ // might still want to show it under certain conditions
+ Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
+
+ // Build the link
+ if ( $rcid ) {
+ $this->getOutput()->preventClickjacking();
+ if ( $wgEnableAPI && $wgEnableWriteAPI
+ && $user->isAllowed( 'writeapi' )
+ ) {
+ $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
}
- } else {
- $wgOut->addWikiTextTidy( $this->mNewtext );
+
+ return [
+ 'rcid' => $rcid,
+ ];
}
}
- if ( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) {
- $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
- }
+ // No mark as patrolled link applicable
+ return false;
+ }
- # Add redundant patrol link on bottom...
- if ( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan( 'patrol' ) ) {
- $sk = $wgUser->getSkin();
- $token = $wgUser->editToken( $this->mRcidMarkPatrolled );
- $wgOut->preventClickjacking();
- $wgOut->addHTML(
- "<div class='patrollink'>[" . $sk->link(
- $this->mTitle,
- wfMsgHtml( 'markaspatrolleddiff' ),
- array(),
- array(
- 'action' => 'markpatrolled',
- 'rcid' => $this->mRcidMarkPatrolled,
- 'token' => $token,
- )
- ) . ']</div>'
- );
+ /**
+ * @param Revision $rev
+ *
+ * @return string
+ */
+ protected function revisionDeleteLink( $rev ) {
+ $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
+ if ( $link !== '' ) {
+ $link = '   ' . $link . ' ';
}
- wfProfileOut( __METHOD__ );
+ return $link;
}
/**
- * Show the first revision of an article. Uses normal diff headers in
- * contrast to normal "old revision" display style.
+ * Show the new revision of the page.
*/
- function showFirstRevision() {
- global $wgOut, $wgUser;
- wfProfileIn( __METHOD__ );
-
- # Get article text from the DB
- #
- if ( ! $this->loadNewText() ) {
- $t = $this->mTitle->getPrefixedText();
- $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
- $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
- $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
- wfProfileOut( __METHOD__ );
- return;
- }
- if ( $this->mNewRev->isCurrent() ) {
- $wgOut->setArticleFlag( true );
+ public function renderNewRevision() {
+ $out = $this->getOutput();
+ $revHeader = $this->getRevisionHeader( $this->mNewRev );
+ # Add "current version as of X" title
+ $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
+ <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
+ # Page content may be handled by a hooked call instead...
+ # @codingStandardsIgnoreStart Ignoring long lines.
+ if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
+ $this->loadNewText();
+ $out->setRevisionId( $this->mNewid );
+ $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
+ $out->setArticleFlag( true );
+
+ if ( !Hooks::run( 'ArticleContentViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
+ // Handled by extension
+ } else {
+ // Normal page
+ if ( $this->getTitle()->equals( $this->mNewPage ) ) {
+ // If the Title stored in the context is the same as the one
+ // of the new revision, we can use its associated WikiPage
+ // object.
+ $wikiPage = $this->getWikiPage();
+ } else {
+ // Otherwise we need to create our own WikiPage object
+ $wikiPage = WikiPage::factory( $this->mNewPage );
+ }
+
+ $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
+
+ # WikiPage::getParserOutput() should not return false, but just in case
+ if ( $parserOutput ) {
+ // Allow extensions to change parser output here
+ if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput', [ $this, $out, $parserOutput, $wikiPage ] ) ) {
+ $out->addParserOutput( $parserOutput );
+ }
+ }
+ }
}
+ # @codingStandardsIgnoreEnd
- # Check if user is allowed to look at this page. If not, bail out.
- #
- if ( !$this->mTitle->userCanRead() ) {
- $wgOut->loginToUse();
- $wgOut->output();
- wfProfileOut( __METHOD__ );
- throw new MWException( "Permission Error: you do not have access to view this page" );
+ // Allow extensions to optionally not show the final patrolled link
+ if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
+ # Add redundant patrol link on bottom...
+ $out->addHTML( $this->markPatrolledLink() );
}
+ }
- # Prepare the header box
- #
- $sk = $wgUser->getSkin();
+ protected function getParserOutput( WikiPage $page, Revision $rev ) {
+ $parserOptions = $page->makeParserOptions( $this->getContext() );
- $next = $this->mTitle->getNextRevisionID( $this->mNewid );
- if ( !$next ) {
- $nextlink = '';
- } else {
- $nextlink = '<br />' . $sk->link(
- $this->mTitle,
- wfMsgHtml( 'nextdiff' ),
- array(
- 'id' => 'differences-nextlink'
- ),
- array(
- 'diff' => 'next',
- 'oldid' => $this->mNewid,
- ),
- array(
- 'known',
- 'noclasses'
- )
- );
+ if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( 'edit', $this->getUser() ) ) {
+ $parserOptions->setEditSection( false );
}
- $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\">" .
- $sk->revUserTools( $this->mNewRev ) . "<br />" . $sk->revComment( $this->mNewRev ) . $nextlink . "</div>\n";
- $wgOut->addHTML( $header );
+ $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
- $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
- wfProfileOut( __METHOD__ );
+ return $parserOutput;
}
/**
- * Get the diff text, send it to $wgOut
+ * Get the diff text, send it to the OutputPage object
* Returns false if the diff could not be generated, otherwise returns true
+ *
+ * @param string|bool $otitle Header for old text or false
+ * @param string|bool $ntitle Header for new text or false
+ * @param string $notice HTML between diff header and body
+ *
+ * @return bool
*/
- function showDiff( $otitle, $ntitle, $notice = '' ) {
- global $wgOut;
+ public function showDiff( $otitle, $ntitle, $notice = '' ) {
+ // Allow extensions to affect the output here
+ Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
+
$diff = $this->getDiff( $otitle, $ntitle, $notice );
if ( $diff === false ) {
- $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
+ $this->showMissingRevision();
+
return false;
} else {
$this->showDiffStyle();
- $wgOut->addHTML( $diff );
+ $this->getOutput()->addHTML( $diff );
+
return true;
}
}
/**
- * Add style sheets and supporting JS for diff display.
+ * Add style sheets for diff display.
*/
- function showDiffStyle() {
- global $wgOut;
- $wgOut->addModuleStyles( 'mediawiki.legacy.diff' );
- $wgOut->addModuleScripts( 'mediawiki.legacy.diff' );
+ public function showDiffStyle() {
+ $this->getOutput()->addModuleStyles( 'mediawiki.diff.styles' );
}
/**
* Get complete diff table, including header
*
- * @param $otitle Title: old title
- * @param $ntitle Title: new title
- * @param $notice String: HTML between diff header and body
+ * @param string|bool $otitle Header for old text or false
+ * @param string|bool $ntitle Header for new text or false
+ * @param string $notice HTML between diff header and body
+ *
* @return mixed
*/
- function getDiff( $otitle, $ntitle, $notice = '' ) {
+ public function getDiff( $otitle, $ntitle, $notice = '' ) {
$body = $this->getDiffBody();
if ( $body === false ) {
return false;
- } else {
- $multi = $this->getMultiNotice();
- return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
}
+
+ $multi = $this->getMultiNotice();
+ // Display a message when the diff is empty
+ if ( $body === '' ) {
+ $notice .= '<div class="mw-diff-empty">' .
+ $this->msg( 'diff-empty' )->parse() .
+ "</div>\n";
+ }
+
+ return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
}
/**
* @return mixed (string/false)
*/
public function getDiffBody() {
- global $wgMemc;
- wfProfileIn( __METHOD__ );
$this->mCacheHit = true;
// Check if the diff should be hidden from this user
if ( !$this->loadRevisionData() ) {
- wfProfileOut( __METHOD__ );
return false;
- } elseif ( $this->mOldRev && !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
- wfProfileOut( __METHOD__ );
+ } elseif ( $this->mOldRev &&
+ !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
+ ) {
return false;
- } elseif ( $this->mNewRev && !$this->mNewRev->userCan( Revision::DELETED_TEXT ) ) {
- wfProfileOut( __METHOD__ );
+ } elseif ( $this->mNewRev &&
+ !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() )
+ ) {
return false;
}
// Short-circuit
- if ( $this->mOldRev && $this->mNewRev
- && $this->mOldRev->getID() == $this->mNewRev->getID() )
- {
- wfProfileOut( __METHOD__ );
- return '';
+ if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
+ && $this->mOldRev->getId() == $this->mNewRev->getId() )
+ ) {
+ if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
+ return '';
+ }
}
// Cacheable?
$key = false;
+ $cache = ObjectCache::getMainWANInstance();
if ( $this->mOldid && $this->mNewid ) {
- $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION,
- 'oldid', $this->mOldid, 'newid', $this->mNewid );
+ $key = $this->getDiffBodyCacheKey();
+
// Try cache
if ( !$this->mRefreshCache ) {
- $difftext = $wgMemc->get( $key );
+ $difftext = $cache->get( $key );
if ( $difftext ) {
- wfIncrStats( 'diff_cache_hit' );
+ wfIncrStats( 'diff_cache.hit' );
$difftext = $this->localiseLineNumbers( $difftext );
$difftext .= "\n<!-- diff cache key $key -->\n";
- wfProfileOut( __METHOD__ );
+
return $difftext;
}
} // don't try to load but save the result
// Loadtext is permission safe, this just clears out the diff
if ( !$this->loadText() ) {
- wfProfileOut( __METHOD__ );
return false;
}
- $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
+ $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
+
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $diffEngine = $this;
// Save to cache for 7 days
- if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
- wfIncrStats( 'diff_uncacheable' );
+ if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
+ wfIncrStats( 'diff_cache.uncacheable' );
} elseif ( $key !== false && $difftext !== false ) {
- wfIncrStats( 'diff_cache_miss' );
- $wgMemc->set( $key, $difftext, 7 * 86400 );
+ wfIncrStats( 'diff_cache.miss' );
+ $cache->set( $key, $difftext, 7 * 86400 );
} else {
- wfIncrStats( 'diff_uncacheable' );
+ wfIncrStats( 'diff_cache.uncacheable' );
}
// Replace line numbers with the text in the user's language
if ( $difftext !== false ) {
$difftext = $this->localiseLineNumbers( $difftext );
}
- wfProfileOut( __METHOD__ );
+
return $difftext;
}
/**
- * Make sure the proper modules are loaded before we try to
- * make the diff
+ * Returns the cache key for diff body text or content.
+ *
+ * @since 1.23
+ *
+ * @throws MWException
+ * @return string
+ */
+ protected function getDiffBodyCacheKey() {
+ if ( !$this->mOldid || !$this->mNewid ) {
+ throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
+ }
+
+ return wfMemcKey( 'diff', 'version', self::DIFF_VERSION,
+ 'oldid', $this->mOldid, 'newid', $this->mNewid );
+ }
+
+ /**
+ * Generate a diff, no caching.
+ *
+ * This implementation uses generateTextDiffBody() to generate a diff based on the default
+ * serialization of the given Content objects. This will fail if $old or $new are not
+ * instances of TextContent.
+ *
+ * Subclasses may override this to provide a different rendering for the diff,
+ * perhaps taking advantage of the content's native form. This is required for all content
+ * models that are not text based.
+ *
+ * @since 1.21
+ *
+ * @param Content $old Old content
+ * @param Content $new New content
+ *
+ * @throws MWException If old or new content is not an instance of TextContent.
+ * @return bool|string
*/
- private function initDiffEngines() {
- global $wgExternalDiffEngine;
- if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) {
- wfProfileIn( __METHOD__ . '-php_wikidiff.so' );
- wfSuppressWarnings();
- dl( 'php_wikidiff.so' );
- wfRestoreWarnings();
- wfProfileOut( __METHOD__ . '-php_wikidiff.so' );
- }
- else if ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) {
- wfProfileIn( __METHOD__ . '-php_wikidiff2.so' );
- wfSuppressWarnings();
- wfDl( 'wikidiff2' );
- wfRestoreWarnings();
- wfProfileOut( __METHOD__ . '-php_wikidiff2.so' );
+ public function generateContentDiffBody( Content $old, Content $new ) {
+ if ( !( $old instanceof TextContent ) ) {
+ throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " .
+ "override generateContentDiffBody to fix this." );
}
+
+ if ( !( $new instanceof TextContent ) ) {
+ throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
+ . "override generateContentDiffBody to fix this." );
+ }
+
+ $otext = $old->serialize();
+ $ntext = $new->serialize();
+
+ return $this->generateTextDiffBody( $otext, $ntext );
}
/**
* Generate a diff, no caching
*
- * @param $otext String: old text, must be already segmented
- * @param $ntext String: new text, must be already segmented
+ * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
+ *
+ * @param string $otext Old text, must be already segmented
+ * @param string $ntext New text, must be already segmented
+ *
+ * @return bool|string
+ */
+ public function generateTextDiffBody( $otext, $ntext ) {
+ $diff = function () use ( $otext, $ntext ) {
+ $time = microtime( true );
+
+ $result = $this->textDiff( $otext, $ntext );
+
+ $time = intval( ( microtime( true ) - $time ) * 1000 );
+ MediaWikiServices::getInstance()->getStatsdDataFactory()->timing( 'diff_time', $time );
+ // Log requests slower than 99th percentile
+ if ( $time > 100 && $this->mOldPage && $this->mNewPage ) {
+ wfDebugLog( 'diff',
+ "$time ms diff: {$this->mOldid} -> {$this->mNewid} {$this->mNewPage}" );
+ }
+
+ return $result;
+ };
+
+ /**
+ * @param Status $status
+ * @throws FatalError
+ */
+ $error = function ( $status ) {
+ throw new FatalError( $status->getWikiText() );
+ };
+
+ // Use PoolCounter if the diff looks like it can be expensive
+ if ( strlen( $otext ) + strlen( $ntext ) > 20000 ) {
+ $work = new PoolCounterWorkViaCallback( 'diff',
+ md5( $otext ) . md5( $ntext ),
+ [ 'doWork' => $diff, 'error' => $error ]
+ );
+ return $work->execute();
+ }
+
+ return $diff();
+ }
+
+ /**
+ * Generates diff, to be wrapped internally in a logging/instrumentation
+ *
+ * @param string $otext Old text, must be already segmented
+ * @param string $ntext New text, must be already segmented
+ * @return bool|string
*/
- function generateDiffBody( $otext, $ntext ) {
+ protected function textDiff( $otext, $ntext ) {
global $wgExternalDiffEngine, $wgContLang;
$otext = str_replace( "\r\n", "\n", $otext );
$ntext = str_replace( "\r\n", "\n", $ntext );
- $this->initDiffEngines();
+ if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
+ wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
+ $wgExternalDiffEngine = false;
+ } elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
+ // Same as above, but with no deprecation warnings
+ $wgExternalDiffEngine = false;
+ } elseif ( !is_string( $wgExternalDiffEngine ) && $wgExternalDiffEngine !== false ) {
+ // And prevent people from shooting themselves in the foot...
+ wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
+ $wgExternalDiffEngine = false;
+ }
+
+ // Better external diff engine, the 2 may some day be dropped
+ // This one does the escaping and segmenting itself
+ if ( function_exists( 'wikidiff2_do_diff' ) && $wgExternalDiffEngine === false ) {
+ $wikidiff2Version = phpversion( 'wikidiff2' );
+ if (
+ $wikidiff2Version !== false &&
+ version_compare( $wikidiff2Version, '1.5.0', '>=' )
+ ) {
+ $text = wikidiff2_do_diff(
+ $otext,
+ $ntext,
+ 2,
+ $this->getConfig()->get( 'WikiDiff2MovedParagraphDetectionCutoff' )
+ );
+ } else {
+ // Don't pass the 4th parameter for compatibility with older versions of wikidiff2
+ $text = wikidiff2_do_diff(
+ $otext,
+ $ntext,
+ 2
+ );
- if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) {
- # For historical reasons, external diff engine expects
- # input text to be HTML-escaped already
- $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
- $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
- return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
- $this->debug( 'wikidiff1' );
- }
+ // Log a warning in case the configuration value is set to not silently ignore it
+ if ( $this->getConfig()->get( 'WikiDiff2MovedParagraphDetectionCutoff' ) > 0 ) {
+ wfLogWarning( '$wgWikiDiff2MovedParagraphDetectionCutoff is set but has no
+ effect since the used version of WikiDiff2 does not support it.' );
+ }
+ }
- if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) {
- # Better external diff engine, the 2 may some day be dropped
- # This one does the escaping and segmenting itself
- wfProfileIn( 'wikidiff2_do_diff' );
- $text = wikidiff2_do_diff( $otext, $ntext, 2 );
$text .= $this->debug( 'wikidiff2' );
- wfProfileOut( 'wikidiff2_do_diff' );
+
return $text;
- }
- if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
+ } elseif ( $wgExternalDiffEngine !== false && is_executable( $wgExternalDiffEngine ) ) {
# Diff via the shell
- global $wgTmpDirectory;
- $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
- $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
+ $tmpDir = wfTempDir();
+ $tempName1 = tempnam( $tmpDir, 'diff_' );
+ $tempName2 = tempnam( $tmpDir, 'diff_' );
$tempFile1 = fopen( $tempName1, "w" );
if ( !$tempFile1 ) {
- wfProfileOut( __METHOD__ );
return false;
}
$tempFile2 = fopen( $tempName2, "w" );
if ( !$tempFile2 ) {
- wfProfileOut( __METHOD__ );
return false;
}
fwrite( $tempFile1, $otext );
fclose( $tempFile1 );
fclose( $tempFile2 );
$cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
- wfProfileIn( __METHOD__ . "-shellexec" );
$difftext = wfShellExec( $cmd );
$difftext .= $this->debug( "external $wgExternalDiffEngine" );
- wfProfileOut( __METHOD__ . "-shellexec" );
unlink( $tempName1 );
unlink( $tempName2 );
- wfProfileOut( __METHOD__ );
+
return $difftext;
}
$nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
$diffs = new Diff( $ota, $nta );
$formatter = new TableDiffFormatter();
- $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
- wfProfileOut( __METHOD__ );
+ $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
+
return $difftext;
- $this->debug();
}
/**
* Generate a debug comment indicating diff generating time,
* server node, and generator backend.
+ *
+ * @param string $generator : What diff engine was used
+ *
+ * @return string
*/
protected function debug( $generator = "internal" ) {
global $wgShowHostnames;
if ( !$this->enableDebugComment ) {
return '';
}
- $data = array( $generator );
+ $data = [ $generator ];
if ( $wgShowHostnames ) {
$data[] = wfHostname();
}
$data[] = wfTimestamp( TS_DB );
+
return "<!-- diff generator: " .
- implode( " ",
- array_map(
- "htmlspecialchars",
- $data ) ) .
+ implode( " ", array_map( "htmlspecialchars", $data ) ) .
" -->\n";
}
/**
* Replace line numbers with the text in the user's language
+ *
+ * @param string $text
+ *
+ * @return mixed
*/
- function localiseLineNumbers( $text ) {
- return preg_replace_callback( '/<!--LINE (\d+)-->/',
- array( &$this, 'localiseLineNumbersCb' ), $text );
+ public function localiseLineNumbers( $text ) {
+ return preg_replace_callback(
+ '/<!--LINE (\d+)-->/',
+ [ $this, 'localiseLineNumbersCb' ],
+ $text
+ );
}
- function localiseLineNumbersCb( $matches ) {
- global $wgLang;
- if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return '';
- return wfMsgExt( 'lineno', 'escape', $wgLang->formatNum( $matches[1] ) );
- }
+ public function localiseLineNumbersCb( $matches ) {
+ if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
+ return '';
+ }
+ return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
+ }
/**
* If there are revisions between the ones being compared, return a note saying so.
+ *
* @return string
*/
- function getMultiNotice() {
+ public function getMultiNotice() {
if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) {
return '';
} elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) {
return '';
}
- $oldid = $this->mOldRev->getId();
- $newid = $this->mNewRev->getId();
- if ( $oldid > $newid ) {
- $tmp = $oldid; $oldid = $newid; $newid = $tmp;
+ if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
+ $oldRev = $this->mNewRev; // flip
+ $newRev = $this->mOldRev; // flip
+ } else { // normal case
+ $oldRev = $this->mOldRev;
+ $newRev = $this->mNewRev;
}
- $nEdits = $this->mTitle->countRevisionsBetween( $oldid, $newid );
- if ( $nEdits > 0 ) {
- $limit = 100;
- // We use ($limit + 1) so we can detect if there are > 100 authors
- // in a given revision range. In that case, diff-multi-manyusers is used.
- $numUsers = $this->mTitle->countAuthorsBetween( $oldid, $newid, $limit + 1 );
+ // Sanity: don't show the notice if too many rows must be scanned
+ // @todo show some special message for that case
+ $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
+ if ( $nEdits > 0 && $nEdits <= 1000 ) {
+ $limit = 100; // use diff-multi-manyusers if too many users
+ $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
+ $numUsers = count( $users );
+
+ if ( $numUsers == 1 && $users[0] == $newRev->getUserText( Revision::RAW ) ) {
+ $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
+ }
+
return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
}
+
return ''; // nothing
}
/**
* Get a notice about how many intermediate edits and users there are
- * @param $numEdits int
- * @param $numUsers int
- * @param $limit int
+ *
+ * @param int $numEdits
+ * @param int $numUsers
+ * @param int $limit
+ *
* @return string
*/
public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
- global $wgLang;
- if ( $numUsers > $limit ) {
+ if ( $numUsers === 0 ) {
+ $msg = 'diff-multi-sameuser';
+ } elseif ( $numUsers > $limit ) {
$msg = 'diff-multi-manyusers';
$numUsers = $limit;
} else {
- $msg = 'diff-multi';
+ $msg = 'diff-multi-otherusers';
+ }
+
+ return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
+ }
+
+ /**
+ * Get a header for a specified revision.
+ *
+ * @param Revision $rev
+ * @param string $complete 'complete' to get the header wrapped depending
+ * the visibility of the revision and a link to edit the page.
+ *
+ * @return string HTML fragment
+ */
+ public function getRevisionHeader( Revision $rev, $complete = '' ) {
+ $lang = $this->getLanguage();
+ $user = $this->getUser();
+ $revtimestamp = $rev->getTimestamp();
+ $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
+ $dateofrev = $lang->userDate( $revtimestamp, $user );
+ $timeofrev = $lang->userTime( $revtimestamp, $user );
+
+ $header = $this->msg(
+ $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
+ $timestamp,
+ $dateofrev,
+ $timeofrev
+ )->escaped();
+
+ if ( $complete !== 'complete' ) {
+ return $header;
+ }
+
+ $title = $rev->getTitle();
+
+ $header = Linker::linkKnown( $title, $header, [],
+ [ 'oldid' => $rev->getId() ] );
+
+ if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
+ $editQuery = [ 'action' => 'edit' ];
+ if ( !$rev->isCurrent() ) {
+ $editQuery['oldid'] = $rev->getId();
+ }
+
+ $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
+ $msg = $this->msg( $key )->escaped();
+ $editLink = $this->msg( 'parentheses' )->rawParams(
+ Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
+ $header .= ' ' . Html::rawElement(
+ 'span',
+ [ 'class' => 'mw-diff-edit' ],
+ $editLink
+ );
+ if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $header = Html::rawElement(
+ 'span',
+ [ 'class' => 'history-deleted' ],
+ $header
+ );
+ }
+ } else {
+ $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
}
- return wfMsgExt( $msg, 'parseinline',
- $wgLang->formatnum( $numEdits ), $wgLang->formatnum( $numUsers ) );
+
+ return $header;
}
/**
* Add the header to a diff body
+ *
+ * @param string $diff Diff body
+ * @param string $otitle Old revision header
+ * @param string $ntitle New revision header
+ * @param string $multi Notice telling user that there are intermediate
+ * revisions between the ones being compared
+ * @param string $notice Other notices, e.g. that user is viewing deleted content
+ *
+ * @return string
*/
- static function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
- $header = "<table class='diff'>";
- if ( $diff ) { // Safari/Chrome show broken output if cols not used
+ public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
+ // shared.css sets diff in interface language/dir, but the actual content
+ // is often in a different language, mostly the page content language/dir
+ $header = Html::openElement( 'table', [
+ 'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
+ 'data-mw' => 'interface',
+ ] );
+ $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
+
+ if ( !$diff && !$otitle ) {
$header .= "
- <col class='diff-marker' />
- <col class='diff-content' />
- <col class='diff-marker' />
- <col class='diff-content' />";
- $colspan = 2;
- $multiColspan = 4;
+ <tr style=\"vertical-align: top;\" lang=\"{$userLang}\">
+ <td class=\"diff-ntitle\">{$ntitle}</td>
+ </tr>";
+ $multiColspan = 1;
} else {
- $colspan = 1;
- $multiColspan = 2;
+ if ( $diff ) { // Safari/Chrome show broken output if cols not used
+ $header .= "
+ <col class=\"diff-marker\" />
+ <col class=\"diff-content\" />
+ <col class=\"diff-marker\" />
+ <col class=\"diff-content\" />";
+ $colspan = 2;
+ $multiColspan = 4;
+ } else {
+ $colspan = 1;
+ $multiColspan = 2;
+ }
+ if ( $otitle || $ntitle ) {
+ $header .= "
+ <tr style=\"vertical-align: top;\" lang=\"{$userLang}\">
+ <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
+ <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
+ </tr>";
+ }
}
- $header .= "
- <tr valign='top'>
- <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
- <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
- </tr>";
if ( $multi != '' ) {
- $header .= "<tr><td colspan='{$multiColspan}' align='center' class='diff-multi'>{$multi}</td></tr>";
+ $header .= "<tr><td colspan=\"{$multiColspan}\" style=\"text-align: center;\" " .
+ "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
}
if ( $notice != '' ) {
- $header .= "<tr><td colspan='{$multiColspan}' align='center'>{$notice}</td></tr>";
+ $header .= "<tr><td colspan=\"{$multiColspan}\" style=\"text-align: center;\" " .
+ "lang=\"{$userLang}\">{$notice}</td></tr>";
}
return $header . $diff . "</table>";
/**
* Use specified text instead of loading from the database
+ * @param Content $oldContent
+ * @param Content $newContent
+ * @since 1.21
*/
- function setText( $oldText, $newText ) {
- $this->mOldtext = $oldText;
- $this->mNewtext = $newText;
+ public function setContent( Content $oldContent, Content $newContent ) {
+ $this->mOldContent = $oldContent;
+ $this->mNewContent = $newContent;
+
$this->mTextLoaded = 2;
$this->mRevisionsLoaded = true;
}
+ /**
+ * Set the language in which the diff text is written
+ * (Defaults to page content language).
+ * @param Language|string $lang
+ * @since 1.19
+ */
+ public function setTextLanguage( $lang ) {
+ $this->mDiffLang = wfGetLangObj( $lang );
+ }
+
+ /**
+ * Maps a revision pair definition as accepted by DifferenceEngine constructor
+ * to a pair of actual integers representing revision ids.
+ *
+ * @param int $old Revision id, e.g. from URL parameter 'oldid'
+ * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff'
+ *
+ * @return int[] List of two revision ids, older first, later second.
+ * Zero signifies invalid argument passed.
+ * false signifies that there is no previous/next revision ($old is the oldest/newest one).
+ */
+ public function mapDiffPrevNext( $old, $new ) {
+ if ( $new === 'prev' ) {
+ // Show diff between revision $old and the previous one. Get previous one from DB.
+ $newid = intval( $old );
+ $oldid = $this->getTitle()->getPreviousRevisionID( $newid );
+ } elseif ( $new === 'next' ) {
+ // Show diff between revision $old and the next one. Get next one from DB.
+ $oldid = intval( $old );
+ $newid = $this->getTitle()->getNextRevisionID( $oldid );
+ } else {
+ $oldid = intval( $old );
+ $newid = intval( $new );
+ }
+
+ return [ $oldid, $newid ];
+ }
+
+ /**
+ * Load revision IDs
+ */
+ private function loadRevisionIds() {
+ if ( $this->mRevisionsIdsLoaded ) {
+ return;
+ }
+
+ $this->mRevisionsIdsLoaded = true;
+
+ $old = $this->mOldid;
+ $new = $this->mNewid;
+
+ list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
+ if ( $new === 'next' && $this->mNewid === false ) {
+ # if no result, NewId points to the newest old revision. The only newer
+ # revision is cur, which is "0".
+ $this->mNewid = 0;
+ }
+
+ Hooks::run(
+ 'NewDifferenceEngine',
+ [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
+ );
+ }
+
/**
* Load revision metadata for the specified articles. If newid is 0, then compare
* the old article in oldid to the current article; if oldid is 0, then
* If oldid is false, leave the corresponding revision object set
* to false. This is impossible via ordinary user input, and is provided for
* API convenience.
+ *
+ * @return bool
*/
- function loadRevisionData() {
- global $wgLang, $wgUser;
+ public function loadRevisionData() {
if ( $this->mRevisionsLoaded ) {
return true;
- } else {
- // Whether it succeeds or fails, we don't want to try again
- $this->mRevisionsLoaded = true;
}
+ // Whether it succeeds or fails, we don't want to try again
+ $this->mRevisionsLoaded = true;
+
+ $this->loadRevisionIds();
+
// Load the new revision object
- $this->mNewRev = $this->mNewid
- ? Revision::newFromId( $this->mNewid )
- : Revision::newFromTitle( $this->mTitle );
- if ( !$this->mNewRev instanceof Revision )
+ if ( $this->mNewid ) {
+ $this->mNewRev = Revision::newFromId( $this->mNewid );
+ } else {
+ $this->mNewRev = Revision::newFromTitle(
+ $this->getTitle(),
+ false,
+ Revision::READ_NORMAL
+ );
+ }
+
+ if ( !$this->mNewRev instanceof Revision ) {
return false;
+ }
// Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
$this->mNewid = $this->mNewRev->getId();
-
- // Check if page is editable
- $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
-
- // Set assorted variables
- $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
- $dateofrev = $wgLang->date( $this->mNewRev->getTimestamp(), true );
- $timeofrev = $wgLang->time( $this->mNewRev->getTimestamp(), true );
$this->mNewPage = $this->mNewRev->getTitle();
- if ( $this->mNewRev->isCurrent() ) {
- $newLink = $this->mNewPage->escapeLocalUrl( array(
- 'oldid' => $this->mNewid
- ) );
- $this->mPagetitle = htmlspecialchars( wfMsg(
- 'currentrev-asof',
- $timestamp,
- $dateofrev,
- $timeofrev
- ) );
- $newEdit = $this->mNewPage->escapeLocalUrl( array(
- 'action' => 'edit'
- ) );
-
- $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
- $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
- } else {
- $newLink = $this->mNewPage->escapeLocalUrl( array(
- 'oldid' => $this->mNewid
- ) );
- $newEdit = $this->mNewPage->escapeLocalUrl( array(
- 'action' => 'edit',
- 'oldid' => $this->mNewid
- ) );
- $this->mPagetitle = htmlspecialchars( wfMsg(
- 'revisionasof',
- $timestamp,
- $dateofrev,
- $timeofrev
- ) );
-
- $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
- $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
- }
- if ( !$this->mNewRev->userCan( Revision::DELETED_TEXT ) ) {
- $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
- } else if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $this->mNewtitle = "<span class='history-deleted'>{$this->mNewtitle}</span>";
- }
// Load the old revision object
$this->mOldRev = false;
if ( $this->mOldRev ) {
$this->mOldPage = $this->mOldRev->getTitle();
+ }
- $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
- $dateofrev = $wgLang->date( $this->mOldRev->getTimestamp(), true );
- $timeofrev = $wgLang->time( $this->mOldRev->getTimestamp(), true );
- $oldLink = $this->mOldPage->escapeLocalUrl( array(
- 'oldid' => $this->mOldid
- ) );
- $oldEdit = $this->mOldPage->escapeLocalUrl( array(
- 'action' => 'edit',
- 'oldid' => $this->mOldid
- ) );
- $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t, $dateofrev, $timeofrev ) );
-
- $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
- . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
- // Add an "undo" link
- $newUndo = $this->mNewPage->escapeLocalUrl( array(
- 'action' => 'edit',
- 'undoafter' => $this->mOldid,
- 'undo' => $this->mNewid
- ) );
- $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
- $htmlTitle = Xml::expandAttributes( array( 'title' => $wgUser->getSkin()->titleAttrib( 'undo' ) ) );
- if ( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</a>)";
- }
-
- if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
- $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
- } else if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
- }
+ // Load tags information for both revisions
+ $dbr = wfGetDB( DB_REPLICA );
+ if ( $this->mOldid !== false ) {
+ $this->mOldTags = $dbr->selectField(
+ 'tag_summary',
+ 'ts_tags',
+ [ 'ts_rev_id' => $this->mOldid ],
+ __METHOD__
+ );
+ } else {
+ $this->mOldTags = false;
}
+ $this->mNewTags = $dbr->selectField(
+ 'tag_summary',
+ 'ts_tags',
+ [ 'ts_rev_id' => $this->mNewid ],
+ __METHOD__
+ );
return true;
}
/**
* Load the text of the revisions, as well as revision data.
+ *
+ * @return bool
*/
- function loadText() {
+ public function loadText() {
if ( $this->mTextLoaded == 2 ) {
return true;
- } else {
- // Whether it succeeds or fails, we don't want to try again
- $this->mTextLoaded = 2;
}
+ // Whether it succeeds or fails, we don't want to try again
+ $this->mTextLoaded = 2;
+
if ( !$this->loadRevisionData() ) {
return false;
}
+
if ( $this->mOldRev ) {
- $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
- if ( $this->mOldtext === false ) {
+ $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+ if ( $this->mOldContent === null ) {
return false;
}
}
+
if ( $this->mNewRev ) {
- $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
- if ( $this->mNewtext === false ) {
+ $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+ Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
+ if ( $this->mNewContent === null ) {
return false;
}
}
+
return true;
}
/**
* Load the text of the new revision, not the old one
+ *
+ * @return bool
*/
- function loadNewText() {
+ public function loadNewText() {
if ( $this->mTextLoaded >= 1 ) {
return true;
- } else {
- $this->mTextLoaded = 1;
}
+
+ $this->mTextLoaded = 1;
+
if ( !$this->loadRevisionData() ) {
return false;
}
- $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
+
+ $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+
+ Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
+
return true;
}
+
}