2020-12-03 17:53:55 +00:00
|
|
|
<?php
|
|
|
|
|
|
2023-01-13 21:30:21 +00:00
|
|
|
namespace MediaWiki\Tests\Rest\Handler\Helper;
|
2020-12-03 17:53:55 +00:00
|
|
|
|
|
|
|
|
use Exception;
|
2024-05-17 21:55:30 +00:00
|
|
|
use MediaWiki\Content\CssContent;
|
2022-10-29 18:52:23 +00:00
|
|
|
use MediaWiki\Content\IContentHandlerFactory;
|
2024-08-06 13:40:20 +00:00
|
|
|
use MediaWiki\Content\WikitextContent;
|
2023-11-21 21:08:14 +00:00
|
|
|
use MediaWiki\Deferred\DeferredUpdates;
|
2023-11-06 15:22:44 +00:00
|
|
|
use MediaWiki\Edit\ParsoidRenderID;
|
2022-06-13 09:31:50 +00:00
|
|
|
use MediaWiki\Edit\SimpleParsoidOutputStash;
|
2022-12-12 21:04:31 +00:00
|
|
|
use MediaWiki\Hook\ParserLogLinterDataHook;
|
2023-08-29 20:13:43 +00:00
|
|
|
use MediaWiki\Logger\Spi as LoggerSpi;
|
2022-07-06 14:05:52 +00:00
|
|
|
use MediaWiki\MainConfigNames;
|
2022-09-21 17:12:39 +00:00
|
|
|
use MediaWiki\Page\PageIdentity;
|
2022-10-07 14:27:01 +00:00
|
|
|
use MediaWiki\Page\PageIdentityValue;
|
2022-06-28 09:49:36 +00:00
|
|
|
use MediaWiki\Page\PageRecord;
|
2024-05-16 20:42:16 +00:00
|
|
|
use MediaWiki\Page\PageReference;
|
2023-08-29 20:13:43 +00:00
|
|
|
use MediaWiki\Page\ParserOutputAccess;
|
2022-11-24 19:58:56 +00:00
|
|
|
use MediaWiki\Parser\ParserCacheFactory;
|
2023-12-14 19:20:33 +00:00
|
|
|
use MediaWiki\Parser\ParserOutput;
|
2024-02-26 18:27:49 +00:00
|
|
|
use MediaWiki\Parser\Parsoid\HtmlTransformFactory;
|
|
|
|
|
use MediaWiki\Parser\Parsoid\LanguageVariantConverter;
|
2022-10-07 14:27:01 +00:00
|
|
|
use MediaWiki\Parser\Parsoid\PageBundleParserOutputConverter;
|
2023-08-29 20:13:43 +00:00
|
|
|
use MediaWiki\Parser\Parsoid\ParsoidParser;
|
|
|
|
|
use MediaWiki\Parser\Parsoid\ParsoidParserFactory;
|
2022-11-24 19:58:56 +00:00
|
|
|
use MediaWiki\Parser\RevisionOutputCache;
|
2023-11-09 13:32:23 +00:00
|
|
|
use MediaWiki\Permissions\Authority;
|
2023-11-01 19:10:00 +00:00
|
|
|
use MediaWiki\Permissions\PermissionStatus;
|
2023-01-13 21:30:21 +00:00
|
|
|
use MediaWiki\Rest\Handler\Helper\HtmlOutputRendererHelper;
|
2020-12-03 17:53:55 +00:00
|
|
|
use MediaWiki\Rest\LocalizedHttpException;
|
2022-11-25 10:00:18 +00:00
|
|
|
use MediaWiki\Rest\Response;
|
2022-10-10 14:46:54 +00:00
|
|
|
use MediaWiki\Rest\ResponseInterface;
|
2022-08-30 10:26:39 +00:00
|
|
|
use MediaWiki\Revision\MutableRevisionRecord;
|
2022-12-14 17:33:58 +00:00
|
|
|
use MediaWiki\Revision\RevisionAccessException;
|
2022-06-28 09:49:36 +00:00
|
|
|
use MediaWiki\Revision\RevisionRecord;
|
2022-08-30 10:26:39 +00:00
|
|
|
use MediaWiki\Revision\SlotRecord;
|
2023-08-25 12:29:41 +00:00
|
|
|
use MediaWiki\Status\Status;
|
2023-08-19 03:35:06 +00:00
|
|
|
use MediaWiki\Utils\MWTimestamp;
|
2020-12-03 17:53:55 +00:00
|
|
|
use MediaWikiIntegrationTestCase;
|
|
|
|
|
use NullStatsdDataFactory;
|
2022-11-24 19:58:56 +00:00
|
|
|
use ParserCache;
|
2022-06-28 09:49:36 +00:00
|
|
|
use ParserOptions;
|
2020-12-03 17:53:55 +00:00
|
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
2022-06-28 09:49:36 +00:00
|
|
|
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
|
2024-02-16 16:49:05 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
2023-08-29 20:13:43 +00:00
|
|
|
use Psr\Log\NullLogger;
|
2024-02-26 18:27:49 +00:00
|
|
|
use Wikimedia\Bcp47Code\Bcp47Code;
|
Use Bcp47Code when interfacing with Parsoid
It is very easy for developers and maintainers to mix up "internal
MediaWiki language codes" and "BCP-47 language codes"; the latter are
standards-compliant and used in web protocols like HTTP, HTML, and
SVG; but much of WMF production is very dependent on historical codes
used by MediaWiki which in some cases predate the IANA standardized
name for the language in question.
Phan and other static checking tools aren't much help distinguishing
BCP-47 from internal codes when both are represented with the PHP
string type, so the wikimedia/bcp-47-code package introduced a very
lightweight wrapper type in order to uniquely identify BCP-47 codes.
Language implements Bcp47Code, and LanguageFactory::getLanguage() is
an easy way to convert (or downcast) between Bcp47Code and Language
objects.
This patch updates the Parsoid integration code and the associated
REST handlers to use Bcp47Code in APIs so that the standalone Parsoid
library does not need to know anything about MediaWiki-internal codes.
The principle has been, first, to try to convert a string to a
Bcp47Code as soon as possible and as close to the original input as
possible, so it is easy to see *why* a given string is a BCP-47 code
(usually, because it is coming from HTTP/HTML/etc) and we're not stuck
deep inside some method trying to figure out where a string we're
given is coming from and therefore what sort of string code it might
be. Second, we've added explicit compatibility code to accept
MediaWiki internal codes and convert them to Bcp47Code for backward
compatibility with existing clients, using the @internal
LanguageCode::normalizeNonstandardCodeAndWarn() method. The intention
is to gradually remove these backward compatibility thunks and replace
them with HTTP 400 errors or wfDeprecated messages in order to
identify and repair callers who are incorrectly using
non-standard-compliant language codes in web standards
(HTTP/HTML/SVG/etc).
Finally, maintaining a code as a Bcp47Code and not immediately
converting to Language helps us delay or even avoid full loading of a
Language object in some cases, which is another reason to occasionally
push Bcp47Code (instead of Language) down the call stack.
Bug: T327379
Depends-On: I830867d58f8962d6a57be16ce3735e8384f9ac1c
Change-Id: I982e0df706a633b05dcc02b5220b737c19adc401
2022-11-04 17:29:23 +00:00
|
|
|
use Wikimedia\Bcp47Code\Bcp47CodeValue;
|
2020-12-03 17:53:55 +00:00
|
|
|
use Wikimedia\Message\MessageValue;
|
2024-07-09 13:37:44 +00:00
|
|
|
use Wikimedia\ObjectCache\EmptyBagOStuff;
|
|
|
|
|
use Wikimedia\ObjectCache\HashBagOStuff;
|
2020-12-03 17:53:55 +00:00
|
|
|
use Wikimedia\Parsoid\Core\ClientError;
|
2022-11-24 19:58:56 +00:00
|
|
|
use Wikimedia\Parsoid\Core\PageBundle;
|
2020-12-03 17:53:55 +00:00
|
|
|
use Wikimedia\Parsoid\Core\ResourceLimitExceededException;
|
2022-10-05 18:47:44 +00:00
|
|
|
use Wikimedia\Parsoid\Parsoid;
|
2022-10-07 14:27:01 +00:00
|
|
|
use Wikimedia\TestingAccessWrapper;
|
2020-12-03 17:53:55 +00:00
|
|
|
|
|
|
|
|
/**
|
2023-01-13 21:30:21 +00:00
|
|
|
* @covers \MediaWiki\Rest\Handler\Helper\HtmlOutputRendererHelper
|
2020-12-03 17:53:55 +00:00
|
|
|
* @group Database
|
|
|
|
|
*/
|
2022-09-06 09:07:45 +00:00
|
|
|
class HtmlOutputRendererHelperTest extends MediaWikiIntegrationTestCase {
|
2020-12-15 22:12:49 +00:00
|
|
|
private const CACHE_EPOCH = '20001111010101';
|
|
|
|
|
|
|
|
|
|
private const TIMESTAMP_OLD = '20200101112233';
|
|
|
|
|
private const TIMESTAMP = '20200101223344';
|
|
|
|
|
private const TIMESTAMP_LATER = '20200101234200';
|
|
|
|
|
|
|
|
|
|
private const WIKITEXT_OLD = 'Hello \'\'\'Goat\'\'\'';
|
2020-12-03 17:53:55 +00:00
|
|
|
private const WIKITEXT = 'Hello \'\'\'World\'\'\'';
|
|
|
|
|
|
2022-05-16 12:43:23 +00:00
|
|
|
private const HTML_OLD = '>Goat<';
|
|
|
|
|
private const HTML = '>World<';
|
2020-12-03 17:53:55 +00:00
|
|
|
|
2022-05-24 21:13:42 +00:00
|
|
|
private const PARAM_DEFAULTS = [
|
|
|
|
|
'stash' => false,
|
2022-10-05 18:47:44 +00:00
|
|
|
'flavor' => 'view',
|
2022-05-24 21:13:42 +00:00
|
|
|
];
|
|
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
private const MOCK_HTML = 'mocked HTML';
|
2022-10-10 14:46:54 +00:00
|
|
|
private const MOCK_HTML_VARIANT = 'ockedmay HTML';
|
2022-06-28 09:49:36 +00:00
|
|
|
|
|
|
|
|
private function exactlyOrAny( ?int $count ): InvocationOrder {
|
|
|
|
|
return $count === null ? $this->any() : $this->exactly( $count );
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 20:13:43 +00:00
|
|
|
/**
|
|
|
|
|
* @param LoggerInterface|null $logger
|
|
|
|
|
*
|
|
|
|
|
* @return LoggerSpi
|
|
|
|
|
*/
|
|
|
|
|
private function getLoggerSpi( $logger = null ) {
|
|
|
|
|
$logger = $logger ?: new NullLogger();
|
|
|
|
|
$spi = $this->createNoOpMock( LoggerSpi::class, [ 'getLogger' ] );
|
|
|
|
|
$spi->method( 'getLogger' )->willReturn( $logger );
|
|
|
|
|
return $spi;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-15 16:57:56 +00:00
|
|
|
/**
|
|
|
|
|
* @return MockObject|ParserOutputAccess
|
|
|
|
|
*/
|
|
|
|
|
public function newMockParserOutputAccess( ?string $expectedHtml ): ParserOutputAccess {
|
|
|
|
|
$expectedCalls = [
|
|
|
|
|
'getParserOutput' => null,
|
|
|
|
|
];
|
|
|
|
|
$access = $this->createNoOpMock( ParserOutputAccess::class, array_keys( $expectedCalls ) );
|
|
|
|
|
$access->expects( $this->exactlyOrAny( $expectedCalls[ 'getParserOutput' ] ) )
|
|
|
|
|
->method( 'getParserOutput' )
|
|
|
|
|
->willReturnCallback( function (
|
|
|
|
|
PageRecord $page,
|
|
|
|
|
ParserOptions $parserOpts,
|
|
|
|
|
?RevisionRecord $rev = null,
|
|
|
|
|
int $options = 0
|
|
|
|
|
) use ( $expectedHtml ) {
|
|
|
|
|
// Note that HtmlOutputRendererHelper only passes
|
|
|
|
|
// non-null RevisionRecords here, so getMockHtml() will
|
|
|
|
|
// always return <p>-wrapped main slot content.
|
|
|
|
|
$pout = $this->makeParserOutput(
|
|
|
|
|
$parserOpts,
|
|
|
|
|
$expectedHtml ?? $this->getMockHtml( $rev ),
|
|
|
|
|
$rev,
|
|
|
|
|
$page
|
|
|
|
|
); // will use fake time
|
|
|
|
|
return Status::newGood( $pout );
|
|
|
|
|
} );
|
|
|
|
|
return $access;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-20 14:01:39 +00:00
|
|
|
private function getMockHtml( $rev ) {
|
2022-10-07 14:27:01 +00:00
|
|
|
if ( $rev instanceof RevisionRecord ) {
|
|
|
|
|
$html = '<p>' . $rev->getContent( SlotRecord::MAIN )->getText() . '</p>';
|
|
|
|
|
} elseif ( is_int( $rev ) ) {
|
|
|
|
|
$html = '<p>rev:' . $rev . '</p>';
|
|
|
|
|
} else {
|
|
|
|
|
$html = self::MOCK_HTML;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param ParserOptions $parserOpts
|
|
|
|
|
* @param string $html
|
|
|
|
|
* @param RevisionRecord|int|null $rev
|
|
|
|
|
* @param PageIdentity $page
|
2023-06-16 19:48:03 +00:00
|
|
|
* @param string|null $version
|
2022-10-07 14:27:01 +00:00
|
|
|
*
|
|
|
|
|
* @return ParserOutput
|
|
|
|
|
*/
|
|
|
|
|
private function makeParserOutput(
|
|
|
|
|
ParserOptions $parserOpts,
|
|
|
|
|
string $html,
|
|
|
|
|
$rev,
|
2023-05-29 19:18:38 +00:00
|
|
|
PageIdentity $page,
|
2023-06-16 19:48:03 +00:00
|
|
|
string $version = null
|
2022-10-07 14:27:01 +00:00
|
|
|
): ParserOutput {
|
Add ParserOutput::{get,set}RenderId() and set render id in ContentRenderer
Set the render ID for each parse stored into cache so that we are able
to identify a specific parse when there are dependencies (for example
in an edit based on that parse). This is recorded as a property added
to the ParserOutput, not the parent CacheTime interface. Even though
the render ID is /related/ to the CacheTime interface, CacheTime is
also used directly as a parser cache key, and the UUID should not be
part of the lookup key.
In general we are trying to move the location where these cache
properties are set as early as possible, so we check at each location
to ensure we don't overwrite a previously-set value. Eventually we
can convert most of these checks into assertions that the cache
properties have already been set (T350538). The primary location for
setting cache properties is the ContentRenderer.
Moved setting the revision timestamp into ContentRenderer as well, as
it was set along the same code paths. An extra parameter was added to
ContentRenderer::getParserOutput() to support this.
Added merge code to ParserOutput::mergeInternalMetaDataFrom() which
should ensure that cache time, revision, timestamp, and render id are
all set properly when multiple slots are combined together in MCR.
In order to ensure the render ID is set on all codepaths we needed to
plumb the GlobalIdGenerator service into ContentRenderer, ParserCache,
ParserCacheFactory, and RevisionOutputCache. Eventually (T350538) it
should only be necessary in the ContentRenderer.
Bug: T350538
Bug: T349868
Followup-To: Ic9b7cc0fcf365e772b7d080d76a065e3fd585f80
Change-Id: I72c5e6f86b7f081ab5ce7a56f5365d2f75067a78
2023-09-14 16:11:20 +00:00
|
|
|
static $counter = 0;
|
2022-10-07 14:27:01 +00:00
|
|
|
$lang = $parserOpts->getTargetLanguage();
|
|
|
|
|
$lang = $lang ? $lang->getCode() : 'en';
|
2023-06-16 19:48:03 +00:00
|
|
|
$version ??= Parsoid::defaultHTMLVersion();
|
2022-10-07 14:27:01 +00:00
|
|
|
|
2022-11-25 10:00:18 +00:00
|
|
|
$html = "<!DOCTYPE html><html lang=\"$lang\"><body><div id='t3s7'>$html</div></body></html>";
|
2022-10-07 14:27:01 +00:00
|
|
|
|
Add ParserOutput::{get,set}RenderId() and set render id in ContentRenderer
Set the render ID for each parse stored into cache so that we are able
to identify a specific parse when there are dependencies (for example
in an edit based on that parse). This is recorded as a property added
to the ParserOutput, not the parent CacheTime interface. Even though
the render ID is /related/ to the CacheTime interface, CacheTime is
also used directly as a parser cache key, and the UUID should not be
part of the lookup key.
In general we are trying to move the location where these cache
properties are set as early as possible, so we check at each location
to ensure we don't overwrite a previously-set value. Eventually we
can convert most of these checks into assertions that the cache
properties have already been set (T350538). The primary location for
setting cache properties is the ContentRenderer.
Moved setting the revision timestamp into ContentRenderer as well, as
it was set along the same code paths. An extra parameter was added to
ContentRenderer::getParserOutput() to support this.
Added merge code to ParserOutput::mergeInternalMetaDataFrom() which
should ensure that cache time, revision, timestamp, and render id are
all set properly when multiple slots are combined together in MCR.
In order to ensure the render ID is set on all codepaths we needed to
plumb the GlobalIdGenerator service into ContentRenderer, ParserCache,
ParserCacheFactory, and RevisionOutputCache. Eventually (T350538) it
should only be necessary in the ContentRenderer.
Bug: T350538
Bug: T349868
Followup-To: Ic9b7cc0fcf365e772b7d080d76a065e3fd585f80
Change-Id: I72c5e6f86b7f081ab5ce7a56f5365d2f75067a78
2023-09-14 16:11:20 +00:00
|
|
|
$revTimestamp = null;
|
2022-10-07 14:27:01 +00:00
|
|
|
if ( $rev instanceof RevisionRecord ) {
|
Add ParserOutput::{get,set}RenderId() and set render id in ContentRenderer
Set the render ID for each parse stored into cache so that we are able
to identify a specific parse when there are dependencies (for example
in an edit based on that parse). This is recorded as a property added
to the ParserOutput, not the parent CacheTime interface. Even though
the render ID is /related/ to the CacheTime interface, CacheTime is
also used directly as a parser cache key, and the UUID should not be
part of the lookup key.
In general we are trying to move the location where these cache
properties are set as early as possible, so we check at each location
to ensure we don't overwrite a previously-set value. Eventually we
can convert most of these checks into assertions that the cache
properties have already been set (T350538). The primary location for
setting cache properties is the ContentRenderer.
Moved setting the revision timestamp into ContentRenderer as well, as
it was set along the same code paths. An extra parameter was added to
ContentRenderer::getParserOutput() to support this.
Added merge code to ParserOutput::mergeInternalMetaDataFrom() which
should ensure that cache time, revision, timestamp, and render id are
all set properly when multiple slots are combined together in MCR.
In order to ensure the render ID is set on all codepaths we needed to
plumb the GlobalIdGenerator service into ContentRenderer, ParserCache,
ParserCacheFactory, and RevisionOutputCache. Eventually (T350538) it
should only be necessary in the ContentRenderer.
Bug: T350538
Bug: T349868
Followup-To: Ic9b7cc0fcf365e772b7d080d76a065e3fd585f80
Change-Id: I72c5e6f86b7f081ab5ce7a56f5365d2f75067a78
2023-09-14 16:11:20 +00:00
|
|
|
$revTimestamp = $rev->getTimestamp();
|
2024-02-08 21:07:04 +00:00
|
|
|
$rev = $rev->getId() ?? 0;
|
2022-10-07 14:27:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$pout = new ParserOutput( $html );
|
2024-02-08 21:07:04 +00:00
|
|
|
$pout->setCacheRevisionId( $rev ?? $page->getLatest() );
|
2022-10-07 14:27:01 +00:00
|
|
|
$pout->setCacheTime( wfTimestampNow() ); // will use fake time
|
Add ParserOutput::{get,set}RenderId() and set render id in ContentRenderer
Set the render ID for each parse stored into cache so that we are able
to identify a specific parse when there are dependencies (for example
in an edit based on that parse). This is recorded as a property added
to the ParserOutput, not the parent CacheTime interface. Even though
the render ID is /related/ to the CacheTime interface, CacheTime is
also used directly as a parser cache key, and the UUID should not be
part of the lookup key.
In general we are trying to move the location where these cache
properties are set as early as possible, so we check at each location
to ensure we don't overwrite a previously-set value. Eventually we
can convert most of these checks into assertions that the cache
properties have already been set (T350538). The primary location for
setting cache properties is the ContentRenderer.
Moved setting the revision timestamp into ContentRenderer as well, as
it was set along the same code paths. An extra parameter was added to
ContentRenderer::getParserOutput() to support this.
Added merge code to ParserOutput::mergeInternalMetaDataFrom() which
should ensure that cache time, revision, timestamp, and render id are
all set properly when multiple slots are combined together in MCR.
In order to ensure the render ID is set on all codepaths we needed to
plumb the GlobalIdGenerator service into ContentRenderer, ParserCache,
ParserCacheFactory, and RevisionOutputCache. Eventually (T350538) it
should only be necessary in the ContentRenderer.
Bug: T350538
Bug: T349868
Followup-To: Ic9b7cc0fcf365e772b7d080d76a065e3fd585f80
Change-Id: I72c5e6f86b7f081ab5ce7a56f5365d2f75067a78
2023-09-14 16:11:20 +00:00
|
|
|
if ( $revTimestamp ) {
|
2023-11-06 21:25:07 +00:00
|
|
|
$pout->setRevisionTimestamp( $revTimestamp );
|
Add ParserOutput::{get,set}RenderId() and set render id in ContentRenderer
Set the render ID for each parse stored into cache so that we are able
to identify a specific parse when there are dependencies (for example
in an edit based on that parse). This is recorded as a property added
to the ParserOutput, not the parent CacheTime interface. Even though
the render ID is /related/ to the CacheTime interface, CacheTime is
also used directly as a parser cache key, and the UUID should not be
part of the lookup key.
In general we are trying to move the location where these cache
properties are set as early as possible, so we check at each location
to ensure we don't overwrite a previously-set value. Eventually we
can convert most of these checks into assertions that the cache
properties have already been set (T350538). The primary location for
setting cache properties is the ContentRenderer.
Moved setting the revision timestamp into ContentRenderer as well, as
it was set along the same code paths. An extra parameter was added to
ContentRenderer::getParserOutput() to support this.
Added merge code to ParserOutput::mergeInternalMetaDataFrom() which
should ensure that cache time, revision, timestamp, and render id are
all set properly when multiple slots are combined together in MCR.
In order to ensure the render ID is set on all codepaths we needed to
plumb the GlobalIdGenerator service into ContentRenderer, ParserCache,
ParserCacheFactory, and RevisionOutputCache. Eventually (T350538) it
should only be necessary in the ContentRenderer.
Bug: T350538
Bug: T349868
Followup-To: Ic9b7cc0fcf365e772b7d080d76a065e3fd585f80
Change-Id: I72c5e6f86b7f081ab5ce7a56f5365d2f75067a78
2023-09-14 16:11:20 +00:00
|
|
|
}
|
|
|
|
|
// We test that UUIDs are unique, so make a cheap unique UUID
|
|
|
|
|
$pout->setRenderId( 'bogus-uuid-' . strval( $counter++ ) );
|
2022-10-07 14:27:01 +00:00
|
|
|
$pout->setExtensionData( PageBundleParserOutputConverter::PARSOID_PAGE_BUNDLE_KEY, [
|
2022-11-25 10:00:18 +00:00
|
|
|
'parsoid' => [ 'ids' => [
|
|
|
|
|
't3s7' => [ 'dsr' => [ 0, 0, 0, 0 ] ],
|
|
|
|
|
] ],
|
2022-10-07 14:27:01 +00:00
|
|
|
'mw' => [ 'ids' => [] ],
|
2023-05-29 19:18:38 +00:00
|
|
|
'version' => $version,
|
2022-10-07 14:27:01 +00:00
|
|
|
'headers' => [
|
|
|
|
|
'content-language' => $lang
|
|
|
|
|
]
|
|
|
|
|
] );
|
|
|
|
|
|
2024-03-04 20:17:20 +00:00
|
|
|
$pout->setLanguage( new Bcp47CodeValue( $lang ) );
|
2022-10-07 14:27:01 +00:00
|
|
|
return $pout;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-03 17:53:55 +00:00
|
|
|
protected function setUp(): void {
|
|
|
|
|
parent::setUp();
|
|
|
|
|
|
2022-07-06 14:05:52 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::CacheEpoch, self::CACHE_EPOCH );
|
2020-12-03 17:53:55 +00:00
|
|
|
}
|
|
|
|
|
|
2022-06-13 09:31:50 +00:00
|
|
|
/**
|
2023-11-09 13:32:23 +00:00
|
|
|
* @return MockObject|Authority
|
2022-06-13 09:31:50 +00:00
|
|
|
*/
|
2023-11-01 19:10:00 +00:00
|
|
|
private function newAuthority(): MockObject {
|
|
|
|
|
$authority = $this->createNoOpMock( Authority::class, [ 'authorizeWrite' ] );
|
|
|
|
|
$authority->method( 'authorizeWrite' )->willReturn( true );
|
|
|
|
|
return $authority;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return MockObject|Authority
|
|
|
|
|
*/
|
|
|
|
|
private function newAuthorityWhoCantStash(): MockObject {
|
|
|
|
|
$authority = $this->createNoOpMock( Authority::class, [ 'authorizeWrite' ] );
|
|
|
|
|
$authority->method( 'authorizeWrite' )->willReturnCallback(
|
|
|
|
|
static function ( $action, $target, PermissionStatus $status ) {
|
|
|
|
|
if ( $action === 'stashbasehtml' ) {
|
|
|
|
|
$status->setRateLimitExceeded();
|
|
|
|
|
$status->setPermission( $action );
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
);
|
2023-11-09 13:32:23 +00:00
|
|
|
return $authority;
|
2022-06-13 09:31:50 +00:00
|
|
|
}
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
private function newHelper(
|
|
|
|
|
array $options,
|
|
|
|
|
PageIdentity $page,
|
|
|
|
|
array $parameters = [],
|
|
|
|
|
?Authority $authority = null,
|
|
|
|
|
$revision = null,
|
|
|
|
|
bool $lenientRevHandling = false
|
|
|
|
|
): HtmlOutputRendererHelper {
|
2022-10-29 18:52:23 +00:00
|
|
|
$chFactory = $this->getServiceContainer()->getContentHandlerFactory();
|
2024-02-26 18:27:49 +00:00
|
|
|
$cache = $options['cache'] ?? new EmptyBagOStuff();
|
2022-10-29 18:52:23 +00:00
|
|
|
$stash = new SimpleParsoidOutputStash( $chFactory, $cache, 1 );
|
2022-06-13 09:31:50 +00:00
|
|
|
|
2022-11-24 21:54:11 +00:00
|
|
|
$services = $this->getServiceContainer();
|
|
|
|
|
|
2024-02-08 21:07:04 +00:00
|
|
|
if ( isset( $options['ParsoidParserFactory'] ) ) {
|
|
|
|
|
$this->resetServicesWithMockedParsoidParserFactory( $options['ParsoidParserFactory'] );
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
return new HtmlOutputRendererHelper(
|
2022-06-13 09:31:50 +00:00
|
|
|
$stash,
|
2022-06-13 15:48:44 +00:00
|
|
|
new NullStatsdDataFactory(),
|
2024-05-15 16:57:56 +00:00
|
|
|
$options['ParserOutputAccess'] ?? $this->newMockParserOutputAccess(
|
|
|
|
|
$options['expectedHtml'] ?? null
|
|
|
|
|
),
|
|
|
|
|
$services->getPageStore(),
|
|
|
|
|
$services->getRevisionLookup(),
|
2024-02-08 21:07:04 +00:00
|
|
|
$services->getRevisionRenderer(),
|
2024-05-15 16:57:56 +00:00
|
|
|
$services->getParsoidSiteConfig(),
|
2024-02-26 18:27:49 +00:00
|
|
|
$options['HtmlTransformFactory'] ?? $services->getHtmlTransformFactory(),
|
2022-11-24 21:54:11 +00:00
|
|
|
$services->getContentHandlerFactory(),
|
2024-06-05 15:49:49 +00:00
|
|
|
$services->getLanguageFactory(),
|
|
|
|
|
$page, $parameters, $authority, $revision, $lenientRevHandling
|
2020-12-03 17:53:55 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-15 22:12:49 +00:00
|
|
|
private function getExistingPageWithRevisions( $name ) {
|
|
|
|
|
$page = $this->getNonexistingTestPage( $name );
|
|
|
|
|
|
|
|
|
|
MWTimestamp::setFakeTime( self::TIMESTAMP_OLD );
|
|
|
|
|
$this->editPage( $page, self::WIKITEXT_OLD );
|
|
|
|
|
$revisions['first'] = $page->getRevisionRecord();
|
|
|
|
|
|
|
|
|
|
MWTimestamp::setFakeTime( self::TIMESTAMP );
|
|
|
|
|
$this->editPage( $page, self::WIKITEXT );
|
|
|
|
|
$revisions['latest'] = $page->getRevisionRecord();
|
|
|
|
|
|
|
|
|
|
MWTimestamp::setFakeTime( self::TIMESTAMP_LATER );
|
|
|
|
|
return [ $page, $revisions ];
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-30 10:26:39 +00:00
|
|
|
private function getNonExistingPageWithFakeRevision( $name ) {
|
|
|
|
|
$page = $this->getNonexistingTestPage( $name );
|
|
|
|
|
MWTimestamp::setFakeTime( self::TIMESTAMP_OLD );
|
|
|
|
|
|
2022-09-01 10:03:03 +00:00
|
|
|
$content = new WikitextContent( self::WIKITEXT_OLD );
|
2022-08-30 10:26:39 +00:00
|
|
|
$rev = new MutableRevisionRecord( $page->getTitle() );
|
|
|
|
|
$rev->setPageId( $page->getId() );
|
|
|
|
|
$rev->setContent( SlotRecord::MAIN, $content );
|
|
|
|
|
|
|
|
|
|
return [ $page, $rev ];
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideRevisionReferences() {
|
2020-12-15 22:12:49 +00:00
|
|
|
return [
|
|
|
|
|
'current' => [ null, [ 'html' => self::HTML, 'timestamp' => self::TIMESTAMP ] ],
|
|
|
|
|
'old' => [ 'first', [ 'html' => self::HTML_OLD, 'timestamp' => self::TIMESTAMP_OLD ] ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideRevisionReferences()
|
|
|
|
|
*/
|
2022-08-30 10:26:39 +00:00
|
|
|
public function testGetHtml( $revRef ) {
|
2020-12-15 22:12:49 +00:00
|
|
|
[ $page, $revisions ] = $this->getExistingPageWithRevisions( __METHOD__ );
|
2022-10-07 14:27:01 +00:00
|
|
|
|
|
|
|
|
// Test with just the revision ID, not the object! We do that elsewhere.
|
2022-12-04 21:35:20 +00:00
|
|
|
$revId = $revRef ? $revisions[ $revRef ]->getId() : null;
|
2020-12-03 17:53:55 +00:00
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [ 'expectedHtml' => $this->getMockHtml( $revId ) ], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-10-07 14:27:01 +00:00
|
|
|
|
2022-12-04 21:35:20 +00:00
|
|
|
if ( $revId ) {
|
|
|
|
|
$helper->setRevision( $revId );
|
|
|
|
|
$this->assertSame( $revId, $helper->getRevisionId() );
|
|
|
|
|
} else {
|
|
|
|
|
// current revision
|
|
|
|
|
$this->assertSame( 0, $helper->getRevisionId() );
|
2022-10-07 14:27:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$htmlresult = $helper->getHtml()->getRawText();
|
|
|
|
|
|
2022-12-04 21:35:20 +00:00
|
|
|
$this->assertStringContainsString( $this->getMockHtml( $revId ), $htmlresult );
|
2022-10-07 14:27:01 +00:00
|
|
|
}
|
|
|
|
|
|
2022-10-10 14:46:54 +00:00
|
|
|
public function testGetHtmlWithVariant() {
|
2023-07-27 02:36:17 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::UsePigLatinVariant, true );
|
2022-10-10 14:46:54 +00:00
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [ 'expectedHtml' => self::MOCK_HTML ], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
Use Bcp47Code when interfacing with Parsoid
It is very easy for developers and maintainers to mix up "internal
MediaWiki language codes" and "BCP-47 language codes"; the latter are
standards-compliant and used in web protocols like HTTP, HTML, and
SVG; but much of WMF production is very dependent on historical codes
used by MediaWiki which in some cases predate the IANA standardized
name for the language in question.
Phan and other static checking tools aren't much help distinguishing
BCP-47 from internal codes when both are represented with the PHP
string type, so the wikimedia/bcp-47-code package introduced a very
lightweight wrapper type in order to uniquely identify BCP-47 codes.
Language implements Bcp47Code, and LanguageFactory::getLanguage() is
an easy way to convert (or downcast) between Bcp47Code and Language
objects.
This patch updates the Parsoid integration code and the associated
REST handlers to use Bcp47Code in APIs so that the standalone Parsoid
library does not need to know anything about MediaWiki-internal codes.
The principle has been, first, to try to convert a string to a
Bcp47Code as soon as possible and as close to the original input as
possible, so it is easy to see *why* a given string is a BCP-47 code
(usually, because it is coming from HTTP/HTML/etc) and we're not stuck
deep inside some method trying to figure out where a string we're
given is coming from and therefore what sort of string code it might
be. Second, we've added explicit compatibility code to accept
MediaWiki internal codes and convert them to Bcp47Code for backward
compatibility with existing clients, using the @internal
LanguageCode::normalizeNonstandardCodeAndWarn() method. The intention
is to gradually remove these backward compatibility thunks and replace
them with HTTP 400 errors or wfDeprecated messages in order to
identify and repair callers who are incorrectly using
non-standard-compliant language codes in web standards
(HTTP/HTML/SVG/etc).
Finally, maintaining a code as a Bcp47Code and not immediately
converting to Language helps us delay or even avoid full loading of a
Language object in some cases, which is another reason to occasionally
push Bcp47Code (instead of Language) down the call stack.
Bug: T327379
Depends-On: I830867d58f8962d6a57be16ce3735e8384f9ac1c
Change-Id: I982e0df706a633b05dcc02b5220b737c19adc401
2022-11-04 17:29:23 +00:00
|
|
|
$helper->setVariantConversionLanguage( new Bcp47CodeValue( 'en-x-piglatin' ) );
|
2022-10-10 14:46:54 +00:00
|
|
|
|
|
|
|
|
$htmlResult = $helper->getHtml()->getRawText();
|
|
|
|
|
$this->assertStringContainsString( self::MOCK_HTML_VARIANT, $htmlResult );
|
|
|
|
|
$this->assertStringContainsString( 'en-x-piglatin', $helper->getETag() );
|
2023-02-09 13:04:27 +00:00
|
|
|
|
|
|
|
|
$pbResult = $helper->getPageBundle();
|
|
|
|
|
$this->assertStringContainsString( self::MOCK_HTML_VARIANT, $pbResult->html );
|
|
|
|
|
$this->assertStringContainsString( 'en-x-piglatin', $pbResult->headers['content-language'] );
|
2022-10-10 14:46:54 +00:00
|
|
|
}
|
|
|
|
|
|
2022-12-13 12:35:06 +00:00
|
|
|
public function testGetHtmlWillLint() {
|
2022-12-12 21:04:31 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::ParsoidSettings, [
|
|
|
|
|
'linting' => true
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
$mockHandler = $this->createMock( ParserLogLinterDataHook::class );
|
|
|
|
|
$mockHandler->expects( $this->once() ) // this is the critical assertion in this test case!
|
|
|
|
|
->method( 'onParserLogLinterData' );
|
|
|
|
|
|
|
|
|
|
$this->setTemporaryHook(
|
|
|
|
|
'ParserLogLinterData',
|
|
|
|
|
$mockHandler
|
|
|
|
|
);
|
|
|
|
|
|
2024-05-15 16:57:56 +00:00
|
|
|
// Use the real ParserOutputAccess, so we use the real hook container.
|
|
|
|
|
$access = $this->getServiceContainer()->getParserOutputAccess();
|
2022-12-12 21:04:31 +00:00
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [ 'ParserOutputAccess' => $access ], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-12-12 21:04:31 +00:00
|
|
|
|
|
|
|
|
// Do it.
|
|
|
|
|
$helper->getHtml();
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-02 13:52:35 +00:00
|
|
|
public function testGetPageBundleWithOptions() {
|
2023-09-29 00:41:50 +00:00
|
|
|
$this->markTestSkipped( 'T347426: Support for non-default output content major version has been disabled.' );
|
2022-11-25 10:00:18 +00:00
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-11-25 10:00:18 +00:00
|
|
|
|
|
|
|
|
// Calling setParsoidOptions must disable caching and force the ETag to null
|
|
|
|
|
$helper->setOutputProfileVersion( '999.0.0' );
|
|
|
|
|
|
2022-12-02 13:52:35 +00:00
|
|
|
$pb = $helper->getPageBundle();
|
|
|
|
|
|
|
|
|
|
// NOTE: Check that the options are present in the HTML.
|
|
|
|
|
// We don't do real parsing, so this is how they are represented in the output.
|
|
|
|
|
$this->assertStringContainsString( '"outputContentVersion":"999.0.0"', $pb->html );
|
2023-08-29 20:13:43 +00:00
|
|
|
$this->assertStringContainsString( '"offsetType":"byte"', $pb->html );
|
2022-11-25 10:00:18 +00:00
|
|
|
|
|
|
|
|
$response = new Response();
|
|
|
|
|
$helper->putHeaders( $response, true );
|
|
|
|
|
$this->assertStringContainsString( 'private', $response->getHeaderLine( 'Cache-Control' ) );
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-24 21:54:11 +00:00
|
|
|
public function testGetPreviewHtml_setContent() {
|
2022-10-07 14:27:01 +00:00
|
|
|
$page = $this->getNonexistingTestPage();
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-10-07 14:27:01 +00:00
|
|
|
$helper->setContent( new WikitextContent( 'text to preview' ) );
|
2020-12-03 17:53:55 +00:00
|
|
|
|
2022-12-04 21:35:20 +00:00
|
|
|
// getRevisionId() should return null for fake revisions.
|
|
|
|
|
$this->assertNull( $helper->getRevisionId() );
|
|
|
|
|
|
2020-12-03 17:53:55 +00:00
|
|
|
$htmlresult = $helper->getHtml()->getRawText();
|
|
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
$this->assertStringContainsString( 'text to preview', $htmlresult );
|
2020-12-03 17:53:55 +00:00
|
|
|
}
|
|
|
|
|
|
2022-11-24 21:54:11 +00:00
|
|
|
public function testGetPreviewHtml_setContentSource() {
|
|
|
|
|
$page = $this->getNonexistingTestPage();
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-11-24 21:54:11 +00:00
|
|
|
$helper->setContentSource( 'text to preview', CONTENT_MODEL_WIKITEXT );
|
|
|
|
|
|
|
|
|
|
$htmlresult = $helper->getHtml()->getRawText();
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString( 'text to preview', $htmlresult );
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-29 18:52:23 +00:00
|
|
|
public function testHtmlIsStashedForExistingPage() {
|
2022-06-13 09:31:50 +00:00
|
|
|
[ $page, ] = $this->getExistingPageWithRevisions( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
$cache = new HashBagOStuff();
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper(
|
|
|
|
|
[ 'cache' => $cache, 'expectedHtml' => self::MOCK_HTML ], $page, self::PARAM_DEFAULTS, $this->newAuthority()
|
|
|
|
|
);
|
2022-10-07 14:27:01 +00:00
|
|
|
$helper->setStashingEnabled( true );
|
|
|
|
|
|
2022-06-13 09:31:50 +00:00
|
|
|
$htmlresult = $helper->getHtml()->getRawText();
|
2022-10-07 14:27:01 +00:00
|
|
|
$this->assertStringContainsString( self::MOCK_HTML, $htmlresult );
|
2022-06-13 09:31:50 +00:00
|
|
|
|
|
|
|
|
$eTag = $helper->getETag();
|
|
|
|
|
$parsoidStashKey = ParsoidRenderID::newFromETag( $eTag );
|
|
|
|
|
|
2022-10-29 18:52:23 +00:00
|
|
|
$chFactory = $this->createNoOpMock( IContentHandlerFactory::class );
|
|
|
|
|
$stash = new SimpleParsoidOutputStash( $chFactory, $cache, 1 );
|
2022-06-13 09:31:50 +00:00
|
|
|
$this->assertNotNull( $stash->get( $parsoidStashKey ) );
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-29 18:52:23 +00:00
|
|
|
public function testHtmlIsStashedForFakeRevision() {
|
|
|
|
|
$page = $this->getNonexistingTestPage();
|
|
|
|
|
|
|
|
|
|
$cache = new HashBagOStuff();
|
|
|
|
|
$text = 'just some wikitext';
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [ 'cache' => $cache ], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-10-29 18:52:23 +00:00
|
|
|
$helper->setContent( new WikitextContent( $text ) );
|
|
|
|
|
$helper->setStashingEnabled( true );
|
|
|
|
|
|
|
|
|
|
$htmlresult = $helper->getHtml()->getRawText();
|
|
|
|
|
$this->assertStringContainsString( $text, $htmlresult );
|
|
|
|
|
|
|
|
|
|
$eTag = $helper->getETag();
|
|
|
|
|
$parsoidStashKey = ParsoidRenderID::newFromETag( $eTag );
|
|
|
|
|
|
|
|
|
|
$chFactory = $this->getServiceContainer()->getContentHandlerFactory();
|
|
|
|
|
$stash = new SimpleParsoidOutputStash( $chFactory, $cache, 1 );
|
|
|
|
|
|
|
|
|
|
$selserContext = $stash->get( $parsoidStashKey );
|
|
|
|
|
$this->assertNotNull( $selserContext );
|
|
|
|
|
|
|
|
|
|
/** @var WikitextContent $stashedContent */
|
|
|
|
|
$stashedContent = $selserContext->getContent();
|
|
|
|
|
$this->assertNotNull( $stashedContent );
|
|
|
|
|
$this->assertInstanceOf( WikitextContent::class, $stashedContent );
|
|
|
|
|
$this->assertSame( $text, $stashedContent->getText() );
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-13 09:31:50 +00:00
|
|
|
public function testStashRateLimit() {
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
2023-11-01 19:10:00 +00:00
|
|
|
$authority = $this->newAuthorityWhoCantStash();
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [], $page, self::PARAM_DEFAULTS, $authority );
|
2022-10-07 14:27:01 +00:00
|
|
|
$helper->setStashingEnabled( true );
|
2022-06-13 09:31:50 +00:00
|
|
|
|
|
|
|
|
$this->expectException( LocalizedHttpException::class );
|
|
|
|
|
$this->expectExceptionCode( 429 );
|
|
|
|
|
$helper->getHtml();
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-06 10:18:28 +00:00
|
|
|
public function testInteractionOfStashAndFlavor() {
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
2023-11-01 19:10:00 +00:00
|
|
|
$authority = $this->newAuthorityWhoCantStash();
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [], $page, self::PARAM_DEFAULTS, $authority );
|
2023-04-06 10:18:28 +00:00
|
|
|
|
|
|
|
|
// Assert that the initial flavor is "view"
|
|
|
|
|
$this->assertSame( 'view', $helper->getFlavor() );
|
|
|
|
|
|
|
|
|
|
// Assert that we can change the flavor to "edit"
|
|
|
|
|
$helper->setFlavor( 'edit' );
|
|
|
|
|
$this->assertSame( 'edit', $helper->getFlavor() );
|
|
|
|
|
|
|
|
|
|
// Assert that enabling stashing will force the flavor to be "stash"
|
|
|
|
|
$helper->setStashingEnabled( true );
|
|
|
|
|
$this->assertSame( 'stash', $helper->getFlavor() );
|
|
|
|
|
|
|
|
|
|
// Assert that disabling stashing will reset the flavor to "view"
|
|
|
|
|
$helper->setStashingEnabled( false );
|
|
|
|
|
$this->assertSame( 'view', $helper->getFlavor() );
|
|
|
|
|
|
|
|
|
|
// Assert that we cannot change the flavor to "view" when stashing is enabled
|
|
|
|
|
$helper->setStashingEnabled( true );
|
|
|
|
|
$helper->setFlavor( 'view' );
|
|
|
|
|
$this->assertSame( 'stash', $helper->getFlavor() );
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
public function testGetHtmlFragment() {
|
|
|
|
|
$page = $this->getExistingTestPage();
|
2022-10-05 18:47:44 +00:00
|
|
|
|
2024-05-16 20:42:16 +00:00
|
|
|
$expectedHtml = '<html><body><section data-mw-section-id=0><p>Contents</p></section></body></html>';
|
|
|
|
|
$helper = $this->newHelper( [
|
|
|
|
|
'ParsoidParserFactory' => $this->newMockParsoidParserFactory( [
|
|
|
|
|
'expectedHtml' => $expectedHtml
|
|
|
|
|
] ),
|
|
|
|
|
'expectedHtml' => $expectedHtml,
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-10-07 14:27:01 +00:00
|
|
|
$helper->setFlavor( 'fragment' );
|
2024-05-16 20:42:16 +00:00
|
|
|
$helper->setContentSource( 'Contents', CONTENT_MODEL_WIKITEXT );
|
2022-10-05 18:47:44 +00:00
|
|
|
|
|
|
|
|
$htmlresult = $helper->getHtml()->getRawText();
|
|
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
$this->assertStringContainsString( 'fragment', $helper->getETag() );
|
2024-05-16 20:42:16 +00:00
|
|
|
$this->assertStringContainsString( '<p>Contents</p>', $htmlresult );
|
2023-08-29 20:13:43 +00:00
|
|
|
$this->assertStringNotContainsString( "<body", $htmlresult );
|
|
|
|
|
$this->assertStringNotContainsString( "<section", $htmlresult );
|
2022-10-05 18:47:44 +00:00
|
|
|
}
|
|
|
|
|
|
2022-11-25 10:00:18 +00:00
|
|
|
public function testGetHtmlForEdit() {
|
|
|
|
|
$page = $this->getExistingTestPage();
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-11-25 10:00:18 +00:00
|
|
|
$helper->setContentSource( 'hello {{world}}', CONTENT_MODEL_WIKITEXT );
|
|
|
|
|
$helper->setFlavor( 'edit' );
|
|
|
|
|
|
|
|
|
|
$htmlresult = $helper->getHtml()->getRawText();
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString( 'edit', $helper->getETag() );
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString( 'hello', $htmlresult );
|
|
|
|
|
$this->assertStringContainsString( 'data-parsoid=', $htmlresult );
|
|
|
|
|
$this->assertStringContainsString( '"dsr":', $htmlresult );
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-15 22:12:49 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideRevisionReferences()
|
|
|
|
|
*/
|
2024-05-16 20:42:16 +00:00
|
|
|
public function testETagLastModified( $revRef ) {
|
2020-12-15 22:12:49 +00:00
|
|
|
[ $page, $revisions ] = $this->getExistingPageWithRevisions( __METHOD__ );
|
|
|
|
|
$rev = $revRef ? $revisions[ $revRef ] : null;
|
2020-12-03 17:53:55 +00:00
|
|
|
|
|
|
|
|
$cache = new HashBagOStuff();
|
|
|
|
|
|
|
|
|
|
// First, test it works if nothing was cached yet.
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [ 'cache' => $cache ], $page, self::PARAM_DEFAULTS, $this->newAuthority(), $rev );
|
2022-12-04 21:50:35 +00:00
|
|
|
|
|
|
|
|
// put HTML into the cache
|
|
|
|
|
$pout = $helper->getHtml();
|
|
|
|
|
|
Add ParserOutput::{get,set}RenderId() and set render id in ContentRenderer
Set the render ID for each parse stored into cache so that we are able
to identify a specific parse when there are dependencies (for example
in an edit based on that parse). This is recorded as a property added
to the ParserOutput, not the parent CacheTime interface. Even though
the render ID is /related/ to the CacheTime interface, CacheTime is
also used directly as a parser cache key, and the UUID should not be
part of the lookup key.
In general we are trying to move the location where these cache
properties are set as early as possible, so we check at each location
to ensure we don't overwrite a previously-set value. Eventually we
can convert most of these checks into assertions that the cache
properties have already been set (T350538). The primary location for
setting cache properties is the ContentRenderer.
Moved setting the revision timestamp into ContentRenderer as well, as
it was set along the same code paths. An extra parameter was added to
ContentRenderer::getParserOutput() to support this.
Added merge code to ParserOutput::mergeInternalMetaDataFrom() which
should ensure that cache time, revision, timestamp, and render id are
all set properly when multiple slots are combined together in MCR.
In order to ensure the render ID is set on all codepaths we needed to
plumb the GlobalIdGenerator service into ContentRenderer, ParserCache,
ParserCacheFactory, and RevisionOutputCache. Eventually (T350538) it
should only be necessary in the ContentRenderer.
Bug: T350538
Bug: T349868
Followup-To: Ic9b7cc0fcf365e772b7d080d76a065e3fd585f80
Change-Id: I72c5e6f86b7f081ab5ce7a56f5365d2f75067a78
2023-09-14 16:11:20 +00:00
|
|
|
$renderId = ParsoidRenderID::newFromParserOutput( $pout );
|
2022-12-04 21:50:35 +00:00
|
|
|
$lastModified = $pout->getCacheTime();
|
2020-12-03 17:53:55 +00:00
|
|
|
|
2022-12-04 21:35:20 +00:00
|
|
|
if ( $rev ) {
|
|
|
|
|
$this->assertSame( $rev->getId(), $helper->getRevisionId() );
|
|
|
|
|
} else {
|
|
|
|
|
// current revision
|
|
|
|
|
$this->assertSame( 0, $helper->getRevisionId() );
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-15 22:12:49 +00:00
|
|
|
// make sure the etag didn't change after getHtml();
|
2022-12-04 21:50:35 +00:00
|
|
|
$this->assertStringContainsString( $renderId->getKey(), $helper->getETag() );
|
2020-12-15 22:12:49 +00:00
|
|
|
$this->assertSame(
|
|
|
|
|
MWTimestamp::convert( TS_MW, $lastModified ),
|
|
|
|
|
MWTimestamp::convert( TS_MW, $helper->getLastModified() )
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Now, expire the cache. etag and timestamp should change
|
|
|
|
|
$now = MWTimestamp::convert( TS_UNIX, self::TIMESTAMP_LATER ) + 10000;
|
|
|
|
|
MWTimestamp::setFakeTime( $now );
|
2020-12-03 17:53:55 +00:00
|
|
|
$this->assertTrue(
|
2020-12-15 22:12:49 +00:00
|
|
|
$page->getTitle()->invalidateCache( MWTimestamp::convert( TS_MW, $now ) ),
|
2022-03-09 01:49:21 +00:00
|
|
|
'Cannot invalidate cache'
|
2020-12-03 17:53:55 +00:00
|
|
|
);
|
|
|
|
|
DeferredUpdates::doUpdates();
|
2021-05-04 20:45:30 +00:00
|
|
|
$page->clear();
|
2020-12-03 17:53:55 +00:00
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [ 'cache' => $cache ], $page, self::PARAM_DEFAULTS, $this->newAuthority(), $rev );
|
2020-12-03 17:53:55 +00:00
|
|
|
|
2022-12-04 21:50:35 +00:00
|
|
|
$this->assertStringNotContainsString( $renderId->getKey(), $helper->getETag() );
|
2020-12-03 17:53:55 +00:00
|
|
|
$this->assertSame(
|
2020-12-15 22:12:49 +00:00
|
|
|
MWTimestamp::convert( TS_MW, $now ),
|
|
|
|
|
MWTimestamp::convert( TS_MW, $helper->getLastModified() )
|
2020-12-03 17:53:55 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-30 10:26:39 +00:00
|
|
|
/**
|
2023-09-29 10:35:16 +00:00
|
|
|
* @covers \MediaWiki\Rest\Handler\Helper\HtmlOutputRendererHelper::init
|
2024-05-16 20:42:16 +00:00
|
|
|
* @covers \MediaWiki\Rest\Handler\Helper\HtmlOutputRendererHelper::parseUncacheable
|
2022-08-30 10:26:39 +00:00
|
|
|
*/
|
2024-05-16 20:42:16 +00:00
|
|
|
public function testETagLastModifiedWithPageIdentity() {
|
2022-08-30 10:26:39 +00:00
|
|
|
[ $fakePage, $fakeRevision ] = $this->getNonExistingPageWithFakeRevision( __METHOD__ );
|
2024-05-16 20:42:16 +00:00
|
|
|
$pp = $this->createMock( ParsoidParser::class );
|
|
|
|
|
$pp->expects( $this->once() )
|
2024-02-08 21:07:04 +00:00
|
|
|
->method( 'parse' )
|
2022-10-07 14:27:01 +00:00
|
|
|
->willReturnCallback( function (
|
2024-02-08 21:07:04 +00:00
|
|
|
string $text,
|
2024-05-16 20:42:16 +00:00
|
|
|
PageReference $page,
|
2024-02-08 21:07:04 +00:00
|
|
|
ParserOptions $parserOpts,
|
|
|
|
|
bool $linestart = true,
|
|
|
|
|
bool $clearState = true,
|
|
|
|
|
?int $revId = null
|
2022-08-30 10:26:39 +00:00
|
|
|
) use ( $fakePage, $fakeRevision ) {
|
2024-02-08 21:07:04 +00:00
|
|
|
self::assertTrue( $page->isSamePageAs( $fakePage ), '$page and $fakePage should be the same' );
|
|
|
|
|
self::assertSame( $revId, $fakeRevision->getId(), '$rev and $fakeRevision should be the same' );
|
2022-08-30 10:26:39 +00:00
|
|
|
|
2024-02-08 21:07:04 +00:00
|
|
|
$html = $this->getMockHtml( $fakeRevision );
|
|
|
|
|
$pout = $this->makeParserOutput( $parserOpts, $html, $fakeRevision, $page );
|
2024-05-16 20:42:16 +00:00
|
|
|
return $pout;
|
2022-08-30 10:26:39 +00:00
|
|
|
} );
|
2024-05-16 20:42:16 +00:00
|
|
|
$options['ParsoidParser'] = $pp;
|
|
|
|
|
$options['ParsoidParserFactory'] = $this->newMockParsoidParserFactory(
|
|
|
|
|
$options
|
|
|
|
|
);
|
2022-08-30 10:26:39 +00:00
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( $options, $fakePage, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-10-07 14:27:01 +00:00
|
|
|
$helper->setRevision( $fakeRevision );
|
2022-08-30 10:26:39 +00:00
|
|
|
|
2022-12-04 21:35:20 +00:00
|
|
|
$this->assertNull( $helper->getRevisionId() );
|
|
|
|
|
|
2022-12-04 21:50:35 +00:00
|
|
|
$pout = $helper->getHtml();
|
Add ParserOutput::{get,set}RenderId() and set render id in ContentRenderer
Set the render ID for each parse stored into cache so that we are able
to identify a specific parse when there are dependencies (for example
in an edit based on that parse). This is recorded as a property added
to the ParserOutput, not the parent CacheTime interface. Even though
the render ID is /related/ to the CacheTime interface, CacheTime is
also used directly as a parser cache key, and the UUID should not be
part of the lookup key.
In general we are trying to move the location where these cache
properties are set as early as possible, so we check at each location
to ensure we don't overwrite a previously-set value. Eventually we
can convert most of these checks into assertions that the cache
properties have already been set (T350538). The primary location for
setting cache properties is the ContentRenderer.
Moved setting the revision timestamp into ContentRenderer as well, as
it was set along the same code paths. An extra parameter was added to
ContentRenderer::getParserOutput() to support this.
Added merge code to ParserOutput::mergeInternalMetaDataFrom() which
should ensure that cache time, revision, timestamp, and render id are
all set properly when multiple slots are combined together in MCR.
In order to ensure the render ID is set on all codepaths we needed to
plumb the GlobalIdGenerator service into ContentRenderer, ParserCache,
ParserCacheFactory, and RevisionOutputCache. Eventually (T350538) it
should only be necessary in the ContentRenderer.
Bug: T350538
Bug: T349868
Followup-To: Ic9b7cc0fcf365e772b7d080d76a065e3fd585f80
Change-Id: I72c5e6f86b7f081ab5ce7a56f5365d2f75067a78
2023-09-14 16:11:20 +00:00
|
|
|
$renderId = ParsoidRenderID::newFromParserOutput( $pout );
|
2022-12-04 21:50:35 +00:00
|
|
|
$lastModified = $pout->getCacheTime();
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString( $renderId->getKey(), $helper->getETag() );
|
2022-08-30 10:26:39 +00:00
|
|
|
$this->assertSame(
|
|
|
|
|
MWTimestamp::convert( TS_MW, $lastModified ),
|
|
|
|
|
MWTimestamp::convert( TS_MW, $helper->getLastModified() )
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideETagSuffix() {
|
2022-05-24 21:13:42 +00:00
|
|
|
yield 'stash + html' =>
|
2022-06-17 14:00:27 +00:00
|
|
|
[ [ 'stash' => true ], 'html', '/stash/html' ];
|
2022-05-24 21:13:42 +00:00
|
|
|
|
|
|
|
|
yield 'view html' =>
|
2022-06-17 14:00:27 +00:00
|
|
|
[ [], 'html', '/view/html' ];
|
2022-05-24 21:13:42 +00:00
|
|
|
|
|
|
|
|
yield 'stash + wrapped' =>
|
2022-06-17 14:00:27 +00:00
|
|
|
[ [ 'stash' => true ], 'with_html', '/stash/with_html' ];
|
2022-05-24 21:13:42 +00:00
|
|
|
|
|
|
|
|
yield 'view wrapped' =>
|
2022-06-17 14:00:27 +00:00
|
|
|
[ [], 'with_html', '/view/with_html' ];
|
2022-05-24 21:13:42 +00:00
|
|
|
|
|
|
|
|
yield 'stash' =>
|
2022-06-17 14:00:27 +00:00
|
|
|
[ [ 'stash' => true ], '', '/stash' ];
|
2022-05-24 21:13:42 +00:00
|
|
|
|
2022-10-05 18:47:44 +00:00
|
|
|
yield 'flavor = fragment' =>
|
|
|
|
|
[ [ 'flavor' => 'fragment' ], '', '/fragment' ];
|
|
|
|
|
|
|
|
|
|
yield 'flavor = fragment + stash = true: stash should take over' =>
|
|
|
|
|
[ [ 'stash' => true, 'flavor' => 'fragment' ], '', '/stash' ];
|
|
|
|
|
|
2022-05-24 21:13:42 +00:00
|
|
|
yield 'nothing' =>
|
2022-06-17 14:00:27 +00:00
|
|
|
[ [], '', '/view' ];
|
2022-05-24 21:13:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideETagSuffix()
|
|
|
|
|
*/
|
|
|
|
|
public function testETagSuffix( array $params, string $mode, string $suffix ) {
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
$cache = new HashBagOStuff();
|
|
|
|
|
|
|
|
|
|
// First, test it works if nothing was cached yet.
|
2024-05-16 20:42:16 +00:00
|
|
|
$helper = $this->newHelper( [
|
|
|
|
|
'cache' => $cache,
|
2024-06-05 15:49:49 +00:00
|
|
|
], $page, $params + self::PARAM_DEFAULTS, $this->newAuthority() );
|
2024-05-16 20:42:16 +00:00
|
|
|
if ( ( $params['flavor'] ?? null ) === 'fragment' ) {
|
|
|
|
|
$helper->setContentSource( "fragment test", CONTENT_MODEL_WIKITEXT );
|
|
|
|
|
}
|
2022-05-24 21:13:42 +00:00
|
|
|
|
|
|
|
|
$etag = $helper->getETag( $mode );
|
|
|
|
|
$etag = trim( $etag, '"' );
|
|
|
|
|
$this->assertStringEndsWith( $suffix, $etag );
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-26 18:27:49 +00:00
|
|
|
public static function provideVariantConversionLanguage() {
|
|
|
|
|
yield 'simple code'
|
|
|
|
|
=> [ 'en', new Bcp47CodeValue( 'en' ) ];
|
|
|
|
|
|
|
|
|
|
yield 'code with dashes'
|
|
|
|
|
=> [ 'en-x-piglatin', new Bcp47CodeValue( 'en-x-piglatin' ) ];
|
|
|
|
|
|
|
|
|
|
yield 'obsolete alias'
|
|
|
|
|
=> [ 'zh-min-nan', new Bcp47CodeValue( 'nan' ) ];
|
|
|
|
|
|
|
|
|
|
yield 'obsolete alias in source language'
|
|
|
|
|
=> [ 'en', new Bcp47CodeValue( 'en' ),
|
|
|
|
|
'zh-min-nan', new Bcp47CodeValue( 'nan' ) ];
|
|
|
|
|
|
|
|
|
|
yield 'target and source given as objects'
|
|
|
|
|
=> [ new Bcp47CodeValue( 'x y z' ), new Bcp47CodeValue( 'x y z' ),
|
|
|
|
|
new Bcp47CodeValue( 'a,b,c' ), new Bcp47CodeValue( 'a,b,c' ) ];
|
|
|
|
|
|
|
|
|
|
yield 'complex accept-language header (T350852)'
|
|
|
|
|
=> [ 'da, en-gb;q=0.8, en;q=0.7', new Bcp47CodeValue( 'da' ) ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideVariantConversionLanguage
|
|
|
|
|
*/
|
|
|
|
|
public function testSetVariantConversionLanguage(
|
|
|
|
|
$target,
|
|
|
|
|
Bcp47Code $expectedTarget,
|
|
|
|
|
$source = null,
|
|
|
|
|
?Bcp47Code $expectedSource = null
|
|
|
|
|
) {
|
|
|
|
|
$converter = $this->createNoOpMock(
|
|
|
|
|
LanguageVariantConverter::class,
|
|
|
|
|
[ 'convertPageBundleVariant' ]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// This is the key assertion in this test:
|
|
|
|
|
$converter->expects( $this->once() )
|
|
|
|
|
->method( 'convertPageBundleVariant' )->with(
|
|
|
|
|
$this->anything(),
|
|
|
|
|
$expectedTarget,
|
|
|
|
|
$expectedSource
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$transformFactory = $this->createNoOpMock(
|
|
|
|
|
HtmlTransformFactory::class,
|
|
|
|
|
[ 'getLanguageVariantConverter' ]
|
|
|
|
|
);
|
|
|
|
|
$transformFactory->method( 'getLanguageVariantConverter' )
|
|
|
|
|
->willReturn( $converter );
|
|
|
|
|
|
|
|
|
|
$this->overrideConfigValue( MainConfigNames::UsePigLatinVariant, true );
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [ 'HtmlTransformFactory' => $transformFactory ], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2024-02-26 18:27:49 +00:00
|
|
|
|
|
|
|
|
// call method under test
|
|
|
|
|
$helper->setVariantConversionLanguage( $target, $source );
|
|
|
|
|
|
|
|
|
|
// Secondary assertion, to ensure that the ETag varies on the right thing.
|
|
|
|
|
$this->assertStringEndsWith( "+lang:$expectedTarget\"", $helper->getETag() );
|
|
|
|
|
|
|
|
|
|
$helper->getPageBundle();
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideHandlesParsoidError() {
|
2020-12-03 17:53:55 +00:00
|
|
|
yield 'ClientError' => [
|
|
|
|
|
new ClientError( 'TEST_TEST' ),
|
|
|
|
|
new LocalizedHttpException(
|
|
|
|
|
new MessageValue( 'rest-html-backend-error' ),
|
|
|
|
|
400,
|
|
|
|
|
[
|
|
|
|
|
'reason' => 'TEST_TEST'
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
];
|
|
|
|
|
yield 'ResourceLimitExceededException' => [
|
|
|
|
|
new ResourceLimitExceededException( 'TEST_TEST' ),
|
|
|
|
|
new LocalizedHttpException(
|
|
|
|
|
new MessageValue( 'rest-resource-limit-exceeded' ),
|
|
|
|
|
413,
|
|
|
|
|
[
|
|
|
|
|
'reason' => 'TEST_TEST'
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
];
|
2022-12-14 17:33:58 +00:00
|
|
|
yield 'RevisionAccessException' => [
|
|
|
|
|
new RevisionAccessException( 'TEST_TEST' ),
|
|
|
|
|
new LocalizedHttpException(
|
|
|
|
|
new MessageValue( 'rest-nonexistent-title' ),
|
|
|
|
|
404,
|
|
|
|
|
[
|
|
|
|
|
'reason' => 'TEST_TEST'
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
];
|
2020-12-03 17:53:55 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-16 20:42:16 +00:00
|
|
|
private function newMockParsoidParserFactory( array $options = [] ) {
|
|
|
|
|
if ( isset( $options['Parsoid'] ) ) {
|
|
|
|
|
$mockParsoid = $options['Parsoid'];
|
|
|
|
|
} else {
|
|
|
|
|
$mockParsoid = $this->createNoOpMock( Parsoid::class, [
|
|
|
|
|
'wikitext2html',
|
|
|
|
|
] );
|
|
|
|
|
$mockParsoid
|
|
|
|
|
->method( 'wikitext2html' )
|
|
|
|
|
->willReturn( new PageBundle(
|
|
|
|
|
$options['expectedHtml'] ?? 'This is HTML'
|
|
|
|
|
) );
|
2022-11-24 19:58:56 +00:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 20:13:43 +00:00
|
|
|
// Install it in the ParsoidParser object
|
2024-05-16 20:42:16 +00:00
|
|
|
if ( isset( $options['ParsoidParser'] ) ) {
|
|
|
|
|
$parsoidParser = $options['ParsoidParser'];
|
|
|
|
|
} else {
|
|
|
|
|
$services = $this->getServiceContainer();
|
|
|
|
|
$parsoidParser = new ParsoidParser(
|
|
|
|
|
$mockParsoid,
|
|
|
|
|
$services->getParsoidPageConfigFactory(),
|
|
|
|
|
$services->getLanguageConverterFactory(),
|
|
|
|
|
$services->getParserFactory(),
|
|
|
|
|
$services->getGlobalIdGenerator()
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-08-29 20:13:43 +00:00
|
|
|
|
|
|
|
|
// Create a mock Parsoid factory that returns the ParsoidParser object
|
|
|
|
|
// with the mocked Parsoid object.
|
|
|
|
|
$mockParsoidParserFactory = $this->createNoOpMock( ParsoidParserFactory::class, [ 'create' ] );
|
|
|
|
|
$mockParsoidParserFactory->method( 'create' )->willReturn( $parsoidParser );
|
2024-05-16 20:42:16 +00:00
|
|
|
return $mockParsoidParserFactory;
|
|
|
|
|
}
|
2023-08-29 20:13:43 +00:00
|
|
|
|
2024-05-16 20:42:16 +00:00
|
|
|
private function resetServicesWithMockedParsoid( ?Parsoid $mockParsoid = null ): void {
|
|
|
|
|
$services = $this->getServiceContainer();
|
|
|
|
|
$mockParsoidParserFactory = $this->newMockParsoidParserFactory( [
|
|
|
|
|
'Parsoid' => $mockParsoid,
|
|
|
|
|
] );
|
2024-02-08 21:07:04 +00:00
|
|
|
$this->resetServicesWithMockedParsoidParserFactory( $mockParsoidParserFactory );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function resetServicesWithMockedParsoidParserFactory( ?ParsoidParserFactory $mockParsoidParserFactory = null ): void {
|
2023-08-29 20:13:43 +00:00
|
|
|
$this->setService( 'ParsoidParserFactory', $mockParsoidParserFactory );
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-15 16:57:56 +00:00
|
|
|
private function newRealParserOutputAccess( $overrides = [] ): array {
|
2023-08-29 20:13:43 +00:00
|
|
|
$services = $this->getServiceContainer();
|
|
|
|
|
|
2022-11-24 19:58:56 +00:00
|
|
|
if ( isset( $overrides['parserCache'] ) ) {
|
|
|
|
|
$parserCache = $overrides['parserCache'];
|
|
|
|
|
} else {
|
2024-03-06 09:04:24 +00:00
|
|
|
$parserCache = $this->createNoOpMock(
|
|
|
|
|
ParserCache::class,
|
|
|
|
|
[ 'get', 'save', 'makeParserOutputKey', ]
|
|
|
|
|
);
|
2022-11-24 19:58:56 +00:00
|
|
|
$parserCache->method( 'get' )->willReturn( false );
|
|
|
|
|
$parserCache->method( 'save' )->willReturn( null );
|
2024-03-06 09:04:24 +00:00
|
|
|
$parserCache->method( 'makeParserOutputKey' )->willReturn( 'test-key' );
|
2022-11-24 19:58:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $overrides['revisionCache'] ) ) {
|
|
|
|
|
$revisionCache = $overrides['revisionCache'];
|
|
|
|
|
} else {
|
|
|
|
|
$revisionCache = $this->createNoOpMock( RevisionOutputCache::class, [ 'get', 'save' ] );
|
|
|
|
|
$revisionCache->method( 'get' )->willReturn( false );
|
|
|
|
|
$revisionCache->method( 'save' )->willReturn( null );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$parserCacheFactory = $this->createNoOpMock(
|
|
|
|
|
ParserCacheFactory::class,
|
|
|
|
|
[ 'getParserCache', 'getRevisionOutputCache' ]
|
|
|
|
|
);
|
|
|
|
|
$parserCacheFactory->method( 'getParserCache' )->willReturn( $parserCache );
|
|
|
|
|
$parserCacheFactory->method( 'getRevisionOutputCache' )->willReturn( $revisionCache );
|
2023-08-29 20:13:43 +00:00
|
|
|
$parserOutputAccess = new ParserOutputAccess(
|
|
|
|
|
$parserCacheFactory,
|
|
|
|
|
$services->getRevisionLookup(),
|
|
|
|
|
$services->getRevisionRenderer(),
|
|
|
|
|
new NullStatsdDataFactory(),
|
|
|
|
|
$services->getDBLoadBalancerFactory(),
|
|
|
|
|
$services->getChronologyProtector(),
|
|
|
|
|
$this->getLoggerSpi(),
|
|
|
|
|
$services->getWikiPageFactory(),
|
|
|
|
|
$services->getTitleFormatter()
|
|
|
|
|
);
|
2024-05-15 16:57:56 +00:00
|
|
|
return [
|
|
|
|
|
'ParserOutputAccess' => $parserOutputAccess,
|
|
|
|
|
];
|
2022-11-24 19:58:56 +00:00
|
|
|
}
|
|
|
|
|
|
2020-12-03 17:53:55 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideHandlesParsoidError
|
|
|
|
|
*/
|
|
|
|
|
public function testHandlesParsoidError(
|
|
|
|
|
Exception $parsoidException,
|
|
|
|
|
Exception $expectedException
|
|
|
|
|
) {
|
2020-12-15 22:12:49 +00:00
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
2020-12-03 17:53:55 +00:00
|
|
|
|
2022-10-05 18:47:44 +00:00
|
|
|
$parsoid = $this->createNoOpMock( Parsoid::class, [ 'wikitext2html' ] );
|
|
|
|
|
$parsoid->method( 'wikitext2html' )
|
2020-12-03 17:53:55 +00:00
|
|
|
->willThrowException( $parsoidException );
|
|
|
|
|
|
2024-02-18 13:33:18 +00:00
|
|
|
$parserCache = $this->createNoOpMock( ParserCache::class, [ 'get', 'makeParserOutputKey' ] );
|
2023-08-29 20:13:43 +00:00
|
|
|
$parserCache->method( 'get' )->willReturn( false );
|
|
|
|
|
$parserCache->expects( $this->atLeastOnce() )->method( 'makeParserOutputKey' );
|
|
|
|
|
|
|
|
|
|
$this->resetServicesWithMockedParsoid( $parsoid );
|
2024-05-15 16:57:56 +00:00
|
|
|
$access = $this->newRealParserOutputAccess( [ 'parserCache' => $parserCache ] );
|
2022-10-05 18:47:44 +00:00
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( $access, $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2020-12-03 17:53:55 +00:00
|
|
|
|
|
|
|
|
$this->expectExceptionObject( $expectedException );
|
|
|
|
|
$helper->getHtml();
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-11 10:00:34 +00:00
|
|
|
public static function provideParsoidOutputStatus() {
|
|
|
|
|
yield 'parsoid-client-error' => [
|
|
|
|
|
Status::newFatal( 'parsoid-client-error' ),
|
|
|
|
|
new LocalizedHttpException(
|
|
|
|
|
new MessageValue( 'rest-html-backend-error' ),
|
|
|
|
|
400,
|
|
|
|
|
[
|
|
|
|
|
'reason' => 'parsoid-client-error'
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
];
|
|
|
|
|
yield 'parsoid-resource-limit-exceeded' => [
|
|
|
|
|
Status::newFatal( 'parsoid-resource-limit-exceeded' ),
|
|
|
|
|
new LocalizedHttpException(
|
|
|
|
|
new MessageValue( 'rest-resource-limit-exceeded' ),
|
|
|
|
|
413,
|
|
|
|
|
[
|
|
|
|
|
'reason' => 'TEST_TEST'
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
];
|
|
|
|
|
yield 'missing-revision-permission' => [
|
|
|
|
|
Status::newFatal( 'missing-revision-permission' ),
|
|
|
|
|
new LocalizedHttpException(
|
|
|
|
|
new MessageValue( 'rest-permission-denied-revision' ),
|
|
|
|
|
403,
|
|
|
|
|
[
|
|
|
|
|
'reason' => 'missing-revision-permission'
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
];
|
|
|
|
|
yield 'parsoid-revision-access' => [
|
|
|
|
|
Status::newFatal( 'parsoid-revision-access' ),
|
|
|
|
|
new LocalizedHttpException(
|
|
|
|
|
new MessageValue( 'rest-specified-revision-unavailable' ),
|
|
|
|
|
404,
|
|
|
|
|
[
|
|
|
|
|
'reason' => 'parsoid-revision-access'
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideParsoidOutputStatus
|
|
|
|
|
*/
|
|
|
|
|
public function testParsoidOutputStatus(
|
2024-05-15 16:57:56 +00:00
|
|
|
Status $parserOutputStatus,
|
2024-04-11 10:00:34 +00:00
|
|
|
Exception $expectedException
|
|
|
|
|
) {
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
2024-05-15 16:57:56 +00:00
|
|
|
$parserAccess = $this->createNoOpMock( ParserOutputAccess::class, [ 'getParserOutput' ] );
|
|
|
|
|
$parserAccess->method( 'getParserOutput' )
|
|
|
|
|
->willReturn( $parserOutputStatus );
|
|
|
|
|
|
|
|
|
|
$helper = $this->newHelper( [
|
|
|
|
|
'ParserOutputAccess' => $parserAccess,
|
2024-06-05 15:49:49 +00:00
|
|
|
], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2024-04-11 10:00:34 +00:00
|
|
|
|
|
|
|
|
$this->expectExceptionObject( $expectedException );
|
|
|
|
|
$helper->getHtml();
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-14 19:09:53 +00:00
|
|
|
public function testWillUseParserCache() {
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
// NOTE: Use a simple PageIdentity here, to make sure the relevant PageRecord
|
|
|
|
|
// will be looked up as needed.
|
|
|
|
|
$page = PageIdentityValue::localIdentity( $page->getId(), $page->getNamespace(), $page->getDBkey() );
|
|
|
|
|
|
|
|
|
|
// This is the key assertion in this test case: get() and save() are both called.
|
2024-02-18 13:33:18 +00:00
|
|
|
$parserCache = $this->createNoOpMock( ParserCache::class, [ 'get', 'save', 'makeParserOutputKey' ] );
|
2023-10-23 20:10:05 +00:00
|
|
|
$parserCache->expects( $this->once() )->method( 'get' )->willReturn( false );
|
2022-12-14 19:09:53 +00:00
|
|
|
$parserCache->expects( $this->once() )->method( 'save' );
|
2023-08-29 20:13:43 +00:00
|
|
|
$parserCache->expects( $this->atLeastOnce() )->method( 'makeParserOutputKey' );
|
2022-12-14 19:09:53 +00:00
|
|
|
|
2023-08-29 20:13:43 +00:00
|
|
|
$this->resetServicesWithMockedParsoid();
|
2024-05-15 16:57:56 +00:00
|
|
|
$access = $this->newRealParserOutputAccess( [
|
2022-12-14 19:09:53 +00:00
|
|
|
'parserCache' => $parserCache,
|
2023-08-29 20:13:43 +00:00
|
|
|
'revisionCache' => $this->createNoOpMock( RevisionOutputCache::class )
|
2022-12-14 19:09:53 +00:00
|
|
|
] );
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( $access, $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-12-14 19:09:53 +00:00
|
|
|
|
|
|
|
|
$helper->getHtml();
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-24 19:58:56 +00:00
|
|
|
public function testDisableParserCacheWrite() {
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
// NOTE: The save() method is not supported and will throw!
|
|
|
|
|
// The point of this test case is asserting that save() isn't called.
|
2024-02-18 13:33:18 +00:00
|
|
|
$parserCache = $this->createNoOpMock( ParserCache::class, [ 'get', 'makeParserOutputKey' ] );
|
2022-11-24 19:58:56 +00:00
|
|
|
$parserCache->method( 'get' )->willReturn( false );
|
2023-08-29 20:13:43 +00:00
|
|
|
$parserCache->expects( $this->atLeastOnce() )->method( 'makeParserOutputKey' );
|
2022-11-24 19:58:56 +00:00
|
|
|
|
2023-08-29 20:13:43 +00:00
|
|
|
$this->resetServicesWithMockedParsoid();
|
2024-05-15 16:57:56 +00:00
|
|
|
$access = $this->newRealParserOutputAccess( [
|
2022-11-24 19:58:56 +00:00
|
|
|
'parserCache' => $parserCache,
|
|
|
|
|
'revisionCache' => $this->createNoOpMock( RevisionOutputCache::class ),
|
|
|
|
|
] );
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( $access, $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-11-24 19:58:56 +00:00
|
|
|
|
|
|
|
|
// Set read = true, write = false
|
|
|
|
|
$helper->setUseParserCache( true, false );
|
|
|
|
|
$helper->getHtml();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDisableParserCacheRead() {
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
// NOTE: The get() method is not supported and will throw!
|
|
|
|
|
// The point of this test case is asserting that get() isn't called.
|
|
|
|
|
// We also check that save() is still called.
|
2024-02-18 13:33:18 +00:00
|
|
|
$parserCache = $this->createNoOpMock( ParserCache::class, [ 'save', 'makeParserOutputKey' ] );
|
2022-11-24 19:58:56 +00:00
|
|
|
$parserCache->expects( $this->once() )->method( 'save' );
|
2023-08-29 20:13:43 +00:00
|
|
|
$parserCache->expects( $this->atLeastOnce() )->method( 'makeParserOutputKey' );
|
2022-11-24 19:58:56 +00:00
|
|
|
|
2023-08-29 20:13:43 +00:00
|
|
|
$this->resetServicesWithMockedParsoid();
|
2024-05-15 16:57:56 +00:00
|
|
|
$access = $this->newRealParserOutputAccess( [
|
2022-11-24 19:58:56 +00:00
|
|
|
'parserCache' => $parserCache,
|
|
|
|
|
'revisionCache' => $this->createNoOpMock( RevisionOutputCache::class ),
|
|
|
|
|
] );
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( $access, $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-11-24 19:58:56 +00:00
|
|
|
|
|
|
|
|
// Set read = false, write = true
|
|
|
|
|
$helper->setUseParserCache( false, true );
|
|
|
|
|
$helper->getHtml();
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
public function testGetParserOutputWithLanguageOverride() {
|
2022-09-01 10:03:03 +00:00
|
|
|
[ $page, $revision ] = $this->getNonExistingPageWithFakeRevision( __METHOD__ );
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [], $page, [], $this->newAuthority(), $revision );
|
2022-11-24 21:54:11 +00:00
|
|
|
$helper->setPageLanguage( 'ar' );
|
2022-09-01 10:03:03 +00:00
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
// check nominal content language
|
Use Bcp47Code when interfacing with Parsoid
It is very easy for developers and maintainers to mix up "internal
MediaWiki language codes" and "BCP-47 language codes"; the latter are
standards-compliant and used in web protocols like HTTP, HTML, and
SVG; but much of WMF production is very dependent on historical codes
used by MediaWiki which in some cases predate the IANA standardized
name for the language in question.
Phan and other static checking tools aren't much help distinguishing
BCP-47 from internal codes when both are represented with the PHP
string type, so the wikimedia/bcp-47-code package introduced a very
lightweight wrapper type in order to uniquely identify BCP-47 codes.
Language implements Bcp47Code, and LanguageFactory::getLanguage() is
an easy way to convert (or downcast) between Bcp47Code and Language
objects.
This patch updates the Parsoid integration code and the associated
REST handlers to use Bcp47Code in APIs so that the standalone Parsoid
library does not need to know anything about MediaWiki-internal codes.
The principle has been, first, to try to convert a string to a
Bcp47Code as soon as possible and as close to the original input as
possible, so it is easy to see *why* a given string is a BCP-47 code
(usually, because it is coming from HTTP/HTML/etc) and we're not stuck
deep inside some method trying to figure out where a string we're
given is coming from and therefore what sort of string code it might
be. Second, we've added explicit compatibility code to accept
MediaWiki internal codes and convert them to Bcp47Code for backward
compatibility with existing clients, using the @internal
LanguageCode::normalizeNonstandardCodeAndWarn() method. The intention
is to gradually remove these backward compatibility thunks and replace
them with HTTP 400 errors or wfDeprecated messages in order to
identify and repair callers who are incorrectly using
non-standard-compliant language codes in web standards
(HTTP/HTML/SVG/etc).
Finally, maintaining a code as a Bcp47Code and not immediately
converting to Language helps us delay or even avoid full loading of a
Language object in some cases, which is another reason to occasionally
push Bcp47Code (instead of Language) down the call stack.
Bug: T327379
Depends-On: I830867d58f8962d6a57be16ce3735e8384f9ac1c
Change-Id: I982e0df706a633b05dcc02b5220b737c19adc401
2022-11-04 17:29:23 +00:00
|
|
|
$this->assertSame( 'ar', $helper->getHtmlOutputContentLanguage()->toBcp47Code() );
|
2022-09-01 10:03:03 +00:00
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
// check content language in HTML
|
|
|
|
|
$output = $helper->getHtml();
|
|
|
|
|
$html = $output->getRawText();
|
|
|
|
|
$this->assertStringContainsString( 'lang="ar"', $html );
|
|
|
|
|
}
|
2022-09-01 10:03:03 +00:00
|
|
|
|
2023-04-26 19:12:23 +00:00
|
|
|
public function testGetParserOutputWithRedundantPageLanguage() {
|
2024-05-15 16:57:56 +00:00
|
|
|
$poa = $this->createMock( ParserOutputAccess::class );
|
2023-04-26 19:12:23 +00:00
|
|
|
$poa->expects( $this->once() )
|
|
|
|
|
->method( 'getParserOutput' )
|
|
|
|
|
->willReturnCallback( function (
|
|
|
|
|
PageIdentity $page,
|
|
|
|
|
ParserOptions $parserOpts,
|
|
|
|
|
$revision = null,
|
|
|
|
|
int $options = 0
|
|
|
|
|
) {
|
|
|
|
|
$usedOptions = [ 'targetLanguage' ];
|
|
|
|
|
self::assertNull( $parserOpts->getTargetLanguage(), 'No target language should be set in ParserOptions' );
|
|
|
|
|
self::assertTrue( $parserOpts->isSafeToCache( $usedOptions ) );
|
|
|
|
|
|
|
|
|
|
$html = $this->getMockHtml( $revision );
|
|
|
|
|
$pout = $this->makeParserOutput( $parserOpts, $html, $revision, $page );
|
|
|
|
|
return Status::newGood( $pout );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
$page = $this->getExistingTestPage();
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [ 'ParserOutputAccess' => $poa ], $page, [], $this->newAuthority() );
|
2023-04-26 19:12:23 +00:00
|
|
|
|
|
|
|
|
// Explicitly set the page language to the default.
|
|
|
|
|
$pageLanguage = $page->getTitle()->getPageLanguage();
|
|
|
|
|
$helper->setPageLanguage( $pageLanguage );
|
|
|
|
|
|
|
|
|
|
// Trigger parsing, so the assertions in the mock are executed.
|
|
|
|
|
$helper->getHtml();
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
public function provideInit() {
|
|
|
|
|
$page = PageIdentityValue::localIdentity( 7, NS_MAIN, 'Köfte' );
|
2023-11-09 13:32:23 +00:00
|
|
|
$authority = $this->createNoOpMock( Authority::class );
|
2022-09-01 10:03:03 +00:00
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
yield 'Minimal' => [
|
|
|
|
|
$page,
|
|
|
|
|
[],
|
2023-11-09 13:32:23 +00:00
|
|
|
$authority,
|
2022-10-07 14:27:01 +00:00
|
|
|
null,
|
|
|
|
|
[
|
|
|
|
|
'page' => $page,
|
2023-11-09 13:32:23 +00:00
|
|
|
'authority' => $authority,
|
2022-10-07 14:27:01 +00:00
|
|
|
'revisionOrId' => null,
|
|
|
|
|
'stash' => false,
|
|
|
|
|
'flavor' => 'view',
|
|
|
|
|
]
|
|
|
|
|
];
|
2022-09-01 10:03:03 +00:00
|
|
|
|
2023-06-16 19:48:03 +00:00
|
|
|
$rev = $this->createNoOpMock( RevisionRecord::class, [ 'getId' ] );
|
|
|
|
|
$rev->method( 'getId' )->willReturn( 7 );
|
|
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
yield 'Revision and Language' => [
|
|
|
|
|
$page,
|
|
|
|
|
[],
|
2023-11-09 13:32:23 +00:00
|
|
|
$authority,
|
2022-10-07 14:27:01 +00:00
|
|
|
$rev,
|
|
|
|
|
[
|
|
|
|
|
'revisionOrId' => $rev,
|
|
|
|
|
]
|
|
|
|
|
];
|
2022-09-01 10:03:03 +00:00
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
yield 'revid and stash' => [
|
|
|
|
|
$page,
|
|
|
|
|
[ 'stash' => true ],
|
2023-11-09 13:32:23 +00:00
|
|
|
$authority,
|
2022-10-07 14:27:01 +00:00
|
|
|
8,
|
|
|
|
|
[
|
|
|
|
|
'stash' => true,
|
|
|
|
|
'flavor' => 'stash',
|
|
|
|
|
'revisionOrId' => 8,
|
|
|
|
|
]
|
|
|
|
|
];
|
2022-09-27 09:54:01 +00:00
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
yield 'flavor' => [
|
|
|
|
|
$page,
|
|
|
|
|
[ 'flavor' => 'fragment' ],
|
2023-11-09 13:32:23 +00:00
|
|
|
$authority,
|
2022-10-07 14:27:01 +00:00
|
|
|
8,
|
|
|
|
|
[
|
|
|
|
|
'flavor' => 'fragment',
|
|
|
|
|
]
|
|
|
|
|
];
|
2022-09-27 09:54:01 +00:00
|
|
|
|
2022-10-07 14:27:01 +00:00
|
|
|
yield 'stash winds over flavor' => [
|
|
|
|
|
$page,
|
|
|
|
|
[ 'flavor' => 'fragment', 'stash' => true ],
|
2023-11-09 13:32:23 +00:00
|
|
|
$authority,
|
2022-10-07 14:27:01 +00:00
|
|
|
8,
|
|
|
|
|
[
|
|
|
|
|
'flavor' => 'stash',
|
|
|
|
|
]
|
|
|
|
|
];
|
2022-09-27 09:54:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2022-10-07 14:27:01 +00:00
|
|
|
* Whitebox test for ensuring that init() sets the correct members.
|
|
|
|
|
* Testing init() against behavior would mean duplicating all tests that use setters.
|
|
|
|
|
*
|
|
|
|
|
* @param PageIdentity $page
|
|
|
|
|
* @param array $parameters
|
2023-11-09 13:32:23 +00:00
|
|
|
* @param Authority $authority
|
2022-10-07 14:27:01 +00:00
|
|
|
* @param RevisionRecord|int|null $revision
|
|
|
|
|
* @param array $expected
|
|
|
|
|
*
|
|
|
|
|
* @dataProvider provideInit
|
2022-09-27 09:54:01 +00:00
|
|
|
*/
|
2022-10-07 14:27:01 +00:00
|
|
|
public function testInit(
|
|
|
|
|
PageIdentity $page,
|
|
|
|
|
array $parameters,
|
2023-11-09 13:32:23 +00:00
|
|
|
Authority $authority,
|
2022-10-07 14:27:01 +00:00
|
|
|
$revision,
|
|
|
|
|
array $expected
|
|
|
|
|
) {
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [], $page, $parameters, $authority, $revision );
|
2022-10-07 14:27:01 +00:00
|
|
|
|
|
|
|
|
$wrapper = TestingAccessWrapper::newFromObject( $helper );
|
|
|
|
|
foreach ( $expected as $name => $value ) {
|
|
|
|
|
$this->assertSame( $value, $wrapper->$name );
|
|
|
|
|
}
|
2022-09-27 09:54:01 +00:00
|
|
|
}
|
|
|
|
|
|
2022-10-10 14:46:54 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider providePutHeaders
|
|
|
|
|
*/
|
|
|
|
|
public function testPutHeaders( ?string $targetLanguage, bool $setContentLanguageHeader ) {
|
2023-07-27 02:36:17 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::UsePigLatinVariant, true );
|
2022-10-10 14:46:54 +00:00
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
$expectedCalls = [];
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [], $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2022-10-10 14:46:54 +00:00
|
|
|
|
|
|
|
|
if ( $targetLanguage ) {
|
Use Bcp47Code when interfacing with Parsoid
It is very easy for developers and maintainers to mix up "internal
MediaWiki language codes" and "BCP-47 language codes"; the latter are
standards-compliant and used in web protocols like HTTP, HTML, and
SVG; but much of WMF production is very dependent on historical codes
used by MediaWiki which in some cases predate the IANA standardized
name for the language in question.
Phan and other static checking tools aren't much help distinguishing
BCP-47 from internal codes when both are represented with the PHP
string type, so the wikimedia/bcp-47-code package introduced a very
lightweight wrapper type in order to uniquely identify BCP-47 codes.
Language implements Bcp47Code, and LanguageFactory::getLanguage() is
an easy way to convert (or downcast) between Bcp47Code and Language
objects.
This patch updates the Parsoid integration code and the associated
REST handlers to use Bcp47Code in APIs so that the standalone Parsoid
library does not need to know anything about MediaWiki-internal codes.
The principle has been, first, to try to convert a string to a
Bcp47Code as soon as possible and as close to the original input as
possible, so it is easy to see *why* a given string is a BCP-47 code
(usually, because it is coming from HTTP/HTML/etc) and we're not stuck
deep inside some method trying to figure out where a string we're
given is coming from and therefore what sort of string code it might
be. Second, we've added explicit compatibility code to accept
MediaWiki internal codes and convert them to Bcp47Code for backward
compatibility with existing clients, using the @internal
LanguageCode::normalizeNonstandardCodeAndWarn() method. The intention
is to gradually remove these backward compatibility thunks and replace
them with HTTP 400 errors or wfDeprecated messages in order to
identify and repair callers who are incorrectly using
non-standard-compliant language codes in web standards
(HTTP/HTML/SVG/etc).
Finally, maintaining a code as a Bcp47Code and not immediately
converting to Language helps us delay or even avoid full loading of a
Language object in some cases, which is another reason to occasionally
push Bcp47Code (instead of Language) down the call stack.
Bug: T327379
Depends-On: I830867d58f8962d6a57be16ce3735e8384f9ac1c
Change-Id: I982e0df706a633b05dcc02b5220b737c19adc401
2022-11-04 17:29:23 +00:00
|
|
|
$helper->setVariantConversionLanguage( new Bcp47CodeValue( $targetLanguage ) );
|
2022-10-10 14:46:54 +00:00
|
|
|
$expectedCalls['addHeader'] = [ [ 'Vary', 'Accept-Language' ] ];
|
2023-06-16 19:48:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $setContentLanguageHeader ) {
|
|
|
|
|
$expectedCalls['setHeader'][] = [ 'Content-Language', $targetLanguage ?: 'en' ];
|
2022-10-10 14:46:54 +00:00
|
|
|
|
2023-06-16 19:48:03 +00:00
|
|
|
$version = Parsoid::defaultHTMLVersion();
|
|
|
|
|
$expectedCalls['setHeader'][] = [
|
|
|
|
|
'Content-Type',
|
|
|
|
|
'text/html; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/HTML/' . $version . '"',
|
|
|
|
|
];
|
2022-10-10 14:46:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$responseInterface = $this->getResponseInterfaceMock( $expectedCalls );
|
|
|
|
|
$helper->putHeaders( $responseInterface, $setContentLanguageHeader );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function providePutHeaders() {
|
2022-10-10 14:46:54 +00:00
|
|
|
yield 'no target variant language' => [ null, true ];
|
|
|
|
|
yield 'target language is set but setContentLanguageHeader is false' => [ 'en-x-piglatin', false ];
|
|
|
|
|
yield 'target language and setContentLanguageHeader flag is true' =>
|
|
|
|
|
[ 'en-x-piglatin', true ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function getResponseInterfaceMock( array $expectedCalls ) {
|
|
|
|
|
$responseInterface = $this->createNoOpMock( ResponseInterface::class, array_keys( $expectedCalls ) );
|
2024-01-17 17:15:38 +00:00
|
|
|
foreach ( $expectedCalls as $method => $arguments ) {
|
2022-10-10 14:46:54 +00:00
|
|
|
$responseInterface
|
2024-01-17 17:15:38 +00:00
|
|
|
->expects( $this->exactly( count( $arguments ) ) )
|
2022-10-10 14:46:54 +00:00
|
|
|
->method( $method )
|
2024-01-17 17:15:38 +00:00
|
|
|
->willReturnCallback( function ( ...$actualArgs ) use ( $arguments ) {
|
|
|
|
|
static $expectedArgs;
|
|
|
|
|
if ( $expectedArgs === null ) {
|
|
|
|
|
$expectedArgs = $arguments;
|
|
|
|
|
}
|
|
|
|
|
$this->assertContains( $actualArgs, $expectedArgs );
|
|
|
|
|
$argIdx = array_search( $actualArgs, $expectedArgs, true );
|
|
|
|
|
unset( $expectedArgs[$argIdx] );
|
|
|
|
|
} );
|
2022-10-10 14:46:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $responseInterface;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideFlavorsForBadModelOutput() {
|
2023-01-09 10:45:25 +00:00
|
|
|
yield 'view' => [ 'view' ];
|
|
|
|
|
yield 'edit' => [ 'edit' ];
|
2023-08-29 20:13:43 +00:00
|
|
|
// fragment mode is only for posted wikitext fragments not part of a revision
|
|
|
|
|
// and should not be used with real revisions
|
|
|
|
|
//
|
|
|
|
|
// yield 'fragment' => [ 'fragment' ];
|
2023-01-09 10:45:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideFlavorsForBadModelOutput
|
|
|
|
|
*/
|
2024-03-06 09:04:24 +00:00
|
|
|
public function testNonParsoidOutput( string $flavor ) {
|
2023-08-29 20:13:43 +00:00
|
|
|
$this->resetServicesWithMockedParsoid();
|
2023-01-09 10:45:25 +00:00
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, new CssContent( '"not wikitext"' ) );
|
|
|
|
|
|
2024-06-05 15:49:49 +00:00
|
|
|
$helper = $this->newHelper( [
|
|
|
|
|
'cache' => new HashBagOStuff(),
|
|
|
|
|
] + $this->newRealParserOutputAccess(), $page, self::PARAM_DEFAULTS, $this->newAuthority() );
|
2023-01-09 10:45:25 +00:00
|
|
|
$helper->setFlavor( $flavor );
|
|
|
|
|
|
|
|
|
|
$output = $helper->getHtml();
|
2024-03-06 09:04:24 +00:00
|
|
|
$this->assertStringContainsString( 'not wikitext', $output->getRawText() );
|
|
|
|
|
$this->assertNotNull( ParsoidRenderID::newFromParserOutput( $output )->getKey() );
|
2023-01-09 10:45:25 +00:00
|
|
|
}
|
|
|
|
|
|
2020-12-03 17:53:55 +00:00
|
|
|
}
|