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