From 222b01f5169f1c7e69762e0e8904c24f78f71882 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 28 Jul 2010 11:52:48 +0200 Subject: update to MediaWiki 1.16.0 --- includes/diff/DifferenceEngine.php | 933 ------------------------------ includes/diff/DifferenceInterface.php | 1021 +++++++++++++++++++++++++++++++++ includes/diff/HTMLDiff.php | 1009 -------------------------------- includes/diff/Nodes.php | 439 -------------- 4 files changed, 1021 insertions(+), 2381 deletions(-) create mode 100644 includes/diff/DifferenceInterface.php delete mode 100644 includes/diff/HTMLDiff.php delete mode 100644 includes/diff/Nodes.php (limited to 'includes/diff') diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index aa48f9f3..184d1fc2 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -3,939 +3,6 @@ * @defgroup DifferenceEngine DifferenceEngine */ -/** - * 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. - */ -define( 'MW_DIFF_VERSION', '1.11a' ); - -/** - * @todo document - * @ingroup DifferenceEngine - */ -class DifferenceEngine { - /**#@+ - * @private - */ - 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? - var $htmldiff; - - protected $unhide = 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 $htmldiff boolean If set, output using HTMLDiff instead of raw wikicode diff - * @param $unhide boolean If set, allow viewing deleted revs - */ - function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false , $htmldiff = false, $unhide = false ) { - $this->mTitle = $titleObj; - 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->mRefreshCache = $refreshCache; - $this->htmldiff = $htmldiff; - $this->unhide = $unhide; - } - - function getTitle() { - return $this->mTitle; - } - - function wasCacheHit() { - return $this->mCacheHit; - } - - function getOldid() { - return $this->mOldid; - } - - function getNewid() { - return $this->mNewid; - } - - function showDiffPage( $diffOnly = false ) { - global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff; - wfProfileIn( __METHOD__ ); - - - # 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("action=raw&oldid=".$this->mOldid); - $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid); - $special=$wgLang->getNsText(NS_SPECIAL); - $control=<<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', "$t", $d ); - wfProfileOut( __METHOD__ ); - return; - } - - wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) ); - - if ( $this->mNewRev->isCurrent() ) { - $wgOut->setArticleFlag( true ); - } - - # 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__ ); - return; - } - - $wgOut->suppressQuickbar(); - - $oldTitle = $this->mOldPage->getPrefixedText(); - $newTitle = $this->mNewPage->getPrefixedText(); - if( $oldTitle == $newTitle ) { - $wgOut->setPageTitle( $newTitle ); - } else { - $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle ); - } - $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - - if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) { - $wgOut->loginToUse(); - $wgOut->output(); - $wgOut->disable(); - wfProfileOut( __METHOD__ ); - return; - } - - $sk = $wgUser->getSkin(); - - // Check if page is editable - $editable = $this->mNewRev->getTitle()->userCan( 'edit' ); - if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) { - $rollback = '   ' . $sk->generateRollback( $this->mNewRev ); - } else { - $rollback = ''; - } - - // 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; - } else { - // Look for an unpatrolled change corresponding to this diff - $db = wfGetDB( DB_SLAVE ); - $change = RecentChange::newFromConds( - array( - // Add 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; - } - } - // Build the link - if( $rcid ) { - $patrol = ' [' . $sk->makeKnownLinkObj( $this->mTitle, - wfMsgHtml( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$rcid}" ) . ']'; - } else { - $patrol = ''; - } - } else { - $patrol = ''; - } - - $diffOnlyArg = ''; - # Carry over 'diffonly' param via navigation links - if( $diffOnly != $wgUser->getBoolOption('diffonly') ) { - $diffOnlyArg = '&diffonly='.$diffOnly; - } - $htmldiffarg = $this->htmlDiffArgument(); - # Make "previous revision link" - $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ), - "diff=prev&oldid={$this->mOldid}{$htmldiffarg}{$diffOnlyArg}", '', '', 'id="differences-prevlink"' ); - # Make "next revision link" - if( $this->mNewRev->isCurrent() ) { - $nextlink = ' '; - } else { - $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), - "diff=next&oldid={$this->mNewid}{$htmldiffarg}{$diffOnlyArg}", '', '', 'id="differences-nextlink"' ); - } - - $oldminor = ''; - $newminor = ''; - - if( $this->mOldRev->isMinor() ) { - $oldminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' '; - } - if( $this->mNewRev->isMinor() ) { - $newminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' '; - } - - $rdel = ''; $ldel = ''; - if( $wgUser->isAllowed( 'deleterevision' ) ) { - if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) { - // If revision was hidden from sysops - $ldel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' ); - } else { - $query = array( 'target' => $this->mOldRev->mTitle->getPrefixedDbkey(), - 'oldid' => $this->mOldRev->getId() - ); - $ldel = $sk->revDeleteLink( $query, $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ); - } - $ldel = "   $ldel "; - // We don't currently handle well changing the top revision's settings - if( $this->mNewRev->isCurrent() ) { - $rdel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' ); - } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) { - // If revision was hidden from sysops - $rdel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' ); - } else { - $query = array( 'target' => $this->mNewRev->mTitle->getPrefixedDbkey(), - 'oldid' => $this->mNewRev->getId() - ); - $rdel = $sk->revDeleteLink( $query, $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ); - } - $rdel = "   $rdel "; - } - - $oldHeader = '
'.$this->mOldtitle.'
' . - '
' . $sk->revUserTools( $this->mOldRev, !$this->unhide ) . "
" . - '
' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel."
" . - '
' . $prevlink .'
'; - $newHeader = '
'.$this->mNewtitle.'
' . - '
' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) . " $rollback
" . - '
' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel."
" . - '
' . $nextlink . $patrol . '
'; - - # Check if this user can see the revisions - $allowed = $this->mOldRev->userCan(Revision::DELETED_TEXT) - && $this->mNewRev->userCan(Revision::DELETED_TEXT); - $deleted = $this->mOldRev->isDeleted(Revision::DELETED_TEXT) - || $this->mNewRev->isDeleted(Revision::DELETED_TEXT); - # Output the diff if allowed... - if( $deleted && (!$this->unhide || !$allowed) ) { - $this->showDiffStyle(); - $multi = $this->getMultiNotice(); - $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); - if( !$allowed ) { - # Give explanation for why revision is not visible - $wgOut->wrapWikiMsg( "\n", - array( 'rev-deleted-no-diff' ) ); - } else { - # Give explanation and add a link to view the diff... - $link = $this->mTitle->getFullUrl( "diff={$this->mNewid}&oldid={$this->mOldid}". - '&unhide=1&token='.urlencode( $wgUser->editToken($this->mNewid) ) ); - $wgOut->wrapWikiMsg( "\n", - array( 'rev-deleted-unhide-diff', $link ) ); - } - } else if( $wgEnableHtmlDiff && $this->htmldiff ) { - $multi = $this->getMultiNotice(); - $wgOut->addHTML('
'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ), - 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'
'); - $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); - $this->renderHtmlDiff(); - } else { - if( $wgEnableHtmlDiff ) { - $wgOut->addHTML('
'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ), - 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'
'); - } - $this->showDiff( $oldHeader, $newHeader ); - if( !$diffOnly ) { - $this->renderNewRevision(); - } - } - wfProfileOut( __METHOD__ ); - } - - /** - * Show the new revision of the page. - */ - function renderNewRevision() { - global $wgOut, $wgUser; - wfProfileIn( __METHOD__ ); - - $wgOut->addHTML( "

{$this->mPagetitle}

\n" ); - # Add deleted rev tag if needed - if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); - } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-view' ); - } - - if( !$this->mNewRev->isCurrent() ) { - $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( 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
 and don't parse
-				$m = array();
-				preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
-				$wgOut->addHTML( "
\n" );
-				$wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
-				$wgOut->addHTML( "\n
\n" ); - } - } else { - $wgOut->addWikiTextTidy( $this->mNewtext ); - } - - if( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) { - $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); - } - # Add redundant patrol link on bottom... - if( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan('patrol') ) { - $sk = $wgUser->getSkin(); - $wgOut->addHTML( - "' - ); - } - - wfProfileOut( __METHOD__ ); - } - - - function renderHtmlDiff() { - global $wgOut, $wgTitle, $wgParser, $wgDebugComments; - wfProfileIn( __METHOD__ ); - - $this->showDiffStyle(); - - $wgOut->addHTML( '

'.wfMsgHtml( 'visual-comparison' )."

\n" ); - #add deleted rev tag if needed - if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); - } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-view' ); - } - - if( !$this->mNewRev->isCurrent() ) { - $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); - } - - $this->loadText(); - - // Old revision - if( is_object( $this->mOldRev ) ) { - $wgOut->setRevisionId( $this->mOldRev->getId() ); - } - - $popts = $wgOut->parserOptions(); - $oldTidy = $popts->setTidy( true ); - $popts->setEditSection( false ); - - $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() ); - $popts->setTidy( $oldTidy ); - - //only for new? - //$wgOut->addParserOutputNoText( $parserOutput ); - $oldHtml = $parserOutput->getText(); - wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) ); - - // New revision - if( is_object( $this->mNewRev ) ) { - $wgOut->setRevisionId( $this->mNewRev->getId() ); - } - - $popts = $wgOut->parserOptions(); - $oldTidy = $popts->setTidy( true ); - - $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() ); - $popts->setTidy( $oldTidy ); - - $wgOut->addParserOutputNoText( $parserOutput ); - $newHtml = $parserOutput->getText(); - wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) ); - - unset($parserOutput, $popts); - - $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut)); - $differ->htmlDiff($oldHtml, $newHtml); - if ( $wgDebugComments ) { - $wgOut->addHTML( "\n" ); - } - - wfProfileOut( __METHOD__ ); - } - - /** - * Show the first revision of an article. Uses normal diff headers in - * contrast to normal "old revision" display style. - */ - 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', "$t", $d ); - wfProfileOut( __METHOD__ ); - return; - } - if ( $this->mNewRev->isCurrent() ) { - $wgOut->setArticleFlag( true ); - } - - # 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"); - } - - # Prepare the header box - # - $sk = $wgUser->getSkin(); - - $next = $this->mTitle->getNextRevisionID( $this->mNewid ); - if( !$next ) { - $nextlink = ''; - } else { - $nextlink = '
' . $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), - 'diff=next&oldid=' . $this->mNewid.$this->htmlDiffArgument(), '', '', 'id="differences-nextlink"' ); - } - $header = "
" . - $sk->revUserTools( $this->mNewRev ) . "
" . $sk->revComment( $this->mNewRev ) . $nextlink . "
\n"; - - $wgOut->addHTML( $header ); - - $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - - wfProfileOut( __METHOD__ ); - } - - function htmlDiffArgument(){ - global $wgEnableHtmlDiff; - if($wgEnableHtmlDiff){ - if($this->htmldiff){ - return '&htmldiff=1'; - }else{ - return '&htmldiff=0'; - } - }else{ - return ''; - } - } - - /** - * Get the diff text, send it to $wgOut - * Returns false if the diff could not be generated, otherwise returns true - */ - function showDiff( $otitle, $ntitle ) { - global $wgOut; - $diff = $this->getDiff( $otitle, $ntitle ); - if ( $diff === false ) { - $wgOut->addWikiMsg( 'missing-article', "(fixme, bug)", '' ); - return false; - } else { - $this->showDiffStyle(); - $wgOut->addHTML( $diff ); - return true; - } - } - - /** - * Add style sheets and supporting JS for diff display. - */ - function showDiffStyle() { - global $wgStylePath, $wgStyleVersion, $wgOut; - $wgOut->addStyle( 'common/diff.css' ); - - // JS is needed to detect old versions of Mozilla to work around an annoyance bug. - $wgOut->addScript( "" ); - } - - /** - * Get complete diff table, including header - * - * @param Title $otitle Old title - * @param Title $ntitle New title - * @return mixed - */ - function getDiff( $otitle, $ntitle ) { - $body = $this->getDiffBody(); - if ( $body === false ) { - return false; - } else { - $multi = $this->getMultiNotice(); - return $this->addHeader( $body, $otitle, $ntitle, $multi ); - } - } - - /** - * Get the diff table body, without header - * - * @return mixed - */ - function getDiffBody() { - global $wgMemc; - wfProfileIn( __METHOD__ ); - $this->mCacheHit = true; - // Check if the diff should be hidden from this user - if ( !$this->loadRevisionData() ) - return ''; - if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { - return ''; - } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - return ''; - } else if ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) { - return ''; - } - // Cacheable? - $key = false; - if ( $this->mOldid && $this->mNewid ) { - $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid ); - // Try cache - if ( !$this->mRefreshCache ) { - $difftext = $wgMemc->get( $key ); - if ( $difftext ) { - wfIncrStats( 'diff_cache_hit' ); - $difftext = $this->localiseLineNumbers( $difftext ); - $difftext .= "\n\n"; - wfProfileOut( __METHOD__ ); - return $difftext; - } - } // don't try to load but save the result - } - $this->mCacheHit = false; - - // Loadtext is permission safe, this just clears out the diff - if ( !$this->loadText() ) { - wfProfileOut( __METHOD__ ); - return false; - } - - $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); - - // Save to cache for 7 days - if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { - wfIncrStats( 'diff_uncacheable' ); - } else if ( $key !== false && $difftext !== false ) { - wfIncrStats( 'diff_cache_miss' ); - $wgMemc->set( $key, $difftext, 7*86400 ); - } else { - wfIncrStats( 'diff_uncacheable' ); - } - // Replace line numbers with the text in the user's language - if ( $difftext !== false ) { - $difftext = $this->localiseLineNumbers( $difftext ); - } - wfProfileOut( __METHOD__ ); - return $difftext; - } - - /** - * Generate a diff, no caching - * $otext and $ntext must be already segmented - */ - function generateDiffBody( $otext, $ntext ) { - global $wgExternalDiffEngine, $wgContLang; - - $otext = str_replace( "\r\n", "\n", $otext ); - $ntext = str_replace( "\r\n", "\n", $ntext ); - - if ( $wgExternalDiffEngine == 'wikidiff' ) { - # For historical reasons, external diff engine expects - # input text to be HTML-escaped already - $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) ); - $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) ); - if( !function_exists( 'wikidiff_do_diff' ) ) { - dl('php_wikidiff.so'); - } - return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . - $this->debug( 'wikidiff1' ); - } - - if ( $wgExternalDiffEngine == 'wikidiff2' ) { - # 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' ) ) { - wfProfileIn( __METHOD__ . "-dl" ); - @dl('php_wikidiff2.so'); - wfProfileOut( __METHOD__ . "-dl" ); - } - if ( function_exists( 'wikidiff2_do_diff' ) ) { - 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 ) { - # Diff via the shell - global $wgTmpDirectory; - $tempName1 = tempnam( $wgTmpDirectory, 'diff_' ); - $tempName2 = tempnam( $wgTmpDirectory, 'diff_' ); - - $tempFile1 = fopen( $tempName1, "w" ); - if ( !$tempFile1 ) { - wfProfileOut( __METHOD__ ); - return false; - } - $tempFile2 = fopen( $tempName2, "w" ); - if ( !$tempFile2 ) { - wfProfileOut( __METHOD__ ); - return false; - } - fwrite( $tempFile1, $otext ); - fwrite( $tempFile2, $ntext ); - 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 ); - return $difftext; - } - - # Native PHP diff - $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); - $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); - $diffs = new Diff( $ota, $nta ); - $formatter = new TableDiffFormatter(); - return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . - $this->debug(); - } - - /** - * Generate a debug comment indicating diff generating time, - * server node, and generator backend. - */ - protected function debug( $generator="internal" ) { - global $wgShowHostnames; - $data = array( $generator ); - if( $wgShowHostnames ) { - $data[] = wfHostname(); - } - $data[] = wfTimestamp( TS_DB ); - return "\n"; - } - - /** - * Replace line numbers with the text in the user's language - */ - function localiseLineNumbers( $text ) { - return preg_replace_callback( '//', - array( &$this, 'localiseLineNumbersCb' ), $text ); - } - - function localiseLineNumbersCb( $matches ) { - global $wgLang; - return wfMsgExt( 'lineno', array (), $wgLang->formatNum( $matches[1] ) ); - } - - - /** - * If there are revisions between the ones being compared, return a note saying so. - */ - function getMultiNotice() { - if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) ) - return ''; - - if( !$this->mOldPage->equals( $this->mNewPage ) ) { - // Comparing two different pages? Count would be meaningless. - return ''; - } - - $oldid = $this->mOldRev->getId(); - $newid = $this->mNewRev->getId(); - if ( $oldid > $newid ) { - $tmp = $oldid; $oldid = $newid; $newid = $tmp; - } - - $n = $this->mTitle->countRevisionsBetween( $oldid, $newid ); - if ( !$n ) - return ''; - - return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n ); - } - - - /** - * Add the header to a diff body - */ - static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) { - $header = " - - - - - - - - - - "; - - if ( $multi != '' ) - $header .= ""; - - return $header . $diff . "
{$otitle}{$ntitle}
{$multi}
"; - } - - /** - * Use specified text instead of loading from the database - */ - function setText( $oldText, $newText ) { - $this->mOldtext = $oldText; - $this->mNewtext = $newText; - $this->mTextLoaded = 2; - $this->mRevisionsLoaded = true; - } - - /** - * 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 - * compare the current article to the immediately previous one (ignoring the - * value of newid). - * - * 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. - */ - function loadRevisionData() { - global $wgLang, $wgUser; - if ( $this->mRevisionsLoaded ) { - return true; - } else { - // Whether it succeeds or fails, we don't want to try again - $this->mRevisionsLoaded = true; - } - - // Load the new revision object - $this->mNewRev = $this->mNewid - ? Revision::newFromId( $this->mNewid ) - : Revision::newFromTitle( $this->mTitle ); - 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 ); - $this->mNewPage = $this->mNewRev->getTitle(); - if( $this->mNewRev->isCurrent() ) { - $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); - $this->mPagetitle = wfMsgHTML( 'currentrev-asof', $timestamp ); - $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' ); - - $this->mNewtitle = "{$this->mPagetitle}"; - $this->mNewtitle .= " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; - - } else { - $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); - $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid ); - $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp ); - - $this->mNewtitle = "{$this->mPagetitle}"; - $this->mNewtitle .= " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; - } - if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - $this->mNewtitle = "{$this->mPagetitle}"; - } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - $this->mNewtitle = ''.$this->mNewtitle.''; - } - - // Load the old revision object - $this->mOldRev = false; - if( $this->mOldid ) { - $this->mOldRev = Revision::newFromId( $this->mOldid ); - } elseif ( $this->mOldid === 0 ) { - $rev = $this->mNewRev->getPrevious(); - if( $rev ) { - $this->mOldid = $rev->getId(); - $this->mOldRev = $rev; - } else { - // No previous revision; mark to show as first-version only. - $this->mOldid = false; - $this->mOldRev = false; - } - }/* elseif ( $this->mOldid === false ) leave mOldRev false; */ - - if( is_null( $this->mOldRev ) ) { - return false; - } - - if ( $this->mOldRev ) { - $this->mOldPage = $this->mOldRev->getTitle(); - - $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true ); - $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid ); - $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid ); - $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) ); - - $this->mOldtitle = "{$this->mOldPagetitle}" - . " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; - // Add an "undo" link - $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid); - $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) ); - $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' ); - if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { - $this->mNewtitle .= " (" . $htmlLink . ")"; - } - - if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) { - $this->mOldtitle = '' . $this->mOldPagetitle . ''; - } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { - $this->mOldtitle = '' . $this->mOldtitle . ''; - } - } - - return true; - } - - /** - * Load the text of the revisions, as well as revision data. - */ - function loadText() { - if ( $this->mTextLoaded == 2 ) { - return true; - } else { - // 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 ) { - return false; - } - } - if ( $this->mNewRev ) { - $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); - if ( $this->mNewtext === false ) { - return false; - } - } - return true; - } - - /** - * Load the text of the new revision, not the old one - */ - function loadNewText() { - if ( $this->mTextLoaded >= 1 ) { - return true; - } else { - $this->mTextLoaded = 1; - } - if ( !$this->loadRevisionData() ) { - return false; - } - $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); - return true; - } - - -} - // A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3) // // Copyright (C) 2000, 2001 Geoffrey T. Dairiki diff --git a/includes/diff/DifferenceInterface.php b/includes/diff/DifferenceInterface.php new file mode 100644 index 00000000..d7d36799 --- /dev/null +++ b/includes/diff/DifferenceInterface.php @@ -0,0 +1,1021 @@ +mTitle = $titleObj; + } else { + global $wgTitle; + $this->mTitle = $wgTitle; + } + 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->mRefreshCache = $refreshCache; + $this->unhide = $unhide; + } + + function setReducedLineNumbers( $value = true ) { + $this->mReducedLineNumbers = $value; + } + + function getTitle() { + return $this->mTitle; + } + + function wasCacheHit() { + return $this->mCacheHit; + } + + function getOldid() { + return $this->mOldid; + } + + function getNewid() { + return $this->mNewid; + } + + function showDiffPage( $diffOnly = false ) { + global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol; + wfProfileIn( __METHOD__ ); + + + # 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=<<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', "$t", $d ); + wfProfileOut( __METHOD__ ); + return; + } + + wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) ); + + if ( $this->mNewRev->isCurrent() ) { + $wgOut->setArticleFlag( true ); + } + + # 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__ ); + return; + } + + $wgOut->suppressQuickbar(); + + $oldTitle = $this->mOldPage->getPrefixedText(); + $newTitle = $this->mNewPage->getPrefixedText(); + if( $oldTitle == $newTitle ) { + $wgOut->setPageTitle( $newTitle ); + } else { + $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle ); + } + $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + + if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) { + $wgOut->loginToUse(); + $wgOut->output(); + $wgOut->disable(); + wfProfileOut( __METHOD__ ); + return; + } + + $sk = $wgUser->getSkin(); + + // Check if page is editable + $editable = $this->mNewRev->getTitle()->userCan( 'edit' ); + if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) { + $rollback = '   ' . $sk->generateRollback( $this->mNewRev ); + } else { + $rollback = ''; + } + + // 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; + } 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; + } + } + // Build the link + if( $rcid ) { + $patrol = ' [' . $sk->link( + $this->mTitle, + wfMsgHtml( 'markaspatrolleddiff' ), + array(), + array( + 'action' => 'markpatrolled', + 'rcid' => $rcid + ), + array( + 'known', + 'noclasses' + ) + ) . ']'; + } else { + $patrol = ''; + } + } else { + $patrol = ''; + } + + # Carry over 'diffonly' param via navigation links + if( $diffOnly != $wgUser->getBoolOption('diffonly') ) { + $query['diffonly'] = $diffOnly; + } + + # 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; + } + $prevlink = $sk->link( + $this->mTitle, + wfMsgHtml( 'previousdiff' ), + array( + 'id' => 'differences-prevlink' + ), + $query, + array( + 'known', + 'noclasses' + ) + ); + + # 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' + ) + ); + } + + $oldminor = ''; + $newminor = ''; + + if( $this->mOldRev->isMinor() ) { + $oldminor = ChangesList::flag( 'minor' ); + } + if( $this->mNewRev->isMinor() ) { + $newminor = ChangesList::flag( 'minor' ); + } + + # Handle RevisionDelete links... + $ldel = $this->revisionDeleteLink( $this->mOldRev ); + $rdel = $this->revisionDeleteLink( $this->mNewRev ); + + $oldHeader = '
'.$this->mOldtitle.'
' . + '
' . + $sk->revUserTools( $this->mOldRev, !$this->unhide ).'
' . + '
' . $oldminor . + $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel.'
' . + '
' . $prevlink .'
'; + $newHeader = '
'.$this->mNewtitle.'
' . + '
' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) . + " $rollback
" . + '
' . $newminor . + $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel.'
' . + '
' . $nextlink . $patrol . '
'; + + # 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) ) + $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 ) ); + if( !$allowed ) { + $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff'; + # Give explanation for why revision is not visible + $wgOut->wrapWikiMsg( "\n", + array( $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 + ) ); + $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff'; + $wgOut->wrapWikiMsg( "\n", array( $msg, $link ) ); + } + # Otherwise, output a regular diff... + } else { + # Add deletion notice if the user is viewing deleted content + $notice = ''; + if( $deleted ) { + $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view'; + $notice = "\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 + } else { + $query = array( + 'type' => 'revision', + 'target' => $rev->mTitle->getPrefixedDbkey(), + 'ids' => $rev->getId() + ); + $link = $sk->revDeleteLink( $query, + $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide ); + } + $link = '   ' . $link . ' '; + } + return $link; + } + + /** + * Show the new revision of the page. + */ + function renderNewRevision() { + global $wgOut, $wgUser; + wfProfileIn( __METHOD__ ); + + $wgOut->addHTML( "

{$this->mPagetitle}

\n" ); + # Add deleted rev tag if needed + if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); + } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-view' ); + } + + if( !$this->mNewRev->isCurrent() ) { + $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( 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
 and don't parse
+				$m = array();
+				preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
+				$wgOut->addHTML( "
\n" );
+				$wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
+				$wgOut->addHTML( "\n
\n" ); + } + } else { + $wgOut->addWikiTextTidy( $this->mNewtext ); + } + + if( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) { + $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); + } + # Add redundant patrol link on bottom... + if( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan('patrol') ) { + $sk = $wgUser->getSkin(); + $wgOut->addHTML( + "' + ); + } + + wfProfileOut( __METHOD__ ); + } + + /** + * Show the first revision of an article. Uses normal diff headers in + * contrast to normal "old revision" display style. + */ + 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', "$t", $d ); + wfProfileOut( __METHOD__ ); + return; + } + if ( $this->mNewRev->isCurrent() ) { + $wgOut->setArticleFlag( true ); + } + + # 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"); + } + + # Prepare the header box + # + $sk = $wgUser->getSkin(); + + $next = $this->mTitle->getNextRevisionID( $this->mNewid ); + if( !$next ) { + $nextlink = ''; + } else { + $nextlink = '
' . $sk->link( + $this->mTitle, + wfMsgHtml( 'nextdiff' ), + array( + 'id' => 'differences-nextlink' + ), + array( + 'diff' => 'next', + 'oldid' => $this->mNewid, + ), + array( + 'known', + 'noclasses' + ) + ); + } + $header = "
" . + $sk->revUserTools( $this->mNewRev ) . "
" . $sk->revComment( $this->mNewRev ) . $nextlink . "
\n"; + + $wgOut->addHTML( $header ); + + $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + + wfProfileOut( __METHOD__ ); + } + + /** + * Get the diff text, send it to $wgOut + * Returns false if the diff could not be generated, otherwise returns true + */ + function showDiff( $otitle, $ntitle, $notice = '' ) { + global $wgOut; + $diff = $this->getDiff( $otitle, $ntitle, $notice ); + if ( $diff === false ) { + $wgOut->addWikiMsg( 'missing-article', "(fixme, bug)", '' ); + return false; + } else { + $this->showDiffStyle(); + $wgOut->addHTML( $diff ); + return true; + } + } + + /** + * Add style sheets and supporting JS for diff display. + */ + function showDiffStyle() { + global $wgStylePath, $wgStyleVersion, $wgOut; + $wgOut->addStyle( 'common/diff.css' ); + + // JS is needed to detect old versions of Mozilla to work around an annoyance bug. + $wgOut->addScript( "" ); + } + + /** + * Get complete diff table, including header + * + * @param Title $otitle Old title + * @param Title $ntitle New title + * @param string $notice HTML between diff header and body + * @return mixed + */ + 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 ); + } + } + + /** + * Get the diff table body, without header + * + * @return mixed + */ + function getDiffBody() { + global $wgMemc; + wfProfileIn( __METHOD__ ); + $this->mCacheHit = true; + // Check if the diff should be hidden from this user + if ( !$this->loadRevisionData() ) + return ''; + if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { + return ''; + } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + return ''; + } else if ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) { + return ''; + } + // Cacheable? + $key = false; + if ( $this->mOldid && $this->mNewid ) { + $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid ); + // Try cache + if ( !$this->mRefreshCache ) { + $difftext = $wgMemc->get( $key ); + if ( $difftext ) { + wfIncrStats( 'diff_cache_hit' ); + $difftext = $this->localiseLineNumbers( $difftext ); + $difftext .= "\n\n"; + wfProfileOut( __METHOD__ ); + return $difftext; + } + } // don't try to load but save the result + } + $this->mCacheHit = false; + + // Loadtext is permission safe, this just clears out the diff + if ( !$this->loadText() ) { + wfProfileOut( __METHOD__ ); + return false; + } + + $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); + + // Save to cache for 7 days + if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { + wfIncrStats( 'diff_uncacheable' ); + } else if ( $key !== false && $difftext !== false ) { + wfIncrStats( 'diff_cache_miss' ); + $wgMemc->set( $key, $difftext, 7*86400 ); + } else { + wfIncrStats( 'diff_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 + */ + 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(); + dl( 'php_wikidiff2.so' ); + wfRestoreWarnings(); + wfProfileOut( __METHOD__ . '-php_wikidiff2.so' ); + } + } + + /** + * Generate a diff, no caching + * $otext and $ntext must be already segmented + */ + function generateDiffBody( $otext, $ntext ) { + global $wgExternalDiffEngine, $wgContLang; + + $otext = str_replace( "\r\n", "\n", $otext ); + $ntext = str_replace( "\r\n", "\n", $ntext ); + + $this->initDiffEngines(); + + 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->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . + $this->debug( 'wikidiff1' ); + } + + 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 ) { + # Diff via the shell + global $wgTmpDirectory; + $tempName1 = tempnam( $wgTmpDirectory, 'diff_' ); + $tempName2 = tempnam( $wgTmpDirectory, 'diff_' ); + + $tempFile1 = fopen( $tempName1, "w" ); + if ( !$tempFile1 ) { + wfProfileOut( __METHOD__ ); + return false; + } + $tempFile2 = fopen( $tempName2, "w" ); + if ( !$tempFile2 ) { + wfProfileOut( __METHOD__ ); + return false; + } + fwrite( $tempFile1, $otext ); + fwrite( $tempFile2, $ntext ); + 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 ); + return $difftext; + } + + # Native PHP diff + $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); + $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); + $diffs = new Diff( $ota, $nta ); + $formatter = new TableDiffFormatter(); + return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . + $this->debug(); + } + + /** + * Generate a debug comment indicating diff generating time, + * server node, and generator backend. + */ + protected function debug( $generator="internal" ) { + global $wgShowHostnames; + if ( !$this->enableDebugComment ) { + return ''; + } + $data = array( $generator ); + if( $wgShowHostnames ) { + $data[] = wfHostname(); + } + $data[] = wfTimestamp( TS_DB ); + return "\n"; + } + + /** + * Replace line numbers with the text in the user's language + */ + function localiseLineNumbers( $text ) { + return preg_replace_callback( '//', + array( &$this, 'localiseLineNumbersCb' ), $text ); + } + + function localiseLineNumbersCb( $matches ) { + global $wgLang; + if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return ''; + return wfMsgExt( 'lineno', 'escape', $wgLang->formatNum( $matches[1] ) ); + } + + + /** + * If there are revisions between the ones being compared, return a note saying so. + */ + function getMultiNotice() { + if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) ) + return ''; + + if( !$this->mOldPage->equals( $this->mNewPage ) ) { + // Comparing two different pages? Count would be meaningless. + return ''; + } + + $oldid = $this->mOldRev->getId(); + $newid = $this->mNewRev->getId(); + if ( $oldid > $newid ) { + $tmp = $oldid; $oldid = $newid; $newid = $tmp; + } + + $n = $this->mTitle->countRevisionsBetween( $oldid, $newid ); + if ( !$n ) + return ''; + + return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n ); + } + + + /** + * Add the header to a diff body + */ + static function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) { + $header = ""; + if( $diff ) { // Safari/Chrome show broken output if cols not used + $header .= " + + + + "; + $colspan = 2; + $multiColspan = 4; + } else { + $colspan = 1; + $multiColspan = 2; + } + $header .= " + + + + "; + + if ( $multi != '' ) { + $header .= ""; + } + if ( $notice != '' ) { + $header .= ""; + } + + return $header . $diff . "
{$otitle}{$ntitle}
{$multi}
{$notice}
"; + } + + /** + * Use specified text instead of loading from the database + */ + function setText( $oldText, $newText ) { + $this->mOldtext = $oldText; + $this->mNewtext = $newText; + $this->mTextLoaded = 2; + $this->mRevisionsLoaded = true; + } + + /** + * 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 + * compare the current article to the immediately previous one (ignoring the + * value of newid). + * + * 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. + */ + function loadRevisionData() { + global $wgLang, $wgUser; + if ( $this->mRevisionsLoaded ) { + return true; + } else { + // Whether it succeeds or fails, we don't want to try again + $this->mRevisionsLoaded = true; + } + + // Load the new revision object + $this->mNewRev = $this->mNewid + ? Revision::newFromId( $this->mNewid ) + : Revision::newFromTitle( $this->mTitle ); + 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 = "{$this->mPagetitle}"; + $this->mNewtitle .= " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; + } 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 = "{$this->mPagetitle}"; + $this->mNewtitle .= " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; + } + if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + $this->mNewtitle = "{$this->mPagetitle}"; + } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + $this->mNewtitle = "{$this->mNewtitle}"; + } + + // Load the old revision object + $this->mOldRev = false; + if( $this->mOldid ) { + $this->mOldRev = Revision::newFromId( $this->mOldid ); + } elseif ( $this->mOldid === 0 ) { + $rev = $this->mNewRev->getPrevious(); + if( $rev ) { + $this->mOldid = $rev->getId(); + $this->mOldRev = $rev; + } else { + // No previous revision; mark to show as first-version only. + $this->mOldid = false; + $this->mOldRev = false; + } + }/* elseif ( $this->mOldid === false ) leave mOldRev false; */ + + if( is_null( $this->mOldRev ) ) { + return 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 = "{$this->mOldPagetitle}" + . " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; + // Add an "undo" link + $newUndo = $this->mNewPage->escapeLocalUrl( array( + 'action' => 'edit', + 'undoafter' => $this->mOldid, + 'undo' => $this->mNewid + ) ); + $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) ); + $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' ); + if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { + $this->mNewtitle .= " (" . $htmlLink . ")"; + } + + if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) { + $this->mOldtitle = '' . $this->mOldPagetitle . ''; + } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { + $this->mOldtitle = '' . $this->mOldtitle . ''; + } + } + + return true; + } + + /** + * Load the text of the revisions, as well as revision data. + */ + function loadText() { + if ( $this->mTextLoaded == 2 ) { + return true; + } else { + // 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 ) { + return false; + } + } + if ( $this->mNewRev ) { + $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); + if ( $this->mNewtext === false ) { + return false; + } + } + return true; + } + + /** + * Load the text of the new revision, not the old one + */ + function loadNewText() { + if ( $this->mTextLoaded >= 1 ) { + return true; + } else { + $this->mTextLoaded = 1; + } + if ( !$this->loadRevisionData() ) { + return false; + } + $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); + return true; + } +} diff --git a/includes/diff/HTMLDiff.php b/includes/diff/HTMLDiff.php deleted file mode 100644 index df9f4eb8..00000000 --- a/includes/diff/HTMLDiff.php +++ /dev/null @@ -1,1009 +0,0 @@ - - * - * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - * or see http://www.gnu.org/ - * - * @ingroup DifferenceEngine - */ - -/** - * When detecting the last common parent of two nodes, all results are stored as - * a LastCommonParentResult. - */ -class LastCommonParentResult { - - // Parent - public $parent; - - // Splitting - public $splittingNeeded = false; - - // Depth - public $lastCommonParentDepth = -1; - - // Index - public $indexInLastCommonParent = -1; -} - -class Modification{ - - const NONE = 1; - const REMOVED = 2; - const ADDED = 4; - const CHANGED = 8; - - public $type; - - public $id = -1; - - public $firstOfID = false; - - public $changes; - - function __construct($type) { - $this->type = $type; - } - - public static function typeToString($type) { - switch($type) { - case self::NONE: return 'none'; - case self::REMOVED: return 'removed'; - case self::ADDED: return 'added'; - case self::CHANGED: return 'changed'; - } - } -} - -class DomTreeBuilder { - - public $textNodes = array(); - - public $bodyNode; - - private $currentParent; - - private $newWord = ''; - - protected $bodyStarted = false; - - protected $bodyEnded = false; - - private $whiteSpaceBeforeThis = false; - - private $lastSibling; - - private $notInPre = true; - - function __construct() { - $this->bodyNode = $this->currentParent = new BodyNode(); - $this->lastSibling = new DummyNode(); - } - - /** - * Must be called manually - */ - public function endDocument() { - $this->endWord(); - HTMLDiffer::diffDebug( count($this->textNodes) . " text nodes in document.\n" ); - } - - public function startElement($parser, $name, /*array*/ $attributes) { - if (strcasecmp($name, 'body') != 0) { - HTMLDiffer::diffDebug( "Starting $name node.\n" ); - $this->endWord(); - - $newNode = new TagNode($this->currentParent, $name, $attributes); - $this->currentParent->children[] = $newNode; - $this->currentParent = $newNode; - $this->lastSibling = new DummyNode(); - if ($this->whiteSpaceBeforeThis && !in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) { - $this->currentParent->whiteBefore = true; - } - $this->whiteSpaceBeforeThis = false; - if(strcasecmp($name, 'pre') == 0) { - $this->notInPre = false; - } - } - } - - public function endElement($parser, $name) { - if(strcasecmp($name, 'body') != 0) { - HTMLDiffer::diffDebug( "Ending $name node.\n"); - if (0 == strcasecmp($name,'img')) { - // Insert a dummy leaf for the image - $img = new ImageNode($this->currentParent, $this->currentParent->attributes); - $this->currentParent->children[] = $img; - $img->whiteBefore = $this->whiteSpaceBeforeThis; - $this->lastSibling = $img; - $this->textNodes[] = $img; - } - $this->endWord(); - if (!in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) { - $this->lastSibling = $this->currentParent; - } else { - $this->lastSibling = new DummyNode(); - } - $this->currentParent = $this->currentParent->parent; - $this->whiteSpaceBeforeThis = false; - if (!$this->notInPre && strcasecmp($name, 'pre') == 0) { - $this->notInPre = true; - } - } else { - $this->endDocument(); - } - } - - const regex = '/([\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1})/'; - const whitespace = '/^[\s]{1}$/'; - const delimiter = '/^[\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1}$/'; - - public function characters($parser, $data) { - $matches = preg_split(self::regex, $data, -1, PREG_SPLIT_DELIM_CAPTURE); - - foreach($matches as &$word) { - if (preg_match(self::whitespace, $word) && $this->notInPre) { - $this->endWord(); - $this->lastSibling->whiteAfter = true; - $this->whiteSpaceBeforeThis = true; - } else if (preg_match(self::delimiter, $word)) { - $this->endWord(); - $textNode = new TextNode($this->currentParent, $word); - $this->currentParent->children[] = $textNode; - $textNode->whiteBefore = $this->whiteSpaceBeforeThis; - $this->whiteSpaceBeforeThis = false; - $this->lastSibling = $textNode; - $this->textNodes[] = $textNode; - } else { - $this->newWord .= $word; - } - } - } - - private function endWord() { - if ($this->newWord !== '') { - $node = new TextNode($this->currentParent, $this->newWord); - $this->currentParent->children[] = $node; - $node->whiteBefore = $this->whiteSpaceBeforeThis; - $this->whiteSpaceBeforeThis = false; - $this->lastSibling = $node; - $this->textNodes[] = $node; - $this->newWord = ""; - } - } - - public function getDiffLines() { - return array_map(array('TextNode','toDiffLine'), $this->textNodes); - } -} - -class TextNodeDiffer { - - private $textNodes; - public $bodyNode; - - private $oldTextNodes; - private $oldBodyNode; - - private $newID = 0; - - private $changedID = 0; - - private $changedIDUsed = false; - - // used to remove the whitespace between a red and green block - private $whiteAfterLastChangedPart = false; - - private $deletedID = 0; - - function __construct(DomTreeBuilder $tree, DomTreeBuilder $oldTree) { - $this->textNodes = $tree->textNodes; - $this->bodyNode = $tree->bodyNode; - $this->oldTextNodes = $oldTree->textNodes; - $this->oldBodyNode = $oldTree->bodyNode; - } - - public function markAsNew($start, $end) { - if ($end <= $start) { - return; - } - - if ($this->whiteAfterLastChangedPart) { - $this->textNodes[$start]->whiteBefore = false; - } - - for ($i = $start; $i < $end; ++$i) { - $mod = new Modification(Modification::ADDED); - $mod->id = $this->newID; - $this->textNodes[$i]->modification = $mod; - } - if ($start < $end) { - $this->textNodes[$start]->modification->firstOfID = true; - } - ++$this->newID; - } - - public function handlePossibleChangedPart($leftstart, $leftend, $rightstart, $rightend) { - $i = $rightstart; - $j = $leftstart; - - if ($this->changedIDUsed) { - ++$this->changedID; - $this->changedIDUsed = false; - } - - $changes; - while ($i < $rightend) { - $acthis = new AncestorComparator($this->textNodes[$i]->getParentTree()); - $acother = new AncestorComparator($this->oldTextNodes[$j]->getParentTree()); - $result = $acthis->getResult($acother); - unset($acthis, $acother); - - if ( $result ) { - $mod = new Modification(Modification::CHANGED); - - if (!$this->changedIDUsed) { - $mod->firstOfID = true; - } else if (!is_null( $result ) && $result !== $this->changes) { - ++$this->changedID; - $mod->firstOfID = true; - } - - $mod->changes = $result; - $mod->id = $this->changedID; - - $this->textNodes[$i]->modification = $mod; - $this->changes = $result; - $this->changedIDUsed = true; - } else if ($this->changedIDUsed) { - ++$this->changedID; - $this->changedIDUsed = false; - } - ++$i; - ++$j; - } - } - - public function markAsDeleted($start, $end, $before) { - - if ($end <= $start) { - return; - } - - if ($before > 0 && $this->textNodes[$before - 1]->whiteAfter) { - $this->whiteAfterLastChangedPart = true; - } else { - $this->whiteAfterLastChangedPart = false; - } - - for ($i = $start; $i < $end; ++$i) { - $mod = new Modification(Modification::REMOVED); - $mod->id = $this->deletedID; - - // oldTextNodes is used here because we're going to move its deleted - // elements to this tree! - $this->oldTextNodes[$i]->modification = $mod; - } - $this->oldTextNodes[$start]->modification->firstOfID = true; - - $root = $this->oldTextNodes[$start]->getLastCommonParent($this->oldTextNodes[$end-1])->parent; - - $junk1 = $junk2 = null; - $deletedNodes = $root->getMinimalDeletedSet($this->deletedID, $junk1, $junk2); - - HTMLDiffer::diffDebug( "Minimal set of deleted nodes of size " . count($deletedNodes) . "\n" ); - - // Set prevLeaf to the leaf after which the old HTML needs to be - // inserted - if ($before > 0) { - $prevLeaf = $this->textNodes[$before - 1]; - } - // Set nextLeaf to the leaf before which the old HTML needs to be - // inserted - if ($before < count($this->textNodes)) { - $nextLeaf = $this->textNodes[$before]; - } - - while (count($deletedNodes) > 0) { - if (isset($prevLeaf)) { - $prevResult = $prevLeaf->getLastCommonParent($deletedNodes[0]); - } else { - $prevResult = new LastCommonParentResult(); - $prevResult->parent = $this->bodyNode; - $prevResult->indexInLastCommonParent = -1; - } - if (isset($nextleaf)) { - $nextResult = $nextLeaf->getLastCommonParent($deletedNodes[count($deletedNodes) - 1]); - } else { - $nextResult = new LastCommonParentResult(); - $nextResult->parent = $this->bodyNode; - $nextResult->indexInLastCommonParent = $this->bodyNode->getNbChildren(); - } - - if ($prevResult->lastCommonParentDepth == $nextResult->lastCommonParentDepth) { - // We need some metric to choose which way to add-... - if ($deletedNodes[0]->parent === $deletedNodes[count($deletedNodes) - 1]->parent - && $prevResult->parent === $nextResult->parent) { - // The difference is not in the parent - $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1; - } else { - // The difference is in the parent, so compare them - // now THIS is tricky - $distancePrev = $deletedNodes[0]->parent->getMatchRatio($prevResult->parent); - $distanceNext = $deletedNodes[count($deletedNodes) - 1]->parent->getMatchRatio($nextResult->parent); - - if ($distancePrev <= $distanceNext) { - $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1; - } else { - $nextResult->lastCommonParentDepth = $nextResult->lastCommonParentDepth + 1; - } - } - - } - - if ($prevResult->lastCommonParentDepth > $nextResult->lastCommonParentDepth) { - // Inserting at the front - if ($prevResult->splittingNeeded) { - $prevLeaf->parent->splitUntil($prevResult->parent, $prevLeaf, true); - } - $prevLeaf = $deletedNodes[0]->copyTree(); - unset($deletedNodes[0]); - $deletedNodes = array_values($deletedNodes); - $prevLeaf->setParent($prevResult->parent); - $prevResult->parent->addChildAbsolute($prevLeaf,$prevResult->indexInLastCommonParent + 1); - } else if ($prevResult->lastCommonParentDepth < $nextResult->lastCommonParentDepth) { - // Inserting at the back - if ($nextResult->splittingNeeded) { - $splitOccured = $nextLeaf->parent->splitUntil($nextResult->parent, $nextLeaf, false); - if ($splitOccured) { - // The place where to insert is shifted one place to the - // right - $nextResult->indexInLastCommonParent = $nextResult->indexInLastCommonParent + 1; - } - } - $nextLeaf = $deletedNodes[count(deletedNodes) - 1]->copyTree(); - unset($deletedNodes[count(deletedNodes) - 1]); - $deletedNodes = array_values($deletedNodes); - $nextLeaf->setParent($nextResult->parent); - $nextResult->parent->addChildAbsolute($nextLeaf,$nextResult->indexInLastCommonParent); - } - } - ++$this->deletedID; - } - - public function expandWhiteSpace() { - $this->bodyNode->expandWhiteSpace(); - } - - public function lengthNew(){ - return count($this->textNodes); - } - - public function lengthOld(){ - return count($this->oldTextNodes); - } -} - -class HTMLDiffer { - - private $output; - private static $debug = ''; - - function __construct($output) { - $this->output = $output; - } - - function htmlDiff($from, $to) { - wfProfileIn( __METHOD__ ); - // Create an XML parser - $xml_parser = xml_parser_create(''); - - $domfrom = new DomTreeBuilder(); - - // Set the functions to handle opening and closing tags - xml_set_element_handler($xml_parser, array($domfrom, "startElement"), array($domfrom, "endElement")); - - // Set the function to handle blocks of character data - xml_set_character_data_handler($xml_parser, array($domfrom, "characters")); - - HTMLDiffer::diffDebug( "Parsing " . strlen($from) . " characters worth of HTML\n" ); - if (!xml_parse($xml_parser, ''.Sanitizer::hackDocType().'', false) - || !xml_parse($xml_parser, $from, false) - || !xml_parse($xml_parser, '', true)){ - $error = xml_error_string(xml_get_error_code($xml_parser)); - $line = xml_get_current_line_number($xml_parser); - HTMLDiffer::diffDebug( "XML error: $error at line $line\n" ); - } - xml_parser_free($xml_parser); - unset($from); - - $xml_parser = xml_parser_create(''); - - $domto = new DomTreeBuilder(); - - // Set the functions to handle opening and closing tags - xml_set_element_handler($xml_parser, array($domto, "startElement"), array($domto, "endElement")); - - // Set the function to handle blocks of character data - xml_set_character_data_handler($xml_parser, array($domto, "characters")); - - HTMLDiffer::diffDebug( "Parsing " . strlen($to) . " characters worth of HTML\n" ); - if (!xml_parse($xml_parser, ''.Sanitizer::hackDocType().'', false) - || !xml_parse($xml_parser, $to, false) - || !xml_parse($xml_parser, '', true)){ - $error = xml_error_string(xml_get_error_code($xml_parser)); - $line = xml_get_current_line_number($xml_parser); - HTMLDiffer::diffDebug( "XML error: $error at line $line\n" ); - } - xml_parser_free($xml_parser); - unset($to); - - $diffengine = new WikiDiff3(); - $differences = $this->preProcess($diffengine->diff_range($domfrom->getDiffLines(), $domto->getDiffLines())); - unset($xml_parser, $diffengine); - - $domdiffer = new TextNodeDiffer($domto, $domfrom); - - $currentIndexLeft = 0; - $currentIndexRight = 0; - foreach ($differences as &$d) { - if ($d->leftstart > $currentIndexLeft) { - $domdiffer->handlePossibleChangedPart($currentIndexLeft, $d->leftstart, - $currentIndexRight, $d->rightstart); - } - if ($d->leftlength > 0) { - $domdiffer->markAsDeleted($d->leftstart, $d->leftend, $d->rightstart); - } - $domdiffer->markAsNew($d->rightstart, $d->rightend); - - $currentIndexLeft = $d->leftend; - $currentIndexRight = $d->rightend; - } - $oldLength = $domdiffer->lengthOld(); - if ($currentIndexLeft < $oldLength) { - $domdiffer->handlePossibleChangedPart($currentIndexLeft, $oldLength, $currentIndexRight, $domdiffer->lengthNew()); - } - $domdiffer->expandWhiteSpace(); - $output = new HTMLOutput('htmldiff', $this->output); - $output->parse($domdiffer->bodyNode); - wfProfileOut( __METHOD__ ); - } - - private function preProcess(/*array*/ $differences) { - $newRanges = array(); - - $nbDifferences = count($differences); - for ($i = 0; $i < $nbDifferences; ++$i) { - $leftStart = $differences[$i]->leftstart; - $leftEnd = $differences[$i]->leftend; - $rightStart = $differences[$i]->rightstart; - $rightEnd = $differences[$i]->rightend; - - $leftLength = $leftEnd - $leftStart; - $rightLength = $rightEnd - $rightStart; - - while ($i + 1 < $nbDifferences && self::score($leftLength, - $differences[$i + 1]->leftlength, - $rightLength, - $differences[$i + 1]->rightlength) - > ($differences[$i + 1]->leftstart - $leftEnd)) { - $leftEnd = $differences[$i + 1]->leftend; - $rightEnd = $differences[$i + 1]->rightend; - $leftLength = $leftEnd - $leftStart; - $rightLength = $rightEnd - $rightStart; - ++$i; - } - $newRanges[] = new RangeDifference($leftStart, $leftEnd, $rightStart, $rightEnd); - } - return $newRanges; - } - - /** - * Heuristic to merge differences for readability. - */ - public static function score($ll, $nll, $rl, $nrl) { - if (($ll == 0 && $nll == 0) - || ($rl == 0 && $nrl == 0)) { - return 0; - } - $numbers = array($ll, $nll, $rl, $nrl); - $d = 0; - foreach ($numbers as &$number) { - while ($number > 3) { - $d += 3; - $number -= 3; - $number *= 0.5; - } - $d += $number; - - } - return $d / (1.5 * count($numbers)); - } - - /** - * Add to debug output - * @param string $str Debug output - */ - public static function diffDebug( $str ) { - self :: $debug .= $str; - } - - /** - * Get debug output - * @return string - */ - public static function getDebugOutput() { - return self :: $debug; - } - -} - -class TextOnlyComparator { - - public $leafs = array(); - - function _construct(TagNode $tree) { - $this->addRecursive($tree); - $this->leafs = array_map(array('TextNode','toDiffLine'), $this->leafs); - } - - private function addRecursive(TagNode $tree) { - foreach ($tree->children as &$child) { - if ($child instanceof TagNode) { - $this->addRecursive($child); - } else if ($child instanceof TextNode) { - $this->leafs[] = $node; - } - } - } - - public function getMatchRatio(TextOnlyComparator $other) { - $nbOthers = count($other->leafs); - $nbThis = count($this->leafs); - if($nbOthers == 0 || $nbThis == 0){ - return -log(0); - } - - $diffengine = new WikiDiff3(25000, 1.35); - $diffengine->diff($this->leafs, $other->leafs); - - $lcsLength = $diffengine->getLcsLength(); - - $distanceThis = $nbThis-$lcsLength; - - return (2.0 - $lcsLength/$nbOthers - $lcsLength/$nbThis) / 2.0; - } -} - -/** - * A comparator used when calculating the difference in ancestry of two Nodes. - */ -class AncestorComparator { - - public $ancestors; - public $ancestorsText; - - function __construct(/*array*/ $ancestors) { - $this->ancestors = $ancestors; - $this->ancestorsText = array_map(array('TagNode','toDiffLine'), $ancestors); - } - - public $compareTxt = ""; - - public function getResult(AncestorComparator $other) { - - $diffengine = new WikiDiff3(10000, 1.35); - $differences = $diffengine->diff_range($other->ancestorsText,$this->ancestorsText); - - if (count($differences) == 0){ - return null; - } - $changeTxt = new ChangeTextGenerator($this, $other); - - return $changeTxt->getChanged($differences)->toString();; - } -} - -class ChangeTextGenerator { - - private $ancestorComparator; - private $other; - - private $factory; - - function __construct(AncestorComparator $ancestorComparator, AncestorComparator $other) { - $this->ancestorComparator = $ancestorComparator; - $this->other = $other; - $this->factory = new TagToStringFactory(); - } - - public function getChanged(/*array*/ $differences) { - $txt = new ChangeText; - $rootlistopened = false; - if (count($differences) > 1) { - $txt->addHtml('
    '); - $rootlistopened = true; - } - $nbDifferences = count($differences); - for ($j = 0; $j < $nbDifferences; ++$j) { - $d = $differences[$j]; - $lvl1listopened = false; - if ($rootlistopened) { - $txt->addHtml('
  • '); - } - if ($d->leftlength + $d->rightlength > 1) { - $txt->addHtml('
      '); - $lvl1listopened = true; - } - // left are the old ones - for ($i = $d->leftstart; $i < $d->leftend; ++$i) { - if ($lvl1listopened){ - $txt->addHtml('
    • '); - } - // add a bullet for a old tag - $this->addTagOld($txt, $this->other->ancestors[$i]); - if ($lvl1listopened){ - $txt->addHtml('
    • '); - } - } - // right are the new ones - for ($i = $d->rightstart; $i < $d->rightend; ++$i) { - if ($lvl1listopened){ - $txt->addHtml('
    • '); - } - // add a bullet for a new tag - $this->addTagNew($txt, $this->ancestorComparator->ancestors[$i]); - - if ($lvl1listopened){ - $txt->addHtml('
    • '); - } - } - if ($lvl1listopened) { - $txt->addHtml('
    '); - } - if ($rootlistopened) { - $txt->addHtml('
  • '); - } - } - if ($rootlistopened) { - $txt->addHtml('
'); - } - return $txt; - } - - private function addTagOld(ChangeText $txt, TagNode $ancestor) { - $this->factory->create($ancestor)->getRemovedDescription($txt); - } - - private function addTagNew(ChangeText $txt, TagNode $ancestor) { - $this->factory->create($ancestor)->getAddedDescription($txt); - } -} - -class ChangeText { - - private $txt = ""; - - public function addHtml($s) { - $this->txt .= $s; - } - - public function toString() { - return $this->txt; - } -} - -class TagToStringFactory { - - private static $containerTags = array('html', 'body', 'p', 'blockquote', - 'h1', 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li', - 'table', 'tbody', 'tr', 'td', 'th', 'br', 'hr', 'code', 'dl', - 'dt', 'dd', 'input', 'form', 'img', 'span', 'a'); - - private static $styleTags = array('i', 'b', 'strong', 'em', 'font', - 'big', 'del', 'tt', 'sub', 'sup', 'strike'); - - const MOVED = 1; - const STYLE = 2; - const UNKNOWN = 4; - - public function create(TagNode $node) { - $sem = $this->getChangeSemantic($node->qName); - if (strcasecmp($node->qName,'a') == 0) { - return new AnchorToString($node, $sem); - } - if (strcasecmp($node->qName,'img') == 0) { - return new NoContentTagToString($node, $sem); - } - return new TagToString($node, $sem); - } - - protected function getChangeSemantic($qname) { - if (in_array(strtolower($qname),self::$containerTags)) { - return self::MOVED; - } - if (in_array(strtolower($qname),self::$styleTags)) { - return self::STYLE; - } - return self::UNKNOWN; - } -} - -class TagToString { - - protected $node; - - protected $sem; - - function __construct(TagNode $node, $sem) { - $this->node = $node; - $this->sem = $sem; - } - - public function getRemovedDescription(ChangeText $txt) { - $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' ); - if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){ - $tagDescription = "<" . $this->node->qName . ">"; - } - if ($this->sem == TagToStringFactory::MOVED) { - $txt->addHtml( wfMsgExt( 'diff-movedoutof', 'parseinline', $tagDescription ) ); - } else if ($this->sem == TagToStringFactory::STYLE) { - $txt->addHtml( wfMsgExt( 'diff-styleremoved' , 'parseinline', $tagDescription ) ); - } else { - $txt->addHtml( wfMsgExt( 'diff-removed' , 'parseinline', $tagDescription ) ); - } - $this->addAttributes($txt, $this->node->attributes); - $txt->addHtml('.'); - } - - public function getAddedDescription(ChangeText $txt) { - $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' ); - if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){ - $tagDescription = "<" . $this->node->qName . ">"; - } - if ($this->sem == TagToStringFactory::MOVED) { - $txt->addHtml( wfMsgExt( 'diff-movedto' , 'parseinline', $tagDescription) ); - } else if ($this->sem == TagToStringFactory::STYLE) { - $txt->addHtml( wfMsgExt( 'diff-styleadded', 'parseinline', $tagDescription ) ); - } else { - $txt->addHtml( wfMsgExt( 'diff-added', 'parseinline', $tagDescription ) ); - } - $this->addAttributes($txt, $this->node->attributes); - $txt->addHtml('.'); - } - - protected function addAttributes(ChangeText $txt, array $attributes) { - if (count($attributes) < 1) { - return; - } - $firstOne = true; - $nbAttributes_min_1 = count($attributes)-1; - $keys = array_keys($attributes); - for ($i=0;$i<$nbAttributes_min_1;$i++) { - $key = $keys[$i]; - $attr = $attributes[$key]; - if($firstOne) { - $firstOne = false; - $txt->addHtml( wfMsgExt('diff-with', 'escapenoentities', $this->translateArgument($key), htmlspecialchars($attr) ) ); - continue; - } - $txt->addHtml( wfMsgExt( 'comma-separator', 'escapenoentities' ) . - wfMsgExt( 'diff-with-additional', 'escapenoentities', - $this->translateArgument( $key ), htmlspecialchars( $attr ) ) - ); - } - - if ($nbAttributes_min_1 > 0) { - $txt->addHtml( wfMsgExt( 'diff-with-final', 'escapenoentities', - $this->translateArgument($keys[$nbAttributes_min_1]), - htmlspecialchars($attributes[$keys[$nbAttributes_min_1]]) ) ); - } - } - - protected function translateArgument($name) { - $translation = wfMsgExt('diff-' . $name, 'parseinline' ); - if ( wfEmptyMsg( 'diff-' . $name, $translation ) ) { - $translation = "<" . $name . ">";; - } - return htmlspecialchars( $translation ); - } -} - -class NoContentTagToString extends TagToString { - - function __construct(TagNode $node, $sem) { - parent::__construct($node, $sem); - } - - public function getAddedDescription(ChangeText $txt) { - $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' ); - if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){ - $tagDescription = "<" . $this->node->qName . ">"; - } - $txt->addHtml( wfMsgExt('diff-changedto', 'parseinline', $tagDescription ) ); - $this->addAttributes($txt, $this->node->attributes); - $txt->addHtml('.'); - } - - public function getRemovedDescription(ChangeText $txt) { - $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' ); - if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){ - $tagDescription = "<" . $this->node->qName . ">"; - } - $txt->addHtml( wfMsgExt('diff-changedfrom', 'parseinline', $tagDescription ) ); - $this->addAttributes($txt, $this->node->attributes); - $txt->addHtml('.'); - } -} - -class AnchorToString extends TagToString { - - function __construct(TagNode $node, $sem) { - parent::__construct($node, $sem); - } - - protected function addAttributes(ChangeText $txt, array $attributes) { - if (array_key_exists('href', $attributes)) { - $txt->addHtml(' ' . wfMsgExt( 'diff-withdestination', 'parseinline', htmlspecialchars($attributes['href']) ) ); - unset($attributes['href']); - } - parent::addAttributes($txt, $attributes); - } -} - -/** - * Takes a branch root and creates an HTML file for it. - */ -class HTMLOutput{ - - private $prefix; - private $handler; - - function __construct($prefix, $handler) { - $this->prefix = $prefix; - $this->handler = $handler; - } - - public function parse(TagNode $node) { - $handler = &$this->handler; - - if (strcasecmp($node->qName, 'img') != 0 && strcasecmp($node->qName, 'body') != 0) { - $handler->startElement($node->qName, $node->attributes); - } - - $newStarted = false; - $remStarted = false; - $changeStarted = false; - $changeTXT = ''; - - foreach ($node->children as &$child) { - if ($child instanceof TagNode) { - if ($newStarted) { - $handler->endElement('span'); - $newStarted = false; - } else if ($changeStarted) { - $handler->endElement('span'); - $changeStarted = false; - } else if ($remStarted) { - $handler->endElement('span'); - $remStarted = false; - } - $this->parse($child); - } else if ($child instanceof TextNode) { - $mod = $child->modification; - - if ($newStarted && ($mod->type != Modification::ADDED || $mod->firstOfID)) { - $handler->endElement('span'); - $newStarted = false; - } else if ($changeStarted && ($mod->type != Modification::CHANGED - || $mod->changes != $changeTXT || $mod->firstOfID)) { - $handler->endElement('span'); - $changeStarted = false; - } else if ($remStarted && ($mod->type != Modification::REMOVED || $mod ->firstOfID)) { - $handler->endElement('span'); - $remStarted = false; - } - - // no else because a removed part can just be closed and a new - // part can start - if (!$newStarted && $mod->type == Modification::ADDED) { - $attrs = array('class' => 'diff-html-added'); - if ($mod->firstOfID) { - $attrs['id'] = "added-{$this->prefix}-{$mod->id}"; - } - $handler->startElement('span', $attrs); - $newStarted = true; - } else if (!$changeStarted && $mod->type == Modification::CHANGED) { - $attrs = array('class' => 'diff-html-changed'); - if ($mod->firstOfID) { - $attrs['id'] = "changed-{$this->prefix}-{$mod->id}"; - } - $handler->startElement('span', $attrs); - - //tooltip - $handler->startElement('span', array('class' => 'tip')); - $handler->html($mod->changes); - $handler->endElement('span'); - - $changeStarted = true; - $changeTXT = $mod->changes; - } else if (!$remStarted && $mod->type == Modification::REMOVED) { - $attrs = array('class'=>'diff-html-removed'); - if ($mod->firstOfID) { - $attrs['id'] = "removed-{$this->prefix}-{$mod->id}"; - } - $handler->startElement('span', $attrs); - $remStarted = true; - } - - $chars = $child->text; - - if ($child instanceof ImageNode) { - $this->writeImage($child); - } else { - $handler->characters($chars); - } - } - } - - if ($newStarted) { - $handler->endElement('span'); - $newStarted = false; - } else if ($changeStarted) { - $handler->endElement('span'); - $changeStarted = false; - } else if ($remStarted) { - $handler->endElement('span'); - $remStarted = false; - } - - if (strcasecmp($node->qName, 'img') != 0 - && strcasecmp($node->qName, 'body') != 0) { - $handler->endElement($node->qName); - } - } - - private function writeImage(ImageNode $imgNode) { - $attrs = $imgNode->attributes; - $this->handler->startElement('img', $attrs); - $this->handler->endElement('img'); - } -} - -class DelegatingContentHandler { - - private $delegate; - - function __construct($delegate) { - $this->delegate = $delegate; - } - - function startElement($qname, /*array*/ $arguments) { - $this->delegate->addHtml(Xml::openElement($qname, $arguments)); - } - - function endElement($qname){ - $this->delegate->addHtml(Xml::closeElement($qname)); - } - - function characters($chars){ - $this->delegate->addHtml(htmlspecialchars($chars)); - } - - function html($html){ - $this->delegate->addHtml($html); - } -} diff --git a/includes/diff/Nodes.php b/includes/diff/Nodes.php deleted file mode 100644 index 1b1363d4..00000000 --- a/includes/diff/Nodes.php +++ /dev/null @@ -1,439 +0,0 @@ - - * - * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - * or see http://www.gnu.org/ - * - */ - -/** - * Any element in the DOM tree of an HTML document. - * @ingroup DifferenceEngine - */ -class Node { - - public $parent; - - protected $parentTree; - - public $whiteBefore = false; - - public $whiteAfter = false; - - function __construct($parent) { - $this->parent = $parent; - } - - public function getParentTree() { - if (!isset($this->parentTree)) { - if (!is_null($this->parent)) { - $this->parentTree = $this->parent->getParentTree(); - $this->parentTree[] = $this->parent; - } else { - $this->parentTree = array(); - } - } - return $this->parentTree; - } - - public function getLastCommonParent(Node $other) { - $result = new LastCommonParentResult(); - - $myParents = $this->getParentTree(); - $otherParents = $other->getParentTree(); - - $i = 1; - $isSame = true; - $nbMyParents = count($myParents); - $nbOtherParents = count($otherParents); - while ($isSame && $i < $nbMyParents && $i < $nbOtherParents) { - if (!$myParents[$i]->openingTag === $otherParents[$i]->openingTag) { - $isSame = false; - } else { - // After a while, the index i-1 must be the last common parent - $i++; - } - } - - $result->lastCommonParentDepth = $i - 1; - $result->parent = $myParents[$i - 1]; - - if (!$isSame || $nbMyParents > $nbOtherParents) { - // Not all tags matched, or all tags matched but - // there are tags left in this tree - $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($myParents[$i]); - $result->splittingNeeded = true; - } else if ($nbMyParents <= $nbOtherParents) { - $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($this); - } - return $result; - } - - public function setParent($parent) { - $this->parent = $parent; - unset($this->parentTree); - } - - public function inPre() { - $tree = $this->getParentTree(); - foreach ($tree as &$ancestor) { - if ($ancestor->isPre()) { - return true; - } - } - return false; - } -} - -/** - * Node that can contain other nodes. Represents an HTML tag. - * @ingroup DifferenceEngine - */ -class TagNode extends Node { - - public $children = array(); - - public $qName; - - public $attributes = array(); - - public $openingTag; - - function __construct($parent, $qName, /*array*/ $attributes) { - parent::__construct($parent); - $this->qName = strtolower($qName); - foreach($attributes as $key => &$value){ - $this->attributes[strtolower($key)] = $value; - } - return $this->openingTag = Xml::openElement($this->qName, $this->attributes); - } - - public function addChildAbsolute(Node $node, $index) { - array_splice($this->children, $index, 0, array($node)); - } - - public function getIndexOf(Node $child) { - // don't trust array_search with objects - foreach ($this->children as $key => &$value){ - if ($value === $child) { - return $key; - } - } - return null; - } - - public function getNbChildren() { - return count($this->children); - } - - public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) { - $nodes = array(); - - $allDeleted = false; - $somethingDeleted = false; - $hasNonDeletedDescendant = false; - - if (empty($this->children)) { - return $nodes; - } - - foreach ($this->children as &$child) { - $allDeleted_local = false; - $somethingDeleted_local = false; - $childrenChildren = $child->getMinimalDeletedSet($id, $allDeleted_local, $somethingDeleted_local); - if ($somethingDeleted_local) { - $nodes = array_merge($nodes, $childrenChildren); - $somethingDeleted = true; - } - if (!$allDeleted_local) { - $hasNonDeletedDescendant = true; - } - } - if (!$hasNonDeletedDescendant) { - $nodes = array($this); - $allDeleted = true; - } - return $nodes; - } - - public function splitUntil(TagNode $parent, Node $split, $includeLeft) { - $splitOccured = false; - if ($parent !== $this) { - $part1 = new TagNode(null, $this->qName, $this->attributes); - $part2 = new TagNode(null, $this->qName, $this->attributes); - $part1->setParent($this->parent); - $part2->setParent($this->parent); - - $onSplit = false; - $pastSplit = false; - foreach ($this->children as &$child) - { - if ($child === $split) { - $onSplit = true; - } - if(!$pastSplit || ($onSplit && $includeLeft)) { - $child->setParent($part1); - $part1->children[] = $child; - } else { - $child->setParent($part2); - $part2->children[] = $child; - } - if ($onSplit) { - $onSplit = false; - $pastSplit = true; - } - } - $myindexinparent = $this->parent->getIndexOf($this); - if (!empty($part1->children)) { - $this->parent->addChildAbsolute($part1, $myindexinparent); - } - if (!empty($part2->children)) { - $this->parent->addChildAbsolute($part2, $myindexinparent); - } - if (!empty($part1->children) && !empty($part2->children)) { - $splitOccured = true; - } - - $this->parent->removeChild($myindexinparent); - - if ($includeLeft) { - $this->parent->splitUntil($parent, $part1, $includeLeft); - } else { - $this->parent->splitUntil($parent, $part2, $includeLeft); - } - } - return $splitOccured; - - } - - private function removeChild($index) { - unset($this->children[$index]); - $this->children = array_values($this->children); - } - - public static $blocks = array('html', 'body','p','blockquote', 'h1', - 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li', 'table', - 'tbody', 'tr', 'td', 'th', 'br'); - - public function copyTree() { - $newThis = new TagNode(null, $this->qName, $this->attributes); - $newThis->whiteBefore = $this->whiteBefore; - $newThis->whiteAfter = $this->whiteAfter; - foreach ($this->children as &$child) { - $newChild = $child->copyTree(); - $newChild->setParent($newThis); - $newThis->children[] = $newChild; - } - return $newThis; - } - - public function getMatchRatio(TagNode $other) { - $txtComp = new TextOnlyComparator($other); - return $txtComp->getMatchRatio(new TextOnlyComparator($this)); - } - - public function expandWhiteSpace() { - $shift = 0; - $spaceAdded = false; - - $nbOriginalChildren = $this->getNbChildren(); - for ($i = 0; $i < $nbOriginalChildren; ++$i) { - $child = $this->children[$i + $shift]; - - if ($child instanceof TagNode) { - if (!$child->isPre()) { - $child->expandWhiteSpace(); - } - } - if (!$spaceAdded && $child->whiteBefore) { - $ws = new WhiteSpaceNode(null, ' ', $child->getLeftMostChild()); - $ws->setParent($this); - $this->addChildAbsolute($ws,$i + ($shift++)); - } - if ($child->whiteAfter) { - $ws = new WhiteSpaceNode(null, ' ', $child->getRightMostChild()); - $ws->setParent($this); - $this->addChildAbsolute($ws,$i + 1 + ($shift++)); - $spaceAdded = true; - } else { - $spaceAdded = false; - } - - } - } - - public function getLeftMostChild() { - if (empty($this->children)) { - return $this; - } - return $this->children[0]->getLeftMostChild(); - } - - public function getRightMostChild() { - if (empty($this->children)) { - return $this; - } - return $this->children[$this->getNbChildren() - 1]->getRightMostChild(); - } - - public function isPre() { - return 0 == strcasecmp($this->qName,'pre'); - } - - public static function toDiffLine(TagNode $node) { - return $node->openingTag; - } -} - -/** - * Represents a piece of text in the HTML file. - * @ingroup DifferenceEngine - */ -class TextNode extends Node { - - public $text; - - public $modification; - - function __construct($parent, $text) { - parent::__construct($parent); - $this->modification = new Modification(Modification::NONE); - $this->text = $text; - } - - public function copyTree() { - $clone = clone $this; - $clone->setParent(null); - return $clone; - } - - public function getLeftMostChild() { - return $this; - } - - public function getRightMostChild() { - return $this; - } - - public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) { - if ($this->modification->type == Modification::REMOVED - && $this->modification->id == $id){ - $somethingDeleted = true; - $allDeleted = true; - return array($this); - } - return array(); - } - - public function isSameText($other) { - if (is_null($other) || ! $other instanceof TextNode) { - return false; - } - return str_replace('\n', ' ',$this->text) === str_replace('\n', ' ',$other->text); - } - - public static function toDiffLine(TextNode $node) { - return str_replace('\n', ' ',$node->text); - } -} - -/** - * @todo Document - * @ingroup DifferenceEngine - */ -class WhiteSpaceNode extends TextNode { - - function __construct($parent, $s, Node $like = null) { - parent::__construct($parent, $s); - if(!is_null($like) && $like instanceof TextNode) { - $newModification = clone $like->modification; - $newModification->firstOfID = false; - $this->modification = $newModification; - } - } -} - -/** - * Represents the root of a HTML document. - * @ingroup DifferenceEngine - */ -class BodyNode extends TagNode { - - function __construct() { - parent::__construct(null, 'body', array()); - } - - public function copyTree() { - $newThis = new BodyNode(); - foreach ($this->children as &$child) { - $newChild = $child->copyTree(); - $newChild->setParent($newThis); - $newThis->children[] = $newChild; - } - return $newThis; - } - - public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) { - $nodes = array(); - foreach ($this->children as &$child) { - $childrenChildren = $child->getMinimalDeletedSet($id, - $allDeleted, $somethingDeleted); - $nodes = array_merge($nodes, $childrenChildren); - } - return $nodes; - } - -} - -/** - * Represents an image in HTML. Even though images do not contain any text they - * are independent visible objects on the page. They are logically a TextNode. - * @ingroup DifferenceEngine - */ -class ImageNode extends TextNode { - - public $attributes; - - function __construct(TagNode $parent, /*array*/ $attrs) { - if(!array_key_exists('src', $attrs)) { - HTMLDiffer::diffDebug( "Image without a source\n" ); - parent::__construct($parent, ''); - }else{ - parent::__construct($parent, '' . strtolower($attrs['src']) . ''); - } - $this->attributes = $attrs; - } - - public function isSameText($other) { - if (is_null($other) || ! $other instanceof ImageNode) { - return false; - } - return $this->text === $other->text; - } - -} - -/** - * No-op node - * @ingroup DifferenceEngine - */ -class DummyNode extends Node { - - function __construct() { - // no op - } - -} -- cgit v1.2.2