One more time... :P shouldn't commit this late at night.
[lhc/web/wiklou.git] / includes / DifferenceEngine.php
index 8b80c27..52bf654 100644 (file)
@@ -1,49 +1,99 @@
 <?php
-# See diff.doc
+/**
+ * See diff.doc
+ * @package MediaWiki 
+ */
 
+/**
+ * @todo document
+ * @package MediaWiki
+ */
 class DifferenceEngine {
        /* private */ var $mOldid, $mNewid;
-       /* private */ var $mOldtitle, $mNewtitle;
+       /* private */ var $mOldtitle, $mNewtitle, $mPagetitle;
        /* private */ var $mOldtext, $mNewtext;
        /* private */ var $mOldUser, $mNewUser;
        /* private */ var $mOldComment, $mNewComment;
        /* private */ var $mOldPage, $mNewPage;
-       
-       function DifferenceEngine( $old, $new )
+       /* private */ var $mRcidMarkPatrolled;
+
+       function DifferenceEngine( $old, $new, $rcid = 0 )
        {
-               $this->mOldid = $old;
-               $this->mNewid = $new;
+               global $wgTitle;
+               if ( 'prev' == $new ) {
+                       # Show diff between revision $old and the previous one.
+                       # Get previous one from DB.
+                       #
+                       $this->mNewid = intval($old);
+                       $dbr =& wfGetDB( DB_SLAVE );
+                       $this->mOldid = $dbr->selectField( 'old', 'old_id',
+                               "old_title='" . $wgTitle->getDBkey() . "'" .
+                               ' AND old_namespace=' . $wgTitle->getNamespace() .
+                               " AND old_id<{$this->mNewid} ORDER BY old_id DESC" );
+
+               } elseif ( 'next' == $new ) {
+
+                       # Show diff between revision $old and the previous one.
+                       # Get previous one from DB.
+                       #
+                       $this->mOldid = intval($old);
+                       $dbr =& wfGetDB( DB_SLAVE );
+                       $this->mNewid = $dbr->selectField( 'old', 'old_id',
+                               "old_title='" . $wgTitle->getDBkey() . "'" .
+                               ' AND old_namespace=' . $wgTitle->getNamespace() .
+                               " AND old_id>{$this->mOldid} ORDER BY old_id " );
+                       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);
+               }
+               $this->mRcidMarkPatrolled = intval($rcid);  # force it to be an integer
        }
 
        function showDiffPage()
        {
-               global $wgUser, $wgTitle, $wgOut, $wgLang;
-               $fname = "DifferenceEngine::showDiffPage";
+               global $wgUser, $wgTitle, $wgOut, $wgLang, $wgOnlySysopsCanPatrol, $wgUseRCPatrol;
+               $fname = 'DifferenceEngine::showDiffPage';
                wfProfileIn( $fname );
-               
+
+               # 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();
+                       wfProfileOut( $fname );
+                       return;
+               }
+
                $t = $wgTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
                  "{$this->mNewid})";
-               $mtext = wfMsg( "missingarticle", $t );
+               $mtext = wfMsg( 'missingarticle', $t );
 
                $wgOut->setArticleFlag( false );
                if ( ! $this->loadText() ) {
-                       $wgOut->setPagetitle( wfMsg( "errorpagetitle" ) );
+                       $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
                        $wgOut->addHTML( $mtext );
                        wfProfileOut( $fname );
                        return;
                }
                $wgOut->suppressQuickbar();
-               
+
                $oldTitle = $this->mOldPage->getPrefixedText();
                $newTitle = $this->mNewPage->getPrefixedText();
                if( $oldTitle == $newTitle ) {
                        $wgOut->setPageTitle( $newTitle );
                } else {
-                       $wgOut->setPageTitle( $oldTitle . ", " . $newTitle );
+                       $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
                }
-               $wgOut->setSubtitle( wfMsg( "difference" ) );
-               $wgOut->setRobotpolicy( "noindex,follow" );
-               
+               $wgOut->setSubtitle( wfMsg( 'difference' ) );
+               $wgOut->setRobotpolicy( 'noindex,follow' );
+
                if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
                        $wgOut->loginToUse();
                        $wgOut->output();
@@ -53,38 +103,114 @@ class DifferenceEngine {
 
                $sk = $wgUser->getSkin();
                $talk = $wgLang->getNsText( NS_TALK );
-               $contribs = wfMsg( "contribslink" );
-
+               $contribs = wfMsg( 'contribslink' );
 
                $this->mOldComment = $sk->formatComment($this->mOldComment);
                $this->mNewComment = $sk->formatComment($this->mNewComment);
 
-
-               $oldUserLink = $sk->makeLinkObj( Title::makeTitle( NS_USER, $this->mOldUser ), $this->mOldUser );
-               $newUserLink = $sk->makeLinkObj( Title::makeTitle( NS_USER, $this->mNewUser ), $this->mNewUser );
-               $oldUTLink = $sk->makeLinkObj( Title::makeTitle( NS_USER_TALK, $this->mOldUser ), $talk );
-               $newUTLink = $sk->makeLinkObj( Title::makeTitle( NS_USER_TALK, $this->mNewUser ), $talk );
-               $oldContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, "Contributions" ), $contribs,
-                       "target=" . urlencode($this->mOldUser) );
-               $newContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, "Contributions" ), $contribs,
-                       "target=" . urlencode($this->mNewUser) );
+               $oldUserLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mOldUser ), $this->mOldUser );
+               $newUserLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mNewUser ), $this->mNewUser );
+               $oldUTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mOldUser ), $talk );
+               $newUTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mNewUser ), $talk );
+               $oldContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $contribs,
+                       'target=' . urlencode($this->mOldUser) );
+               $newContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $contribs,
+                       'target=' . urlencode($this->mNewUser) );
                if ( !$this->mNewid && $wgUser->isSysop() ) {
-                       $rollback = "&nbsp;&nbsp;&nbsp;<strong>[" . $sk->makeKnownLinkObj( $wgTitle, wfMsg( "rollbacklink" ),
-                               "action=rollback&from=" . urlencode($this->mNewUser) ) . "]</strong>";
+                       $rollback = '&nbsp;&nbsp;&nbsp;<strong>[' . $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'rollbacklink' ),
+                               'action=rollback&from=' . urlencode($this->mNewUser) ) . ']</strong>';
+               } else {
+                       $rollback = '';
+               }
+               if ( $wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->getID() != 0 &&
+                    ( $wgUser->isSysop() || !$wgOnlySysopsCanPatrol ) )
+               {
+                       $patrol = ' [' . $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'markaspatrolleddiff' ),
+                               "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) . ']';
+               } else {
+                       $patrol = '';
+               }
+
+               $prevlink = $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'previousdiff' ), 'diff=prev&oldid='.$this->mOldid );
+               if ( $this->mNewid == 0 ) {
+                       $nextlink = '';
                } else {
-                       $rollback = "";
+                       $nextlink = $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid );
                }
 
-               $oldHeader = "<strong>{$this->mOldtitle}</strong><br />$oldUserLink ($oldUTLink | $oldContribs)<br />". 
-                 $this->mOldComment;
-               $newHeader = "<strong>{$this->mNewtitle}</strong><br />$newUserLink ($newUTLink | $newContribs) $rollback<br />".
-                 $this->mNewComment;
+               $oldHeader = "<strong>{$this->mOldtitle}</strong><br />$oldUserLink " .
+                       "($oldUTLink | $oldContribs)<br />" . $this->mOldComment .
+                       '<br />' . $prevlink;
+               $newHeader = "<strong>{$this->mNewtitle}</strong><br />$newUserLink " .
+                       "($newUTLink | $newContribs) $rollback<br />" . $this->mNewComment .
+                       '<br />' . $nextlink . $patrol;
 
                DifferenceEngine::showDiff( $this->mOldtext, $this->mNewtext,
                  $oldHeader, $newHeader );
-               $wgOut->addHTML( "<hr /><h2>{$this->mNewtitle}</h2>\n" );
+               $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
                $wgOut->addWikiText( $this->mNewtext );
-               
+
+               wfProfileOut( $fname );
+       }
+
+       # Show the first revision of an article. Uses normal diff headers in contrast to normal
+       # "old revision" display style.
+       #
+       function showFirstRevision()
+       {
+               global $wgOut, $wgTitle, $wgUser, $wgLang;
+
+               $fname = 'DifferenceEngine::showFirstRevision';
+               wfProfileIn( $fname );
+
+
+               $this->mOldid = $this->mNewid; # hack to make loadText() work.
+
+               # Get article text from the DB
+               #
+               if ( ! $this->loadText() ) {
+                       $t = $wgTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
+                         "{$this->mNewid})";
+                       $mtext = wfMsg( 'missingarticle', $t );
+                       $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
+                       $wgOut->addHTML( $mtext );
+                       wfProfileOut( $fname );
+                       return;
+               }
+
+               # Check if user is allowed to look at this page. If not, bail out.
+               #
+               if ( !( $this->mOldPage->userCanRead() ) ) {
+                       $wgOut->loginToUse();
+                       $wgOut->output();
+                       wfProfileOut( $fname );
+                       exit;
+               }
+
+               # Prepare the header box
+               #
+               $sk = $wgUser->getSkin();
+
+               $uTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mOldUser ),  $wgLang->getNsText( NS_TALK ) );
+               $userLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mOldUser ), $this->mOldUser );
+               $contribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), wfMsg( 'contribslink' ),
+                       'target=' . urlencode($this->mOldUser) );
+               $nextlink = $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid );
+               $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />$userLink " .
+                       "($uTLink | $contribs)<br />" . $this->mOldComment .
+                       '<br />' . $nextlink. "</div>\n";
+
+               $wgOut->addHTML( $header );
+
+               $wgOut->setSubtitle( wfMsg( 'difference' ) );
+               $wgOut->setRobotpolicy( 'noindex,follow' );
+
+
+               # Show current revision
+               #
+               $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
+               $wgOut->addWikiText( $this->mNewtext );
+
                wfProfileOut( $fname );
        }
 
@@ -92,23 +218,24 @@ class DifferenceEngine {
        {
                global $wgOut, $wgUseExternalDiffEngine;
 
-               $otext = str_replace( "\r\n", "\n", htmlspecialchars( $otext ) );
-               $ntext = str_replace( "\r\n", "\n", htmlspecialchars( $ntext ) );
+               $wgOut->addHTML( "
+                       <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>
+                       <tr>
+                               <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td>
+                               <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td>
+                       </tr>
+               " );
 
-
-                       $wgOut->addHTML( "<table border='0' width='98%'
-cellpadding='0' cellspacing='4px' class='diff'><tr>
-<td colspan='2' width='50%' align='center' class='diff-otitle'>
-{$otitle}</td>
-<td colspan='2' width='50%' align='center' class='diff-ntitle'>
-{$ntitle}</td>
-</tr>\n" );
                if ( $wgUseExternalDiffEngine ) {
-                       dl("php_wikidiff.so");
+                       # For historical reasons, external diff engine expects
+                       # input text to be HTML-escaped already
+                       $otext = str_replace( "\r\n", "\n", htmlspecialchars ( $otext ) );
+                       $ntext = str_replace( "\r\n", "\n", htmlspecialchars ( $ntext ) );
+                       dl('php_wikidiff.so');
                        $wgOut->addHTML( wikidiff_do_diff( $otext, $ntext, 2) );
                } else {
-                       $ota = explode( "\n", $otext);
-                       $nta = explode( "\n", $ntext);
+                       $ota = explode( "\n", str_replace( "\r\n", "\n", $otext ) );
+                       $nta = explode( "\n", str_replace( "\r\n", "\n", $ntext ) );
                        $diffs = new Diff( $ota, $nta );
                        $formatter = new TableDiffFormatter();
                        $formatter->format( $diffs );
@@ -124,18 +251,21 @@ cellpadding='0' cellspacing='4px' class='diff'><tr>
        function loadText()
        {
                global $wgTitle, $wgOut, $wgLang;
-               $fname = "DifferenceEngine::loadText";
-               
+               $fname = 'DifferenceEngine::loadText';
+
                $dbr =& wfGetDB( DB_SLAVE );
                if ( 0 == $this->mNewid || 0 == $this->mOldid ) {
                        $wgOut->setArticleFlag( true );
-                       $this->mNewtitle = wfMsg( "currentrev" );
+                       $newLink = $wgTitle->getLocalUrl();
+                       $this->mPagetitle = wfMsg( 'currentrev' );
+                       $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
                        $id = $wgTitle->getArticleID();
-                       
-                       $s = $dbr->getArray( 'cur', array( 'cur_text', 'cur_user_text', 'cur_comment' ), 
+
+                       $s = $dbr->getArray( 'cur', array( 'cur_text', 'cur_user_text', 'cur_comment' ),
                                array( 'cur_id' => $id ), $fname );
-                       if ( $s === false ) { 
-                               return false; 
+                       if ( $s === false ) {
+                               wfDebug( "Unable to load cur_id $id\n" );
+                               return false;
                        }
 
                        $this->mNewPage = &$wgTitle;
@@ -146,44 +276,53 @@ cellpadding='0' cellspacing='4px' class='diff'><tr>
                        $s = $dbr->getArray( 'old', array( 'old_namespace','old_title','old_timestamp', 'old_text',
                                'old_flags','old_user_text','old_comment' ), array( 'old_id' => $this->mNewid ), $fname );
 
-                       if ( $s === false ) { 
-                               return false; 
+                       if ( $s === false ) {
+                               wfDebug( "Unable to load old_id {$this->mNewid}\n" );
+                               return false;
                        }
 
                        $this->mNewtext = Article::getRevisionText( $s );
 
                        $t = $wgLang->timeanddate( $s->old_timestamp, true );
                        $this->mNewPage = Title::MakeTitle( $s->old_namespace, $s->old_title );
-                       $this->mNewtitle = wfMsg( "revisionasof", $t );
+                       $newLink = $wgTitle->getLocalUrl ('oldid=' . $this->mNewid);
+                       $this->mPagetitle = wfMsg( 'revisionasof', $t );
+                       $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
                        $this->mNewUser = $s->old_user_text;
                        $this->mNewComment = $s->old_comment;
                }
                if ( 0 == $this->mOldid ) {
-                       $s = $dbr->getArray( 'old', 
-                               array( 'old_namespace','old_title','old_timestamp','old_text', 'old_flags','old_user_text','old_comment' ), 
+                       $s = $dbr->getArray( 'old',
+                               array( 'old_namespace','old_title','old_timestamp','old_text', 'old_flags','old_user_text','old_comment' ),
                                array( /* WHERE */
-                                       'old_namespace' => $this->mNewPage->getNamespace(), 
-                                       'old_title' => $this->mNewPage->getDBkey() 
+                                       'old_namespace' => $this->mNewPage->getNamespace(),
+                                       'old_title' => $this->mNewPage->getDBkey()
                                ), $fname, array( 'ORDER BY' => 'inverse_timestamp', 'USE INDEX' => 'name_title_timestamp' )
                        );
+                       if ( $s === false ) {
+                               wfDebug( 'Unable to load ' . $this->mNewPage->getPrefixedDBkey . " from old\n" );
+                               return false;
+                       }
                } else {
-                       $s = $dbr->getArray( 'old', 
+                       $s = $dbr->getArray( 'old',
                                array( 'old_namespace','old_title','old_timestamp','old_text','old_flags','old_user_text','old_comment'),
-                               array( 'old_id' => $this->mOldid ), 
+                               array( 'old_id' => $this->mOldid ),
                                $fname
                        );
-               }
-               if ( $s === false ) { 
-                       return false; 
+                       if ( $s === false ) {
+                               wfDebug( "Unable to load old_id {$this->mOldid}\n" );
+                               return false;
+                       }
                }
                $this->mOldPage = Title::MakeTitle( $s->old_namespace, $s->old_title );
                $this->mOldtext = Article::getRevisionText( $s );
 
                $t = $wgLang->timeanddate( $s->old_timestamp, true );
-               $this->mOldtitle = wfMsg( "revisionasof", $t );
+               $oldLink = $this->mOldPage->getLocalUrl ('oldid=' . $this->mOldid);
+               $this->mOldtitle = "<a href='$oldLink'>" . wfMsg( 'revisionasof', $t ) . '</a>';
                $this->mOldUser = $s->old_user_text;
                $this->mOldComment = $s->old_comment;
-               
+
                return true;
        }
 }
@@ -196,13 +335,17 @@ cellpadding='0' cellspacing='4px' class='diff'><tr>
 
 define('USE_ASSERTS', function_exists('assert'));
 
+/**
+ * @todo document
+ * @package MediaWiki
+ */
 class _DiffOp {
        var $type;
        var $orig;
        var $closing;
-       
+
        function reverse() {
-               trigger_error("pure virtual", E_USER_ERROR);
+               trigger_error('pure virtual', E_USER_ERROR);
        }
 
        function norig() {
@@ -214,9 +357,13 @@ class _DiffOp {
        }
 }
 
+/**
+ * @todo document
+ * @package MediaWiki
+ */
 class _DiffOp_Copy extends _DiffOp {
        var $type = 'copy';
-       
+
        function _DiffOp_Copy ($orig, $closing = false) {
                if (!is_array($closing))
                        $closing = $orig;
@@ -229,9 +376,13 @@ class _DiffOp_Copy extends _DiffOp {
        }
 }
 
+/**
+ * @todo document
+ * @package MediaWiki
+ */
 class _DiffOp_Delete extends _DiffOp {
        var $type = 'delete';
-       
+
        function _DiffOp_Delete ($lines) {
                $this->orig = $lines;
                $this->closing = false;
@@ -242,9 +393,13 @@ class _DiffOp_Delete extends _DiffOp {
        }
 }
 
+/**
+ * @todo document
+ * @package MediaWiki
+ */
 class _DiffOp_Add extends _DiffOp {
        var $type = 'add';
-       
+
        function _DiffOp_Add ($lines) {
                $this->closing = $lines;
                $this->orig = false;
@@ -255,9 +410,13 @@ class _DiffOp_Add extends _DiffOp {
        }
 }
 
+/**
+ * @todo document
+ * @package MediaWiki
+ */
 class _DiffOp_Change extends _DiffOp {
        var $type = 'change';
-       
+
        function _DiffOp_Change ($orig, $closing) {
                $this->orig = $orig;
                $this->closing = $closing;
@@ -267,8 +426,8 @@ class _DiffOp_Change extends _DiffOp {
                return new _DiffOp_Change($this->closing, $this->orig);
        }
 }
-               
-         
+
+
 /**
  * Class used internally by Diff to actually compute the diffs.
  *
@@ -287,6 +446,7 @@ class _DiffOp_Change extends _DiffOp {
  * are my own.
  *
  * @author Geoffrey T. Dairiki
+ * @package MediaWiki
  * @access private
  */
 class _DiffEngine
@@ -301,7 +461,7 @@ class _DiffEngine
                unset($this->seq);
                unset($this->in_seq);
                unset($this->lcs);
-                
+
                // Skip leading common lines.
                for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
                        if ($from_lines[$skip] != $to_lines[$skip])
@@ -315,7 +475,7 @@ class _DiffEngine
                                break;
                        $this->xchanged[$xi] = $this->ychanged[$yi] = false;
                }
-               
+
                // Ignore lines which do not exist in both files.
                for ($xi = $skip; $xi < $n_from - $endskip; $xi++)
                        $xhash[$from_lines[$xi]] = 1;
@@ -367,7 +527,7 @@ class _DiffEngine
                        $add = array();
                        while ($yi < $n_to && $this->ychanged[$yi])
                                $add[] = $to_lines[$yi++];
-                       
+
                        if ($delete && $add)
                                $edits[] = new _DiffOp_Change($delete, $add);
                        elseif ($delete)
@@ -377,7 +537,7 @@ class _DiffEngine
                }
                return $edits;
        }
-       
+
 
        /* Divide the Largest Common Subsequence (LCS) of the sequences
         * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
@@ -397,7 +557,7 @@ class _DiffEngine
         */
        function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
        $flip = false;
-       
+
        if ($xlim - $xoff > $ylim - $yoff) {
                // Things seems faster (I'm not sure I understand why)
                        // when the shortest sequence in X.
@@ -417,7 +577,7 @@ class _DiffEngine
        $this->seq[0]= $yoff - 1;
        $this->in_seq = array();
        $ymids[0] = array();
-       
+
        $numer = $xlim - $xoff + $nchunks - 1;
        $x = $xoff;
        for ($chunk = 0; $chunk < $nchunks; $chunk++) {
@@ -584,14 +744,14 @@ class _DiffEngine
                 */
                while ($j < $other_len && $other_changed[$j])
                $j++;
-               
+
                while ($i < $len && ! $changed[$i]) {
                USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
                $i++; $j++;
                while ($j < $other_len && $other_changed[$j])
                        $j++;
                        }
-                       
+
                if ($i == $len)
                break;
 
@@ -672,8 +832,10 @@ class _DiffEngine
 
 /**
  * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @package MediaWiki
  */
-class Diff 
+class Diff
 {
        var $edits;
 
@@ -722,7 +884,7 @@ class Diff
                }
                return true;
        }
-  
+
        /**
         * Compute the length of the Longest Common Subsequence (LCS).
         *
@@ -749,7 +911,7 @@ class Diff
         */
        function orig() {
                $lines = array();
-               
+
                foreach ($this->edits as $edit) {
                        if ($edit->orig)
                                array_splice($lines, sizeof($lines), 0, $edit->orig);
@@ -767,7 +929,7 @@ class Diff
         */
        function closing() {
                $lines = array();
-               
+
                foreach ($this->edits as $edit) {
                        if ($edit->closing)
                                array_splice($lines, sizeof($lines), 0, $edit->closing);
@@ -776,7 +938,7 @@ class Diff
        }
 
        /**
-        * Check a Diff for validity. 
+        * Check a Diff for validity.
         *
         * This is here only for debugging purposes.
         */
@@ -801,15 +963,16 @@ class Diff
                }
 
                $lcs = $this->lcs();
-               trigger_error("Diff okay: LCS = $lcs", E_USER_NOTICE);
+               trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
        }
 }
-                       
+
 /**
  * FIXME: bad name.
+ * @todo document
+ * @package MediaWiki
  */
-class MappedDiff
-extends Diff
+class MappedDiff extends Diff
 {
        /**
         * Constructor.
@@ -839,7 +1002,7 @@ extends Diff
 
                assert(sizeof($from_lines) == sizeof($mapped_from_lines));
                assert(sizeof($to_lines) == sizeof($mapped_to_lines));
-               
+
                $this->Diff($mapped_from_lines, $mapped_to_lines);
 
                $xi = $yi = 0;
@@ -849,7 +1012,7 @@ extends Diff
                                $orig = array_slice($from_lines, $xi, sizeof($orig));
                                $xi += sizeof($orig);
                        }
-                       
+
                        $closing = &$this->edits[$i]->closing;
                        if (is_array($closing)) {
                                $closing = array_slice($to_lines, $yi, sizeof($closing));
@@ -865,6 +1028,8 @@ extends Diff
  * This class formats the diff in classic diff format.
  * It is intended that this class be customized via inheritance,
  * to obtain fancier outputs.
+ * @todo document
+ * @package MediaWiki
  */
 class DiffFormatter
 {
@@ -958,7 +1123,7 @@ class DiffFormatter
                        elseif ($edit->type == 'change')
                                $this->_changed($edit->orig, $edit->closing);
                        else
-                               trigger_error("Unknown edit type", E_USER_ERROR);
+                               trigger_error('Unknown edit type', E_USER_ERROR);
                }
                $this->_end_block();
        }
@@ -981,11 +1146,11 @@ class DiffFormatter
 
                return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
        }
-       
+
        function _start_block($header) {
                echo $header;
        }
-       
+
        function _end_block() {
        }
 
@@ -993,16 +1158,16 @@ class DiffFormatter
                foreach ($lines as $line)
                        echo "$prefix $line\n";
        }
-       
+
        function _context($lines) {
                $this->_lines($lines);
        }
 
        function _added($lines) {
-               $this->_lines($lines, ">");
+               $this->_lines($lines, '>');
        }
        function _deleted($lines) {
-               $this->_lines($lines, "<");
+               $this->_lines($lines, '<');
        }
 
        function _changed($orig, $closing) {
@@ -1015,11 +1180,15 @@ class DiffFormatter
 
 /**
  *     Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
- * 
+ *
  */
 
 define('NBSP', '&#160;');                      // iso-8859-x non-breaking space.
 
+/**
+ * @todo document
+ * @package MediaWiki
+ */
 class _HWLDF_WordAccumulator {
        function _HWLDF_WordAccumulator () {
                $this->_lines = array();
@@ -1030,22 +1199,26 @@ class _HWLDF_WordAccumulator {
 
        function _flushGroup ($new_tag) {
                if ($this->_group !== '') {
-         if ($this->_tag == 'mark') 
-                       $this->_line .= '<span class="diffchange">'.$this->_group.'</span>';
-         else
-               $this->_line .= $this->_group;
-       }
+                       if ($this->_tag == 'mark')
+                               $this->_line .= '<span class="diffchange">' .
+                                       htmlspecialchars ( $this->_group ) . '</span>';
+                       else
+                               $this->_line .= htmlspecialchars ( $this->_group );
+               }
                $this->_group = '';
                $this->_tag = $new_tag;
        }
-       
+
        function _flushLine ($new_tag) {
                $this->_flushGroup($new_tag);
                if ($this->_line != '')
-                       $this->_lines[] = $this->_line;
+                       array_push ( $this->_lines, $this->_line );
+               else
+                       # make empty lines visible by inserting an NBSP
+                       array_push ( $this->_lines, NBSP );
                $this->_line = '';
        }
-                               
+
        function addWords ($words, $tag = '') {
                if ($tag != $this->_tag)
                        $this->_flushGroup($tag);
@@ -1055,7 +1228,6 @@ class _HWLDF_WordAccumulator {
                        if ($word == '')
                                continue;
                        if ($word[0] == "\n") {
-                               $this->_group .= NBSP;
                                $this->_flushLine($tag);
                                $word = substr($word, 1);
                        }
@@ -1070,20 +1242,22 @@ class _HWLDF_WordAccumulator {
        }
 }
 
+/**
+ * @todo document
+ * @package MediaWiki
+ */
 class WordLevelDiff extends MappedDiff
 {
        function WordLevelDiff ($orig_lines, $closing_lines) {
                list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
                list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
 
-               
+
                $this->MappedDiff($orig_words, $closing_words,
                                                  $orig_stripped, $closing_stripped);
        }
 
        function _split($lines) {
-               // FIXME: fix POSIX char class.
-#               if (!preg_match_all('/ ( [^\S\n]+ | [[:alnum:]]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
                if (!preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
                                                        implode("\n", $lines),
                                                        $m)) {
@@ -1094,7 +1268,7 @@ class WordLevelDiff extends MappedDiff
 
        function orig () {
                $orig = new _HWLDF_WordAccumulator;
-               
+
                foreach ($this->edits as $edit) {
                        if ($edit->type == 'copy')
                                $orig->addWords($edit->orig);
@@ -1106,7 +1280,7 @@ class WordLevelDiff extends MappedDiff
 
        function closing () {
                $closing = new _HWLDF_WordAccumulator;
-               
+
                foreach ($this->edits as $edit) {
                        if ($edit->type == 'copy')
                                $closing->addWords($edit->closing);
@@ -1119,7 +1293,8 @@ class WordLevelDiff extends MappedDiff
 
 /**
  *     Wikipedia Table style diff formatter.
- * 
+ * @todo document
+ * @package MediaWiki
  */
 class TableDiffFormatter extends DiffFormatter
 {
@@ -1127,10 +1302,10 @@ class TableDiffFormatter extends DiffFormatter
                $this->leading_context_lines = 2;
                $this->trailing_context_lines = 2;
        }
-       
+
        function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
-               $l1 = wfMsg( "lineno", $xbeg );
-               $l2 = wfMsg( "lineno", $ybeg );
+               $l1 = wfMsg( 'lineno', $xbeg );
+               $l2 = wfMsg( 'lineno', $ybeg );
 
                $r = '<tr><td colspan="2" align="left"><strong>'.$l1."</strong></td>\n" .
                  '<td colspan="2" align="left"><strong>'.$l2."</strong></td></tr>\n";
@@ -1145,39 +1320,40 @@ class TableDiffFormatter extends DiffFormatter
        function _end_block() {
        }
 
-       function _lines( $lines, $prefix=' ', $color="white" ) {
+       function _lines( $lines, $prefix=' ', $color='white' ) {
        }
 
+       # HTML-escape parameter before calling this
        function addedLine( $line ) {
-               return '<td>+</td><td class="diff-addedline">' .
-                 $line.'</td>';
+               return "<td>+</td><td class='diff-addedline'>{$line}</td>";
        }
 
+       # HTML-escape parameter before calling this
        function deletedLine( $line ) {
-               return '<td>-</td><td class="diff-deletedline">' .
-                 $line.'</td>';
+               return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
+       }
+
+       # HTML-escape parameter before calling this
+       function contextLine( $line ) {
+               return "<td> </td><td class='diff-context'>{$line}</td>";
        }
 
        function emptyLine() {
                return '<td colspan="2">&nbsp;</td>';
        }
 
-       function contextLine( $line ) {
-               return '<td> </td><td class="diff-context">'.$line.'</td>';
-       }
-       
-       function _added($lines) {
+       function _added( $lines ) {
                global $wgOut;
                foreach ($lines as $line) {
                        $wgOut->addHTML( '<tr>' . $this->emptyLine() .
-                         $this->addedLine( $line ) . "</tr>\n" );
+                               $this->addedLine( htmlspecialchars ( $line ) ) . "</tr>\n" );
                }
        }
 
        function _deleted($lines) {
                global $wgOut;
                foreach ($lines as $line) {
-                       $wgOut->addHTML( '<tr>' . $this->deletedLine( $line ) .
+                       $wgOut->addHTML( '<tr>' . $this->deletedLine( htmlspecialchars ( $line ) ) .
                          $this->emptyLine() . "</tr>\n" );
                }
        }
@@ -1185,8 +1361,9 @@ class TableDiffFormatter extends DiffFormatter
        function _context( $lines ) {
                global $wgOut;
                foreach ($lines as $line) {
-                       $wgOut->addHTML( '<tr>' . $this->contextLine( $line ) .
-                         $this->contextLine( $line ) . "</tr>\n" );
+                       $wgOut->addHTML( '<tr>' .
+                               $this->contextLine( htmlspecialchars ( $line ) ) .
+                               $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n" );
                }
        }
 
@@ -1196,12 +1373,18 @@ class TableDiffFormatter extends DiffFormatter
                $del = $diff->orig();
                $add = $diff->closing();
 
+               # Notice that WordLevelDiff returns HTML-escaped output.
+               # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
                while ( $line = array_shift( $del ) ) {
                        $aline = array_shift( $add );
                        $wgOut->addHTML( '<tr>' . $this->deletedLine( $line ) .
-                         $this->addedLine( $aline ) . "</tr>\n" );
+                               $this->addedLine( $aline ) . "</tr>\n" );
+               }
+               foreach ($add as $line) {       # If any leftovers
+                       $wgOut->addHTML( '<tr>' . $this->emptyLine() .
+                               $this->addedLine( $line ) . "</tr>\n" );
                }
-               $this->_added( $add ); # If any leftovers
        }
 }