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