From c1f9b1f7b1b77776192048005dcc66dcf3df2bfb Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 27 Dec 2014 15:41:37 +0100 Subject: Update to MediaWiki 1.24.1 --- includes/diff/ArrayDiffFormatter.php | 82 +++ includes/diff/DairikiDiff.php | 911 +++++++++------------------------ includes/diff/DiffFormatter.php | 247 +++++++++ includes/diff/DifferenceEngine.php | 502 +++++++++++------- includes/diff/TableDiffFormatter.php | 214 ++++++++ includes/diff/UnifiedDiffFormatter.php | 74 +++ includes/diff/WikiDiff3.php | 89 +++- 7 files changed, 1254 insertions(+), 865 deletions(-) create mode 100644 includes/diff/ArrayDiffFormatter.php create mode 100644 includes/diff/DiffFormatter.php create mode 100644 includes/diff/TableDiffFormatter.php create mode 100644 includes/diff/UnifiedDiffFormatter.php (limited to 'includes/diff') diff --git a/includes/diff/ArrayDiffFormatter.php b/includes/diff/ArrayDiffFormatter.php new file mode 100644 index 00000000..c12b76ad --- /dev/null +++ b/includes/diff/ArrayDiffFormatter.php @@ -0,0 +1,82 @@ + + * You may copy this code freely under the conditions of the GPL. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup DifferenceEngine + */ + +/** + * A pseudo-formatter that just passes along the Diff::$edits array + * @ingroup DifferenceEngine + */ +class ArrayDiffFormatter extends DiffFormatter { + + /** + * @param Diff $diff A Diff object. + * + * @return array[] List of associative arrays, each describing a difference. + */ + public function format( $diff ) { + $oldline = 1; + $newline = 1; + $retval = array(); + foreach ( $diff->getEdits() as $edit ) { + switch ( $edit->getType() ) { + case 'add': + foreach ( $edit->getClosing() as $line ) { + $retval[] = array( + 'action' => 'add', + 'new' => $line, + 'newline' => $newline++ + ); + } + break; + case 'delete': + foreach ( $edit->getOrig() as $line ) { + $retval[] = array( + 'action' => 'delete', + 'old' => $line, + 'oldline' => $oldline++, + ); + } + break; + case 'change': + foreach ( $edit->getOrig() as $key => $line ) { + $retval[] = array( + 'action' => 'change', + 'old' => $line, + 'new' => $edit->getClosing( $key ), + 'oldline' => $oldline++, + 'newline' => $newline++, + ); + } + break; + case 'copy': + $oldline += count( $edit->getOrig() ); + $newline += count( $edit->getOrig() ); + } + } + + return $retval; + } + +} diff --git a/includes/diff/DairikiDiff.php b/includes/diff/DairikiDiff.php index 298d7240..a4c0168f 100644 --- a/includes/diff/DairikiDiff.php +++ b/includes/diff/DairikiDiff.php @@ -30,26 +30,64 @@ * @private * @ingroup DifferenceEngine */ -class _DiffOp { - var $type; - var $orig; - var $closing; +abstract class DiffOp { - function reverse() { - trigger_error( 'pure virtual', E_USER_ERROR ); + /** + * @var string + */ + public $type; + + /** + * @var string[] + */ + public $orig; + + /** + * @var string[] + */ + public $closing; + + /** + * @return string + */ + public function getType() { + return $this->type; } + /** + * @return string[] + */ + public function getOrig() { + return $this->orig; + } + + /** + * @param int $i + * @return string|null + */ + public function getClosing( $i = null ) { + if ( $i === null ) { + return $this->closing; + } + if ( array_key_exists( $i, $this->closing ) ) { + return $this->closing[$i]; + } + return null; + } + + abstract public function reverse(); + /** * @return int */ - function norig() { + public function norig() { return $this->orig ? count( $this->orig ) : 0; } /** * @return int */ - function nclosing() { + public function nclosing() { return $this->closing ? count( $this->closing ) : 0; } } @@ -59,10 +97,10 @@ class _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffOp_Copy extends _DiffOp { - var $type = 'copy'; +class DiffOpCopy extends DiffOp { + public $type = 'copy'; - function __construct( $orig, $closing = false ) { + public function __construct( $orig, $closing = false ) { if ( !is_array( $closing ) ) { $closing = $orig; } @@ -71,10 +109,10 @@ class _DiffOp_Copy extends _DiffOp { } /** - * @return _DiffOp_Copy + * @return DiffOpCopy */ - function reverse() { - return new _DiffOp_Copy( $this->closing, $this->orig ); + public function reverse() { + return new DiffOpCopy( $this->closing, $this->orig ); } } @@ -83,19 +121,19 @@ class _DiffOp_Copy extends _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffOp_Delete extends _DiffOp { - var $type = 'delete'; +class DiffOpDelete extends DiffOp { + public $type = 'delete'; - function __construct( $lines ) { + public function __construct( $lines ) { $this->orig = $lines; $this->closing = false; } /** - * @return _DiffOp_Add + * @return DiffOpAdd */ - function reverse() { - return new _DiffOp_Add( $this->orig ); + public function reverse() { + return new DiffOpAdd( $this->orig ); } } @@ -104,19 +142,19 @@ class _DiffOp_Delete extends _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffOp_Add extends _DiffOp { - var $type = 'add'; +class DiffOpAdd extends DiffOp { + public $type = 'add'; - function __construct( $lines ) { + public function __construct( $lines ) { $this->closing = $lines; $this->orig = false; } /** - * @return _DiffOp_Delete + * @return DiffOpDelete */ - function reverse() { - return new _DiffOp_Delete( $this->closing ); + public function reverse() { + return new DiffOpDelete( $this->closing ); } } @@ -125,19 +163,19 @@ class _DiffOp_Add extends _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffOp_Change extends _DiffOp { - var $type = 'change'; +class DiffOpChange extends DiffOp { + public $type = 'change'; - function __construct( $orig, $closing ) { + public function __construct( $orig, $closing ) { $this->orig = $orig; $this->closing = $closing; } /** - * @return _DiffOp_Change + * @return DiffOpChange */ - function reverse() { - return new _DiffOp_Change( $this->closing, $this->orig ); + public function reverse() { + return new DiffOpChange( $this->closing, $this->orig ); } } @@ -146,14 +184,14 @@ class _DiffOp_Change extends _DiffOp { * * The algorithm used here is mostly lifted from the perl module * Algorithm::Diff (version 1.06) by Ned Konz, which is available at: - * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip + * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip * * More ideas are taken from: - * http://www.ics.uci.edu/~eppstein/161/960229.html + * http://www.ics.uci.edu/~eppstein/161/960229.html * * Some ideas are (and a bit of code) are from from analyze.c, from GNU * diffutils-2.7, which can be found at: - * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz + * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz * * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations) * are my own. @@ -165,8 +203,7 @@ class _DiffOp_Change extends _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffEngine { - +class DiffEngine { const MAX_XREF_LENGTH = 10000; protected $xchanged, $ychanged; @@ -179,19 +216,20 @@ class _DiffEngine { protected $lcs = 0; /** - * @param $from_lines - * @param $to_lines - * @return array + * @param string[] $from_lines + * @param string[] $to_lines + * + * @return DiffOp[] */ - function diff( $from_lines, $to_lines ) { + public function diff( $from_lines, $to_lines ) { wfProfileIn( __METHOD__ ); // Diff and store locally - $this->diff_local( $from_lines, $to_lines ); + $this->diffLocal( $from_lines, $to_lines ); // Merge edits when possible - $this->_shift_boundaries( $from_lines, $this->xchanged, $this->ychanged ); - $this->_shift_boundaries( $to_lines, $this->ychanged, $this->xchanged ); + $this->shiftBoundaries( $from_lines, $this->xchanged, $this->ychanged ); + $this->shiftBoundaries( $to_lines, $this->ychanged, $this->xchanged ); // Compute the edit operations. $n_from = count( $from_lines ); @@ -206,12 +244,13 @@ class _DiffEngine { // Skip matching "snake". $copy = array(); while ( $xi < $n_from && $yi < $n_to - && !$this->xchanged[$xi] && !$this->ychanged[$yi] ) { + && !$this->xchanged[$xi] && !$this->ychanged[$yi] + ) { $copy[] = $from_lines[$xi++]; ++$yi; } if ( $copy ) { - $edits[] = new _DiffOp_Copy( $copy ); + $edits[] = new DiffOpCopy( $copy ); } // Find deletes & adds. @@ -226,22 +265,23 @@ class _DiffEngine { } if ( $delete && $add ) { - $edits[] = new _DiffOp_Change( $delete, $add ); + $edits[] = new DiffOpChange( $delete, $add ); } elseif ( $delete ) { - $edits[] = new _DiffOp_Delete( $delete ); + $edits[] = new DiffOpDelete( $delete ); } elseif ( $add ) { - $edits[] = new _DiffOp_Add( $add ); + $edits[] = new DiffOpAdd( $add ); } } wfProfileOut( __METHOD__ ); + return $edits; } /** - * @param $from_lines - * @param $to_lines + * @param string[] $from_lines + * @param string[] $to_lines */ - function diff_local( $from_lines, $to_lines ) { + private function diffLocal( $from_lines, $to_lines ) { global $wgExternalDiffEngine; wfProfileIn( __METHOD__ ); @@ -282,21 +322,21 @@ class _DiffEngine { // Ignore lines which do not exist in both files. for ( $xi = $skip; $xi < $n_from - $endskip; $xi++ ) { - $xhash[$this->_line_hash( $from_lines[$xi] )] = 1; + $xhash[$this->lineHash( $from_lines[$xi] )] = 1; } for ( $yi = $skip; $yi < $n_to - $endskip; $yi++ ) { $line = $to_lines[$yi]; - if ( ( $this->ychanged[$yi] = empty( $xhash[$this->_line_hash( $line )] ) ) ) { + if ( ( $this->ychanged[$yi] = empty( $xhash[$this->lineHash( $line )] ) ) ) { continue; } - $yhash[$this->_line_hash( $line )] = 1; + $yhash[$this->lineHash( $line )] = 1; $this->yv[] = $line; $this->yind[] = $yi; } for ( $xi = $skip; $xi < $n_from - $endskip; $xi++ ) { $line = $from_lines[$xi]; - if ( ( $this->xchanged[$xi] = empty( $yhash[$this->_line_hash( $line )] ) ) ) { + if ( ( $this->xchanged[$xi] = empty( $yhash[$this->lineHash( $line )] ) ) ) { continue; } $this->xv[] = $line; @@ -304,17 +344,19 @@ class _DiffEngine { } // Find the LCS. - $this->_compareseq( 0, count( $this->xv ), 0, count( $this->yv ) ); + $this->compareSeq( 0, count( $this->xv ), 0, count( $this->yv ) ); } wfProfileOut( __METHOD__ ); } /** * Returns the whole line if it's small enough, or the MD5 hash otherwise - * @param $line string + * + * @param string $line + * * @return string */ - function _line_hash( $line ) { + private function lineHash( $line ) { if ( strlen( $line ) > self::MAX_XREF_LENGTH ) { return md5( $line ); } else { @@ -338,14 +380,16 @@ class _DiffEngine { * of the two files do not match, and likewise that the last lines do not * match. The caller must trim matching lines from the beginning and end * of the portions it is going to specify. - * @param $xoff - * @param $xlim - * @param $yoff - * @param $ylim - * @param $nchunks - * @return array + * + * @param int $xoff + * @param int $xlim + * @param int $yoff + * @param int $ylim + * @param int $nchunks + * + * @return array List of two elements, integer and array[]. */ - function _diag( $xoff, $xlim, $yoff, $ylim, $nchunks ) { + private function diag( $xoff, $xlim, $yoff, $ylim, $nchunks ) { $flip = false; if ( $xlim - $xoff > $ylim - $yoff ) { @@ -375,28 +419,33 @@ class _DiffEngine { for ( $chunk = 0; $chunk < $nchunks; $chunk++ ) { if ( $chunk > 0 ) { for ( $i = 0; $i <= $this->lcs; $i++ ) { - $ymids[$i][$chunk -1] = $this->seq[$i]; + $ymids[$i][$chunk - 1] = $this->seq[$i]; } } - $x1 = $xoff + (int)( ( $numer + ( $xlim -$xoff ) * $chunk ) / $nchunks ); + $x1 = $xoff + (int)( ( $numer + ( $xlim - $xoff ) * $chunk ) / $nchunks ); + // @codingStandardsIgnoreFile Ignore Squiz.WhiteSpace.SemicolonSpacing.Incorrect for ( ; $x < $x1; $x++ ) { + // @codingStandardsIgnoreEnd $line = $flip ? $this->yv[$x] : $this->xv[$x]; if ( empty( $ymatches[$line] ) ) { continue; } + + $k = 0; $matches = $ymatches[$line]; reset( $matches ); while ( list( , $y ) = each( $matches ) ) { if ( empty( $this->in_seq[$y] ) ) { - $k = $this->_lcs_pos( $y ); + $k = $this->lcsPos( $y ); assert( '$k > 0' ); - $ymids[$k] = $ymids[$k -1]; + $ymids[$k] = $ymids[$k - 1]; break; } } + while ( list( , $y ) = each( $matches ) ) { - if ( $y > $this->seq[$k -1] ) { + if ( $y > $this->seq[$k - 1] ) { assert( '$y < $this->seq[$k]' ); // Optimization: this is a common case: // next match is just replacing previous match. @@ -404,9 +453,9 @@ class _DiffEngine { $this->seq[$k] = $y; $this->in_seq[$y] = 1; } elseif ( empty( $this->in_seq[$y] ) ) { - $k = $this->_lcs_pos( $y ); + $k = $this->lcsPos( $y ); assert( '$k > 0' ); - $ymids[$k] = $ymids[$k -1]; + $ymids[$k] = $ymids[$k - 1]; } } } @@ -425,14 +474,16 @@ class _DiffEngine { } /** - * @param $ypos + * @param int $ypos + * * @return int */ - function _lcs_pos( $ypos ) { + private function lcsPos( $ypos ) { $end = $this->lcs; if ( $end == 0 || $ypos > $this->seq[$end] ) { $this->seq[++$this->lcs] = $ypos; $this->in_seq[$ypos] = 1; + return $this->lcs; } @@ -451,6 +502,7 @@ class _DiffEngine { $this->in_seq[$this->seq[$end]] = false; $this->seq[$end] = $ypos; $this->in_seq[$ypos] = 1; + return $end; } @@ -465,12 +517,13 @@ class _DiffEngine { * * Note that XLIM, YLIM are exclusive bounds. * All line numbers are origin-0 and discarded lines are not counted. - * @param $xoff - * @param $xlim - * @param $yoff - * @param $ylim + * + * @param int $xoff + * @param int $xlim + * @param int $yoff + * @param int $ylim */ - function _compareseq( $xoff, $xlim, $yoff, $ylim ) { + private function compareSeq( $xoff, $xlim, $yoff, $ylim ) { // Slide down the bottom initial diagonal. while ( $xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff] ) { ++$xoff; @@ -479,7 +532,8 @@ class _DiffEngine { // Slide up the top initial diagonal. while ( $xlim > $xoff && $ylim > $yoff - && $this->xv[$xlim - 1] == $this->yv[$ylim - 1] ) { + && $this->xv[$xlim - 1] == $this->yv[$ylim - 1] + ) { --$xlim; --$ylim; } @@ -491,7 +545,7 @@ class _DiffEngine { // $nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); // $nchunks = max(2,min(8,(int)$nchunks)); $nchunks = min( 7, $xlim - $xoff, $ylim - $yoff ) + 1; - list( $lcs, $seps ) = $this->_diag( $xoff, $xlim, $yoff, $ylim, $nchunks ); + list( $lcs, $seps ) = $this->diag( $xoff, $xlim, $yoff, $ylim, $nchunks ); } if ( $lcs == 0 ) { @@ -508,7 +562,7 @@ class _DiffEngine { reset( $seps ); $pt1 = $seps[0]; while ( $pt2 = next( $seps ) ) { - $this->_compareseq( $pt1[0], $pt2[0], $pt1[1], $pt2[1] ); + $this->compareSeq( $pt1[0], $pt2[0], $pt1[1], $pt2[1] ); $pt1 = $pt2; } } @@ -527,7 +581,7 @@ class _DiffEngine { * * This is extracted verbatim from analyze.c (GNU diffutils-2.7). */ - function _shift_boundaries( $lines, &$changed, $other_changed ) { + private function shiftBoundaries( $lines, &$changed, $other_changed ) { wfProfileIn( __METHOD__ ); $i = 0; $j = 0; @@ -552,7 +606,7 @@ class _DiffEngine { $j++; } - while ( $i < $len && ! $changed[$i] ) { + while ( $i < $len && !$changed[$i] ) { assert( '$j < $other_len && ! $other_changed[$j]' ); $i++; $j++; @@ -654,20 +708,30 @@ class _DiffEngine { * @ingroup DifferenceEngine */ class Diff { - var $edits; + + /** + * @var DiffOp[] + */ + public $edits; /** * Constructor. * Computes diff between sequences of strings. * - * @param $from_lines array An array of strings. - * (Typically these are lines from a file.) - * @param $to_lines array An array of strings. + * @param string[] $from_lines An array of strings. + * Typically these are lines from a file. + * @param string[] $to_lines An array of strings. */ - function __construct( $from_lines, $to_lines ) { - $eng = new _DiffEngine; + public function __construct( $from_lines, $to_lines ) { + $eng = new DiffEngine; $this->edits = $eng->diff( $from_lines, $to_lines ); - // $this->_check($from_lines, $to_lines); + } + + /** + * @return DiffOp[] + */ + public function getEdits() { + return $this->edits; } /** @@ -675,17 +739,20 @@ class Diff { * * SYNOPSIS: * - * $diff = new Diff($lines1, $lines2); - * $rev = $diff->reverse(); + * $diff = new Diff($lines1, $lines2); + * $rev = $diff->reverse(); + * * @return Object A Diff object representing the inverse of the - * original diff. + * original diff. */ - function reverse() { + public function reverse() { $rev = $this; $rev->edits = array(); + /** @var DiffOp $edit */ foreach ( $this->edits as $edit ) { $rev->edits[] = $edit->reverse(); } + return $rev; } @@ -694,12 +761,13 @@ class Diff { * * @return bool True if two sequences were identical. */ - function isEmpty() { + public function isEmpty() { foreach ( $this->edits as $edit ) { if ( $edit->type != 'copy' ) { return false; } } + return true; } @@ -710,13 +778,14 @@ class Diff { * * @return int The length of the LCS. */ - function lcs() { + public function lcs() { $lcs = 0; foreach ( $this->edits as $edit ) { if ( $edit->type == 'copy' ) { $lcs += count( $edit->orig ); } } + return $lcs; } @@ -726,9 +795,9 @@ class Diff { * This reconstructs the $from_lines parameter passed to the * constructor. * - * @return array The original sequence of strings. + * @return string[] The original sequence of strings. */ - function orig() { + public function orig() { $lines = array(); foreach ( $this->edits as $edit ) { @@ -736,6 +805,7 @@ class Diff { array_splice( $lines, count( $lines ), 0, $edit->orig ); } } + return $lines; } @@ -745,9 +815,9 @@ class Diff { * This reconstructs the $to_lines parameter passed to the * constructor. * - * @return array The sequence of strings. + * @return string[] The sequence of strings. */ - function closing() { + public function closing() { $lines = array(); foreach ( $this->edits as $edit ) { @@ -755,44 +825,8 @@ class Diff { array_splice( $lines, count( $lines ), 0, $edit->closing ); } } - return $lines; - } - - /** - * Check a Diff for validity. - * - * This is here only for debugging purposes. - * @param $from_lines - * @param $to_lines - */ - function _check( $from_lines, $to_lines ) { - wfProfileIn( __METHOD__ ); - if ( serialize( $from_lines ) != serialize( $this->orig() ) ) { - trigger_error( "Reconstructed original doesn't match", E_USER_ERROR ); - } - if ( serialize( $to_lines ) != serialize( $this->closing() ) ) { - trigger_error( "Reconstructed closing doesn't match", E_USER_ERROR ); - } - - $rev = $this->reverse(); - if ( serialize( $to_lines ) != serialize( $rev->orig() ) ) { - trigger_error( "Reversed original doesn't match", E_USER_ERROR ); - } - if ( serialize( $from_lines ) != serialize( $rev->closing() ) ) { - trigger_error( "Reversed closing doesn't match", E_USER_ERROR ); - } - - $prevtype = 'none'; - foreach ( $this->edits as $edit ) { - if ( $prevtype == $edit->type ) { - trigger_error( 'Edit sequence is non-optimal', E_USER_ERROR ); - } - $prevtype = $edit->type; - } - $lcs = $this->lcs(); - trigger_error( 'Diff okay: LCS = ' . $lcs, E_USER_NOTICE ); - wfProfileOut( __METHOD__ ); + return $lines; } } @@ -811,21 +845,18 @@ class MappedDiff extends Diff { * case-insensitve diffs, or diffs which ignore * changes in white-space. * - * @param $from_lines array An array of strings. - * (Typically these are lines from a file.) - * - * @param $to_lines array An array of strings. - * - * @param $mapped_from_lines array This array should - * have the same size number of elements as $from_lines. - * The elements in $mapped_from_lines and - * $mapped_to_lines are what is actually compared - * when computing the diff. - * - * @param $mapped_to_lines array This array should - * have the same number of elements as $to_lines. + * @param string[] $from_lines An array of strings. + * Typically these are lines from a file. + * @param string[] $to_lines An array of strings. + * @param string[] $mapped_from_lines This array should + * have the same size number of elements as $from_lines. + * The elements in $mapped_from_lines and + * $mapped_to_lines are what is actually compared + * when computing the diff. + * @param string[] $mapped_to_lines This array should + * have the same number of elements as $to_lines. */ - function __construct( $from_lines, $to_lines, + public function __construct( $from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines ) { wfProfileIn( __METHOD__ ); @@ -835,7 +866,8 @@ class MappedDiff extends Diff { parent::__construct( $mapped_from_lines, $mapped_to_lines ); $xi = $yi = 0; - for ( $i = 0; $i < count( $this->edits ); $i++ ) { + $editCount = count( $this->edits ); + for ( $i = 0; $i < $editCount; $i++ ) { $orig = &$this->edits[$i]->orig; if ( is_array( $orig ) ) { $orig = array_slice( $from_lines, $xi, count( $orig ) ); @@ -852,363 +884,64 @@ class MappedDiff extends Diff { } } -/** - * A class to format Diffs - * - * 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 - * @private - * @ingroup DifferenceEngine - */ -class DiffFormatter { - /** - * Number of leading context "lines" to preserve. - * - * This should be left at zero for this class, but subclasses - * may want to set this to other values. - */ - var $leading_context_lines = 0; - - /** - * Number of trailing context "lines" to preserve. - * - * This should be left at zero for this class, but subclasses - * may want to set this to other values. - */ - var $trailing_context_lines = 0; - - /** - * Format a diff. - * - * @param $diff Diff A Diff object. - * @return string The formatted output. - */ - function format( $diff ) { - wfProfileIn( __METHOD__ ); - - $xi = $yi = 1; - $block = false; - $context = array(); - - $nlead = $this->leading_context_lines; - $ntrail = $this->trailing_context_lines; - - $this->_start_diff(); - - foreach ( $diff->edits as $edit ) { - if ( $edit->type == 'copy' ) { - if ( is_array( $block ) ) { - if ( count( $edit->orig ) <= $nlead + $ntrail ) { - $block[] = $edit; - } else { - if ( $ntrail ) { - $context = array_slice( $edit->orig, 0, $ntrail ); - $block[] = new _DiffOp_Copy( $context ); - } - $this->_block( $x0, $ntrail + $xi - $x0, - $y0, $ntrail + $yi - $y0, - $block ); - $block = false; - } - } - $context = $edit->orig; - } else { - if ( !is_array( $block ) ) { - $context = array_slice( $context, count( $context ) - $nlead ); - $x0 = $xi - count( $context ); - $y0 = $yi - count( $context ); - $block = array(); - if ( $context ) { - $block[] = new _DiffOp_Copy( $context ); - } - } - $block[] = $edit; - } - - if ( $edit->orig ) { - $xi += count( $edit->orig ); - } - if ( $edit->closing ) { - $yi += count( $edit->closing ); - } - } - - if ( is_array( $block ) ) { - $this->_block( $x0, $xi - $x0, - $y0, $yi - $y0, - $block ); - } - - $end = $this->_end_diff(); - wfProfileOut( __METHOD__ ); - return $end; - } - - /** - * @param $xbeg - * @param $xlen - * @param $ybeg - * @param $ylen - * @param $edits - */ - function _block( $xbeg, $xlen, $ybeg, $ylen, &$edits ) { - wfProfileIn( __METHOD__ ); - $this->_start_block( $this->_block_header( $xbeg, $xlen, $ybeg, $ylen ) ); - foreach ( $edits as $edit ) { - if ( $edit->type == 'copy' ) { - $this->_context( $edit->orig ); - } elseif ( $edit->type == 'add' ) { - $this->_added( $edit->closing ); - } elseif ( $edit->type == 'delete' ) { - $this->_deleted( $edit->orig ); - } elseif ( $edit->type == 'change' ) { - $this->_changed( $edit->orig, $edit->closing ); - } else { - trigger_error( 'Unknown edit type', E_USER_ERROR ); - } - } - $this->_end_block(); - wfProfileOut( __METHOD__ ); - } - - function _start_diff() { - ob_start(); - } - - /** - * @return string - */ - function _end_diff() { - $val = ob_get_contents(); - ob_end_clean(); - return $val; - } - - /** - * @param $xbeg - * @param $xlen - * @param $ybeg - * @param $ylen - * @return string - */ - function _block_header( $xbeg, $xlen, $ybeg, $ylen ) { - if ( $xlen > 1 ) { - $xbeg .= ',' . ( $xbeg + $xlen - 1 ); - } - if ( $ylen > 1 ) { - $ybeg .= ',' . ( $ybeg + $ylen - 1 ); - } - - return $xbeg . ( $xlen ? ( $ylen ? 'c' : 'd' ) : 'a' ) . $ybeg; - } - - function _start_block( $header ) { - echo $header . "\n"; - } - - function _end_block() { - } - - /** - * @param $lines - * @param $prefix string - */ - function _lines( $lines, $prefix = ' ' ) { - foreach ( $lines as $line ) { - echo "$prefix $line\n"; - } - } - - /** - * @param $lines - */ - function _context( $lines ) { - $this->_lines( $lines ); - } - - /** - * @param $lines - */ - function _added( $lines ) { - $this->_lines( $lines, '>' ); - } - - /** - * @param $lines - */ - function _deleted( $lines ) { - $this->_lines( $lines, '<' ); - } - - /** - * @param $orig - * @param $closing - */ - function _changed( $orig, $closing ) { - $this->_deleted( $orig ); - echo "---\n"; - $this->_added( $closing ); - } -} - -/** - * A formatter that outputs unified diffs - * @ingroup DifferenceEngine - */ -class UnifiedDiffFormatter extends DiffFormatter { - var $leading_context_lines = 2; - var $trailing_context_lines = 2; - - /** - * @param $lines - */ - function _added( $lines ) { - $this->_lines( $lines, '+' ); - } - - /** - * @param $lines - */ - function _deleted( $lines ) { - $this->_lines( $lines, '-' ); - } - - /** - * @param $orig - * @param $closing - */ - function _changed( $orig, $closing ) { - $this->_deleted( $orig ); - $this->_added( $closing ); - } - - /** - * @param $xbeg - * @param $xlen - * @param $ybeg - * @param $ylen - * @return string - */ - function _block_header( $xbeg, $xlen, $ybeg, $ylen ) { - return "@@ -$xbeg,$xlen +$ybeg,$ylen @@"; - } -} - -/** - * A pseudo-formatter that just passes along the Diff::$edits array - * @ingroup DifferenceEngine - */ -class ArrayDiffFormatter extends DiffFormatter { - - /** - * @param $diff - * @return array - */ - function format( $diff ) { - $oldline = 1; - $newline = 1; - $retval = array(); - foreach ( $diff->edits as $edit ) { - switch ( $edit->type ) { - case 'add': - foreach ( $edit->closing as $l ) { - $retval[] = array( - 'action' => 'add', - 'new' => $l, - 'newline' => $newline++ - ); - } - break; - case 'delete': - foreach ( $edit->orig as $l ) { - $retval[] = array( - 'action' => 'delete', - 'old' => $l, - 'oldline' => $oldline++, - ); - } - break; - case 'change': - foreach ( $edit->orig as $i => $l ) { - $retval[] = array( - 'action' => 'change', - 'old' => $l, - 'new' => isset( $edit->closing[$i] ) ? $edit->closing[$i] : null, - 'oldline' => $oldline++, - 'newline' => $newline++, - ); - } - break; - case 'copy': - $oldline += count( $edit->orig ); - $newline += count( $edit->orig ); - } - } - return $retval; - } -} - /** * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3 */ -define( 'NBSP', ' ' ); // iso-8859-x non-breaking space. - /** * @todo document * @private * @ingroup DifferenceEngine */ -class _HWLDF_WordAccumulator { - function __construct() { - $this->_lines = array(); - $this->_line = ''; - $this->_group = ''; - $this->_tag = ''; - } +class HWLDFWordAccumulator { + public $insClass = ' class="diffchange diffchange-inline"'; + public $delClass = ' class="diffchange diffchange-inline"'; + + private $lines = array(); + private $line = ''; + private $group = ''; + private $tag = ''; /** - * @param $new_tag + * @param string $new_tag */ - function _flushGroup( $new_tag ) { - if ( $this->_group !== '' ) { - if ( $this->_tag == 'ins' ) { - $this->_line .= '' . - htmlspecialchars( $this->_group ) . ''; - } elseif ( $this->_tag == 'del' ) { - $this->_line .= '' . - htmlspecialchars( $this->_group ) . ''; + private function flushGroup( $new_tag ) { + if ( $this->group !== '' ) { + if ( $this->tag == 'ins' ) { + $this->line .= "insClass}>" . + htmlspecialchars( $this->group ) . ''; + } elseif ( $this->tag == 'del' ) { + $this->line .= "delClass}>" . + htmlspecialchars( $this->group ) . ''; } else { - $this->_line .= htmlspecialchars( $this->_group ); + $this->line .= htmlspecialchars( $this->group ); } } - $this->_group = ''; - $this->_tag = $new_tag; + $this->group = ''; + $this->tag = $new_tag; } /** - * @param $new_tag + * @param string $new_tag */ - function _flushLine( $new_tag ) { - $this->_flushGroup( $new_tag ); - if ( $this->_line != '' ) { - array_push( $this->_lines, $this->_line ); + private function flushLine( $new_tag ) { + $this->flushGroup( $new_tag ); + if ( $this->line != '' ) { + array_push( $this->lines, $this->line ); } else { # make empty lines visible by inserting an NBSP - array_push( $this->_lines, NBSP ); + array_push( $this->lines, ' ' ); } - $this->_line = ''; + $this->line = ''; } /** - * @param $words - * @param $tag string + * @param string[] $words + * @param string $tag */ - function addWords( $words, $tag = '' ) { - if ( $tag != $this->_tag ) { - $this->_flushGroup( $tag ); + public function addWords( $words, $tag = '' ) { + if ( $tag != $this->tag ) { + $this->flushGroup( $tag ); } foreach ( $words as $word ) { @@ -1217,20 +950,21 @@ class _HWLDF_WordAccumulator { continue; } if ( $word[0] == "\n" ) { - $this->_flushLine( $tag ); + $this->flushLine( $tag ); $word = substr( $word, 1 ); } assert( '!strstr( $word, "\n" )' ); - $this->_group .= $word; + $this->group .= $word; } } /** - * @return array + * @return string[] */ - function getLines() { - $this->_flushLine( '~done' ); - return $this->_lines; + public function getLines() { + $this->flushLine( '~done' ); + + return $this->lines; } } @@ -1243,25 +977,26 @@ class WordLevelDiff extends MappedDiff { const MAX_LINE_LENGTH = 10000; /** - * @param $orig_lines - * @param $closing_lines + * @param string[] $orig_lines + * @param string[] $closing_lines */ - function __construct( $orig_lines, $closing_lines ) { + public function __construct( $orig_lines, $closing_lines ) { wfProfileIn( __METHOD__ ); - list( $orig_words, $orig_stripped ) = $this->_split( $orig_lines ); - list( $closing_words, $closing_stripped ) = $this->_split( $closing_lines ); + list( $orig_words, $orig_stripped ) = $this->split( $orig_lines ); + list( $closing_words, $closing_stripped ) = $this->split( $closing_lines ); parent::__construct( $orig_words, $closing_words, - $orig_stripped, $closing_stripped ); + $orig_stripped, $closing_stripped ); wfProfileOut( __METHOD__ ); } /** - * @param $lines - * @return array + * @param string[] $lines + * + * @return array[] */ - function _split( $lines ) { + private function split( $lines ) { wfProfileIn( __METHOD__ ); $words = array(); @@ -1282,8 +1017,8 @@ class WordLevelDiff extends MappedDiff { } else { $m = array(); if ( preg_match_all( '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', - $line, $m ) ) - { + $line, $m ) + ) { foreach ( $m[0] as $word ) { $words[] = $word; } @@ -1294,15 +1029,16 @@ class WordLevelDiff extends MappedDiff { } } wfProfileOut( __METHOD__ ); + return array( $words, $stripped ); } /** - * @return array + * @return string[] */ - function orig() { + public function orig() { wfProfileIn( __METHOD__ ); - $orig = new _HWLDF_WordAccumulator; + $orig = new HWLDFWordAccumulator; foreach ( $this->edits as $edit ) { if ( $edit->type == 'copy' ) { @@ -1313,15 +1049,16 @@ class WordLevelDiff extends MappedDiff { } $lines = $orig->getLines(); wfProfileOut( __METHOD__ ); + return $lines; } /** - * @return array + * @return string[] */ - function closing() { + public function closing() { wfProfileIn( __METHOD__ ); - $closing = new _HWLDF_WordAccumulator; + $closing = new HWLDFWordAccumulator; foreach ( $this->edits as $edit ) { if ( $edit->type == 'copy' ) { @@ -1332,164 +1069,8 @@ class WordLevelDiff extends MappedDiff { } $lines = $closing->getLines(); wfProfileOut( __METHOD__ ); - return $lines; - } -} -/** - * Wikipedia Table style diff formatter. - * @todo document - * @private - * @ingroup DifferenceEngine - */ -class TableDiffFormatter extends DiffFormatter { - function __construct() { - $this->leading_context_lines = 2; - $this->trailing_context_lines = 2; - } - - /** - * @static - * @param $msg - * @return mixed - */ - public static function escapeWhiteSpace( $msg ) { - $msg = preg_replace( '/^ /m', '  ', $msg ); - $msg = preg_replace( '/ $/m', '  ', $msg ); - $msg = preg_replace( '/ /', '  ', $msg ); - return $msg; - } - - /** - * @param $xbeg - * @param $xlen - * @param $ybeg - * @param $ylen - * @return string - */ - function _block_header( $xbeg, $xlen, $ybeg, $ylen ) { - $r = '\n" . - '\n"; - return $r; - } - - /** - * @param $header - */ - function _start_block( $header ) { - echo $header; - } - - function _end_block() { - } - - function _lines( $lines, $prefix = ' ', $color = 'white' ) { - } - - /** - * HTML-escape parameter before calling this - * @param $line - * @return string - */ - function addedLine( $line ) { - return $this->wrapLine( '+', 'diff-addedline', $line ); - } - - /** - * HTML-escape parameter before calling this - * @param $line - * @return string - */ - function deletedLine( $line ) { - return $this->wrapLine( '−', 'diff-deletedline', $line ); - } - - /** - * HTML-escape parameter before calling this - * @param $line - * @return string - */ - function contextLine( $line ) { - return $this->wrapLine( ' ', 'diff-context', $line ); - } - - /** - * @param $marker - * @param $class - * @param $line - * @return string - */ - private function wrapLine( $marker, $class, $line ) { - if ( $line !== '' ) { - // The
wrapper is needed for 'overflow: auto' style to scroll properly - $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) ); - } - return "$marker$line"; - } - - /** - * @return string - */ - function emptyLine() { - return ' '; - } - - /** - * @param $lines array - */ - function _added( $lines ) { - foreach ( $lines as $line ) { - echo '' . $this->emptyLine() . - $this->addedLine( '' . - htmlspecialchars( $line ) . '' ) . "\n"; - } - } - - /** - * @param $lines - */ - function _deleted( $lines ) { - foreach ( $lines as $line ) { - echo '' . $this->deletedLine( '' . - htmlspecialchars( $line ) . '' ) . - $this->emptyLine() . "\n"; - } - } - - /** - * @param $lines - */ - function _context( $lines ) { - foreach ( $lines as $line ) { - echo '' . - $this->contextLine( htmlspecialchars( $line ) ) . - $this->contextLine( htmlspecialchars( $line ) ) . "\n"; - } + return $lines; } - /** - * @param $orig - * @param $closing - */ - function _changed( $orig, $closing ) { - wfProfileIn( __METHOD__ ); - - $diff = new WordLevelDiff( $orig, $closing ); - $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 ); - echo '' . $this->deletedLine( $line ) . - $this->addedLine( $aline ) . "\n"; - } - foreach ( $add as $line ) { # If any leftovers - echo '' . $this->emptyLine() . - $this->addedLine( $line ) . "\n"; - } - wfProfileOut( __METHOD__ ); - } } diff --git a/includes/diff/DiffFormatter.php b/includes/diff/DiffFormatter.php new file mode 100644 index 00000000..40df0d75 --- /dev/null +++ b/includes/diff/DiffFormatter.php @@ -0,0 +1,247 @@ + + * You may copy this code freely under the conditions of the GPL. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup DifferenceEngine + */ + +/** + * Base class for diff formatters + * + * 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 + * @ingroup DifferenceEngine + */ +abstract class DiffFormatter { + + /** @var int Number of leading context "lines" to preserve. + * + * This should be left at zero for this class, but subclasses + * may want to set this to other values. + */ + protected $leadingContextLines = 0; + + /** @var int Number of trailing context "lines" to preserve. + * + * This should be left at zero for this class, but subclasses + * may want to set this to other values. + */ + protected $trailingContextLines = 0; + + /** + * Format a diff. + * + * @param Diff $diff + * + * @return string The formatted output. + */ + public function format( $diff ) { + wfProfileIn( __METHOD__ ); + + $xi = $yi = 1; + $block = false; + $context = array(); + + $nlead = $this->leadingContextLines; + $ntrail = $this->trailingContextLines; + + $this->startDiff(); + + // Initialize $x0 and $y0 to prevent IDEs from getting confused. + $x0 = $y0 = 0; + foreach ( $diff->edits as $edit ) { + if ( $edit->type == 'copy' ) { + if ( is_array( $block ) ) { + if ( count( $edit->orig ) <= $nlead + $ntrail ) { + $block[] = $edit; + } else { + if ( $ntrail ) { + $context = array_slice( $edit->orig, 0, $ntrail ); + $block[] = new DiffOpCopy( $context ); + } + $this->block( $x0, $ntrail + $xi - $x0, + $y0, $ntrail + $yi - $y0, + $block ); + $block = false; + } + } + $context = $edit->orig; + } else { + if ( !is_array( $block ) ) { + $context = array_slice( $context, count( $context ) - $nlead ); + $x0 = $xi - count( $context ); + $y0 = $yi - count( $context ); + $block = array(); + if ( $context ) { + $block[] = new DiffOpCopy( $context ); + } + } + $block[] = $edit; + } + + if ( $edit->orig ) { + $xi += count( $edit->orig ); + } + if ( $edit->closing ) { + $yi += count( $edit->closing ); + } + } + + if ( is_array( $block ) ) { + $this->block( $x0, $xi - $x0, + $y0, $yi - $y0, + $block ); + } + + $end = $this->endDiff(); + wfProfileOut( __METHOD__ ); + + return $end; + } + + /** + * @param int $xbeg + * @param int $xlen + * @param int $ybeg + * @param int $ylen + * @param array $edits + * + * @throws MWException If the edit type is not known. + */ + protected function block( $xbeg, $xlen, $ybeg, $ylen, &$edits ) { + wfProfileIn( __METHOD__ ); + $this->startBlock( $this->blockHeader( $xbeg, $xlen, $ybeg, $ylen ) ); + foreach ( $edits as $edit ) { + if ( $edit->type == 'copy' ) { + $this->context( $edit->orig ); + } elseif ( $edit->type == 'add' ) { + $this->added( $edit->closing ); + } elseif ( $edit->type == 'delete' ) { + $this->deleted( $edit->orig ); + } elseif ( $edit->type == 'change' ) { + $this->changed( $edit->orig, $edit->closing ); + } else { + throw new MWException( "Unknown edit type: {$edit->type}" ); + } + } + $this->endBlock(); + wfProfileOut( __METHOD__ ); + } + + protected function startDiff() { + ob_start(); + } + + /** + * @return string + */ + protected function endDiff() { + $val = ob_get_contents(); + ob_end_clean(); + + return $val; + } + + /** + * @param int $xbeg + * @param int $xlen + * @param int $ybeg + * @param int $ylen + * + * @return string + */ + protected function blockHeader( $xbeg, $xlen, $ybeg, $ylen ) { + if ( $xlen > 1 ) { + $xbeg .= ',' . ( $xbeg + $xlen - 1 ); + } + if ( $ylen > 1 ) { + $ybeg .= ',' . ( $ybeg + $ylen - 1 ); + } + + return $xbeg . ( $xlen ? ( $ylen ? 'c' : 'd' ) : 'a' ) . $ybeg; + } + + /** + * Called at the start of a block of connected edits. + * This default implementation writes the header and a newline to the output buffer. + * + * @param string $header + */ + protected function startBlock( $header ) { + echo $header . "\n"; + } + + /** + * Called at the end of a block of connected edits. + * This default implementation does nothing. + */ + protected function endBlock() { + } + + /** + * Writes all (optionally prefixed) lines to the output buffer, separated by newlines. + * + * @param string[] $lines + * @param string $prefix + */ + protected function lines( $lines, $prefix = ' ' ) { + foreach ( $lines as $line ) { + echo "$prefix $line\n"; + } + } + + /** + * @param string[] $lines + */ + protected function context( $lines ) { + $this->lines( $lines ); + } + + /** + * @param string[] $lines + */ + protected function added( $lines ) { + $this->lines( $lines, '>' ); + } + + /** + * @param string[] $lines + */ + protected function deleted( $lines ) { + $this->lines( $lines, '<' ); + } + + /** + * Writes the two sets of lines to the output buffer, separated by "---" and a newline. + * + * @param string[] $orig + * @param string[] $closing + */ + protected function changed( $orig, $closing ) { + $this->deleted( $orig ); + echo "---\n"; + $this->added( $closing ); + } + +} diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index e436f58d..50e08ca1 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -34,60 +34,80 @@ define( 'MW_DIFF_VERSION', '1.11a' ); * @ingroup DifferenceEngine */ class DifferenceEngine extends ContextSource { - /**#@+ - * @private - */ - var $mOldid, $mNewid; - var $mOldTags, $mNewTags; - /** - * @var Content - */ - var $mOldContent, $mNewContent; + + /** @var int */ + public $mOldid; + + /** @var int */ + public $mNewid; + + private $mOldTags; + private $mNewTags; + + /** @var Content */ + public $mOldContent; + + /** @var Content */ + public $mNewContent; + + /** @var Language */ protected $mDiffLang; - /** - * @var Title - */ - var $mOldPage, $mNewPage; + /** @var Title */ + public $mOldPage; - /** - * @var Revision - */ - var $mOldRev, $mNewRev; - private $mRevisionsIdsLoaded = false; // Have the revisions IDs been loaded - 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 Title */ + public $mNewPage; + + /** @var Revision */ + public $mOldRev; + + /** @var Revision */ + public $mNewRev; + + /** @var bool Have the revisions IDs been loaded */ + private $mRevisionsIdsLoaded = false; + + /** @var bool Have the revisions been loaded */ + public $mRevisionsLoaded = false; + + /** @var int How many text blobs have been loaded, 0, 1 or 2? */ + public $mTextLoaded = 0; + + /** @var bool Was the diff fetched from cache? */ + public $mCacheHit = false; /** * Set this to true to add debug info to the HTML output. * Warning: this may cause RSS readers to spuriously mark articles as "new" * (bug 20601) */ - var $enableDebugComment = false; + public $enableDebugComment = false; - // If true, line X is not displayed when X is 1, for example to increase - // readability and conserve space with many small diffs. + /** @var bool If true, line X is not displayed when X is 1, for example + * to increase readability and conserve space with many small diffs. + */ protected $mReducedLineNumbers = false; - // Link to action=markpatrolled + /** @var string Link to action=markpatrolled */ protected $mMarkPatrolledLink = null; - protected $unhide = false; # show rev_deleted content if allowed + /** @var bool Show rev_deleted content if allowed */ + protected $unhide = false; /**#@-*/ /** * Constructor - * @param $context IContextSource context to use, anything else will be ignored - * @param $old Integer old ID we want to show and diff with. - * @param $new String either 'prev' or 'next'. - * @param $rcid Integer Deprecated, no longer used! - * @param $refreshCache boolean If set, refreshes the diff cache - * @param $unhide boolean If set, allow viewing deleted revs + * @param IContextSource $context Context to use, anything else will be ignored + * @param int $old Old ID we want to show and diff with. + * @param string|int $new Either revision ID or 'prev' or 'next'. Default: 0. + * @param int $rcid Deprecated, no longer used! + * @param bool $refreshCache If set, refreshes the diff cache + * @param bool $unhide If set, allow viewing deleted revs */ - function __construct( $context = null, $old = 0, $new = 0, $rcid = 0, - $refreshCache = false, $unhide = false ) - { + public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0, + $refreshCache = false, $unhide = false + ) { if ( $context instanceof IContextSource ) { $this->setContext( $context ); } @@ -101,43 +121,46 @@ class DifferenceEngine extends ContextSource { } /** - * @param $value bool + * @param bool $value */ - function setReducedLineNumbers( $value = true ) { + public function setReducedLineNumbers( $value = true ) { $this->mReducedLineNumbers = $value; } /** * @return Language */ - function getDiffLang() { + public function getDiffLang() { if ( $this->mDiffLang === null ) { # Default language in which the diff text is written. $this->mDiffLang = $this->getTitle()->getPageLanguage(); } + return $this->mDiffLang; } /** * @return bool */ - function wasCacheHit() { + public function wasCacheHit() { return $this->mCacheHit; } /** * @return int */ - function getOldid() { + public function getOldid() { $this->loadRevisionIds(); + return $this->mOldid; } /** - * @return Bool|int + * @return bool|int */ - function getNewid() { + public function getNewid() { $this->loadRevisionIds(); + return $this->mNewid; } @@ -145,10 +168,11 @@ class DifferenceEngine extends ContextSource { * Look up a special:Undelete link to the given deleted revision id, * as a workaround for being unable to load deleted diffs in currently. * - * @param int $id revision ID + * @param int $id Revision ID + * * @return mixed URL or false */ - function deletedLink( $id ) { + public function deletedLink( $id ) { if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'archive', '*', @@ -157,22 +181,25 @@ class DifferenceEngine extends ContextSource { if ( $row ) { $rev = Revision::newFromArchiveRow( $row ); $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); + return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( array( 'target' => $title->getPrefixedText(), 'timestamp' => $rev->getTimestamp() - )); + ) ); } } + return false; } /** * Build a wikitext link toward a deleted revision, if viewable. * - * @param int $id revision ID - * @return string wikitext fragment + * @param int $id Revision ID + * + * @return string Wikitext fragment */ - function deletedIdMarker( $id ) { + public function deletedIdMarker( $id ) { $link = $this->deletedLink( $id ); if ( $link ) { return "[$link $id]"; @@ -201,7 +228,7 @@ class DifferenceEngine extends ContextSource { $this->getLanguage()->listToText( $missing ), count( $missing ) ); } - function showDiffPage( $diffOnly = false ) { + public function showDiffPage( $diffOnly = false ) { wfProfileIn( __METHOD__ ); # Allow frames except in certain special cases @@ -212,6 +239,7 @@ class DifferenceEngine extends ContextSource { if ( !$this->loadRevisionData() ) { $this->showMissingRevision(); wfProfileOut( __METHOD__ ); + return; } @@ -227,7 +255,6 @@ class DifferenceEngine extends ContextSource { } $rollback = ''; - $undoLink = ''; $query = array(); # Carry over 'diffonly' param via navigation links @@ -259,8 +286,8 @@ class DifferenceEngine extends ContextSource { $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); $samePage = true; } else { - $out->setPageTitle( $this->msg( 'difference-title-multipage', $this->mOldPage->getPrefixedText(), - $this->mNewPage->getPrefixedText() ) ); + $out->setPageTitle( $this->msg( 'difference-title-multipage', + $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) ); $out->addSubtitle( $this->msg( 'difference-multipage' ) ); $samePage = false; } @@ -273,17 +300,21 @@ class DifferenceEngine extends ContextSource { $rollback = '   ' . $rollbackLink; } } - if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { + + if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && + !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) + ) { $undoLink = Html::element( 'a', array( 'href' => $this->mNewPage->getLocalURL( array( 'action' => 'edit', 'undoafter' => $this->mOldid, - 'undo' => $this->mNewid ) ), - 'title' => Linker::titleAttrib( 'undo' ) + 'undo' => $this->mNewid + ) ), + 'title' => Linker::titleAttrib( 'undo' ), ), $this->msg( 'editundo' )->text() ); - $revisionTools[] = $undoLink; + $revisionTools['mw-diff-undo'] = $undoLink; } } @@ -311,9 +342,9 @@ class DifferenceEngine extends ContextSource { $oldHeader = '
' . $oldRevisionHeader . '
' . '
' . - Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '
' . + Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '
' . '
' . $oldminor . - Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '
' . + Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '' . '
' . $oldChangeTags[0] . '
' . '
' . $prevlink . '
'; @@ -353,20 +384,27 @@ class DifferenceEngine extends ContextSource { $rdel = $this->revisionDeleteLink( $this->mNewRev ); # Allow extensions to define their own revision tools - wfRunHooks( 'DiffRevisionTools', array( $this->mNewRev, &$revisionTools ) ); + wfRunHooks( 'DiffRevisionTools', array( $this->mNewRev, &$revisionTools, $this->mOldRev ) ); $formattedRevisionTools = array(); // Put each one in parentheses (poor man's button) - foreach ( $revisionTools as $tool ) { - $formattedRevisionTools[] = $this->msg( 'parentheses' )->rawParams( $tool )->escaped(); + foreach ( $revisionTools as $key => $tool ) { + $toolClass = is_string( $key ) ? $key : 'mw-diff-tool'; + $element = Html::rawElement( + 'span', + array( 'class' => $toolClass ), + $this->msg( 'parentheses' )->rawParams( $tool )->escaped() + ); + $formattedRevisionTools[] = $element; } - $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . ' ' . implode( ' ', $formattedRevisionTools ); + $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . + ' ' . implode( ' ', $formattedRevisionTools ); $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff' ); $newHeader = '
' . $newRevisionHeader . '
' . '
' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) . - " $rollback
" . + " $rollback" . '
' . $newminor . - Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '
' . + Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '' . '
' . $newChangeTags[0] . '
' . '
' . $nextlink . $this->markPatrolledLink() . '
'; @@ -390,9 +428,13 @@ class DifferenceEngine extends ContextSource { array( $msg ) ); } else { # Give explanation and add a link to view the diff... - $link = $this->getTitle()->getFullURL( $this->getRequest()->appendQueryValue( 'unhide', '1', true ) ); + $query = $this->getRequest()->appendQueryValue( 'unhide', '1', true ); + $link = $this->getTitle()->getFullURL( $query ); $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff'; - $out->wrapWikiMsg( "\n", array( $msg, $link ) ); + $out->wrapWikiMsg( + "\n", + array( $msg, $link ) + ); } # Otherwise, output a regular diff... } else { @@ -400,7 +442,9 @@ class DifferenceEngine extends ContextSource { $notice = ''; if ( $deleted ) { $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view'; - $notice = "\n"; + $notice = "\n"; } $this->showDiff( $oldHeader, $newHeader, $notice ); if ( !$diffOnly ) { @@ -416,7 +460,7 @@ class DifferenceEngine extends ContextSource { * Side effect: When the patrol link is build, this method will call * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax. * - * @return String + * @return string */ protected function markPatrolledLink() { global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI; @@ -483,21 +527,23 @@ class DifferenceEngine extends ContextSource { } /** - * @param $rev Revision - * @return String + * @param Revision $rev + * + * @return string */ protected function revisionDeleteLink( $rev ) { $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() ); if ( $link !== '' ) { $link = '   ' . $link . ' '; } + return $link; } /** * Show the new revision of the page. */ - function renderNewRevision() { + public function renderNewRevision() { wfProfileIn( __METHOD__ ); $out = $this->getOutput(); $revHeader = $this->getRevisionHeader( $this->mNewRev ); @@ -505,6 +551,7 @@ class DifferenceEngine extends ContextSource { $out->addHTML( "

{$revHeader}

\n" ); # Page content may be handled by a hooked call instead... + # @codingStandardsIgnoreStart Ignoring long lines. if ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $out ) ) ) { $this->loadNewText(); $out->setRevisionId( $this->mNewid ); @@ -513,7 +560,7 @@ class DifferenceEngine extends ContextSource { // NOTE: only needed for B/C: custom rendering of JS/CSS via hook if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { - // Stolen from Article::view --AG 2007-10-11 + // This needs to be synchronised with Article::showCssOrJsPage(), which sucks // Give hooks a chance to customise the output // @todo standardize this crap into one function if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { @@ -521,8 +568,9 @@ class DifferenceEngine extends ContextSource { // use the content object's own rendering $cnt = $this->mNewRev->getContent(); $po = $cnt ? $cnt->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() ) : null; - $txt = $po ? $po->getText() : ''; - $out->addHTML( $txt ); + if ( $po ) { + $out->addParserOutputContent( $po ); + } } } elseif ( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { // Handled by extension @@ -543,23 +591,14 @@ class DifferenceEngine extends ContextSource { $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev ); - # Also try to load it as a redirect - $rt = $this->mNewContent ? $this->mNewContent->getRedirectTarget() : null; - - if ( $rt ) { - $article = Article::newFromTitle( $this->mNewPage, $this->getContext() ); - $out->addHTML( $article->viewRedirect( $rt ) ); - - # WikiPage::getParserOutput() should not return false, but just in case - if ( $parserOutput ) { - # Show categories etc. - $out->addParserOutputNoText( $parserOutput ); - } - } elseif ( $parserOutput ) { + # WikiPage::getParserOutput() should not return false, but just in case + if ( $parserOutput ) { $out->addParserOutput( $parserOutput ); } } } + # @codingStandardsIgnoreEnd + # Add redundant patrol link on bottom... $out->addHTML( $this->markPatrolledLink() ); @@ -574,6 +613,7 @@ class DifferenceEngine extends ContextSource { } $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() ); + return $parserOutput; } @@ -581,16 +621,22 @@ class DifferenceEngine extends ContextSource { * Get the diff text, send it to the OutputPage object * Returns false if the diff could not be generated, otherwise returns true * + * @param string|bool $otitle Header for old text or false + * @param string|bool $ntitle Header for new text or false + * @param string $notice HTML between diff header and body + * * @return bool */ - function showDiff( $otitle, $ntitle, $notice = '' ) { + public function showDiff( $otitle, $ntitle, $notice = '' ) { $diff = $this->getDiff( $otitle, $ntitle, $notice ); if ( $diff === false ) { $this->showMissingRevision(); + return false; } else { $this->showDiffStyle(); $this->getOutput()->addHTML( $diff ); + return true; } } @@ -598,7 +644,7 @@ class DifferenceEngine extends ContextSource { /** * Add style sheets and supporting JS for diff display. */ - function showDiffStyle() { + public function showDiffStyle() { $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' ); } @@ -608,20 +654,24 @@ class DifferenceEngine extends ContextSource { * @param string|bool $otitle Header for old text or false * @param string|bool $ntitle Header for new text or false * @param string $notice HTML between diff header and body + * * @return mixed */ - function getDiff( $otitle, $ntitle, $notice = '' ) { + public function getDiff( $otitle, $ntitle, $notice = '' ) { $body = $this->getDiffBody(); if ( $body === false ) { return false; - } else { - $multi = $this->getMultiNotice(); - // Display a message when the diff is empty - if ( $body === '' ) { - $notice .= '
' . $this->msg( 'diff-empty' )->parse() . "
\n"; - } - return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice ); } + + $multi = $this->getMultiNotice(); + // Display a message when the diff is empty + if ( $body === '' ) { + $notice .= '
' . + $this->msg( 'diff-empty' )->parse() . + "
\n"; + } + + return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice ); } /** @@ -636,26 +686,34 @@ class DifferenceEngine extends ContextSource { // Check if the diff should be hidden from this user if ( !$this->loadRevisionData() ) { wfProfileOut( __METHOD__ ); + return false; - } elseif ( $this->mOldRev && !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { + } elseif ( $this->mOldRev && + !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) + ) { wfProfileOut( __METHOD__ ); + return false; - } elseif ( $this->mNewRev && !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { + } elseif ( $this->mNewRev && + !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) + ) { wfProfileOut( __METHOD__ ); + return false; } // Short-circuit if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev - && $this->mOldRev->getID() == $this->mNewRev->getID() ) ) - { + && $this->mOldRev->getID() == $this->mNewRev->getID() ) + ) { wfProfileOut( __METHOD__ ); + return ''; } // Cacheable? $key = false; if ( $this->mOldid && $this->mNewid ) { - $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, - 'oldid', $this->mOldid, 'newid', $this->mNewid ); + $key = $this->getDiffBodyCacheKey(); + // Try cache if ( !$this->mRefreshCache ) { $difftext = $wgMemc->get( $key ); @@ -664,6 +722,7 @@ class DifferenceEngine extends ContextSource { $difftext = $this->localiseLineNumbers( $difftext ); $difftext .= "\n\n"; wfProfileOut( __METHOD__ ); + return $difftext; } } // don't try to load but save the result @@ -673,6 +732,7 @@ class DifferenceEngine extends ContextSource { // Loadtext is permission safe, this just clears out the diff if ( !$this->loadText() ) { wfProfileOut( __METHOD__ ); + return false; } @@ -692,9 +752,27 @@ class DifferenceEngine extends ContextSource { $difftext = $this->localiseLineNumbers( $difftext ); } wfProfileOut( __METHOD__ ); + return $difftext; } + /** + * Returns the cache key for diff body text or content. + * + * @since 1.23 + * + * @throws MWException + * @return string + */ + protected function getDiffBodyCacheKey() { + if ( !$this->mOldid || !$this->mNewid ) { + throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' ); + } + + return wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, + 'oldid', $this->mOldid, 'newid', $this->mNewid ); + } + /** * Generate a diff, no caching. * @@ -706,17 +784,18 @@ class DifferenceEngine extends ContextSource { * perhaps taking advantage of the content's native form. This is required for all content * models that are not text based. * - * @param $old Content: old content - * @param $new Content: new content + * @since 1.21 + * + * @param Content $old Old content + * @param Content $new New content * + * @throws MWException If old or new content is not an instance of TextContent. * @return bool|string - * @since 1.21 - * @throws MWException if $old or $new are not instances of TextContent. */ - function generateContentDiffBody( Content $old, Content $new ) { + public function generateContentDiffBody( Content $old, Content $new ) { if ( !( $old instanceof TextContent ) ) { - throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " - . "override generateContentDiffBody to fix this." ); + throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " . + "override generateContentDiffBody to fix this." ); } if ( !( $new instanceof TextContent ) ) { @@ -733,12 +812,13 @@ class DifferenceEngine extends ContextSource { /** * Generate a diff, no caching * - * @param string $otext old text, must be already segmented - * @param string $ntext new text, must be already segmented + * @param string $otext Old text, must be already segmented + * @param string $ntext New text, must be already segmented + * * @return bool|string * @deprecated since 1.21, use generateContentDiffBody() instead! */ - function generateDiffBody( $otext, $ntext ) { + public function generateDiffBody( $otext, $ntext ) { ContentHandler::deprecated( __METHOD__, "1.21" ); return $this->generateTextDiffBody( $otext, $ntext ); @@ -749,11 +829,12 @@ class DifferenceEngine extends ContextSource { * * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point. * - * @param string $otext old text, must be already segmented - * @param string $ntext new text, must be already segmented + * @param string $otext Old text, must be already segmented + * @param string $ntext New text, must be already segmented + * * @return bool|string */ - function generateTextDiffBody( $otext, $ntext ) { + public function generateTextDiffBody( $otext, $ntext ) { global $wgExternalDiffEngine, $wgContLang; wfProfileIn( __METHOD__ ); @@ -764,9 +845,10 @@ class DifferenceEngine extends ContextSource { 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 ) ); + $otext = htmlspecialchars( $wgContLang->segmentForDiff( $otext ) ); + $ntext = htmlspecialchars( $wgContLang->segmentForDiff( $ntext ) ); wfProfileOut( __METHOD__ ); + return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . $this->debug( 'wikidiff1' ); } @@ -779,6 +861,7 @@ class DifferenceEngine extends ContextSource { $text .= $this->debug( 'wikidiff2' ); wfProfileOut( 'wikidiff2_do_diff' ); wfProfileOut( __METHOD__ ); + return $text; } if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) { @@ -790,11 +873,13 @@ class DifferenceEngine extends ContextSource { $tempFile1 = fopen( $tempName1, "w" ); if ( !$tempFile1 ) { wfProfileOut( __METHOD__ ); + return false; } $tempFile2 = fopen( $tempName2, "w" ); if ( !$tempFile2 ) { wfProfileOut( __METHOD__ ); + return false; } fwrite( $tempFile1, $otext ); @@ -809,6 +894,7 @@ class DifferenceEngine extends ContextSource { unlink( $tempName1 ); unlink( $tempName2 ); wfProfileOut( __METHOD__ ); + return $difftext; } @@ -818,13 +904,17 @@ class DifferenceEngine extends ContextSource { $diffs = new Diff( $ota, $nta ); $formatter = new TableDiffFormatter(); $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . - wfProfileOut( __METHOD__ ); + wfProfileOut( __METHOD__ ); + return $difftext; } /** * Generate a debug comment indicating diff generating time, * server node, and generator backend. + * + * @param string $generator : What diff engine was used + * * @return string */ protected function debug( $generator = "internal" ) { @@ -837,35 +927,41 @@ class DifferenceEngine extends ContextSource { $data[] = wfHostname(); } $data[] = wfTimestamp( TS_DB ); + return "\n"; } /** * Replace line numbers with the text in the user's language + * + * @param string $text + * * @return mixed */ - function localiseLineNumbers( $text ) { - return preg_replace_callback( '//', - array( &$this, 'localiseLineNumbersCb' ), $text ); + public function localiseLineNumbers( $text ) { + return preg_replace_callback( + '//', + array( &$this, 'localiseLineNumbersCb' ), + $text + ); } - function localiseLineNumbersCb( $matches ) { + public function localiseLineNumbersCb( $matches ) { if ( $matches[1] === '1' && $this->mReducedLineNumbers ) { return ''; } + return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped(); } /** * If there are revisions between the ones being compared, return a note saying so. + * * @return string */ - function getMultiNotice() { + public function getMultiNotice() { if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) { return ''; } elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) { @@ -881,39 +977,54 @@ class DifferenceEngine extends ContextSource { $newRev = $this->mNewRev; } - $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev ); - if ( $nEdits > 0 ) { + // Sanity: don't show the notice if too many rows must be scanned + // @todo show some special message for that case + $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 ); + if ( $nEdits > 0 && $nEdits <= 1000 ) { $limit = 100; // use diff-multi-manyusers if too many users - $numUsers = $this->mNewPage->countAuthorsBetween( $oldRev, $newRev, $limit ); + $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit ); + $numUsers = count( $users ); + + if ( $numUsers == 1 && $users[0] == $newRev->getRawUserText() ) { + $numUsers = 0; // special case to say "by the same user" instead of "by one other user" + } + return self::intermediateEditsMsg( $nEdits, $numUsers, $limit ); } + return ''; // nothing } /** * Get a notice about how many intermediate edits and users there are - * @param $numEdits int - * @param $numUsers int - * @param $limit int + * + * @param int $numEdits + * @param int $numUsers + * @param int $limit + * * @return string */ public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) { - if ( $numUsers > $limit ) { + if ( $numUsers === 0 ) { + $msg = 'diff-multi-sameuser'; + } elseif ( $numUsers > $limit ) { $msg = 'diff-multi-manyusers'; $numUsers = $limit; } else { - $msg = 'diff-multi'; + $msg = 'diff-multi-otherusers'; } + return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse(); } /** * Get a header for a specified revision. * - * @param $rev Revision + * @param Revision $rev * @param string $complete 'complete' to get the header wrapped depending * the visibility of the revision and a link to edit the page. - * @return String HTML fragment + * + * @return string HTML fragment */ protected function getRevisionHeader( Revision $rev, $complete = '' ) { $lang = $this->getLanguage(); @@ -945,11 +1056,21 @@ class DifferenceEngine extends ContextSource { $editQuery['oldid'] = $rev->getID(); } - $msg = $this->msg( $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped(); - $header .= ' ' . $this->msg( 'parentheses' )->rawParams( - Linker::linkKnown( $title, $msg, array(), $editQuery ) )->plain(); + $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold'; + $msg = $this->msg( $key )->escaped(); + $editLink = $this->msg( 'parentheses' )->rawParams( + Linker::linkKnown( $title, $msg, array( ), $editQuery ) )->plain(); + $header .= ' ' . Html::rawElement( + 'span', + array( 'class' => 'mw-diff-edit' ), + $editLink + ); if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); + $header = Html::rawElement( + 'span', + array( 'class' => 'history-deleted' ), + $header + ); } } else { $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); @@ -961,9 +1082,16 @@ class DifferenceEngine extends ContextSource { /** * Add the header to a diff body * + * @param string $diff Diff body + * @param string $otitle Old revision header + * @param string $ntitle New revision header + * @param string $multi Notice telling user that there are intermediate + * revisions between the ones being compared + * @param string $notice Other notices, e.g. that user is viewing deleted content + * * @return string */ - function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) { + public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) { // shared.css sets diff in interface language/dir, but the actual content // is often in a different language, mostly the page content language/dir $tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getDiffLang()->alignStart() ); @@ -998,7 +1126,8 @@ class DifferenceEngine extends ContextSource { } if ( $multi != '' ) { - $header .= "{$multi}"; + $header .= "{$multi}"; } if ( $notice != '' ) { $header .= "{$notice}"; @@ -1011,7 +1140,7 @@ class DifferenceEngine extends ContextSource { * Use specified text instead of loading from the database * @deprecated since 1.21, use setContent() instead. */ - function setText( $oldText, $newText ) { + public function setText( $oldText, $newText ) { ContentHandler::deprecated( __METHOD__, "1.21" ); $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() ); @@ -1022,9 +1151,11 @@ class DifferenceEngine extends ContextSource { /** * Use specified text instead of loading from the database + * @param Content $oldContent + * @param Content $newContent * @since 1.21 */ - function setContent( Content $oldContent, Content $newContent ) { + public function setContent( Content $oldContent, Content $newContent ) { $this->mOldContent = $oldContent; $this->mNewContent = $newContent; @@ -1035,12 +1166,41 @@ class DifferenceEngine extends ContextSource { /** * Set the language in which the diff text is written * (Defaults to page content language). + * @param Language|string $lang * @since 1.19 */ - function setTextLanguage( $lang ) { + public function setTextLanguage( $lang ) { $this->mDiffLang = wfGetLangObj( $lang ); } + /** + * Maps a revision pair definition as accepted by DifferenceEngine constructor + * to a pair of actual integers representing revision ids. + * + * @param int $old Revision id, e.g. from URL parameter 'oldid' + * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff' + * + * @return int[] List of two revision ids, older first, later second. + * Zero signifies invalid argument passed. + * false signifies that there is no previous/next revision ($old is the oldest/newest one). + */ + public function mapDiffPrevNext( $old, $new ) { + if ( $new === 'prev' ) { + // Show diff between revision $old and the previous one. Get previous one from DB. + $newid = intval( $old ); + $oldid = $this->getTitle()->getPreviousRevisionID( $newid ); + } elseif ( $new === 'next' ) { + // Show diff between revision $old and the next one. Get next one from DB. + $oldid = intval( $old ); + $newid = $this->getTitle()->getNextRevisionID( $oldid ); + } else { + $oldid = intval( $old ); + $newid = intval( $new ); + } + + return array( $oldid, $newid ); + } + /** * Load revision IDs */ @@ -1054,26 +1214,17 @@ class DifferenceEngine extends ContextSource { $old = $this->mOldid; $new = $this->mNewid; - if ( $new === 'prev' ) { - # Show diff between revision $old and the previous one. - # Get previous one from DB. - $this->mNewid = intval( $old ); - $this->mOldid = $this->getTitle()->getPreviousRevisionID( $this->mNewid ); - } elseif ( $new === 'next' ) { - # Show diff between revision $old and the next one. - # Get next one from DB. - $this->mOldid = intval( $old ); - $this->mNewid = $this->getTitle()->getNextRevisionID( $this->mOldid ); - if ( $this->mNewid === false ) { - # if no result, NewId points to the newest old revision. The only newer - # revision is cur, which is "0". - $this->mNewid = 0; - } - } else { - $this->mOldid = intval( $old ); - $this->mNewid = intval( $new ); - wfRunHooks( 'NewDifferenceEngine', array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) ); + list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new ); + if ( $new === 'next' && $this->mNewid === false ) { + # if no result, NewId points to the newest old revision. The only newer + # revision is cur, which is "0". + $this->mNewid = 0; } + + wfRunHooks( + 'NewDifferenceEngine', + array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) + ); } /** @@ -1088,7 +1239,7 @@ class DifferenceEngine extends ContextSource { * * @return bool */ - function loadRevisionData() { + public function loadRevisionData() { if ( $this->mRevisionsLoaded ) { return true; } @@ -1099,9 +1250,15 @@ class DifferenceEngine extends ContextSource { $this->loadRevisionIds(); // Load the new revision object - $this->mNewRev = $this->mNewid - ? Revision::newFromId( $this->mNewid ) - : Revision::newFromTitle( $this->getTitle(), false, Revision::READ_NORMAL ); + if ( $this->mNewid ) { + $this->mNewRev = Revision::newFromId( $this->mNewid ); + } else { + $this->mNewRev = Revision::newFromTitle( + $this->getTitle(), + false, + Revision::READ_NORMAL + ); + } if ( !$this->mNewRev instanceof Revision ) { return false; @@ -1162,7 +1319,7 @@ class DifferenceEngine extends ContextSource { * * @return bool */ - function loadText() { + public function loadText() { if ( $this->mTextLoaded == 2 ) { return true; } @@ -1196,7 +1353,7 @@ class DifferenceEngine extends ContextSource { * * @return bool */ - function loadNewText() { + public function loadNewText() { if ( $this->mTextLoaded >= 1 ) { return true; } @@ -1211,4 +1368,5 @@ class DifferenceEngine extends ContextSource { return true; } + } diff --git a/includes/diff/TableDiffFormatter.php b/includes/diff/TableDiffFormatter.php new file mode 100644 index 00000000..db7318f2 --- /dev/null +++ b/includes/diff/TableDiffFormatter.php @@ -0,0 +1,214 @@ + + * You may copy this code freely under the conditions of the GPL. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup DifferenceEngine + */ + +/** + * MediaWiki default table style diff formatter + * @todo document + * @private + * @ingroup DifferenceEngine + */ +class TableDiffFormatter extends DiffFormatter { + + function __construct() { + $this->leadingContextLines = 2; + $this->trailingContextLines = 2; + } + + /** + * @static + * @param string $msg + * + * @return mixed + */ + public static function escapeWhiteSpace( $msg ) { + $msg = preg_replace( '/^ /m', '  ', $msg ); + $msg = preg_replace( '/ $/m', '  ', $msg ); + $msg = preg_replace( '/ /', '  ', $msg ); + + return $msg; + } + + /** + * @param int $xbeg + * @param int $xlen + * @param int $ybeg + * @param int $ylen + * + * @return string + */ + protected function blockHeader( $xbeg, $xlen, $ybeg, $ylen ) { + // '' get replaced by a localised line number + // in DifferenceEngine::localiseLineNumbers + $r = '\n" . + '\n"; + + return $r; + } + + /** + * Writes the header to the output buffer. + * + * @param string $header + */ + protected function startBlock( $header ) { + echo $header; + } + + protected function endBlock() { + } + + /** + * @param string[] $lines + * @param string $prefix + * @param string $color + */ + protected function lines( $lines, $prefix = ' ', $color = 'white' ) { + } + + /** + * HTML-escape parameter before calling this + * + * @param string $line + * + * @return string + */ + protected function addedLine( $line ) { + return $this->wrapLine( '+', 'diff-addedline', $line ); + } + + /** + * HTML-escape parameter before calling this + * + * @param string $line + * + * @return string + */ + protected function deletedLine( $line ) { + return $this->wrapLine( '−', 'diff-deletedline', $line ); + } + + /** + * HTML-escape parameter before calling this + * + * @param string $line + * + * @return string + */ + protected function contextLine( $line ) { + return $this->wrapLine( ' ', 'diff-context', $line ); + } + + /** + * @param string $marker + * @param string $class Unused + * @param string $line + * + * @return string + */ + protected function wrapLine( $marker, $class, $line ) { + if ( $line !== '' ) { + // The
wrapper is needed for 'overflow: auto' style to scroll properly + $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) ); + } + + return "$marker$line"; + } + + /** + * @return string + */ + protected function emptyLine() { + return ' '; + } + + /** + * Writes all lines to the output buffer, each enclosed in . + * + * @param string[] $lines + */ + protected function added( $lines ) { + foreach ( $lines as $line ) { + echo '' . $this->emptyLine() . + $this->addedLine( '' . + htmlspecialchars( $line ) . '' ) . "\n"; + } + } + + /** + * Writes all lines to the output buffer, each enclosed in . + * + * @param string[] $lines + */ + protected function deleted( $lines ) { + foreach ( $lines as $line ) { + echo '' . $this->deletedLine( '' . + htmlspecialchars( $line ) . '' ) . + $this->emptyLine() . "\n"; + } + } + + /** + * Writes all lines to the output buffer, each enclosed in . + * + * @param string[] $lines + */ + protected function context( $lines ) { + foreach ( $lines as $line ) { + echo '' . + $this->contextLine( htmlspecialchars( $line ) ) . + $this->contextLine( htmlspecialchars( $line ) ) . "\n"; + } + } + + /** + * Writes the two sets of lines to the output buffer, each enclosed in . + * + * @param string[] $orig + * @param string[] $closing + */ + protected function changed( $orig, $closing ) { + wfProfileIn( __METHOD__ ); + + $diff = new WordLevelDiff( $orig, $closing ); + $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 ); + echo '' . $this->deletedLine( $line ) . + $this->addedLine( $aline ) . "\n"; + } + foreach ( $add as $line ) { # If any leftovers + echo '' . $this->emptyLine() . + $this->addedLine( $line ) . "\n"; + } + wfProfileOut( __METHOD__ ); + } + +} diff --git a/includes/diff/UnifiedDiffFormatter.php b/includes/diff/UnifiedDiffFormatter.php new file mode 100644 index 00000000..32a76055 --- /dev/null +++ b/includes/diff/UnifiedDiffFormatter.php @@ -0,0 +1,74 @@ + + * You may copy this code freely under the conditions of the GPL. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup DifferenceEngine + */ + +/** + * A formatter that outputs unified diffs + * @ingroup DifferenceEngine + */ +class UnifiedDiffFormatter extends DiffFormatter { + + /** @var int */ + protected $leadingContextLines = 2; + + /** @var int */ + protected $trailingContextLines = 2; + + /** + * @param string[] $lines + */ + protected function added( $lines ) { + $this->lines( $lines, '+' ); + } + + /** + * @param string[] $lines + */ + protected function deleted( $lines ) { + $this->lines( $lines, '-' ); + } + + /** + * @param string[] $orig + * @param string[] $closing + */ + protected function changed( $orig, $closing ) { + $this->deleted( $orig ); + $this->added( $closing ); + } + + /** + * @param int $xbeg + * @param int $xlen + * @param int $ybeg + * @param int $ylen + * + * @return string + */ + protected function blockHeader( $xbeg, $xlen, $ybeg, $ylen ) { + return "@@ -$xbeg,$xlen +$ybeg,$ylen @@"; + } + +} diff --git a/includes/diff/WikiDiff3.php b/includes/diff/WikiDiff3.php index ea6f6e5d..7a0f7403 100644 --- a/includes/diff/WikiDiff3.php +++ b/includes/diff/WikiDiff3.php @@ -138,8 +138,9 @@ class WikiDiff3 { */ $max = min( $this->m, $this->n ); for ( $forwardBound = 0; $forwardBound < $max - && $this->from[$forwardBound] === $this->to[$forwardBound]; - ++$forwardBound ) { + && $this->from[$forwardBound] === $this->to[$forwardBound]; + ++$forwardBound + ) { $this->removed[$forwardBound] = $this->added[$forwardBound] = false; } @@ -147,7 +148,8 @@ class WikiDiff3 { $backBoundL2 = $this->n - 1; while ( $backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound - && $this->from[$backBoundL1] === $this->to[$backBoundL2] ) { + && $this->from[$backBoundL1] === $this->to[$backBoundL2] + ) { $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false; } @@ -156,8 +158,14 @@ class WikiDiff3 { $snake = array( 0, 0, 0 ); $this->length = $forwardBound + $this->m - $backBoundL1 - 1 - + $this->lcs_rec( $forwardBound, $backBoundL1, - $forwardBound, $backBoundL2, $V, $snake ); + + $this->lcs_rec( + $forwardBound, + $backBoundL1, + $forwardBound, + $backBoundL2, + $V, + $snake + ); } $this->m = $m; @@ -189,8 +197,9 @@ class WikiDiff3 { while ( $xi < $this->m || $yi < $this->n ) { // Matching "snake". while ( $xi < $this->m && $yi < $this->n - && !$this->removed[$xi] - && !$this->added[$yi] ) { + && !$this->removed[$xi] + && !$this->added[$yi] + ) { ++$xi; ++$yi; } @@ -206,10 +215,10 @@ class WikiDiff3 { } if ( $xi > $xstart || $yi > $ystart ) { - $ranges[] = new RangeDifference( $xstart, $xi, - $ystart, $yi ); + $ranges[] = new RangeDifference( $xstart, $xi, $ystart, $yi ); } } + return $ranges; } @@ -220,7 +229,7 @@ class WikiDiff3 { } $d = $this->find_middle_snake( $bottoml1, $topl1, $bottoml2, - $topl2, $V, $snake ); + $topl2, $V, $snake ); // need to store these so we don't lose them when they're // overwritten by the recursion @@ -236,9 +245,9 @@ class WikiDiff3 { if ( $d > 1 ) { return $len + $this->lcs_rec( $bottoml1, $startx - 1, $bottoml2, - $starty - 1, $V, $snake ) + $starty - 1, $V, $snake ) + $this->lcs_rec( $startx + $len, $topl1, $starty + $len, - $topl2, $V, $snake ); + $topl2, $V, $snake ); } elseif ( $d == 1 ) { /* * In this case the sequences differ by exactly 1 line. We have @@ -250,8 +259,10 @@ class WikiDiff3 { $this->removed[$bottoml1 + $i] = $this->added[$bottoml2 + $i] = false; } + return $max + $len; } + return $len; } @@ -263,8 +274,8 @@ class WikiDiff3 { $snake0 = &$snake[0]; $snake1 = &$snake[1]; $snake2 = &$snake[2]; - $bottoml1_min_1 = $bottoml1 -1; - $bottoml2_min_1 = $bottoml2 -1; + $bottoml1_min_1 = $bottoml1 - 1; + $bottoml2_min_1 = $bottoml2 - 1; $N = $topl1 - $bottoml1_min_1; $M = $topl2 - $bottoml2_min_1; $delta = $N - $M; @@ -307,7 +318,8 @@ class WikiDiff3 { // compute forward furthest reaching paths for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { if ( $k == -$d || ( $k < $d - && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] ) ) { + && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] ) + ) { $x = $V0[$limit_plus_1 + $k]; } else { $x = $V0[$limit_min_1 + $k] + 1; @@ -320,12 +332,13 @@ class WikiDiff3 { ++$absx; ++$absy; } - $x = $absx -$bottoml1; + $x = $absx - $bottoml1; - $snake2 = $absx -$snake0; + $snake2 = $absx - $snake0; $V0[$limit + $k] = $x; if ( $k >= $delta - $d + 1 && $k <= $delta + $d - 1 - && $x >= $V1[$limit + $k - $delta] ) { + && $x >= $V1[$limit + $k - $delta] + ) { return 2 * $d - 1; } @@ -345,7 +358,8 @@ class WikiDiff3 { // compute backward furthest reaching paths for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { if ( $k == $d - || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] ) ) { + || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] ) + ) { $x = $V1[$limit_min_1 + $k]; } else { $x = $V1[$limit_plus_1 + $k] - 1; @@ -355,7 +369,8 @@ class WikiDiff3 { $snake2 = 0; while ( $x > 0 && $y > 0 - && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1] ) { + && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1] + ) { --$x; --$y; ++$snake2; @@ -380,7 +395,8 @@ class WikiDiff3 { // compute forward furthest reaching paths for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { if ( $k == -$d - || ( $k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] ) ) { + || ( $k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] ) + ) { $x = $V0[$limit_plus_1 + $k]; } else { $x = $V0[$limit_min_1 + $k] + 1; @@ -393,14 +409,14 @@ class WikiDiff3 { ++$absx; ++$absy; } - $x = $absx -$bottoml1; - $snake2 = $absx -$snake0; + $x = $absx - $bottoml1; + $snake2 = $absx - $snake0; $V0[$limit + $k] = $x; // check to see if we can cut down the diagonal range if ( $x >= $N && $end_forward > $k - 1 ) { $end_forward = $k - 1; - } elseif ( $absy -$bottoml2 >= $M ) { + } elseif ( $absy - $bottoml2 >= $M ) { $start_forward = $k + 1; $value_to_add_forward = 0; } @@ -413,7 +429,8 @@ class WikiDiff3 { // compute backward furthest reaching paths for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { if ( $k == $d - || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] ) ) { + || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] ) + ) { $x = $V1[$limit_min_1 + $k]; } else { $x = $V1[$limit_plus_1 + $k] - 1; @@ -423,7 +440,8 @@ class WikiDiff3 { $snake2 = 0; while ( $x > 0 && $y > 0 - && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1] ) { + && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1] + ) { --$x; --$y; ++$snake2; @@ -431,9 +449,11 @@ class WikiDiff3 { $V1[$limit + $k] = $x; if ( $k >= -$delta - $d && $k <= $d - $delta - && $x <= $V0[$limit + $k + $delta] ) { + && $x <= $V0[$limit + $k + $delta] + ) { $snake0 = $bottoml1 + $x; $snake1 = $bottoml2 + $y; + return 2 * $d; } @@ -460,6 +480,7 @@ class WikiDiff3 { $snake2 = 0; wfDebug( "Computing the LCS is too expensive. Using a heuristic.\n" ); $this->heuristicUsed = true; + return 5; /* * HACK: since we didn't really finish the LCS computation * we don't really know the length of the SES. We don't do @@ -554,8 +575,9 @@ class WikiDiff3 { public function getLcsLength() { if ( $this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic ) { $this->lcsLengthCorrectedForHeuristic = true; - $this->length = $this->m -array_sum( $this->added ); + $this->length = $this->m - array_sum( $this->added ); } + return $this->length; } @@ -569,12 +591,22 @@ class WikiDiff3 { */ class RangeDifference { + /** @var int */ public $leftstart; + + /** @var int */ public $leftend; + + /** @var int */ public $leftlength; + /** @var int */ public $rightstart; + + /** @var int */ public $rightend; + + /** @var int */ public $rightlength; function __construct( $leftstart, $leftend, $rightstart, $rightend ) { @@ -585,4 +617,5 @@ class RangeDifference { $this->rightend = $rightend; $this->rightlength = $rightend - $rightstart; } + } -- cgit v1.2.2