Merge "chore: Move authevents logging into AuthManager"
[mediawiki.git] / tests / phpunit / includes / parser / ParserOptionsTest.php
blob0ebe76642514c0455156b28afeeaa85d444ac04b
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\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;
21 use stdClass;
22 use Wikimedia\ScopedCallback;
24 /**
25 * @covers \MediaWiki\Parser\ParserOptions
26 * @group Database
28 class ParserOptionsTest extends MediaWikiLangTestCase {
30 use TempUserTestTrait;
32 protected function setUp(): void {
33 parent::setUp();
35 $this->overrideConfigValues( [
36 MainConfigNames::RenderHashAppend => '',
37 MainConfigNames::UsePigLatinVariant => false,
38 ] );
39 $this->setTemporaryHook( 'PageRenderingHash', HookContainer::NOOP );
42 protected function tearDown(): void {
43 ParserOptions::clearStaticCache();
44 parent::tearDown();
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() );
66 // Passing both works
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
87 try {
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() );
105 $context->setUser(
106 $this->getServiceContainer()->getUserFactory()->newFromUserIdentity( $contextUserIdentity )
108 $context->setLanguage( $contextLanguage );
109 $this->commonTestNewFromContext( $context, $context->getUser() );
112 public static function provideNewFromContext() {
113 return [
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(),
129 'de'
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() );
137 $context->setUser(
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() );
152 $context->setUser(
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 () {
182 return 7;
185 return [
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,
191 ] ],
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,
198 ] ],
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,
207 ], [] ],
208 'Unknown used options do not crash' => [ true, [
209 ], [ 'unknown' ] ],
210 'Non-in-key options are not ok (2)' => [ false, [
211 'wrapclass' => 'foobar',
212 ] ],
213 'Callback not default' => [ true, [
214 'speculativeRevIdCallback' => $seven,
215 ] ],
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() )
248 return [
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' => [
253 $used,
254 'printable=1!thumbsize=200',
256 'thumbsize' => 200,
257 'printable' => true,
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' ] )
277 ->getMock();
278 $lazyFuncs->expects( $this->never() )->method( 'neverCalled' );
279 $lazyFuncs->expects( $this->once() )->method( 'calledOnce' )->willReturn( 'value' );
281 $defaults += [
282 'opt1' => null,
283 'opt2' => null,
284 'opt3' => null,
286 $inCacheKey += [
287 'opt1' => true,
288 'opt2' => true,
290 $lazyOptions += [
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' );
303 } );
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() {
311 $loaded = false;
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 ) {
318 $loaded = true;
319 return 'default!';
324 $po = ParserOptions::newFromAnon();
325 $this->assertSame( 'default!', $po->getOption( 'test_option' ) );
326 $this->assertTrue( $loaded );
327 $this->assertSame(
328 'canonical',
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 ) );
359 $ctr = 0;
360 $this->setTemporaryHook( 'ParserOptionsRegister',
361 static function ( &$defaults, &$inCacheKey, &$lazyOptions ) use ( &$ctr ) {
362 $defaults['testMatches'] = null;
363 $lazyOptions['testMatches'] = static function () use ( &$ctr ) {
364 return ++$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(
390 $user,
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(
413 $user,
414 $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'zh' )
416 $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
419 public function testAllCacheVaryingOptions() {
420 $this->setTemporaryHook( 'ParserOptionsRegister', HookContainer::NOOP );
421 $this->assertSame( [
422 'collapsibleSections',
423 'dateformat', 'printable', 'suppressSectionEditLinks',
424 'thumbsize', 'useParsoid', 'userlang',
425 ], ParserOptions::allCacheVaryingOptions() );
427 ParserOptions::clearStaticCache();
429 $this->setTemporaryHook( 'ParserOptionsRegister', static function ( &$defaults, &$inCacheKey ) {
430 $defaults += [
431 'foo' => 'foo',
432 'bar' => 'bar',
433 'baz' => 'baz',
435 $inCacheKey += [
436 'foo' => true,
437 'bar' => false,
439 } );
440 $this->assertSame( [
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() );
452 $counter = 0;
453 $options->setSpeculativeRevIdCallback( static function () use( &$counter ) {
454 return ++$counter;
455 } );
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() );