Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / parser / ParserOptionsTest.php
bloba4240dcbf0a7dd4d09ca68c3f4fdcec308bbede7
1 <?php
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\Page\PageIdentityValue;
13 use MediaWiki\Parser\ParserOptions;
14 use MediaWiki\Request\FauxRequest;
15 use MediaWiki\Revision\RevisionRecord;
16 use MediaWiki\Revision\SlotRecord;
17 use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
18 use MediaWiki\Title\Title;
19 use MediaWiki\Title\TitleValue;
20 use MediaWiki\User\User;
21 use MediaWiki\User\UserIdentity;
22 use MediaWiki\User\UserIdentityValue;
23 use MediaWikiLangTestCase;
24 use stdClass;
25 use Wikimedia\ScopedCallback;
27 /**
28 * @covers \MediaWiki\Parser\ParserOptions
29 * @group Database
31 class ParserOptionsTest extends MediaWikiLangTestCase {
33 use TempUserTestTrait;
35 protected function setUp(): void {
36 parent::setUp();
38 $this->overrideConfigValues( [
39 MainConfigNames::RenderHashAppend => '',
40 MainConfigNames::UsePigLatinVariant => false,
41 ] );
42 $this->setTemporaryHook( 'PageRenderingHash', HookContainer::NOOP );
45 protected function tearDown(): void {
46 ParserOptions::clearStaticCache();
47 parent::tearDown();
50 public function testNewCanonical() {
51 $user = $this->createMock( User::class );
52 $userLang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'fr' );
53 $contLang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'qqx' );
55 $this->setContentLang( $contLang );
56 $this->setUserLang( $userLang );
58 $lang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'de' );
59 $lang2 = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'bug' );
60 $context = new DerivativeContext( RequestContext::getMain() );
61 $context->setUser( $user );
62 $context->setLanguage( $lang );
64 // Just a user uses $wgLang
65 $popt = ParserOptions::newCanonical( $user );
66 $this->assertSame( $user, $popt->getUserIdentity() );
67 $this->assertSame( $userLang, $popt->getUserLangObj() );
69 // Passing both works
70 $popt = ParserOptions::newCanonical( $user, $lang );
71 $this->assertSame( $user, $popt->getUserIdentity() );
72 $this->assertSame( $lang, $popt->getUserLangObj() );
74 // Passing 'canonical' uses an anon and $contLang, and ignores any passed $userLang
75 $popt = ParserOptions::newFromAnon();
76 $this->assertTrue( $popt->getUserIdentity()->isAnon() );
77 $this->assertSame( $contLang, $popt->getUserLangObj() );
78 $popt = ParserOptions::newCanonical( 'canonical', $lang2 );
79 $this->assertSame( $contLang, $popt->getUserLangObj() );
81 // Passing an IContextSource uses the user and lang from it, and ignores
82 // any passed $userLang
83 $popt = ParserOptions::newCanonical( $context );
84 $this->assertSame( $user, $popt->getUserIdentity() );
85 $this->assertSame( $lang, $popt->getUserLangObj() );
86 $popt = ParserOptions::newCanonical( $context, $lang2 );
87 $this->assertSame( $lang, $popt->getUserLangObj() );
89 // Passing something else raises an exception
90 try {
91 $popt = ParserOptions::newCanonical( 'bogus' );
92 $this->fail( 'Excpected exception not thrown' );
93 } catch ( InvalidArgumentException $ex ) {
97 private function commonTestNewFromContext( IContextSource $context, UserIdentity $expectedUser ) {
98 $popt = ParserOptions::newFromContext( $context );
99 $this->assertTrue( $expectedUser->equals( $popt->getUserIdentity() ) );
100 $this->assertSame( $context->getLanguage(), $popt->getUserLangObj() );
103 /** @dataProvider provideNewFromContext */
104 public function testNewFromContext( $contextUserIdentity, $contextLanguage ) {
105 $this->enableAutoCreateTempUser();
106 // Get a context which has our provided user and language set, then call ::newFromContext with it.
107 $context = new DerivativeContext( RequestContext::getMain() );
108 $context->setUser(
109 $this->getServiceContainer()->getUserFactory()->newFromUserIdentity( $contextUserIdentity )
111 $context->setLanguage( $contextLanguage );
112 $this->commonTestNewFromContext( $context, $context->getUser() );
115 public static function provideNewFromContext() {
116 return [
117 'Username does not exist and user lang as en' => [ UserIdentityValue::newAnonymous( 'Testabc' ), 'en' ],
118 'Username is IP address, no stashed temporary username, and user lang as qqx' => [
119 UserIdentityValue::newAnonymous( '1.2.3.4' ), 'qqx',
124 public function testNewFromContextForNamedAccount() {
125 $this->testNewFromContext( $this->getTestUser()->getUser(), 'qqx' );
128 public function testNewFromContextForTemporaryAccount() {
129 $this->testNewFromContext(
130 $this->getServiceContainer()->getTempUserCreator()
131 ->create( null, new FauxRequest() )->getUser(),
132 'de'
136 public function testNewFromContextForAnonWhenTempNameStashed() {
137 $this->enableAutoCreateTempUser();
138 // Get a context which uses an anon user as the user.
139 $context = new DerivativeContext( RequestContext::getMain() );
140 $context->setUser(
141 $this->getServiceContainer()->getUserFactory()
142 ->newFromUserIdentity( UserIdentityValue::newAnonymous( '1.2.3.4' ) )
144 // Create a temporary account name and stash it in associated Session for the $context
145 $stashedName = $this->getServiceContainer()->getTempUserCreator()
146 ->acquireAndStashName( $context->getRequest()->getSession() );
147 // Call ::newFromContext and expect that that stashed name is used
148 $this->commonTestNewFromContext( $context, UserIdentityValue::newAnonymous( $stashedName ) );
151 public function testNewFromContextForAnonWhenTempNameStashedButFeatureSinceDisabled() {
152 $this->enableAutoCreateTempUser();
153 // Get a context which uses an anon user as the user.
154 $context = new DerivativeContext( RequestContext::getMain() );
155 $context->setUser(
156 $this->getServiceContainer()->getUserFactory()
157 ->newFromUserIdentity( UserIdentityValue::newAnonymous( '1.2.3.4' ) )
159 // Create a temporary account name and stash it in associated Session for the $context
160 $this->getServiceContainer()->getTempUserCreator()
161 ->acquireAndStashName( $context->getRequest()->getSession() );
162 // Simulate that in the interim the temporary accounts system has been disabled, and check that an IP
163 // address is used in this case
164 $this->disableAutoCreateTempUser( [ 'known' => true ] );
165 // Call ::newFromContext and expect that that stashed name is used
166 $this->commonTestNewFromContext( $context, UserIdentityValue::newAnonymous( '1.2.3.4' ) );
170 * @dataProvider provideIsSafeToCache
171 * @param bool $expect Expected value
172 * @param array $options Options to set
173 * @param array|null $usedOptions
175 public function testIsSafeToCache( bool $expect, array $options, ?array $usedOptions = null ) {
176 $popt = ParserOptions::newFromAnon();
177 foreach ( $options as $name => $value ) {
178 $popt->setOption( $name, $value );
180 $this->assertSame( $expect, $popt->isSafeToCache( $usedOptions ) );
183 public static function provideIsSafeToCache() {
184 $seven = static function () {
185 return 7;
188 return [
189 'No overrides' => [ true, [] ],
190 'No overrides, some used' => [ true, [], [ 'thumbsize', 'removeComments' ] ],
191 'In-key options are ok' => [ true, [
192 'thumbsize' => 1e100,
193 'printable' => false,
194 ] ],
195 'In-key options are ok, some used' => [ true, [
196 'thumbsize' => 1e100,
197 'printable' => false,
198 ], [ 'thumbsize', 'removeComments' ] ],
199 'Non-in-key options are not ok' => [ false, [
200 'removeComments' => false,
201 ] ],
202 'Non-in-key options are not ok, used' => [ false, [
203 'removeComments' => false,
204 ], [ 'removeComments' ] ],
205 'Non-in-key options are ok if other used' => [ true, [
206 'removeComments' => false,
207 ], [ 'thumbsize' ] ],
208 'Non-in-key options are ok if nothing used' => [ true, [
209 'removeComments' => false,
210 ], [] ],
211 'Unknown used options do not crash' => [ true, [
212 ], [ 'unknown' ] ],
213 'Non-in-key options are not ok (2)' => [ false, [
214 'wrapclass' => 'foobar',
215 ] ],
216 'Callback not default' => [ true, [
217 'speculativeRevIdCallback' => $seven,
218 ] ],
223 * @dataProvider provideOptionsHash
224 * @param array $usedOptions
225 * @param string $expect Expected value
226 * @param array $options Options to set
227 * @param array $globals Globals to set
228 * @param callable|null $hookFunc PageRenderingHash hook function
230 public function testOptionsHash(
231 $usedOptions, $expect, $options, $globals = [], $hookFunc = null
233 $this->overrideConfigValues( $globals );
234 $this->setTemporaryHook( 'PageRenderingHash', $hookFunc ?: HookContainer::NOOP );
236 $popt = ParserOptions::newFromAnon();
237 foreach ( $options as $name => $value ) {
238 $popt->setOption( $name, $value );
240 $this->assertSame( $expect, $popt->optionsHash( $usedOptions ) );
243 public static function provideOptionsHash() {
244 $used = [ 'thumbsize', 'printable' ];
246 $allUsableOptions = array_diff(
247 ParserOptions::allCacheVaryingOptions(),
248 array_keys( ParserOptions::getLazyOptions() )
251 return [
252 'Canonical options, nothing used' => [ [], 'canonical', [] ],
253 'Canonical options, used some options' => [ $used, 'canonical', [] ],
254 'Canonical options, used some more options' => [ array_merge( $used, [ 'wrapclass' ] ), 'canonical', [] ],
255 'Used some options, non-default values' => [
256 $used,
257 'printable=1!thumbsize=200',
259 'thumbsize' => 200,
260 'printable' => true,
264 'Canonical options, used all non-lazy options' => [ $allUsableOptions, 'canonical', [] ],
265 'Canonical options, nothing used, but with hooks and $wgRenderHashAppend' => [
267 'canonical!wgRenderHashAppend!onPageRenderingHash',
269 [ MainConfigNames::RenderHashAppend => '!wgRenderHashAppend' ],
270 __CLASS__ . '::onPageRenderingHash',
275 public function testUsedLazyOptionsInHash() {
276 $this->setTemporaryHook( 'ParserOptionsRegister',
277 function ( &$defaults, &$inCacheKey, &$lazyOptions ) {
278 $lazyFuncs = $this->getMockBuilder( stdClass::class )
279 ->addMethods( [ 'neverCalled', 'calledOnce' ] )
280 ->getMock();
281 $lazyFuncs->expects( $this->never() )->method( 'neverCalled' );
282 $lazyFuncs->expects( $this->once() )->method( 'calledOnce' )->willReturn( 'value' );
284 $defaults += [
285 'opt1' => null,
286 'opt2' => null,
287 'opt3' => null,
289 $inCacheKey += [
290 'opt1' => true,
291 'opt2' => true,
293 $lazyOptions += [
294 'opt1' => [ $lazyFuncs, 'calledOnce' ],
295 'opt2' => [ $lazyFuncs, 'neverCalled' ],
296 'opt3' => [ $lazyFuncs, 'neverCalled' ],
301 ParserOptions::clearStaticCache();
303 $popt = ParserOptions::newFromAnon();
304 $popt->registerWatcher( function () {
305 $this->fail( 'Watcher should not have been called' );
306 } );
307 $this->assertSame( 'opt1=value', $popt->optionsHash( [ 'opt1', 'opt3' ] ) );
309 // Second call to see that opt1 isn't resolved a second time
310 $this->assertSame( 'opt1=value', $popt->optionsHash( [ 'opt1', 'opt3' ] ) );
313 public function testLazyOptionWithDefault() {
314 $loaded = false;
315 $this->setTemporaryHook(
316 'ParserOptionsRegister',
317 static function ( &$defaults, &$inCacheKey, &$lazyLoad ) use ( &$loaded ) {
318 $defaults['test_option'] = 'default!';
319 $inCacheKey['test_option'] = true;
320 $lazyLoad['test_option'] = static function () use ( &$loaded ) {
321 $loaded = true;
322 return 'default!';
327 $po = ParserOptions::newFromAnon();
328 $this->assertSame( 'default!', $po->getOption( 'test_option' ) );
329 $this->assertTrue( $loaded );
330 $this->assertSame(
331 'canonical',
332 $po->optionsHash( [ 'test_option' ], Title::makeTitle( NS_MAIN, 'Test' ) )
336 public static function onPageRenderingHash( &$confstr ) {
337 $confstr .= '!onPageRenderingHash';
340 public function testGetInvalidOption() {
341 $popt = ParserOptions::newFromAnon();
342 $this->expectException( InvalidArgumentException::class );
343 $this->expectExceptionMessage( "Unknown parser option bogus" );
344 $popt->getOption( 'bogus' );
347 public function testSetInvalidOption() {
348 $popt = ParserOptions::newFromAnon();
349 $this->expectException( InvalidArgumentException::class );
350 $this->expectExceptionMessage( "Unknown parser option bogus" );
351 $popt->setOption( 'bogus', true );
354 public function testMatches() {
355 $popt1 = ParserOptions::newFromAnon();
356 $popt2 = ParserOptions::newFromAnon();
357 $this->assertTrue( $popt1->matches( $popt2 ) );
359 $popt2->setInterfaceMessage( !$popt2->getInterfaceMessage() );
360 $this->assertFalse( $popt1->matches( $popt2 ) );
362 $ctr = 0;
363 $this->setTemporaryHook( 'ParserOptionsRegister',
364 static function ( &$defaults, &$inCacheKey, &$lazyOptions ) use ( &$ctr ) {
365 $defaults['testMatches'] = null;
366 $lazyOptions['testMatches'] = static function () use ( &$ctr ) {
367 return ++$ctr;
371 ParserOptions::clearStaticCache();
373 $popt1 = ParserOptions::newFromAnon();
374 $popt2 = ParserOptions::newFromAnon();
375 $this->assertFalse( $popt1->matches( $popt2 ) );
377 ScopedCallback::consume( $reset );
381 * This test fails if tearDown() does not call ParserOptions::clearStaticCache(),
382 * because the lazy option from the hook in the previous test remains active.
384 public function testTeardownClearedCache() {
385 $popt1 = ParserOptions::newFromAnon();
386 $popt2 = ParserOptions::newFromAnon();
387 $this->assertTrue( $popt1->matches( $popt2 ) );
390 public function testMatchesForCacheKey() {
391 $user = new UserIdentityValue( 0, '127.0.0.1' );
392 $cOpts = ParserOptions::newCanonical(
393 $user,
394 $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'en' )
397 $uOpts = ParserOptions::newFromAnon();
398 $this->assertTrue( $cOpts->matchesForCacheKey( $uOpts ) );
400 $uOpts = ParserOptions::newFromUser( $user );
401 $this->assertTrue( $cOpts->matchesForCacheKey( $uOpts ) );
403 $this->getServiceContainer()
404 ->getUserOptionsManager()
405 ->setOption( $user, 'thumbsize', 251 );
406 $uOpts = ParserOptions::newFromUser( $user );
407 $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
409 $this->getServiceContainer()
410 ->getUserOptionsManager()
411 ->setOption( $user, 'stubthreshold', 800 );
412 $uOpts = ParserOptions::newFromUser( $user );
413 $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
415 $uOpts = ParserOptions::newFromUserAndLang(
416 $user,
417 $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'zh' )
419 $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
422 public function testAllCacheVaryingOptions() {
423 $this->setTemporaryHook( 'ParserOptionsRegister', HookContainer::NOOP );
424 $this->assertSame( [
425 'collapsibleSections',
426 'dateformat', 'printable', 'suppressSectionEditLinks',
427 'thumbsize', 'useParsoid', 'userlang',
428 ], ParserOptions::allCacheVaryingOptions() );
430 ParserOptions::clearStaticCache();
432 $this->setTemporaryHook( 'ParserOptionsRegister', static function ( &$defaults, &$inCacheKey ) {
433 $defaults += [
434 'foo' => 'foo',
435 'bar' => 'bar',
436 'baz' => 'baz',
438 $inCacheKey += [
439 'foo' => true,
440 'bar' => false,
442 } );
443 $this->assertSame( [
444 'collapsibleSections',
445 'dateformat', 'foo', 'printable', 'suppressSectionEditLinks',
446 'thumbsize', 'useParsoid', 'userlang',
447 ], ParserOptions::allCacheVaryingOptions() );
450 public function testGetSpeculativeRevid() {
451 $options = ParserOptions::newFromAnon();
453 $this->assertFalse( $options->getSpeculativeRevId() );
455 $counter = 0;
456 $options->setSpeculativeRevIdCallback( static function () use( &$counter ) {
457 return ++$counter;
458 } );
460 // make sure the same value is re-used once it is determined
461 $this->assertSame( 1, $options->getSpeculativeRevId() );
462 $this->assertSame( 1, $options->getSpeculativeRevId() );
465 public function testSetupFakeRevision_new() {
466 $options = ParserOptions::newFromAnon();
467 $options->setIsPreview( true );
468 $options->setSpeculativePageIdCallback( fn () => 105 );
470 $page = PageIdentityValue::localIdentity( 0, NS_MAIN, __METHOD__ );
471 $content = new DummyContentForTesting( '12345' );
472 $user = UserIdentityValue::newRegistered( 123, 'TestTest' );
473 $fakeRevisionScope = $options->setupFakeRevision( $page, $content, $user );
475 /** @var RevisionRecord $fakeRevision */
476 $fakeRevision = $options->getCurrentRevisionRecordCallback()( $page );
477 $this->assertNotNull( $fakeRevision );
478 $this->assertSame( '12345', $fakeRevision->getContent( SlotRecord::MAIN )->serialize() );
479 $this->assertSame( $user, $fakeRevision->getUser() );
480 $this->assertSame( 0, $fakeRevision->getId() );
481 $this->assertSame( 0, $fakeRevision->getParentId() );
482 $this->assertSame( 105, $fakeRevision->getPageId() );
483 $this->assertSame( 105, $fakeRevision->getPage()->getId() );
484 $this->assertTrue( $fakeRevision->getPage()->exists() );
486 ScopedCallback::consume( $fakeRevisionScope );
487 $this->assertFalse(
488 $options->getCurrentRevisionRecordCallback()(
489 TitleValue::newFromPage( $page )
494 public function testSetupFakeRevision_existing() {
495 $options = ParserOptions::newFromAnon();
496 $options->setIsPreview( true );
498 $page = PageIdentityValue::localIdentity( 105, NS_MAIN, __METHOD__ );
499 $content = new DummyContentForTesting( '12345' );
500 $user = UserIdentityValue::newRegistered( 123, 'TestTest' );
501 $fakeRevisionScope = $options->setupFakeRevision( $page, $content, $user, 1002 );
503 /** @var RevisionRecord $fakeRevision */
504 $fakeRevision = $options->getCurrentRevisionRecordCallback()( $page );
505 $this->assertNotNull( $fakeRevision );
506 $this->assertSame( '12345', $fakeRevision->getContent( SlotRecord::MAIN )->serialize() );
507 $this->assertSame( $user, $fakeRevision->getUser() );
508 $this->assertSame( 0, $fakeRevision->getId() );
509 $this->assertSame( 1002, $fakeRevision->getParentId() );
510 $this->assertSame( 105, $fakeRevision->getPageId() );
511 $this->assertSame( 105, $fakeRevision->getPage()->getId() );
512 $this->assertTrue( $fakeRevision->getPage()->exists() );
514 ScopedCallback::consume( $fakeRevisionScope );
515 $this->assertFalse(
516 $options->getCurrentRevisionRecordCallback()(
517 TitleValue::newFromPage( $page )
522 public function testRenderReason() {
523 $options = ParserOptions::newFromAnon();
525 $this->assertIsString( $options->getRenderReason() );
527 $options->setRenderReason( 'just a test' );
528 $this->assertIsString( 'just a test', $options->getRenderReason() );
531 public function testSuppressSectionEditLinks() {
532 $options = ParserOptions::newFromAnon();
534 $this->assertFalse( $options->getSuppressSectionEditLinks() );
536 $options->setSuppressSectionEditLinks();
537 $this->assertTrue( $options->getSuppressSectionEditLinks() );
540 public function testCollapsibleSections() {
541 $options = ParserOptions::newFromAnon();
543 $this->assertFalse( $options->getCollapsibleSections() );
545 $options->setCollapsibleSections();
546 $this->assertTrue( $options->getCollapsibleSections() );