3 namespace MediaWiki\Tests\Parser
;
5 use DummyContentForTesting
;
6 use InvalidArgumentException
;
7 use MediaWiki\Context\DerivativeContext
;
8 use MediaWiki\Context\IContextSource
;
9 use MediaWiki\Context\RequestContext
;
10 use MediaWiki\HookContainer\HookContainer
;
11 use MediaWiki\MainConfigNames
;
12 use MediaWiki\Parser\ParserOptions
;
13 use MediaWiki\Request\FauxRequest
;
14 use MediaWiki\Revision\SlotRecord
;
15 use MediaWiki\Tests\User\TempUser\TempUserTestTrait
;
16 use MediaWiki\Title\Title
;
17 use MediaWiki\User\User
;
18 use MediaWiki\User\UserIdentity
;
19 use MediaWiki\User\UserIdentityValue
;
20 use MediaWikiLangTestCase
;
22 use Wikimedia\ScopedCallback
;
25 * @covers \MediaWiki\Parser\ParserOptions
28 class ParserOptionsTest
extends MediaWikiLangTestCase
{
30 use TempUserTestTrait
;
32 protected function setUp(): void
{
35 $this->overrideConfigValues( [
36 MainConfigNames
::RenderHashAppend
=> '',
37 MainConfigNames
::UsePigLatinVariant
=> false,
39 $this->setTemporaryHook( 'PageRenderingHash', HookContainer
::NOOP
);
42 protected function tearDown(): void
{
43 ParserOptions
::clearStaticCache();
47 public function testNewCanonical() {
48 $user = $this->createMock( User
::class );
49 $userLang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'fr' );
50 $contLang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'qqx' );
52 $this->setContentLang( $contLang );
53 $this->setUserLang( $userLang );
55 $lang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'de' );
56 $lang2 = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'bug' );
57 $context = new DerivativeContext( RequestContext
::getMain() );
58 $context->setUser( $user );
59 $context->setLanguage( $lang );
61 // Just a user uses $wgLang
62 $popt = ParserOptions
::newCanonical( $user );
63 $this->assertSame( $user, $popt->getUserIdentity() );
64 $this->assertSame( $userLang, $popt->getUserLangObj() );
67 $popt = ParserOptions
::newCanonical( $user, $lang );
68 $this->assertSame( $user, $popt->getUserIdentity() );
69 $this->assertSame( $lang, $popt->getUserLangObj() );
71 // Passing 'canonical' uses an anon and $contLang, and ignores any passed $userLang
72 $popt = ParserOptions
::newFromAnon();
73 $this->assertTrue( $popt->getUserIdentity()->isAnon() );
74 $this->assertSame( $contLang, $popt->getUserLangObj() );
75 $popt = ParserOptions
::newCanonical( 'canonical', $lang2 );
76 $this->assertSame( $contLang, $popt->getUserLangObj() );
78 // Passing an IContextSource uses the user and lang from it, and ignores
79 // any passed $userLang
80 $popt = ParserOptions
::newCanonical( $context );
81 $this->assertSame( $user, $popt->getUserIdentity() );
82 $this->assertSame( $lang, $popt->getUserLangObj() );
83 $popt = ParserOptions
::newCanonical( $context, $lang2 );
84 $this->assertSame( $lang, $popt->getUserLangObj() );
86 // Passing something else raises an exception
88 $popt = ParserOptions
::newCanonical( 'bogus' );
89 $this->fail( 'Excpected exception not thrown' );
90 } catch ( InvalidArgumentException
$ex ) {
94 private function commonTestNewFromContext( IContextSource
$context, UserIdentity
$expectedUser ) {
95 $popt = ParserOptions
::newFromContext( $context );
96 $this->assertTrue( $expectedUser->equals( $popt->getUserIdentity() ) );
97 $this->assertSame( $context->getLanguage(), $popt->getUserLangObj() );
100 /** @dataProvider provideNewFromContext */
101 public function testNewFromContext( $contextUserIdentity, $contextLanguage ) {
102 $this->enableAutoCreateTempUser();
103 // Get a context which has our provided user and language set, then call ::newFromContext with it.
104 $context = new DerivativeContext( RequestContext
::getMain() );
106 $this->getServiceContainer()->getUserFactory()->newFromUserIdentity( $contextUserIdentity )
108 $context->setLanguage( $contextLanguage );
109 $this->commonTestNewFromContext( $context, $context->getUser() );
112 public static function provideNewFromContext() {
114 'Username does not exist and user lang as en' => [ UserIdentityValue
::newAnonymous( 'Testabc' ), 'en' ],
115 'Username is IP address, no stashed temporary username, and user lang as qqx' => [
116 UserIdentityValue
::newAnonymous( '1.2.3.4' ), 'qqx',
121 public function testNewFromContextForNamedAccount() {
122 $this->testNewFromContext( $this->getTestUser()->getUser(), 'qqx' );
125 public function testNewFromContextForTemporaryAccount() {
126 $this->testNewFromContext(
127 $this->getServiceContainer()->getTempUserCreator()
128 ->create( null, new FauxRequest() )->getUser(),
133 public function testNewFromContextForAnonWhenTempNameStashed() {
134 $this->enableAutoCreateTempUser();
135 // Get a context which uses an anon user as the user.
136 $context = new DerivativeContext( RequestContext
::getMain() );
138 $this->getServiceContainer()->getUserFactory()
139 ->newFromUserIdentity( UserIdentityValue
::newAnonymous( '1.2.3.4' ) )
141 // Create a temporary account name and stash it in associated Session for the $context
142 $stashedName = $this->getServiceContainer()->getTempUserCreator()
143 ->acquireAndStashName( $context->getRequest()->getSession() );
144 // Call ::newFromContext and expect that that stashed name is used
145 $this->commonTestNewFromContext( $context, UserIdentityValue
::newAnonymous( $stashedName ) );
148 public function testNewFromContextForAnonWhenTempNameStashedButFeatureSinceDisabled() {
149 $this->enableAutoCreateTempUser();
150 // Get a context which uses an anon user as the user.
151 $context = new DerivativeContext( RequestContext
::getMain() );
153 $this->getServiceContainer()->getUserFactory()
154 ->newFromUserIdentity( UserIdentityValue
::newAnonymous( '1.2.3.4' ) )
156 // Create a temporary account name and stash it in associated Session for the $context
157 $this->getServiceContainer()->getTempUserCreator()
158 ->acquireAndStashName( $context->getRequest()->getSession() );
159 // Simulate that in the interim the temporary accounts system has been disabled, and check that an IP
160 // address is used in this case
161 $this->disableAutoCreateTempUser( [ 'known' => true ] );
162 // Call ::newFromContext and expect that that stashed name is used
163 $this->commonTestNewFromContext( $context, UserIdentityValue
::newAnonymous( '1.2.3.4' ) );
167 * @dataProvider provideIsSafeToCache
168 * @param bool $expect Expected value
169 * @param array $options Options to set
170 * @param array|null $usedOptions
172 public function testIsSafeToCache( bool $expect, array $options, ?
array $usedOptions = null ) {
173 $popt = ParserOptions
::newFromAnon();
174 foreach ( $options as $name => $value ) {
175 $popt->setOption( $name, $value );
177 $this->assertSame( $expect, $popt->isSafeToCache( $usedOptions ) );
180 public static function provideIsSafeToCache() {
181 $seven = static function () {
186 'No overrides' => [ true, [] ],
187 'No overrides, some used' => [ true, [], [ 'thumbsize', 'removeComments' ] ],
188 'In-key options are ok' => [ true, [
189 'thumbsize' => 1e100
,
190 'printable' => false,
192 'In-key options are ok, some used' => [ true, [
193 'thumbsize' => 1e100
,
194 'printable' => false,
195 ], [ 'thumbsize', 'removeComments' ] ],
196 'Non-in-key options are not ok' => [ false, [
197 'removeComments' => false,
199 'Non-in-key options are not ok, used' => [ false, [
200 'removeComments' => false,
201 ], [ 'removeComments' ] ],
202 'Non-in-key options are ok if other used' => [ true, [
203 'removeComments' => false,
204 ], [ 'thumbsize' ] ],
205 'Non-in-key options are ok if nothing used' => [ true, [
206 'removeComments' => false,
208 'Unknown used options do not crash' => [ true, [
210 'Non-in-key options are not ok (2)' => [ false, [
211 'wrapclass' => 'foobar',
213 'Callback not default' => [ true, [
214 'speculativeRevIdCallback' => $seven,
220 * @dataProvider provideOptionsHash
221 * @param array $usedOptions
222 * @param string $expect Expected value
223 * @param array $options Options to set
224 * @param array $globals Globals to set
225 * @param callable|null $hookFunc PageRenderingHash hook function
227 public function testOptionsHash(
228 $usedOptions, $expect, $options, $globals = [], $hookFunc = null
230 $this->overrideConfigValues( $globals );
231 $this->setTemporaryHook( 'PageRenderingHash', $hookFunc ?
: HookContainer
::NOOP
);
233 $popt = ParserOptions
::newFromAnon();
234 foreach ( $options as $name => $value ) {
235 $popt->setOption( $name, $value );
237 $this->assertSame( $expect, $popt->optionsHash( $usedOptions ) );
240 public static function provideOptionsHash() {
241 $used = [ 'thumbsize', 'printable' ];
243 $allUsableOptions = array_diff(
244 ParserOptions
::allCacheVaryingOptions(),
245 array_keys( ParserOptions
::getLazyOptions() )
249 'Canonical options, nothing used' => [ [], 'canonical', [] ],
250 'Canonical options, used some options' => [ $used, 'canonical', [] ],
251 'Canonical options, used some more options' => [ array_merge( $used, [ 'wrapclass' ] ), 'canonical', [] ],
252 'Used some options, non-default values' => [
254 'printable=1!thumbsize=200',
261 'Canonical options, used all non-lazy options' => [ $allUsableOptions, 'canonical', [] ],
262 'Canonical options, nothing used, but with hooks and $wgRenderHashAppend' => [
264 'canonical!wgRenderHashAppend!onPageRenderingHash',
266 [ MainConfigNames
::RenderHashAppend
=> '!wgRenderHashAppend' ],
267 __CLASS__
. '::onPageRenderingHash',
272 public function testUsedLazyOptionsInHash() {
273 $this->setTemporaryHook( 'ParserOptionsRegister',
274 function ( &$defaults, &$inCacheKey, &$lazyOptions ) {
275 $lazyFuncs = $this->getMockBuilder( stdClass
::class )
276 ->addMethods( [ 'neverCalled', 'calledOnce' ] )
278 $lazyFuncs->expects( $this->never() )->method( 'neverCalled' );
279 $lazyFuncs->expects( $this->once() )->method( 'calledOnce' )->willReturn( 'value' );
291 'opt1' => [ $lazyFuncs, 'calledOnce' ],
292 'opt2' => [ $lazyFuncs, 'neverCalled' ],
293 'opt3' => [ $lazyFuncs, 'neverCalled' ],
298 ParserOptions
::clearStaticCache();
300 $popt = ParserOptions
::newFromAnon();
301 $popt->registerWatcher( function () {
302 $this->fail( 'Watcher should not have been called' );
304 $this->assertSame( 'opt1=value', $popt->optionsHash( [ 'opt1', 'opt3' ] ) );
306 // Second call to see that opt1 isn't resolved a second time
307 $this->assertSame( 'opt1=value', $popt->optionsHash( [ 'opt1', 'opt3' ] ) );
310 public function testLazyOptionWithDefault() {
312 $this->setTemporaryHook(
313 'ParserOptionsRegister',
314 static function ( &$defaults, &$inCacheKey, &$lazyLoad ) use ( &$loaded ) {
315 $defaults['test_option'] = 'default!';
316 $inCacheKey['test_option'] = true;
317 $lazyLoad['test_option'] = static function () use ( &$loaded ) {
324 $po = ParserOptions
::newFromAnon();
325 $this->assertSame( 'default!', $po->getOption( 'test_option' ) );
326 $this->assertTrue( $loaded );
329 $po->optionsHash( [ 'test_option' ], Title
::makeTitle( NS_MAIN
, 'Test' ) )
333 public static function onPageRenderingHash( &$confstr ) {
334 $confstr .= '!onPageRenderingHash';
337 public function testGetInvalidOption() {
338 $popt = ParserOptions
::newFromAnon();
339 $this->expectException( InvalidArgumentException
::class );
340 $this->expectExceptionMessage( "Unknown parser option bogus" );
341 $popt->getOption( 'bogus' );
344 public function testSetInvalidOption() {
345 $popt = ParserOptions
::newFromAnon();
346 $this->expectException( InvalidArgumentException
::class );
347 $this->expectExceptionMessage( "Unknown parser option bogus" );
348 $popt->setOption( 'bogus', true );
351 public function testMatches() {
352 $popt1 = ParserOptions
::newFromAnon();
353 $popt2 = ParserOptions
::newFromAnon();
354 $this->assertTrue( $popt1->matches( $popt2 ) );
356 $popt2->setInterfaceMessage( !$popt2->getInterfaceMessage() );
357 $this->assertFalse( $popt1->matches( $popt2 ) );
360 $this->setTemporaryHook( 'ParserOptionsRegister',
361 static function ( &$defaults, &$inCacheKey, &$lazyOptions ) use ( &$ctr ) {
362 $defaults['testMatches'] = null;
363 $lazyOptions['testMatches'] = static function () use ( &$ctr ) {
368 ParserOptions
::clearStaticCache();
370 $popt1 = ParserOptions
::newFromAnon();
371 $popt2 = ParserOptions
::newFromAnon();
372 $this->assertFalse( $popt1->matches( $popt2 ) );
374 ScopedCallback
::consume( $reset );
378 * This test fails if tearDown() does not call ParserOptions::clearStaticCache(),
379 * because the lazy option from the hook in the previous test remains active.
381 public function testTeardownClearedCache() {
382 $popt1 = ParserOptions
::newFromAnon();
383 $popt2 = ParserOptions
::newFromAnon();
384 $this->assertTrue( $popt1->matches( $popt2 ) );
387 public function testMatchesForCacheKey() {
388 $user = new UserIdentityValue( 0, '127.0.0.1' );
389 $cOpts = ParserOptions
::newCanonical(
391 $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'en' )
394 $uOpts = ParserOptions
::newFromAnon();
395 $this->assertTrue( $cOpts->matchesForCacheKey( $uOpts ) );
397 $uOpts = ParserOptions
::newFromUser( $user );
398 $this->assertTrue( $cOpts->matchesForCacheKey( $uOpts ) );
400 $this->getServiceContainer()
401 ->getUserOptionsManager()
402 ->setOption( $user, 'thumbsize', 251 );
403 $uOpts = ParserOptions
::newFromUser( $user );
404 $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
406 $this->getServiceContainer()
407 ->getUserOptionsManager()
408 ->setOption( $user, 'stubthreshold', 800 );
409 $uOpts = ParserOptions
::newFromUser( $user );
410 $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
412 $uOpts = ParserOptions
::newFromUserAndLang(
414 $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'zh' )
416 $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
419 public function testAllCacheVaryingOptions() {
420 $this->setTemporaryHook( 'ParserOptionsRegister', HookContainer
::NOOP
);
422 'collapsibleSections',
423 'dateformat', 'printable', 'suppressSectionEditLinks',
424 'thumbsize', 'useParsoid', 'userlang',
425 ], ParserOptions
::allCacheVaryingOptions() );
427 ParserOptions
::clearStaticCache();
429 $this->setTemporaryHook( 'ParserOptionsRegister', static function ( &$defaults, &$inCacheKey ) {
441 'collapsibleSections',
442 'dateformat', 'foo', 'printable', 'suppressSectionEditLinks',
443 'thumbsize', 'useParsoid', 'userlang',
444 ], ParserOptions
::allCacheVaryingOptions() );
447 public function testGetSpeculativeRevid() {
448 $options = ParserOptions
::newFromAnon();
450 $this->assertFalse( $options->getSpeculativeRevId() );
453 $options->setSpeculativeRevIdCallback( static function () use( &$counter ) {
457 // make sure the same value is re-used once it is determined
458 $this->assertSame( 1, $options->getSpeculativeRevId() );
459 $this->assertSame( 1, $options->getSpeculativeRevId() );
462 public function testSetupFakeRevision() {
463 $options = ParserOptions
::newFromAnon();
465 $page = Title
::newFromText( __METHOD__
);
466 $content = new DummyContentForTesting( '12345' );
467 $user = UserIdentityValue
::newRegistered( 123, 'TestTest' );
468 $fakeRevisionScope = $options->setupFakeRevision( $page, $content, $user );
470 $fakeRevision = $options->getCurrentRevisionRecordCallback()( $page );
471 $this->assertNotNull( $fakeRevision );
472 $this->assertSame( '12345', $fakeRevision->getContent( SlotRecord
::MAIN
)->getNativeData() );
473 $this->assertSame( $user, $fakeRevision->getUser() );
474 $this->assertTrue( $fakeRevision->getPage()->exists() );
476 ScopedCallback
::consume( $fakeRevisionScope );
477 $this->assertFalse( $options->getCurrentRevisionRecordCallback()( $page ) );
480 public function testRenderReason() {
481 $options = ParserOptions
::newFromAnon();
483 $this->assertIsString( $options->getRenderReason() );
485 $options->setRenderReason( 'just a test' );
486 $this->assertIsString( 'just a test', $options->getRenderReason() );
489 public function testSuppressSectionEditLinks() {
490 $options = ParserOptions
::newFromAnon();
492 $this->assertFalse( $options->getSuppressSectionEditLinks() );
494 $options->setSuppressSectionEditLinks();
495 $this->assertTrue( $options->getSuppressSectionEditLinks() );
498 public function testCollapsibleSections() {
499 $options = ParserOptions
::newFromAnon();
501 $this->assertFalse( $options->getCollapsibleSections() );
503 $options->setCollapsibleSections();
504 $this->assertTrue( $options->getCollapsibleSections() );