Parser: Call firstCallInit() in getTags/getFunctionHooks
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiQuerySiteinfoTest.php
1 <?php
2
3 /**
4 * @group API
5 * @group medium
6 *
7 * @covers ApiQuerySiteinfo
8 */
9 class ApiQuerySiteinfoTest extends ApiTestCase {
10 // We don't try to test every single thing for every category, just a sample
11
12 protected function doQuery( $siprop = null, $extraParams = [] ) {
13 $params = [ 'action' => 'query', 'meta' => 'siteinfo' ];
14 if ( $siprop !== null ) {
15 $params['siprop'] = $siprop;
16 }
17 $params = array_merge( $params, $extraParams );
18
19 $res = $this->doApiRequest( $params );
20
21 $this->assertArrayNotHasKey( 'warnings', $res[0] );
22 $this->assertCount( 1, $res[0]['query'] );
23
24 return $res[0]['query'][$siprop === null ? 'general' : $siprop];
25 }
26
27 public function testGeneral() {
28 $this->setMwGlobals( [
29 'wgAllowExternalImagesFrom' => '//localhost/',
30 ] );
31
32 $data = $this->doQuery();
33
34 $this->assertSame( Title::newMainPage()->getPrefixedText(), $data['mainpage'] );
35 $this->assertSame( PHP_VERSION, $data['phpversion'] );
36 $this->assertSame( [ '//localhost/' ], $data['externalimages'] );
37 }
38
39 public function testLinkPrefixCharset() {
40 global $wgContLang;
41
42 $this->setContentLang( 'ar' );
43 $this->assertTrue( $wgContLang->linkPrefixExtension(), 'Sanity check' );
44
45 $data = $this->doQuery();
46
47 $this->assertSame( $wgContLang->linkPrefixCharset(), $data['linkprefixcharset'] );
48 }
49
50 public function testVariants() {
51 global $wgContLang;
52
53 $this->setContentLang( 'zh' );
54 $this->assertTrue( $wgContLang->hasVariants(), 'Sanity check' );
55
56 $data = $this->doQuery();
57
58 $expected = array_map(
59 function ( $code ) use ( $wgContLang ) {
60 return [ 'code' => $code, 'name' => $wgContLang->getVariantname( $code ) ];
61 },
62 $wgContLang->getVariants()
63 );
64
65 $this->assertSame( $expected, $data['variants'] );
66 }
67
68 public function testReadOnly() {
69 $svc = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
70 $svc->setReason( 'Need more donations' );
71 try {
72 $data = $this->doQuery();
73 } finally {
74 $svc->setReason( false );
75 }
76
77 $this->assertTrue( $data['readonly'] );
78 $this->assertSame( 'Need more donations', $data['readonlyreason'] );
79 }
80
81 public function testNamespaces() {
82 global $wgContLang;
83
84 $this->setMwGlobals( 'wgExtraNamespaces', [ '138' => 'Testing' ] );
85
86 $this->assertSame( array_keys( $wgContLang->getFormattedNamespaces() ),
87 array_keys( $this->doQuery( 'namespaces' ) ) );
88 }
89
90 public function testNamespaceAliases() {
91 global $wgNamespaceAliases, $wgContLang;
92
93 $expected = array_merge( $wgNamespaceAliases, $wgContLang->getNamespaceAliases() );
94 $expected = array_map(
95 function ( $key, $val ) {
96 return [ 'id' => $val, 'alias' => strtr( $key, '_', ' ' ) ];
97 },
98 array_keys( $expected ),
99 $expected
100 );
101
102 // Test that we don't list duplicates
103 $this->mergeMwGlobalArrayValue( 'wgNamespaceAliases', [ 'Talk' => NS_TALK ] );
104
105 $this->assertSame( $expected, $this->doQuery( 'namespacealiases' ) );
106 }
107
108 public function testSpecialPageAliases() {
109 $this->assertCount(
110 count( SpecialPageFactory::getNames() ),
111 $this->doQuery( 'specialpagealiases' )
112 );
113 }
114
115 public function testMagicWords() {
116 global $wgContLang;
117
118 $this->assertCount(
119 count( $wgContLang->getMagicWords() ),
120 $this->doQuery( 'magicwords' )
121 );
122 }
123
124 /**
125 * @dataProvider interwikiMapProvider
126 */
127 public function testInterwikiMap( $filter ) {
128 global $wgServer, $wgScriptPath;
129
130 $dbw = wfGetDB( DB_MASTER );
131 $dbw->insert(
132 'interwiki',
133 [
134 [
135 'iw_prefix' => 'self',
136 'iw_url' => "$wgServer$wgScriptPath/index.php?title=$1",
137 'iw_api' => "$wgServer$wgScriptPath/api.php",
138 'iw_wikiid' => 'somedbname',
139 'iw_local' => true,
140 'iw_trans' => true,
141 ],
142 [
143 'iw_prefix' => 'foreign',
144 'iw_url' => '//foreign.example/wiki/$1',
145 'iw_api' => '',
146 'iw_wikiid' => '',
147 'iw_local' => false,
148 'iw_trans' => false,
149 ],
150 ],
151 __METHOD__,
152 'IGNORE'
153 );
154 $this->tablesUsed[] = 'interwiki';
155
156 $this->setMwGlobals( [
157 'wgLocalInterwikis' => [ 'self' ],
158 'wgExtraInterlanguageLinkPrefixes' => [ 'self' ],
159 'wgExtraLanguageNames' => [ 'self' => 'Recursion' ],
160 ] );
161
162 MessageCache::singleton()->enable();
163
164 $this->editPage( 'MediaWiki:Interlanguage-link-self', 'Self!' );
165 $this->editPage( 'MediaWiki:Interlanguage-link-sitename-self', 'Circular logic' );
166
167 $expected = [];
168
169 if ( $filter === null || $filter === '!local' ) {
170 $expected[] = [
171 'prefix' => 'foreign',
172 'url' => wfExpandUrl( '//foreign.example/wiki/$1', PROTO_CURRENT ),
173 'protorel' => true,
174 ];
175 }
176 if ( $filter === null || $filter === 'local' ) {
177 $expected[] = [
178 'prefix' => 'self',
179 'local' => true,
180 'trans' => true,
181 'language' => 'Recursion',
182 'localinterwiki' => true,
183 'extralanglink' => true,
184 'linktext' => 'Self!',
185 'sitename' => 'Circular logic',
186 'url' => "$wgServer$wgScriptPath/index.php?title=$1",
187 'protorel' => false,
188 'wikiid' => 'somedbname',
189 'api' => "$wgServer$wgScriptPath/api.php",
190 ];
191 }
192
193 $data = $this->doQuery( 'interwikimap',
194 $filter === null ? [] : [ 'sifilteriw' => $filter ] );
195
196 $this->assertSame( $expected, $data );
197 }
198
199 public function interwikiMapProvider() {
200 return [ [ 'local' ], [ '!local' ], [ null ] ];
201 }
202
203 /**
204 * @dataProvider dbReplLagProvider
205 */
206 public function testDbReplLagInfo( $showHostnames, $includeAll ) {
207 if ( !$showHostnames && $includeAll ) {
208 $this->setExpectedApiException( 'apierror-siteinfo-includealldenied' );
209 }
210
211 $mockLB = $this->getMockBuilder( LoadBalancer::class )
212 ->disableOriginalConstructor()
213 ->setMethods( [ 'getMaxLag', 'getLagTimes', 'getServerName', '__destruct' ] )
214 ->getMock();
215 $mockLB->method( 'getMaxLag' )->willReturn( [ null, 7, 1 ] );
216 $mockLB->method( 'getLagTimes' )->willReturn( [ 5, 7 ] );
217 $mockLB->method( 'getServerName' )->will( $this->returnValueMap( [
218 [ 0, 'apple' ], [ 1, 'carrot' ]
219 ] ) );
220 $this->setService( 'DBLoadBalancer', $mockLB );
221
222 $this->setMwGlobals( 'wgShowHostnames', $showHostnames );
223
224 $expected = [];
225 if ( $includeAll ) {
226 $expected[] = [ 'host' => $showHostnames ? 'apple' : '', 'lag' => 5 ];
227 }
228 $expected[] = [ 'host' => $showHostnames ? 'carrot' : '', 'lag' => 7 ];
229
230 $data = $this->doQuery( 'dbrepllag', $includeAll ? [ 'sishowalldb' => '' ] : [] );
231
232 $this->assertSame( $expected, $data );
233 }
234
235 public function dbReplLagProvider() {
236 return [
237 'no hostnames, no showalldb' => [ false, false ],
238 'no hostnames, showalldb' => [ false, true ],
239 'hostnames, no showalldb' => [ true, false ],
240 'hostnames, showalldb' => [ true, true ]
241 ];
242 }
243
244 public function testStatistics() {
245 $this->setTemporaryHook( 'APIQuerySiteInfoStatisticsInfo',
246 function ( &$data ) {
247 $data['addedstats'] = 42;
248 }
249 );
250
251 $expected = [
252 'pages' => intval( SiteStats::pages() ),
253 'articles' => intval( SiteStats::articles() ),
254 'edits' => intval( SiteStats::edits() ),
255 'images' => intval( SiteStats::images() ),
256 'users' => intval( SiteStats::users() ),
257 'activeusers' => intval( SiteStats::activeUsers() ),
258 'admins' => intval( SiteStats::numberingroup( 'sysop' ) ),
259 'jobs' => intval( SiteStats::jobs() ),
260 'addedstats' => 42,
261 ];
262
263 $this->assertSame( $expected, $this->doQuery( 'statistics' ) );
264 }
265
266 /**
267 * @dataProvider groupsProvider
268 */
269 public function testUserGroups( $numInGroup ) {
270 global $wgGroupPermissions, $wgAutopromote;
271
272 $this->setGroupPermissions( 'viscount', 'perambulate', 'yes' );
273 $this->setGroupPermissions( 'viscount', 'legislate', '0' );
274 $this->setMwGlobals( [
275 'wgAddGroups' => [ 'viscount' => true, 'bot' => [] ],
276 'wgRemoveGroups' => [ 'viscount' => [ 'sysop' ], 'bot' => [ '*', 'earl' ] ],
277 'wgGroupsAddToSelf' => [ 'bot' => [ 'bureaucrat', 'sysop' ] ],
278 'wgGroupsRemoveFromSelf' => [ 'bot' => [ 'bot' ] ],
279 ] );
280
281 $data = $this->doQuery( 'usergroups', $numInGroup ? [ 'sinumberingroup' => '' ] : [] );
282
283 $names = array_map(
284 function ( $val ) {
285 return $val['name'];
286 },
287 $data
288 );
289
290 $this->assertSame( array_keys( $wgGroupPermissions ), $names );
291
292 foreach ( $data as $val ) {
293 if ( !$numInGroup ) {
294 $expectedSize = null;
295 } elseif ( $val['name'] === 'user' ) {
296 $expectedSize = SiteStats::users();
297 } elseif ( $val['name'] === '*' || isset( $wgAutopromote[$val['name']] ) ) {
298 $expectedSize = null;
299 } else {
300 $expectedSize = SiteStats::numberingroup( $val['name'] );
301 }
302
303 if ( $expectedSize === null ) {
304 $this->assertArrayNotHasKey( 'number', $val );
305 } else {
306 $this->assertSame( $expectedSize, $val['number'] );
307 }
308
309 if ( $val['name'] === 'viscount' ) {
310 $viscountFound = true;
311 $this->assertSame( [ 'perambulate' ], $val['rights'] );
312 $this->assertSame( User::getAllGroups(), $val['add'] );
313 } elseif ( $val['name'] === 'bot' ) {
314 $this->assertArrayNotHasKey( 'add', $val );
315 $this->assertArrayNotHasKey( 'remove', $val );
316 $this->assertSame( [ 'bureaucrat', 'sysop' ], $val['add-self'] );
317 $this->assertSame( [ 'bot' ], $val['remove-self'] );
318 }
319 }
320 }
321
322 public function testFileExtensions() {
323 global $wgFileExtensions;
324
325 $this->stashMwGlobals( 'wgFileExtensions' );
326 // Add duplicate
327 $wgFileExtensions[] = 'png';
328
329 $expected = array_map(
330 function ( $val ) {
331 return [ 'ext' => $val ];
332 },
333 array_unique( $wgFileExtensions )
334 );
335
336 $this->assertSame( $expected, $this->doQuery( 'fileextensions' ) );
337 }
338
339 public function groupsProvider() {
340 return [
341 'numingroup' => [ true ],
342 'nonumingroup' => [ false ],
343 ];
344 }
345
346 public function testInstalledLibraries() {
347 // @todo Test no installed.json? Moving installed.json to a different name temporarily
348 // seems a bit scary, but I don't see any other way to do it.
349 //
350 // @todo Install extensions/skins somehow so that we can test they're filtered out
351 global $IP;
352
353 $path = "$IP/vendor/composer/installed.json";
354 if ( !file_exists( $path ) ) {
355 $this->markTestSkipped( 'No installed libraries' );
356 }
357
358 $expected = ( new ComposerInstalled( $path ) )->getInstalledDependencies();
359
360 $expected = array_filter( $expected,
361 function ( $info ) {
362 return strpos( $info['type'], 'mediawiki-' ) !== 0;
363 }
364 );
365
366 $expected = array_map(
367 function ( $name, $info ) {
368 return [ 'name' => $name, 'version' => $info['version'] ];
369 },
370 array_keys( $expected ),
371 array_values( $expected )
372 );
373
374 $this->assertSame( $expected, $this->doQuery( 'libraries' ) );
375 }
376
377 public function testExtensions() {
378 $tmpdir = $this->getNewTempDirectory();
379 touch( "$tmpdir/ErsatzExtension.php" );
380 touch( "$tmpdir/LICENSE" );
381 touch( "$tmpdir/AUTHORS.txt" );
382
383 $val = [
384 'path' => "$tmpdir/ErsatzExtension.php",
385 'name' => 'Ersatz Extension',
386 'namemsg' => 'ersatz-extension-name',
387 'author' => 'John Smith',
388 'version' => '0.0.2',
389 'url' => 'https://www.example.com/software/ersatz-extension',
390 'description' => 'An extension that is not what it seems.',
391 'descriptionmsg' => 'ersatz-extension-desc',
392 'license-name' => 'PD',
393 ];
394
395 $this->setMwGlobals( 'wgExtensionCredits', [ 'api' => [
396 $val,
397 [
398 'author' => [ 'John Smith', 'John Smith Jr.', '...' ],
399 'descriptionmsg' => [ 'another-extension-desc', 'param' ] ],
400 ] ] );
401
402 $data = $this->doQuery( 'extensions' );
403
404 $this->assertCount( 2, $data );
405
406 $this->assertSame( 'api', $data[0]['type'] );
407
408 $sharedKeys = [ 'name', 'namemsg', 'description', 'descriptionmsg', 'author', 'url',
409 'version', 'license-name' ];
410 foreach ( $sharedKeys as $key ) {
411 $this->assertSame( $val[$key], $data[0][$key] );
412 }
413
414 // @todo Test git info
415
416 $this->assertSame(
417 Title::newFromText( 'Special:Version/License/Ersatz Extension' )->getLinkURL(),
418 $data[0]['license']
419 );
420
421 $this->assertSame(
422 Title::newFromText( 'Special:Version/Credits/Ersatz Extension' )->getLinkURL(),
423 $data[0]['credits']
424 );
425
426 $this->assertSame( 'another-extension-desc', $data[1]['descriptionmsg'] );
427 $this->assertSame( [ 'param' ], $data[1]['descriptionmsgparams'] );
428 $this->assertSame( 'John Smith, John Smith Jr., ...', $data[1]['author'] );
429 }
430
431 /**
432 * @dataProvider rightsInfoProvider
433 */
434 public function testRightsInfo( $page, $url, $text, $expectedUrl, $expectedText ) {
435 $this->setMwGlobals( [
436 'wgRightsPage' => $page,
437 'wgRightsUrl' => $url,
438 'wgRightsText' => $text,
439 ] );
440
441 $this->assertSame(
442 [ 'url' => $expectedUrl, 'text' => $expectedText ],
443 $this->doQuery( 'rightsinfo' )
444 );
445 }
446
447 public function rightsInfoProvider() {
448 $textUrl = wfExpandUrl( Title::newFromText( 'License' ), PROTO_CURRENT );
449 $url = 'http://license.example/';
450
451 return [
452 'No rights info' => [ null, null, null, '', '' ],
453 'Only page' => [ 'License', null, null, $textUrl, 'License' ],
454 'Only URL' => [ null, $url, null, $url, '' ],
455 'Only text' => [ null, null, '!!!', '', '!!!' ],
456 // URL is ignored if page is specified
457 'Page and URL' => [ 'License', $url, null, $textUrl, 'License' ],
458 'URL and text' => [ null, $url, '!!!', $url, '!!!' ],
459 'Page and text' => [ 'License', null, '!!!', $textUrl, '!!!' ],
460 'Page and URL and text' => [ 'License', $url, '!!!', $textUrl, '!!!' ],
461 'Pagename "0"' => [ '0', null, null,
462 wfExpandUrl( Title::newFromText( '0' ), PROTO_CURRENT ), '0' ],
463 'URL "0"' => [ null, '0', null, '0', '' ],
464 'Text "0"' => [ null, null, '0', '', '0' ],
465 ];
466 }
467
468 public function testRestrictions() {
469 global $wgRestrictionTypes, $wgRestrictionLevels, $wgCascadingRestrictionLevels,
470 $wgSemiprotectedRestrictionLevels;
471
472 $this->assertSame( [
473 'types' => $wgRestrictionTypes,
474 'levels' => $wgRestrictionLevels,
475 'cascadinglevels' => $wgCascadingRestrictionLevels,
476 'semiprotectedlevels' => $wgSemiprotectedRestrictionLevels,
477 ], $this->doQuery( 'restrictions' ) );
478 }
479
480 /**
481 * @dataProvider languagesProvider
482 */
483 public function testLanguages( $langCode ) {
484 $expected = Language::fetchLanguageNames( (string)$langCode );
485
486 $expected = array_map(
487 function ( $code, $name ) {
488 return [
489 'code' => $code,
490 'name' => $name
491 ];
492 },
493 array_keys( $expected ),
494 array_values( $expected )
495 );
496
497 $data = $this->doQuery( 'languages',
498 $langCode !== null ? [ 'siinlanguagecode' => $langCode ] : [] );
499
500 $this->assertSame( $expected, $data );
501 }
502
503 public function languagesProvider() {
504 return [ [ null ], [ 'fr' ] ];
505 }
506
507 public function testLanguageVariants() {
508 $expectedKeys = array_filter( LanguageConverter::$languagesWithVariants,
509 function ( $langCode ) {
510 return !Language::factory( $langCode )->getConverter() instanceof FakeConverter;
511 }
512 );
513 sort( $expectedKeys );
514
515 $this->assertSame( $expectedKeys, array_keys( $this->doQuery( 'languagevariants' ) ) );
516 }
517
518 public function testLanguageVariantsDisabled() {
519 $this->setMwGlobals( 'wgDisableLangConversion', true );
520
521 $this->assertSame( [], $this->doQuery( 'languagevariants' ) );
522 }
523
524 /**
525 * @todo Test a skin with a description that's known to be different in a different language.
526 * Vector will do, but it's not installed by default.
527 *
528 * @todo Test that an invalid language code doesn't actually try reading any messages
529 *
530 * @dataProvider skinsProvider
531 */
532 public function testSkins( $code ) {
533 $data = $this->doQuery( 'skins', $code !== null ? [ 'siinlanguagecode' => $code ] : [] );
534
535 $expectedAllowed = Skin::getAllowedSkins();
536 $expectedDefault = Skin::normalizeKey( 'default' );
537
538 $i = 0;
539 foreach ( Skin::getSkinNames() as $name => $displayName ) {
540 $this->assertSame( $name, $data[$i]['code'] );
541
542 $msg = wfMessage( "skinname-$name" );
543 if ( $code && Language::isValidCode( $code ) ) {
544 $msg->inLanguage( $code );
545 } else {
546 $msg->inContentLanguage();
547 }
548 if ( $msg->exists() ) {
549 $displayName = $msg->text();
550 }
551 $this->assertSame( $displayName, $data[$i]['name'] );
552
553 if ( !isset( $expectedAllowed[$name] ) ) {
554 $this->assertTrue( $data[$i]['unusable'], "$name must be unusable" );
555 }
556 if ( $name === $expectedDefault ) {
557 $this->assertTrue( $data[$i]['default'], "$expectedDefault must be default" );
558 }
559 $i++;
560 }
561 }
562
563 public function skinsProvider() {
564 return [
565 'No language specified' => [ null ],
566 'Czech' => [ 'cs' ],
567 'Invalid language' => [ '/invalid/' ],
568 ];
569 }
570
571 public function testExtensionTags() {
572 global $wgParser;
573
574 $expected = array_map(
575 function ( $tag ) {
576 return "<$tag>";
577 },
578 $wgParser->getTags()
579 );
580
581 $this->assertSame( $expected, $this->doQuery( 'extensiontags' ) );
582 }
583
584 public function testFunctionHooks() {
585 global $wgParser;
586
587 $this->assertSame( $wgParser->getFunctionHooks(), $this->doQuery( 'functionhooks' ) );
588 }
589
590 public function testVariables() {
591 $this->assertSame( MagicWord::getVariableIDs(), $this->doQuery( 'variables' ) );
592 }
593
594 public function testProtocols() {
595 global $wgUrlProtocols;
596
597 $this->assertSame( $wgUrlProtocols, $this->doQuery( 'protocols' ) );
598 }
599
600 public function testDefaultOptions() {
601 $this->assertSame( User::getDefaultOptions(), $this->doQuery( 'defaultoptions' ) );
602 }
603
604 public function testUploadDialog() {
605 global $wgUploadDialog;
606
607 $this->assertSame( $wgUploadDialog, $this->doQuery( 'uploaddialog' ) );
608 }
609
610 public function testGetHooks() {
611 global $wgHooks;
612
613 // Make sure there's something to report on
614 $this->setTemporaryHook( 'somehook',
615 function () {
616 return;
617 }
618 );
619
620 $expectedNames = $wgHooks;
621 ksort( $expectedNames );
622
623 $actualNames = array_map(
624 function ( $val ) {
625 return $val['name'];
626 },
627 $this->doQuery( 'showhooks' )
628 );
629
630 $this->assertSame( array_keys( $expectedNames ), $actualNames );
631 }
632
633 public function testContinuation() {
634 // We make lots and lots of URL protocols that are each 100 bytes
635 global $wgAPIMaxResultSize, $wgUrlProtocols;
636
637 $this->setMwGlobals( 'wgUrlProtocols', [] );
638
639 // Just under the limit
640 $chunks = $wgAPIMaxResultSize / 100 - 1;
641
642 for ( $i = 0; $i < $chunks; $i++ ) {
643 $wgUrlProtocols[] = substr( str_repeat( "$i ", 50 ), 0, 100 );
644 }
645
646 $res = $this->doApiRequest( [
647 'action' => 'query',
648 'meta' => 'siteinfo',
649 'siprop' => 'protocols|languages',
650 ] );
651
652 $this->assertSame(
653 wfMessage( 'apiwarn-truncatedresult', Message::numParam( $wgAPIMaxResultSize ) )
654 ->text(),
655 $res[0]['warnings']['result']['warnings']
656 );
657
658 $this->assertSame( $wgUrlProtocols, $res[0]['query']['protocols'] );
659 $this->assertArrayNotHasKey( 'languages', $res[0] );
660 $this->assertTrue( $res[0]['batchcomplete'], 'batchcomplete should be true' );
661 $this->assertSame( [ 'siprop' => 'languages', 'continue' => '-||' ], $res[0]['continue'] );
662 }
663 }