* Follow the TODO comment in TextSlotDiffRenderer ::getTextDiffInternal() by moving the code out to three parallel implementations, namely ExternalTextDiffer, PhpTextDiffer and Wikidiff2TextDiffer. * Add a container/factory class ManifoldTextDiffer to glue them together and collate available formats. * Move the inline legend to Wikidiff2TextDiffer. Not the toggle since the ability to toggle depends on the available format, not the current format. * Update the diff cache keys so that ManifoldTextDiffer can store the engine=>format map it used to generate the diff. * Drop support for the second parameter to TextSlotDiffRenderer ::setEngine(), since nothing used it anymore. * Provide a format batch API, since some engines are able to efficiently generate multiple formats. This might be used by DifferenceEngine in future. Needs risky change notification for the cache key change. Bug: T339184 Depends-On: I8a35b9b8ec1622c9a36d2496bdd24f51bc52c85f Change-Id: I5c506e39162855aff53dd420dd8145156739059c
329 lines
9.5 KiB
PHP
329 lines
9.5 KiB
PHP
<?php
|
|
/**
|
|
* Renders a slot diff by doing a text diff on the native representation.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
* @ingroup DifferenceEngine
|
|
*/
|
|
|
|
use MediaWiki\Diff\TextDiffer\ManifoldTextDiffer;
|
|
use MediaWiki\Diff\TextDiffer\TextDiffer;
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
use MediaWiki\HookContainer\HookRunner;
|
|
use MediaWiki\Html\Html;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Title\Title;
|
|
use OOUI\ButtonGroupWidget;
|
|
use OOUI\ButtonWidget;
|
|
|
|
/**
|
|
* Renders a slot diff by doing a text diff on the native representation.
|
|
*
|
|
* If you want to use this without content objects (to call getTextDiff() on some
|
|
* non-content-related texts), obtain an instance with
|
|
* ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
|
|
* ->getSlotDiffRenderer( RequestContext::getMain() )
|
|
*
|
|
* @ingroup DifferenceEngine
|
|
*/
|
|
class TextSlotDiffRenderer extends SlotDiffRenderer {
|
|
|
|
/** Use the PHP diff implementation (DiffEngine). */
|
|
public const ENGINE_PHP = 'php';
|
|
|
|
/** Use the wikidiff2 PHP module. */
|
|
public const ENGINE_WIKIDIFF2 = 'wikidiff2';
|
|
|
|
/** Use the wikidiff2 PHP module. */
|
|
public const ENGINE_WIKIDIFF2_INLINE = 'wikidiff2inline';
|
|
|
|
/** Use an external executable. */
|
|
public const ENGINE_EXTERNAL = 'external';
|
|
|
|
public const INLINE_LEGEND_KEY = '10_mw-diff-inline-legend';
|
|
|
|
public const INLINE_SWITCHER_KEY = '60_mw-diff-inline-switch';
|
|
|
|
/** @var IBufferingStatsdDataFactory|null */
|
|
private $statsdDataFactory;
|
|
|
|
/** @var HookRunner|null */
|
|
private $hookRunner;
|
|
|
|
/** @var string|null */
|
|
private $format;
|
|
|
|
/** @var string */
|
|
private $contentModel;
|
|
|
|
/** @var TextDiffer|null */
|
|
private $textDiffer;
|
|
|
|
/** @inheritDoc */
|
|
public function getExtraCacheKeys() {
|
|
return $this->textDiffer->getCacheKeys( [ $this->format ] );
|
|
}
|
|
|
|
/**
|
|
* Convenience helper to use getTextDiff without an instance.
|
|
* @param string $oldText
|
|
* @param string $newText
|
|
* @param array $options
|
|
* @return string
|
|
*/
|
|
public static function diff( $oldText, $newText, $options = [] ) {
|
|
/** @var TextSlotDiffRenderer $slotDiffRenderer */
|
|
$slotDiffRenderer = MediaWikiServices::getInstance()
|
|
->getContentHandlerFactory()
|
|
->getContentHandler( CONTENT_MODEL_TEXT )
|
|
->getSlotDiffRenderer( RequestContext::getMain(), $options );
|
|
'@phan-var TextSlotDiffRenderer $slotDiffRenderer';
|
|
return $slotDiffRenderer->getTextDiff( $oldText, $newText );
|
|
}
|
|
|
|
/**
|
|
* @param IBufferingStatsdDataFactory $statsdDataFactory
|
|
*/
|
|
public function setStatsdDataFactory( IBufferingStatsdDataFactory $statsdDataFactory ) {
|
|
$this->statsdDataFactory = $statsdDataFactory;
|
|
}
|
|
|
|
/**
|
|
* This has no effect since MW 1.41. The language is now injected via setTextDiffer().
|
|
*
|
|
* @param Language $language
|
|
* @deprecated since 1.41
|
|
*/
|
|
public function setLanguage( Language $language ) {
|
|
wfDeprecated( __METHOD__, '1.41' );
|
|
}
|
|
|
|
/**
|
|
* @since 1.41
|
|
* @param HookContainer $hookContainer
|
|
*/
|
|
public function setHookContainer( HookContainer $hookContainer ): void {
|
|
$this->hookRunner = new HookRunner( $hookContainer );
|
|
}
|
|
|
|
/**
|
|
* @param string $contentModel
|
|
* @since 1.41
|
|
*/
|
|
public function setContentModel( string $contentModel ) {
|
|
$this->contentModel = $contentModel;
|
|
}
|
|
|
|
/**
|
|
* Set which diff engine to use.
|
|
*
|
|
* @param string $type One of the ENGINE_* constants.
|
|
* @param null $executable Must be null since 1.41. Previously a path to execute.
|
|
*/
|
|
public function setEngine( $type, $executable = null ) {
|
|
if ( $executable !== null ) {
|
|
throw new \InvalidArgumentException(
|
|
'The $executable parameter is no longer supported and must be null'
|
|
);
|
|
}
|
|
switch ( $type ) {
|
|
case self::ENGINE_PHP:
|
|
$engine = 'php';
|
|
$format = 'table';
|
|
break;
|
|
|
|
case self::ENGINE_WIKIDIFF2:
|
|
$engine = 'wikidiff2';
|
|
$format = 'table';
|
|
break;
|
|
|
|
case self::ENGINE_EXTERNAL:
|
|
$engine = 'external';
|
|
$format = 'external';
|
|
break;
|
|
|
|
case self::ENGINE_WIKIDIFF2_INLINE:
|
|
$engine = 'wikidiff2';
|
|
$format = 'inline';
|
|
break;
|
|
|
|
default:
|
|
throw new \InvalidArgumentException( '$type ' .
|
|
'must be one of the TextSlotDiffRenderer::ENGINE_* constants' );
|
|
}
|
|
if ( $this->textDiffer instanceof ManifoldTextDiffer ) {
|
|
$this->textDiffer->setEngine( $engine );
|
|
}
|
|
$this->setFormat( $format );
|
|
}
|
|
|
|
/**
|
|
* Set the TextDiffer format
|
|
*
|
|
* @since 1.41
|
|
* @param string $format
|
|
*/
|
|
public function setFormat( $format ) {
|
|
$this->format = $format;
|
|
}
|
|
|
|
/**
|
|
* @param TextDiffer $textDiffer
|
|
*/
|
|
public function setTextDiffer( TextDiffer $textDiffer ) {
|
|
$this->textDiffer = $textDiffer;
|
|
}
|
|
|
|
/**
|
|
* Get the current TextDiffer, or throw an exception if setTextDiffer() has
|
|
* not been called.
|
|
*
|
|
* @return TextDiffer
|
|
*/
|
|
private function getTextDiffer(): TextDiffer {
|
|
return $this->textDiffer;
|
|
}
|
|
|
|
/**
|
|
* Get the content model ID that this renderer acts on
|
|
*
|
|
* @since 1.41
|
|
* @return string
|
|
*/
|
|
public function getContentModel(): string {
|
|
return $this->contentModel;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function getDiff( Content $oldContent = null, Content $newContent = null ) {
|
|
$this->normalizeContents( $oldContent, $newContent, TextContent::class );
|
|
|
|
$oldText = $oldContent->serialize();
|
|
$newText = $newContent->serialize();
|
|
|
|
return $this->getTextDiff( $oldText, $newText );
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function getTablePrefix( IContextSource $context, Title $newTitle ): array {
|
|
$parts = $this->getTextDiffer()->getTablePrefixes( $this->format );
|
|
|
|
$showDiffToggleSwitch = $context->getConfig()->get( MainConfigNames::ShowDiffToggleSwitch );
|
|
// If we support the inline type, add a toggle switch
|
|
if ( $showDiffToggleSwitch && $this->getTextDiffer()->hasFormat( 'inline' ) ) {
|
|
$values = $context->getRequest()->getValues();
|
|
$isInlineDiffType = $this->format === 'inline';
|
|
unset( $values[ 'diff-type' ] );
|
|
unset( $values[ 'title' ] );
|
|
$parts[self::INLINE_SWITCHER_KEY] = Html::rawElement( 'div',
|
|
[ 'class' => 'mw-diffPage-inlineToggle-container' ],
|
|
// Will be replaced by a ButtonSelectWidget in JS
|
|
new ButtonGroupWidget( [
|
|
'items' => [
|
|
new ButtonWidget( [
|
|
'active' => $isInlineDiffType,
|
|
'label' => $context->msg( 'diff-inline-format-label' )->plain(),
|
|
'href' => $newTitle->getLocalURL( $values ) . '&diff-type=inline'
|
|
] ),
|
|
new ButtonWidget( [
|
|
'active' => !$isInlineDiffType,
|
|
'label' => $context->msg( 'diff-table-format-label' )->plain(),
|
|
'href' => $newTitle->getLocalURL( $values )
|
|
] )
|
|
]
|
|
] )
|
|
);
|
|
}
|
|
// Add an empty placeholder for the legend is added when it's not in
|
|
// use and other items have been added.
|
|
$parts += [ self::INLINE_LEGEND_KEY => null, self::INLINE_SWITCHER_KEY => null ];
|
|
|
|
// Allow extensions to add other parts to this area (or modify the legend).
|
|
$this->hookRunner->onTextSlotDiffRendererTablePrefix( $this, $context, $parts );
|
|
if ( count( $parts ) > 1 && $parts[self::INLINE_LEGEND_KEY] === null ) {
|
|
$parts[self::INLINE_LEGEND_KEY] = Html::element( 'div' );
|
|
}
|
|
return $parts;
|
|
}
|
|
|
|
/**
|
|
* Diff the text representations of two content objects (or just two pieces of text in general).
|
|
* @param string $oldText
|
|
* @param string $newText
|
|
* @return string HTML. One or more <tr> tags, or an empty string if the inputs are identical.
|
|
*/
|
|
public function getTextDiff( string $oldText, string $newText ) {
|
|
$diff = function () use ( $oldText, $newText ) {
|
|
$time = microtime( true );
|
|
|
|
$result = $this->getTextDiffInternal( $oldText, $newText );
|
|
|
|
$time = intval( ( microtime( true ) - $time ) * 1000 );
|
|
if ( $this->statsdDataFactory ) {
|
|
$this->statsdDataFactory->timing( 'diff_time', $time );
|
|
}
|
|
|
|
return $result;
|
|
};
|
|
|
|
/**
|
|
* @param Status $status
|
|
* @throws FatalError
|
|
* @return never
|
|
*/
|
|
$error = static function ( $status ) {
|
|
throw new FatalError( $status->getWikiText() );
|
|
};
|
|
|
|
// Use PoolCounter if the diff looks like it can be expensive
|
|
if ( strlen( $oldText ) + strlen( $newText ) > 20000 ) {
|
|
$work = new PoolCounterWorkViaCallback( 'diff',
|
|
md5( $oldText ) . md5( $newText ),
|
|
[ 'doWork' => $diff, 'error' => $error ]
|
|
);
|
|
return $work->execute();
|
|
}
|
|
|
|
return $diff();
|
|
}
|
|
|
|
/**
|
|
* Diff the text representations of two content objects (or just two pieces of text in general).
|
|
* This does the actual diffing, getTextDiff() wraps it with logging and resource limiting.
|
|
* @param string $oldText
|
|
* @param string $newText
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
protected function getTextDiffInternal( $oldText, $newText ) {
|
|
$oldText = str_replace( "\r\n", "\n", $oldText );
|
|
$newText = str_replace( "\r\n", "\n", $newText );
|
|
|
|
if ( $oldText === $newText ) {
|
|
return '';
|
|
}
|
|
|
|
$textDiffer = $this->getTextDiffer();
|
|
$diffText = $textDiffer->render( $oldText, $newText, $this->format );
|
|
return $textDiffer->addRowWrapper( $this->format, $diffText );
|
|
}
|
|
|
|
}
|