Changes to the use statements done automatically via script Addition of missing use statement done manually Change-Id: I73fb416573f5af600e529d224b5beb5d2e3d27d3
367 lines
10 KiB
PHP
367 lines
10 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\Content\Content;
|
|
use MediaWiki\Content\TextContent;
|
|
use MediaWiki\Context\IContextSource;
|
|
use MediaWiki\Context\RequestContext;
|
|
use MediaWiki\Diff\TextDiffer\ManifoldTextDiffer;
|
|
use MediaWiki\Diff\TextDiffer\TextDiffer;
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
use MediaWiki\HookContainer\HookRunner;
|
|
use MediaWiki\Html\Html;
|
|
use MediaWiki\Language\Language;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\PoolCounter\PoolCounterWorkViaCallback;
|
|
use MediaWiki\Status\Status;
|
|
use MediaWiki\Title\Title;
|
|
use OOUI\ToggleSwitchWidget;
|
|
use Wikimedia\Stats\IBufferingStatsdDataFactory;
|
|
use Wikimedia\Stats\StatsFactory;
|
|
|
|
/**
|
|
* 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 StatsFactory|null */
|
|
private $statsFactory;
|
|
|
|
/** @var HookRunner|null */
|
|
private $hookRunner;
|
|
|
|
/** @var string|null */
|
|
private $format;
|
|
|
|
/** @var string */
|
|
private $contentModel;
|
|
|
|
/** @var TextDiffer|null */
|
|
private $textDiffer;
|
|
|
|
/** @var bool */
|
|
private $inlineToggleEnabled = false;
|
|
|
|
/** @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 );
|
|
}
|
|
|
|
/**
|
|
* This has no effect since MW 1.43.
|
|
*
|
|
* @internal Use ContentHandler::createTextSlotDiffRenderer instead
|
|
* @param IBufferingStatsdDataFactory $statsdDataFactory
|
|
*/
|
|
public function setStatsdDataFactory( IBufferingStatsdDataFactory $statsdDataFactory ) {
|
|
wfDeprecated( __METHOD__, '1.43' );
|
|
}
|
|
|
|
/**
|
|
* @internal Use ContentHandler::createTextSlotDiffRenderer instead
|
|
* @param StatsFactory $statsFactory
|
|
*/
|
|
public function setStatsFactory( StatsFactory $statsFactory ) {
|
|
$this->statsFactory = $statsFactory;
|
|
}
|
|
|
|
/**
|
|
* 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' );
|
|
}
|
|
|
|
/**
|
|
* @internal Use ContentHandler::createTextSlotDiffRenderer instead
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* Set a flag indicating whether the inline toggle switch is shown.
|
|
*
|
|
* @since 1.41
|
|
* @param bool $enabled
|
|
*/
|
|
public function setInlineToggleEnabled( $enabled = true ) {
|
|
$this->inlineToggleEnabled = $enabled;
|
|
}
|
|
|
|
/**
|
|
* 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 );
|
|
}
|
|
|
|
public function localizeDiff( $diff, $options = [] ) {
|
|
return $this->textDiffer->localize( $this->format, $diff, $options );
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function getTablePrefix( IContextSource $context, Title $newTitle ): array {
|
|
$parts = $this->getTextDiffer()->getTablePrefixes( $this->format );
|
|
|
|
$showDiffToggleSwitch = $this->inlineToggleEnabled && $this->getTextDiffer()->hasFormat( 'inline' );
|
|
// If we support the inline type, add a toggle switch
|
|
if ( $showDiffToggleSwitch ) {
|
|
$values = $context->getRequest()->getValues();
|
|
$isInlineDiffType = $this->format === 'inline';
|
|
$values[ 'diff-type' ] = $isInlineDiffType ? 'table' : 'inline';
|
|
unset( $values[ 'title' ] );
|
|
$parts[self::INLINE_SWITCHER_KEY] = Html::rawElement( 'div',
|
|
[ 'class' => 'mw-diffPage-inlineToggle-container' ],
|
|
new OOUI\FieldLayout(
|
|
new ToggleSwitchWidget( [
|
|
'id' => 'mw-diffPage-inline-toggle-switch',
|
|
'href' => $newTitle->getLocalURL( $values ),
|
|
'value' => $isInlineDiffType,
|
|
'title' => $context->msg( 'diff-inline-switch-desc' )->plain()
|
|
] ),
|
|
[
|
|
'id' => 'mw-diffPage-inline-toggle-switch-layout',
|
|
'label' => $context->msg( 'diff-inline-format-label' )->plain(),
|
|
'infusable' => true,
|
|
'title' => $context->msg( 'diff-inline-switch-desc' )->plain()
|
|
]
|
|
),
|
|
);
|
|
}
|
|
// 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->statsFactory ) {
|
|
$this->statsFactory->getTiming( 'diff_text_seconds' )
|
|
->copyToStatsdAt( 'diff_time' )
|
|
->observe( $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 );
|
|
}
|
|
|
|
}
|