MediaWiki REL1_33
MediaWikiTestCase.php
Go to the documentation of this file.
1<?php
2
8use Psr\Log\LoggerInterface;
12use Wikimedia\TestingAccessWrapper;
13
17abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
18
19 use MediaWikiCoversValidator;
20 use PHPUnit4And6Compat;
21
27 private static $originalServices;
28
34
47 private $called = [];
48
53 public static $users;
54
61 protected $db;
62
67 protected $tablesUsed = []; // tables with data
68
69 private static $useTemporaryTables = true;
70 private static $reuseDB = false;
71 private static $dbSetup = false;
72 private static $oldTablePrefix = '';
73
80
87 private $tmpFiles = [];
88
95 private $mwGlobals = [];
96
102 private $mwGlobalsToUnset = [];
103
110 private $iniSettings = [];
111
116 private $loggers = [];
117
122 private $cliArgs = [];
123
130
134 const DB_PREFIX = 'unittest_';
135 const ORA_DB_PREFIX = 'ut_';
136
141 protected $supportedDBs = [
142 'mysql',
143 'sqlite',
144 'postgres',
145 'oracle'
146 ];
147
148 public function __construct( $name = null, array $data = [], $dataName = '' ) {
149 parent::__construct( $name, $data, $dataName );
150
151 $this->backupGlobals = false;
152 $this->backupStaticAttributes = false;
153 }
154
155 public function __destruct() {
156 // Complain if self::setUp() was called, but not self::tearDown()
157 // $this->called['setUp'] will be checked by self::testMediaWikiTestCaseParentSetupCalled()
158 if ( isset( $this->called['setUp'] ) && !isset( $this->called['tearDown'] ) ) {
159 throw new MWException( static::class . "::tearDown() must call parent::tearDown()" );
160 }
161 }
162
163 public static function setUpBeforeClass() {
164 parent::setUpBeforeClass();
165
166 // Get the original service locator
167 if ( !self::$originalServices ) {
168 self::$originalServices = MediaWikiServices::getInstance();
169 }
170 }
171
180 public static function getTestUser( $groups = [] ) {
181 return TestUserRegistry::getImmutableTestUser( $groups );
182 }
183
192 public static function getMutableTestUser( $groups = [] ) {
193 return TestUserRegistry::getMutableTestUser( __CLASS__, $groups );
194 }
195
204 public static function getTestSysop() {
205 return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
206 }
207
220 protected function getExistingTestPage( $title = null ) {
221 if ( !$this->needsDB() ) {
222 throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
223 ' method should return true. Use @group Database or $this->tablesUsed.' );
224 }
225
226 $title = ( $title === null ) ? 'UTPage' : $title;
227 $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
228 $page = WikiPage::factory( $title );
229
230 if ( !$page->exists() ) {
231 $user = self::getTestSysop()->getUser();
232 $page->doEditContent(
233 new WikitextContent( 'UTContent' ),
234 'UTPageSummary',
236 false,
237 $user
238 );
239 }
240
241 return $page;
242 }
243
256 protected function getNonexistingTestPage( $title = null ) {
257 if ( !$this->needsDB() ) {
258 throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
259 ' method should return true. Use @group Database or $this->tablesUsed.' );
260 }
261
262 $title = ( $title === null ) ? 'UTPage-' . rand( 0, 100000 ) : $title;
263 $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
264 $page = WikiPage::factory( $title );
265
266 if ( $page->exists() ) {
267 $page->doDeleteArticle( 'Testing' );
268 }
269
270 return $page;
271 }
272
276 public static function prepareServices( Config $bootstrapConfig ) {
277 }
278
288 private static function makeTestConfig(
289 Config $baseConfig = null,
290 Config $customOverrides = null
291 ) {
292 $defaultOverrides = new HashConfig();
293
294 if ( !$baseConfig ) {
295 $baseConfig = self::$originalServices->getBootstrapConfig();
296 }
297
298 /* Some functions require some kind of caching, and will end up using the db,
299 * which we can't allow, as that would open a new connection for mysql.
300 * Replace with a HashBag. They would not be going to persist anyway.
301 */
302 $hashCache = [ 'class' => HashBagOStuff::class, 'reportDupes' => false ];
303 $objectCaches = [
304 CACHE_DB => $hashCache,
305 CACHE_ACCEL => $hashCache,
306 CACHE_MEMCACHED => $hashCache,
307 'apc' => $hashCache,
308 'apcu' => $hashCache,
309 'wincache' => $hashCache,
310 ] + $baseConfig->get( 'ObjectCaches' );
311
312 $defaultOverrides->set( 'ObjectCaches', $objectCaches );
313 $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
314 $defaultOverrides->set( 'JobTypeConf', [ 'default' => [ 'class' => JobQueueMemory::class ] ] );
315
316 // Use a fast hash algorithm to hash passwords.
317 $defaultOverrides->set( 'PasswordDefault', 'A' );
318
319 $testConfig = $customOverrides
320 ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
321 : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
322
323 return $testConfig;
324 }
325
332 private static function makeTestConfigFactoryInstantiator(
333 ConfigFactory $oldFactory,
334 array $configurations
335 ) {
336 return function ( MediaWikiServices $services ) use ( $oldFactory, $configurations ) {
337 $factory = new ConfigFactory();
338
339 // clone configurations from $oldFactory that are not overwritten by $configurations
340 $namesToClone = array_diff(
341 $oldFactory->getConfigNames(),
342 array_keys( $configurations )
343 );
344
345 foreach ( $namesToClone as $name ) {
346 $factory->register( $name, $oldFactory->makeConfig( $name ) );
347 }
348
349 foreach ( $configurations as $name => $config ) {
350 $factory->register( $name, $config );
351 }
352
353 return $factory;
354 };
355 }
356
361 public static function resetNonServiceCaches() {
363
365 foreach ( $wgJobClasses as $type => $class ) {
366 JobQueueGroup::singleton()->get( $type )->delete();
367 }
368 JobQueueGroup::destroySingletons();
369
370 ObjectCache::clear();
372 DeferredUpdates::clearPendingUpdates();
373
374 // TODO: move global state into MediaWikiServices
375 RequestContext::resetMain();
376 if ( session_id() !== '' ) {
377 session_write_close();
378 session_id( '' );
379 }
380
381 $wgRequest = new FauxRequest();
382 MediaWiki\Session\SessionManager::resetCache();
383 }
384
385 public function run( PHPUnit_Framework_TestResult $result = null ) {
386 if ( $result instanceof MediaWikiTestResult ) {
387 $this->cliArgs = $result->getMediaWikiCliArgs();
388 }
389 $this->overrideMwServices();
390
391 if ( $this->needsDB() && !$this->isTestInDatabaseGroup() ) {
392 throw new Exception(
393 get_class( $this ) . ' apparently needsDB but is not in the Database group'
394 );
395 }
396
397 $needsResetDB = false;
398 if ( !self::$dbSetup || $this->needsDB() ) {
399 // set up a DB connection for this test to use
400
401 self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
402 self::$reuseDB = $this->getCliArg( 'reuse-db' );
403
404 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
405 $this->db = $lb->getConnection( DB_MASTER );
406
407 $this->checkDbIsSupported();
408
409 if ( !self::$dbSetup ) {
410 $this->setupAllTestDBs();
411 $this->addCoreDBData();
412 }
413
414 // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
415 // is available in subclass's setUpBeforeClass() and setUp() methods.
416 // This would also remove the need for the HACK that is oncePerClass().
417 if ( $this->oncePerClass() ) {
418 $this->setUpSchema( $this->db );
419 $this->resetDB( $this->db, $this->tablesUsed );
420 $this->addDBDataOnce();
421 }
422
423 $this->addDBData();
424 $needsResetDB = true;
425 }
426
427 parent::run( $result );
428
429 // We don't mind if we override already-overridden services during cleanup
430 $this->overriddenServices = [];
431
432 if ( $needsResetDB ) {
433 $this->resetDB( $this->db, $this->tablesUsed );
434 }
435
436 self::restoreMwServices();
437 $this->localServices = null;
438 }
439
443 private function oncePerClass() {
444 // Remember current test class in the database connection,
445 // so we know when we need to run addData.
446
447 $class = static::class;
448
449 $first = !isset( $this->db->_hasDataForTestClass )
450 || $this->db->_hasDataForTestClass !== $class;
451
452 $this->db->_hasDataForTestClass = $class;
453 return $first;
454 }
455
461 public function usesTemporaryTables() {
462 return self::$useTemporaryTables;
463 }
464
474 protected function getNewTempFile() {
475 $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' );
476 $this->tmpFiles[] = $fileName;
477
478 return $fileName;
479 }
480
491 protected function getNewTempDirectory() {
492 // Starting of with a temporary /file/.
493 $fileName = $this->getNewTempFile();
494
495 // Converting the temporary /file/ to a /directory/
496 // The following is not atomic, but at least we now have a single place,
497 // where temporary directory creation is bundled and can be improved
498 unlink( $fileName );
499 $this->assertTrue( wfMkdirParents( $fileName ) );
500
501 return $fileName;
502 }
503
504 protected function setUp() {
505 parent::setUp();
506 $this->called['setUp'] = true;
507
508 $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
509
510 $this->overriddenServices = [];
511
512 // Cleaning up temporary files
513 foreach ( $this->tmpFiles as $fileName ) {
514 if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
515 unlink( $fileName );
516 } elseif ( is_dir( $fileName ) ) {
517 wfRecursiveRemoveDir( $fileName );
518 }
519 }
520
521 if ( $this->needsDB() && $this->db ) {
522 // Clean up open transactions
523 while ( $this->db->trxLevel() > 0 ) {
524 $this->db->rollback( __METHOD__, 'flush' );
525 }
526 // Check for unsafe queries
527 if ( $this->db->getType() === 'mysql' ) {
528 $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'", __METHOD__ );
529 }
530 }
531
532 // Reset all caches between tests.
533 self::resetNonServiceCaches();
534
535 // XXX: reset maintenance triggers
536 // Hook into period lag checks which often happen in long-running scripts
537 $lbFactory = $this->localServices->getDBLoadBalancerFactory();
538 Maintenance::setLBFactoryTriggers( $lbFactory, $this->localServices->getMainConfig() );
539
540 ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
541 }
542
543 protected function addTmpFiles( $files ) {
544 $this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
545 }
546
547 protected function tearDown() {
548 global $wgRequest, $wgSQLMode;
549
550 $status = ob_get_status();
551 if ( isset( $status['name'] ) &&
552 $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
553 ) {
554 ob_end_flush();
555 }
556
557 $this->called['tearDown'] = true;
558 // Cleaning up temporary files
559 foreach ( $this->tmpFiles as $fileName ) {
560 if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
561 unlink( $fileName );
562 } elseif ( is_dir( $fileName ) ) {
563 wfRecursiveRemoveDir( $fileName );
564 }
565 }
566
567 if ( $this->needsDB() && $this->db ) {
568 // Clean up open transactions
569 while ( $this->db->trxLevel() > 0 ) {
570 $this->db->rollback( __METHOD__, 'flush' );
571 }
572 if ( $this->db->getType() === 'mysql' ) {
573 $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ),
574 __METHOD__ );
575 }
576 }
577
578 // Re-enable any disabled deprecation warnings
579 MWDebug::clearLog();
580 // Restore mw globals
581 foreach ( $this->mwGlobals as $key => $value ) {
582 $GLOBALS[$key] = $value;
583 }
584 foreach ( $this->mwGlobalsToUnset as $value ) {
585 unset( $GLOBALS[$value] );
586 }
587 foreach ( $this->iniSettings as $name => $value ) {
588 ini_set( $name, $value );
589 }
590 if (
591 array_key_exists( 'wgExtraNamespaces', $this->mwGlobals ) ||
592 in_array( 'wgExtraNamespaces', $this->mwGlobalsToUnset )
593 ) {
594 $this->resetNamespaces();
595 }
596 $this->mwGlobals = [];
597 $this->mwGlobalsToUnset = [];
598 $this->restoreLoggers();
599
600 // TODO: move global state into MediaWikiServices
601 RequestContext::resetMain();
602 if ( session_id() !== '' ) {
603 session_write_close();
604 session_id( '' );
605 }
606 $wgRequest = new FauxRequest();
607 MediaWiki\Session\SessionManager::resetCache();
608 MediaWiki\Auth\AuthManager::resetCache();
609
610 $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
611
612 if ( $phpErrorLevel !== $this->phpErrorLevel ) {
613 ini_set( 'error_reporting', $this->phpErrorLevel );
614
615 $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
616 $newHex = strtoupper( dechex( $phpErrorLevel ) );
617 $message = "PHP error_reporting setting was left dirty: "
618 . "was 0x$oldHex before test, 0x$newHex after test!";
619
620 $this->fail( $message );
621 }
622
623 parent::tearDown();
624 }
625
635 $this->assertArrayHasKey( 'setUp', $this->called,
636 static::class . '::setUp() must call parent::setUp()'
637 );
638 }
639
649 protected function setService( $name, $object ) {
650 if ( !$this->localServices ) {
651 throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
652 }
653
654 if ( $this->localServices !== MediaWikiServices::getInstance() ) {
655 throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
656 . 'instance has been replaced by test code.' );
657 }
658
659 $this->overriddenServices[] = $name;
660
661 $this->localServices->disableService( $name );
662 $this->localServices->redefineService(
663 $name,
664 function () use ( $object ) {
665 return $object;
666 }
667 );
668
669 if ( $name === 'ContentLanguage' ) {
670 $this->doSetMwGlobals( [ 'wgContLang' => $object ] );
671 }
672 }
673
709 protected function setMwGlobals( $pairs, $value = null ) {
710 if ( is_string( $pairs ) ) {
711 $pairs = [ $pairs => $value ];
712 }
713
714 if ( isset( $pairs['wgContLang'] ) ) {
715 throw new MWException(
716 'No setting $wgContLang, use setContentLang() or setService( \'ContentLanguage\' )'
717 );
718 }
719
720 $this->doSetMwGlobals( $pairs, $value );
721 }
722
727 private function doSetMwGlobals( $pairs, $value = null ) {
728 $this->doStashMwGlobals( array_keys( $pairs ) );
729
730 foreach ( $pairs as $key => $value ) {
731 $GLOBALS[$key] = $value;
732 }
733
734 if ( array_key_exists( 'wgExtraNamespaces', $pairs ) ) {
735 $this->resetNamespaces();
736 }
737 }
738
745 protected function setIniSetting( $name, $value ) {
746 $original = ini_get( $name );
747 $this->iniSettings[$name] = $original;
748 ini_set( $name, $value );
749 }
750
755 private function resetNamespaces() {
756 if ( !$this->localServices ) {
757 throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
758 }
759
760 if ( $this->localServices !== MediaWikiServices::getInstance() ) {
761 throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
762 . 'instance has been replaced by test code.' );
763 }
764
765 MWNamespace::clearCaches();
766 Language::clearCaches();
767
768 // We can't have the TitleFormatter holding on to an old Language object either
769 // @todo We shouldn't need to reset all the aliases here.
770 $this->localServices->resetServiceForTesting( 'TitleFormatter' );
771 $this->localServices->resetServiceForTesting( 'TitleParser' );
772 $this->localServices->resetServiceForTesting( '_MediaWikiTitleCodec' );
773 }
774
783 private static function canShallowCopy( $value ) {
784 if ( is_scalar( $value ) || $value === null ) {
785 return true;
786 }
787 if ( is_array( $value ) ) {
788 foreach ( $value as $subValue ) {
789 if ( !is_scalar( $subValue ) && $subValue !== null ) {
790 return false;
791 }
792 }
793 return true;
794 }
795 return false;
796 }
797
816 protected function stashMwGlobals( $globalKeys ) {
817 wfDeprecated( __METHOD__, '1.32' );
818 $this->doStashMwGlobals( $globalKeys );
819 }
820
821 private function doStashMwGlobals( $globalKeys ) {
822 if ( is_string( $globalKeys ) ) {
823 $globalKeys = [ $globalKeys ];
824 }
825
826 foreach ( $globalKeys as $globalKey ) {
827 // NOTE: make sure we only save the global once or a second call to
828 // setMwGlobals() on the same global would override the original
829 // value.
830 if (
831 !array_key_exists( $globalKey, $this->mwGlobals ) &&
832 !array_key_exists( $globalKey, $this->mwGlobalsToUnset )
833 ) {
834 if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
835 $this->mwGlobalsToUnset[$globalKey] = $globalKey;
836 continue;
837 }
838 // NOTE: we serialize then unserialize the value in case it is an object
839 // this stops any objects being passed by reference. We could use clone
840 // and if is_object but this does account for objects within objects!
841 if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) {
842 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
843 } elseif (
844 // Many MediaWiki types are safe to clone. These are the
845 // ones that are most commonly stashed.
846 $GLOBALS[$globalKey] instanceof Language ||
847 $GLOBALS[$globalKey] instanceof User ||
848 $GLOBALS[$globalKey] instanceof FauxRequest
849 ) {
850 $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey];
851 } elseif ( $this->containsClosure( $GLOBALS[$globalKey] ) ) {
852 // Serializing Closure only gives a warning on HHVM while
853 // it throws an Exception on Zend.
854 // Workaround for https://github.com/facebook/hhvm/issues/6206
855 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
856 } else {
857 try {
858 $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
859 } catch ( Exception $e ) {
860 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
861 }
862 }
863 }
864 }
865 }
866
873 private function containsClosure( $var, $maxDepth = 15 ) {
874 if ( $var instanceof Closure ) {
875 return true;
876 }
877 if ( !is_array( $var ) || $maxDepth === 0 ) {
878 return false;
879 }
880
881 foreach ( $var as $value ) {
882 if ( $this->containsClosure( $value, $maxDepth - 1 ) ) {
883 return true;
884 }
885 }
886 return false;
887 }
888
904 protected function mergeMwGlobalArrayValue( $name, $values ) {
905 if ( !isset( $GLOBALS[$name] ) ) {
906 $merged = $values;
907 } else {
908 if ( !is_array( $GLOBALS[$name] ) ) {
909 throw new MWException( "MW global $name is not an array." );
910 }
911
912 // NOTE: do not use array_merge, it screws up for numeric keys.
913 $merged = $GLOBALS[$name];
914 foreach ( $values as $k => $v ) {
915 $merged[$k] = $v;
916 }
917 }
918
919 $this->setMwGlobals( $name, $merged );
920 }
921
937 protected function overrideMwServices(
938 Config $configOverrides = null, array $services = []
939 ) {
940 if ( $this->overriddenServices ) {
941 throw new MWException(
942 'The following services were set and are now being unset by overrideMwServices: ' .
943 implode( ', ', $this->overriddenServices )
944 );
945 }
946 $newInstance = self::installMockMwServices( $configOverrides );
947
948 if ( $this->localServices ) {
949 $this->localServices->destroy();
950 }
951
952 $this->localServices = $newInstance;
953
954 foreach ( $services as $name => $callback ) {
955 $newInstance->redefineService( $name, $callback );
956 }
957
958 return $newInstance;
959 }
960
978 public static function installMockMwServices( Config $configOverrides = null ) {
979 // Make sure we have the original service locator
980 if ( !self::$originalServices ) {
981 self::$originalServices = MediaWikiServices::getInstance();
982 }
983
984 if ( !$configOverrides ) {
985 $configOverrides = new HashConfig();
986 }
987
988 $oldConfigFactory = self::$originalServices->getConfigFactory();
989 $oldLoadBalancerFactory = self::$originalServices->getDBLoadBalancerFactory();
990
991 $testConfig = self::makeTestConfig( null, $configOverrides );
992 $newServices = new MediaWikiServices( $testConfig );
993
994 // Load the default wiring from the specified files.
995 // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
996 $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
997 $newServices->loadWiringFiles( $wiringFiles );
998
999 // Provide a traditional hook point to allow extensions to configure services.
1000 Hooks::run( 'MediaWikiServices', [ $newServices ] );
1001
1002 // Use bootstrap config for all configuration.
1003 // This allows config overrides via global variables to take effect.
1004 $bootstrapConfig = $newServices->getBootstrapConfig();
1005 $newServices->resetServiceForTesting( 'ConfigFactory' );
1006 $newServices->redefineService(
1007 'ConfigFactory',
1008 self::makeTestConfigFactoryInstantiator(
1009 $oldConfigFactory,
1010 [ 'main' => $bootstrapConfig ]
1011 )
1012 );
1013 $newServices->resetServiceForTesting( 'DBLoadBalancerFactory' );
1014 $newServices->redefineService(
1015 'DBLoadBalancerFactory',
1016 function ( MediaWikiServices $services ) use ( $oldLoadBalancerFactory ) {
1017 return $oldLoadBalancerFactory;
1018 }
1019 );
1020
1021 MediaWikiServices::forceGlobalInstance( $newServices );
1022 return $newServices;
1023 }
1024
1036 public static function restoreMwServices() {
1037 if ( !self::$originalServices ) {
1038 return false;
1039 }
1040
1041 $currentServices = MediaWikiServices::getInstance();
1042
1043 if ( self::$originalServices === $currentServices ) {
1044 return false;
1045 }
1046
1047 MediaWikiServices::forceGlobalInstance( self::$originalServices );
1048 $currentServices->destroy();
1049
1050 return true;
1051 }
1052
1057 public function setUserLang( $lang ) {
1058 RequestContext::getMain()->setLanguage( $lang );
1059 $this->setMwGlobals( 'wgLang', RequestContext::getMain()->getLanguage() );
1060 }
1061
1066 public function setContentLang( $lang ) {
1067 if ( $lang instanceof Language ) {
1068 $this->setMwGlobals( 'wgLanguageCode', $lang->getCode() );
1069 // Set to the exact object requested
1070 $this->setService( 'ContentLanguage', $lang );
1071 } else {
1072 $this->setMwGlobals( 'wgLanguageCode', $lang );
1073 // Let the service handler make up the object. Avoid calling setService(), because if
1074 // we do, overrideMwServices() will complain if it's called later on.
1075 $services = MediaWikiServices::getInstance();
1076 $services->resetServiceForTesting( 'ContentLanguage' );
1077 $this->doSetMwGlobals( [ 'wgContLang' => $services->getContentLanguage() ] );
1078 }
1079 }
1080
1095 public function setGroupPermissions( $newPerms, $newKey = null, $newValue = null ) {
1096 global $wgGroupPermissions;
1097
1098 if ( is_string( $newPerms ) ) {
1099 $newPerms = [ $newPerms => [ $newKey => $newValue ] ];
1100 }
1101
1102 $newPermissions = $wgGroupPermissions;
1103 foreach ( $newPerms as $group => $permissions ) {
1104 foreach ( $permissions as $key => $value ) {
1105 $newPermissions[$group][$key] = $value;
1106 }
1107 }
1108
1109 $this->setMwGlobals( 'wgGroupPermissions', $newPermissions );
1110 }
1111
1118 protected function setLogger( $channel, LoggerInterface $logger ) {
1119 // TODO: Once loggers are managed by MediaWikiServices, use
1120 // overrideMwServices() to set loggers.
1121
1122 $provider = LoggerFactory::getProvider();
1123 $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
1124 $singletons = $wrappedProvider->singletons;
1125 if ( $provider instanceof MonologSpi ) {
1126 if ( !isset( $this->loggers[$channel] ) ) {
1127 $this->loggers[$channel] = $singletons['loggers'][$channel] ?? null;
1128 }
1129 $singletons['loggers'][$channel] = $logger;
1130 } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
1131 if ( !isset( $this->loggers[$channel] ) ) {
1132 $this->loggers[$channel] = $singletons[$channel] ?? null;
1133 }
1134 $singletons[$channel] = $logger;
1135 } else {
1136 throw new LogicException( __METHOD__ . ': setting a logger for ' . get_class( $provider )
1137 . ' is not implemented' );
1138 }
1139 $wrappedProvider->singletons = $singletons;
1140 }
1141
1146 private function restoreLoggers() {
1147 $provider = LoggerFactory::getProvider();
1148 $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
1149 $singletons = $wrappedProvider->singletons;
1150 foreach ( $this->loggers as $channel => $logger ) {
1151 if ( $provider instanceof MonologSpi ) {
1152 if ( $logger === null ) {
1153 unset( $singletons['loggers'][$channel] );
1154 } else {
1155 $singletons['loggers'][$channel] = $logger;
1156 }
1157 } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
1158 if ( $logger === null ) {
1159 unset( $singletons[$channel] );
1160 } else {
1161 $singletons[$channel] = $logger;
1162 }
1163 }
1164 }
1165 $wrappedProvider->singletons = $singletons;
1166 $this->loggers = [];
1167 }
1168
1173 public function dbPrefix() {
1174 return self::getTestPrefixFor( $this->db );
1175 }
1176
1182 public static function getTestPrefixFor( IDatabase $db ) {
1183 return $db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
1184 }
1185
1190 public function needsDB() {
1191 // If the test says it uses database tables, it needs the database
1192 return $this->tablesUsed || $this->isTestInDatabaseGroup();
1193 }
1194
1199 protected function isTestInDatabaseGroup() {
1200 // If the test class says it belongs to the Database group, it needs the database.
1201 // NOTE: This ONLY checks for the group in the class level doc comment.
1202 $rc = new ReflectionClass( $this );
1203 return (bool)preg_match( '/@group +Database/im', $rc->getDocComment() );
1204 }
1205
1222 protected function insertPage(
1223 $pageName,
1224 $text = 'Sample page for unit test.',
1225 $namespace = null,
1226 User $user = null
1227 ) {
1228 if ( !$this->needsDB() ) {
1229 throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
1230 ' method should return true. Use @group Database or $this->tablesUsed.' );
1231 }
1232
1233 if ( is_string( $pageName ) ) {
1234 $title = Title::newFromText( $pageName, $namespace );
1235 } else {
1236 $title = $pageName;
1237 }
1238
1239 if ( !$user ) {
1240 $user = static::getTestSysop()->getUser();
1241 }
1242 $comment = __METHOD__ . ': Sample page for unit test.';
1243
1244 $page = WikiPage::factory( $title );
1245 $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
1246
1247 return [
1248 'title' => $title,
1249 'id' => $page->getId(),
1250 ];
1251 }
1252
1268 public function addDBDataOnce() {
1269 }
1270
1280 public function addDBData() {
1281 }
1282
1286 protected function addCoreDBData() {
1287 if ( $this->db->getType() == 'oracle' ) {
1288 # Insert 0 user to prevent FK violations
1289 # Anonymous user
1290 if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) {
1291 $this->db->insert( 'user', [
1292 'user_id' => 0,
1293 'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
1294 }
1295
1296 # Insert 0 page to prevent FK violations
1297 # Blank page
1298 if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) {
1299 $this->db->insert( 'page', [
1300 'page_id' => 0,
1301 'page_namespace' => 0,
1302 'page_title' => ' ',
1303 'page_restrictions' => null,
1304 'page_is_redirect' => 0,
1305 'page_is_new' => 0,
1306 'page_random' => 0,
1307 'page_touched' => $this->db->timestamp(),
1308 'page_latest' => 0,
1309 'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
1310 }
1311 }
1312
1314
1316
1317 // Make sysop user
1318 $user = static::getTestSysop()->getUser();
1319
1320 // Make 1 page with 1 revision
1321 $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
1322 if ( $page->getId() == 0 ) {
1323 $page->doEditContent(
1324 new WikitextContent( 'UTContent' ),
1325 'UTPageSummary',
1327 false,
1328 $user
1329 );
1330 // an edit always attempt to purge backlink links such as history
1331 // pages. That is unnecessary.
1332 JobQueueGroup::singleton()->get( 'htmlCacheUpdate' )->delete();
1333 // WikiPages::doEditUpdates randomly adds RC purges
1334 JobQueueGroup::singleton()->get( 'recentChangesUpdate' )->delete();
1335
1336 // doEditContent() probably started the session via
1337 // User::loadFromSession(). Close it now.
1338 if ( session_id() !== '' ) {
1339 session_write_close();
1340 session_id( '' );
1341 }
1342 }
1343 }
1344
1354 public static function teardownTestDB() {
1355 global $wgJobClasses;
1356
1357 if ( !self::$dbSetup ) {
1358 return;
1359 }
1360
1361 Hooks::run( 'UnitTestsBeforeDatabaseTeardown' );
1362
1363 foreach ( $wgJobClasses as $type => $class ) {
1364 // Delete any jobs under the clone DB (or old prefix in other stores)
1365 JobQueueGroup::singleton()->get( $type )->delete();
1366 }
1367
1368 // T219673: close any connections from code that failed to call reuseConnection()
1369 // or is still holding onto a DBConnRef instance (e.g. in a singleton).
1370 MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->closeAll();
1371 CloneDatabase::changePrefix( self::$oldTablePrefix );
1372
1373 self::$oldTablePrefix = false;
1374 self::$dbSetup = false;
1375 }
1376
1388 protected static function setupDatabaseWithTestPrefix(
1390 $prefix = null
1391 ) {
1392 if ( $prefix === null ) {
1393 $prefix = self::getTestPrefixFor( $db );
1394 }
1395
1396 if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
1397 $db->tablePrefix( $prefix );
1398 return false;
1399 }
1400
1401 if ( !isset( $db->_originalTablePrefix ) ) {
1402 $oldPrefix = $db->tablePrefix();
1403
1404 if ( $oldPrefix === $prefix ) {
1405 // table already has the correct prefix, but presumably no cloned tables
1406 $oldPrefix = self::$oldTablePrefix;
1407 }
1408
1409 $db->tablePrefix( $oldPrefix );
1410 $tablesCloned = self::listTables( $db );
1411 $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix, $oldPrefix );
1412 $dbClone->useTemporaryTables( self::$useTemporaryTables );
1413
1414 $dbClone->cloneTableStructure();
1415
1416 $db->tablePrefix( $prefix );
1417 $db->_originalTablePrefix = $oldPrefix;
1418 }
1419
1420 return true;
1421 }
1422
1426 public function setupAllTestDBs() {
1427 global $wgDBprefix;
1428
1429 self::$oldTablePrefix = $wgDBprefix;
1430
1431 $testPrefix = $this->dbPrefix();
1432
1433 // switch to a temporary clone of the database
1434 self::setupTestDB( $this->db, $testPrefix );
1435
1436 if ( self::isUsingExternalStoreDB() ) {
1437 self::setupExternalStoreTestDBs( $testPrefix );
1438 }
1439
1440 // NOTE: Change the prefix in the LBFactory and $wgDBprefix, to prevent
1441 // *any* database connections to operate on live data.
1442 CloneDatabase::changePrefix( $testPrefix );
1443 }
1444
1466 public static function setupTestDB( IMaintainableDatabase $db, $prefix ) {
1467 if ( self::$dbSetup ) {
1468 return;
1469 }
1470
1471 if ( $db->tablePrefix() === $prefix ) {
1472 throw new MWException(
1473 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
1474 }
1475
1476 // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
1477 // and Database no longer use global state.
1478
1479 self::$dbSetup = true;
1480
1481 if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
1482 return;
1483 }
1484
1485 // Assuming this isn't needed for External Store database, and not sure if the procedure
1486 // would be available there.
1487 if ( $db->getType() == 'oracle' ) {
1488 $db->query( 'BEGIN FILL_WIKI_INFO; END;', __METHOD__ );
1489 }
1490
1491 Hooks::run( 'UnitTestsAfterDatabaseSetup', [ $db, $prefix ] );
1492 }
1493
1500 protected static function setupExternalStoreTestDBs( $testPrefix = null ) {
1501 $connections = self::getExternalStoreDatabaseConnections();
1502 foreach ( $connections as $dbw ) {
1503 self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
1504 }
1505 }
1506
1513 protected static function getExternalStoreDatabaseConnections() {
1515
1517 $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
1518 $defaultArray = (array)$wgDefaultExternalStore;
1519 $dbws = [];
1520 foreach ( $defaultArray as $url ) {
1521 if ( strpos( $url, 'DB://' ) === 0 ) {
1522 list( $proto, $cluster ) = explode( '://', $url, 2 );
1523 // Avoid getMaster() because setupDatabaseWithTestPrefix()
1524 // requires Database instead of plain DBConnRef/IDatabase
1525 $dbws[] = $externalStoreDB->getMaster( $cluster );
1526 }
1527 }
1528
1529 return $dbws;
1530 }
1531
1537 protected static function isUsingExternalStoreDB() {
1539 if ( !$wgDefaultExternalStore ) {
1540 return false;
1541 }
1542
1543 $defaultArray = (array)$wgDefaultExternalStore;
1544 foreach ( $defaultArray as $url ) {
1545 if ( strpos( $url, 'DB://' ) === 0 ) {
1546 return true;
1547 }
1548 }
1549
1550 return false;
1551 }
1552
1559 protected function ensureMockDatabaseConnection( IDatabase $db ) {
1560 if ( $db->tablePrefix() !== $this->dbPrefix() ) {
1561 throw new LogicException(
1562 'Trying to delete mock tables, but table prefix does not indicate a mock database.'
1563 );
1564 }
1565 }
1566
1567 private static $schemaOverrideDefaults = [
1568 'scripts' => [],
1569 'create' => [],
1570 'drop' => [],
1571 'alter' => [],
1572 ];
1573
1588 protected function getSchemaOverrides( IMaintainableDatabase $db ) {
1589 return [];
1590 }
1591
1599 private function undoSchemaOverrides( IMaintainableDatabase $db, $oldOverrides ) {
1600 $this->ensureMockDatabaseConnection( $db );
1601
1602 $oldOverrides = $oldOverrides + self::$schemaOverrideDefaults;
1603 $originalTables = $this->listOriginalTables( $db );
1604
1605 // Drop tables that need to be restored or removed.
1606 $tablesToDrop = array_merge( $oldOverrides['create'], $oldOverrides['alter'] );
1607
1608 // Restore tables that have been dropped or created or altered,
1609 // if they exist in the original schema.
1610 $tablesToRestore = array_merge( $tablesToDrop, $oldOverrides['drop'] );
1611 $tablesToRestore = array_intersect( $originalTables, $tablesToRestore );
1612
1613 if ( $tablesToDrop ) {
1614 $this->dropMockTables( $db, $tablesToDrop );
1615 }
1616
1617 if ( $tablesToRestore ) {
1618 $this->recloneMockTables( $db, $tablesToRestore );
1619
1620 // Reset the restored tables, mainly for the side effect of
1621 // re-calling $this->addCoreDBData() if necessary.
1622 $this->resetDB( $db, $tablesToRestore );
1623 }
1624 }
1625
1631 private function setUpSchema( IMaintainableDatabase $db ) {
1632 // Undo any active overrides.
1633 $oldOverrides = $db->_schemaOverrides ?? self::$schemaOverrideDefaults;
1634
1635 if ( $oldOverrides['alter'] || $oldOverrides['create'] || $oldOverrides['drop'] ) {
1636 $this->undoSchemaOverrides( $db, $oldOverrides );
1637 unset( $db->_schemaOverrides );
1638 }
1639
1640 // Determine new overrides.
1641 $overrides = $this->getSchemaOverrides( $db ) + self::$schemaOverrideDefaults;
1642
1643 $extraKeys = array_diff(
1644 array_keys( $overrides ),
1645 array_keys( self::$schemaOverrideDefaults )
1646 );
1647
1648 if ( $extraKeys ) {
1649 throw new InvalidArgumentException(
1650 'Schema override contains extra keys: ' . var_export( $extraKeys, true )
1651 );
1652 }
1653
1654 if ( !$overrides['scripts'] ) {
1655 // no scripts to run
1656 return;
1657 }
1658
1659 if ( !$overrides['create'] && !$overrides['drop'] && !$overrides['alter'] ) {
1660 throw new InvalidArgumentException(
1661 'Schema override scripts given, but no tables are declared to be '
1662 . 'created, dropped or altered.'
1663 );
1664 }
1665
1666 $this->ensureMockDatabaseConnection( $db );
1667
1668 // Drop the tables that will be created by the schema scripts.
1669 $originalTables = $this->listOriginalTables( $db );
1670 $tablesToDrop = array_intersect( $originalTables, $overrides['create'] );
1671
1672 if ( $tablesToDrop ) {
1673 $this->dropMockTables( $db, $tablesToDrop );
1674 }
1675
1676 // Run schema override scripts.
1677 foreach ( $overrides['scripts'] as $script ) {
1678 $db->sourceFile(
1679 $script,
1680 null,
1681 null,
1682 __METHOD__,
1683 function ( $cmd ) {
1684 return $this->mungeSchemaUpdateQuery( $cmd );
1685 }
1686 );
1687 }
1688
1689 $db->_schemaOverrides = $overrides;
1690 }
1691
1692 private function mungeSchemaUpdateQuery( $cmd ) {
1693 return self::$useTemporaryTables
1694 ? preg_replace( '/\bCREATE\s+TABLE\b/i', 'CREATE TEMPORARY TABLE', $cmd )
1695 : $cmd;
1696 }
1697
1705 $this->ensureMockDatabaseConnection( $db );
1706
1707 foreach ( $tables as $tbl ) {
1708 $tbl = $db->tableName( $tbl );
1709 $db->query( "DROP TABLE IF EXISTS $tbl", __METHOD__ );
1710 }
1711 }
1712
1720 if ( !isset( $db->_originalTablePrefix ) ) {
1721 throw new LogicException( 'No original table prefix know, cannot list tables!' );
1722 }
1723
1724 $originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ );
1725
1726 $unittestPrefixRegex = '/^' . preg_quote( $this->dbPrefix(), '/' ) . '/';
1727 $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/';
1728
1729 $originalTables = array_filter(
1730 $originalTables,
1731 function ( $pt ) use ( $unittestPrefixRegex ) {
1732 return !preg_match( $unittestPrefixRegex, $pt );
1733 }
1734 );
1735
1736 $originalTables = array_map(
1737 function ( $pt ) use ( $originalPrefixRegex ) {
1738 return preg_replace( $originalPrefixRegex, '', $pt );
1739 },
1740 $originalTables
1741 );
1742
1743 return array_unique( $originalTables );
1744 }
1745
1755 $this->ensureMockDatabaseConnection( $db );
1756
1757 if ( !isset( $db->_originalTablePrefix ) ) {
1758 throw new LogicException( 'No original table prefix know, cannot restore tables!' );
1759 }
1760
1761 $originalTables = $this->listOriginalTables( $db );
1762 $tables = array_intersect( $tables, $originalTables );
1763
1764 $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix );
1765 $dbClone->useTemporaryTables( self::$useTemporaryTables );
1766
1767 $dbClone->cloneTableStructure();
1768 }
1769
1776 private function resetDB( $db, $tablesUsed ) {
1777 if ( $db ) {
1778 $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
1779 $pageTables = [
1780 'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive',
1781 'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles',
1782 ];
1783 $coreDBDataTables = array_merge( $userTables, $pageTables );
1784
1785 // If any of the user or page tables were marked as used, we should clear all of them.
1786 if ( array_intersect( $tablesUsed, $userTables ) ) {
1787 $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
1788 TestUserRegistry::clear();
1789
1790 // Reset $wgUser, which is probably 127.0.0.1, as its loaded data is probably not valid
1791 // @todo Should we start setting $wgUser to something nondeterministic
1792 // to encourage tests to be updated to not depend on it?
1793 global $wgUser;
1794 $wgUser->clearInstanceCache( $wgUser->mFrom );
1795 }
1796 if ( array_intersect( $tablesUsed, $pageTables ) ) {
1797 $tablesUsed = array_unique( array_merge( $tablesUsed, $pageTables ) );
1798 }
1799
1800 // Postgres, Oracle, and MSSQL all use mwuser/pagecontent
1801 // instead of user/text. But Postgres does not remap the
1802 // table name in tableExists(), so we mark the real table
1803 // names as being used.
1804 if ( $db->getType() === 'postgres' ) {
1805 if ( in_array( 'user', $tablesUsed ) ) {
1806 $tablesUsed[] = 'mwuser';
1807 }
1808 if ( in_array( 'text', $tablesUsed ) ) {
1809 $tablesUsed[] = 'pagecontent';
1810 }
1811 }
1812
1813 foreach ( $tablesUsed as $tbl ) {
1814 $this->truncateTable( $tbl, $db );
1815 }
1816
1817 if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) {
1818 // Reset services that may contain information relating to the truncated tables
1819 $this->overrideMwServices();
1820 // Re-add core DB data that was deleted
1821 $this->addCoreDBData();
1822 }
1823 }
1824 }
1825
1834 protected function truncateTable( $tableName, IDatabase $db = null ) {
1835 if ( !$db ) {
1836 $db = $this->db;
1837 }
1838
1839 if ( !$db->tableExists( $tableName ) ) {
1840 return;
1841 }
1842
1843 $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
1844
1845 if ( $truncate ) {
1846 $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tableName ), __METHOD__ );
1847 } else {
1848 $db->delete( $tableName, '*', __METHOD__ );
1849 }
1850
1851 if ( $db instanceof DatabasePostgres || $db instanceof DatabaseSqlite ) {
1852 // Reset the table's sequence too.
1853 $db->resetSequenceForTable( $tableName, __METHOD__ );
1854 }
1855
1856 // re-initialize site_stats table
1857 if ( $tableName === 'site_stats' ) {
1859 }
1860 }
1861
1862 private static function unprefixTable( &$tableName, $ind, $prefix ) {
1863 $tableName = substr( $tableName, strlen( $prefix ) );
1864 }
1865
1866 private static function isNotUnittest( $table ) {
1867 return strpos( $table, self::DB_PREFIX ) !== 0;
1868 }
1869
1877 public static function listTables( IMaintainableDatabase $db ) {
1878 $prefix = $db->tablePrefix();
1879 $tables = $db->listTables( $prefix, __METHOD__ );
1880
1881 if ( $db->getType() === 'mysql' ) {
1882 static $viewListCache = null;
1883 if ( $viewListCache === null ) {
1884 $viewListCache = $db->listViews( null, __METHOD__ );
1885 }
1886 // T45571: cannot clone VIEWs under MySQL
1887 $tables = array_diff( $tables, $viewListCache );
1888 }
1889 array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
1890
1891 // Don't duplicate test tables from the previous fataled run
1892 $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );
1893
1894 if ( $db->getType() == 'sqlite' ) {
1895 $tables = array_flip( $tables );
1896 // these are subtables of searchindex and don't need to be duped/dropped separately
1897 unset( $tables['searchindex_content'] );
1898 unset( $tables['searchindex_segdir'] );
1899 unset( $tables['searchindex_segments'] );
1900 $tables = array_flip( $tables );
1901 }
1902
1903 return $tables;
1904 }
1905
1914 public function copyTestData( IDatabase $source, IDatabase $target ) {
1915 if ( $this->db->getType() === 'sqlite' ) {
1916 // SQLite uses a non-temporary copy of the searchindex table for testing,
1917 // which gets deleted and re-created when setting up the secondary connection,
1918 // causing "Error 17" when trying to copy the data. See T191863#4130112.
1919 throw new RuntimeException(
1920 'Setting up a secondary database connection with test data is currently not'
1921 . 'with SQLite. You may want to use markTestSkippedIfDbType() to bypass this issue.'
1922 );
1923 }
1924
1925 $tables = self::listOriginalTables( $source );
1926
1927 foreach ( $tables as $table ) {
1928 $res = $source->select( $table, '*', [], __METHOD__ );
1929 $allRows = [];
1930
1931 foreach ( $res as $row ) {
1932 $allRows[] = (array)$row;
1933 }
1934
1935 $target->insert( $table, $allRows, __METHOD__, [ 'IGNORE' ] );
1936 }
1937 }
1938
1943 protected function checkDbIsSupported() {
1944 if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
1945 throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
1946 }
1947 }
1948
1954 public function getCliArg( $offset ) {
1955 return $this->cliArgs[$offset] ?? null;
1956 }
1957
1963 public function setCliArg( $offset, $value ) {
1964 $this->cliArgs[$offset] = $value;
1965 }
1966
1974 public function hideDeprecated( $function ) {
1975 Wikimedia\suppressWarnings();
1976 wfDeprecated( $function );
1977 Wikimedia\restoreWarnings();
1978 }
1979
2000 protected function assertSelect(
2001 $table, $fields, $condition, array $expectedRows, array $options = [], array $join_conds = []
2002 ) {
2003 if ( !$this->needsDB() ) {
2004 throw new MWException( 'When testing database state, the test cases\'s needDB()' .
2005 ' method should return true. Use @group Database or $this->tablesUsed.' );
2006 }
2007
2008 $db = wfGetDB( DB_REPLICA );
2009
2010 $res = $db->select(
2011 $table,
2012 $fields,
2013 $condition,
2014 wfGetCaller(),
2015 $options + [ 'ORDER BY' => $fields ],
2016 $join_conds
2017 );
2018 $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
2019
2020 $i = 0;
2021
2022 foreach ( $expectedRows as $expected ) {
2023 $r = $res->fetchRow();
2024 self::stripStringKeys( $r );
2025
2026 $i += 1;
2027 $this->assertNotEmpty( $r, "row #$i missing" );
2028
2029 $this->assertEquals( $expected, $r, "row #$i mismatches" );
2030 }
2031
2032 $r = $res->fetchRow();
2033 self::stripStringKeys( $r );
2034
2035 $this->assertFalse( $r, "found extra row (after #$i)" );
2036 }
2037
2049 protected function arrayWrap( array $elements ) {
2050 return array_map(
2051 function ( $element ) {
2052 return [ $element ];
2053 },
2054 $elements
2055 );
2056 }
2057
2070 protected function assertArrayEquals( array $expected, array $actual,
2071 $ordered = false, $named = false
2072 ) {
2073 if ( !$ordered ) {
2074 $this->objectAssociativeSort( $expected );
2075 $this->objectAssociativeSort( $actual );
2076 }
2077
2078 if ( !$named ) {
2079 $expected = array_values( $expected );
2080 $actual = array_values( $actual );
2081 }
2082
2083 call_user_func_array(
2084 [ $this, 'assertEquals' ],
2085 array_merge( [ $expected, $actual ], array_slice( func_get_args(), 4 ) )
2086 );
2087 }
2088
2101 protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
2102 $expected = str_replace( '>', ">\n", $expected );
2103 $actual = str_replace( '>', ">\n", $actual );
2104
2105 $this->assertEquals( $expected, $actual, $msg );
2106 }
2107
2115 protected function objectAssociativeSort( array &$array ) {
2116 uasort(
2117 $array,
2118 function ( $a, $b ) {
2119 return serialize( $a ) <=> serialize( $b );
2120 }
2121 );
2122 }
2123
2133 protected static function stripStringKeys( &$r ) {
2134 if ( !is_array( $r ) ) {
2135 return;
2136 }
2137
2138 foreach ( $r as $k => $v ) {
2139 if ( is_string( $k ) ) {
2140 unset( $r[$k] );
2141 }
2142 }
2143 }
2144
2158 protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
2159 if ( $actual === $value ) {
2160 $this->assertTrue( true, $message );
2161 } else {
2162 $this->assertType( $type, $actual, $message );
2163 }
2164 }
2165
2177 protected function assertType( $type, $actual, $message = '' ) {
2178 if ( class_exists( $type ) || interface_exists( $type ) ) {
2179 $this->assertInstanceOf( $type, $actual, $message );
2180 } else {
2181 $this->assertInternalType( $type, $actual, $message );
2182 }
2183 }
2184
2194 protected function isWikitextNS( $ns ) {
2196
2197 if ( isset( $wgNamespaceContentModels[$ns] ) ) {
2199 }
2200
2201 return true;
2202 }
2203
2211 protected function getDefaultWikitextNS() {
2213
2214 static $wikitextNS = null; // this is not going to change
2215 if ( $wikitextNS !== null ) {
2216 return $wikitextNS;
2217 }
2218
2219 // quickly short out on most common case:
2220 if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
2221 return NS_MAIN;
2222 }
2223
2224 // NOTE: prefer content namespaces
2225 $namespaces = array_unique( array_merge(
2226 MWNamespace::getContentNamespaces(),
2227 [ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
2228 MWNamespace::getValidNamespaces()
2229 ) );
2230
2231 $namespaces = array_diff( $namespaces, [
2232 NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
2233 ] );
2234
2235 $talk = array_filter( $namespaces, function ( $ns ) {
2236 return MWNamespace::isTalk( $ns );
2237 } );
2238
2239 // prefer non-talk pages
2240 $namespaces = array_diff( $namespaces, $talk );
2241 $namespaces = array_merge( $namespaces, $talk );
2242
2243 // check default content model of each namespace
2244 foreach ( $namespaces as $ns ) {
2245 if ( !isset( $wgNamespaceContentModels[$ns] ) ||
2247 ) {
2248 $wikitextNS = $ns;
2249
2250 return $wikitextNS;
2251 }
2252 }
2253
2254 // give up
2255 // @todo Inside a test, we could skip the test as incomplete.
2256 // But frequently, this is used in fixture setup.
2257 throw new MWException( "No namespace defaults to wikitext!" );
2258 }
2259
2266 protected function markTestSkippedIfNoDiff3() {
2267 global $wgDiff3;
2268
2269 # This check may also protect against code injection in
2270 # case of broken installations.
2271 Wikimedia\suppressWarnings();
2272 $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
2273 Wikimedia\restoreWarnings();
2274
2275 if ( !$haveDiff3 ) {
2276 $this->markTestSkipped( "Skip test, since diff3 is not configured" );
2277 }
2278 }
2279
2288 protected function checkPHPExtension( $extName ) {
2289 $loaded = extension_loaded( $extName );
2290 if ( !$loaded ) {
2291 $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
2292 }
2293
2294 return $loaded;
2295 }
2296
2303 protected function markTestSkippedIfDbType( $type ) {
2304 if ( $this->db->getType() === $type ) {
2305 $this->markTestSkipped( "The $type database type isn't supported for this test" );
2306 }
2307 }
2308
2314 public static function wfResetOutputBuffersBarrier( $buffer ) {
2315 return $buffer;
2316 }
2317
2325 protected function setTemporaryHook( $hookName, $handler ) {
2326 $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hookName => [ $handler ] ] );
2327 }
2328
2338 protected function assertFileContains(
2339 $fileName,
2340 $actualData,
2341 $createIfMissing = false,
2342 $msg = ''
2343 ) {
2344 if ( $createIfMissing ) {
2345 if ( !file_exists( $fileName ) ) {
2346 file_put_contents( $fileName, $actualData );
2347 $this->markTestSkipped( 'Data file $fileName does not exist' );
2348 }
2349 } else {
2350 self::assertFileExists( $fileName );
2351 }
2352 self::assertEquals( file_get_contents( $fileName ), $actualData, $msg );
2353 }
2354
2367 protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
2368 if ( !$this->needsDB() ) {
2369 throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
2370 ' method should return true. Use @group Database or $this->tablesUsed.' );
2371 }
2372
2373 $title = Title::newFromText( $pageName, $defaultNs );
2374 $page = WikiPage::factory( $title );
2375
2376 return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
2377 }
2378
2387 protected function revisionDelete(
2388 $rev, array $value = [ Revision::DELETED_TEXT => 1 ], $comment = ''
2389 ) {
2390 if ( is_int( $rev ) ) {
2392 }
2394 'revision', RequestContext::getMain(), $rev->getTitle(), [ $rev->getId() ]
2395 )->setVisibility( [
2396 'value' => $value,
2397 'comment' => $comment,
2398 ] );
2399 }
2400}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
serialize()
unserialize( $serialized)
$GLOBALS['IP']
$wgDBprefix
Table name prefix.
$wgSQLMode
SQL Mode - default is turning off all modes, including strict, if set.
array $wgDefaultExternalStore
The place to put new revisions, false to put them in the local text table.
$wgDiff3
Path to the GNU diff3 utility.
$wgGroupPermissions
Permission keys given to users in each group.
$wgJobClasses
Maps jobs to their handlers; extensions can add to this to provide custom jobs.
$wgNamespaceContentModels
Associative array mapping namespace IDs to the name of the content model pages in that namespace shou...
wfTempDir()
Tries to get the system directory for temporary files.
wfRecursiveRemoveDir( $dir)
Remove a directory and all its content.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfGetCaller( $level=2)
Get the name of the function which called this function wfGetCaller( 1 ) is the function with the wfG...
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:728
static changePrefix( $prefix)
Change the table prefix on all open DB connections.
Factory class to create Config objects.
makeConfig( $name)
Create a given Config using the registered callback for $name.
WebRequest clone which takes values from a provided array.
static destroySingleton()
Destroy the singleton instance.
A Config instance which stores all settings as a member variable.
Internationalisation code.
Definition Language.php:36
MediaWiki exception.
static setLBFactoryTriggers(LBFactory $LBFactory, Config $config)
dropMockTables(IMaintainableDatabase $db, array $tables)
Drops the given mock tables.
setupAllTestDBs()
Set up all test DBs.
int $phpErrorLevel
Original value of PHP's error_reporting setting.
getNonexistingTestPage( $title=null)
Returns a WikiPage representing a non-existing page.
getDefaultWikitextNS()
Returns the ID of a namespace that defaults to Wikitext.
containsClosure( $var, $maxDepth=15)
static TestUser[] $users
listOriginalTables(IMaintainableDatabase $db)
Lists all tables in the live database schema, without a prefix.
static stripStringKeys(&$r)
Utility function for eliminating all string keys from an array.
Database $db
Primary database.
getNewTempFile()
Obtains a new temporary file name.
truncateTable( $tableName, IDatabase $db=null)
Empties the given table and resets any auto-increment counters.
static isNotUnittest( $table)
getExistingTestPage( $title=null)
Returns a WikiPage representing an existing page.
recloneMockTables(IMaintainableDatabase $db, array $tables)
Re-clones the given mock tables to restore them based on the live database schema.
static getMutableTestUser( $groups=[])
Convenience method for getting a mutable test user.
markTestSkippedIfDbType( $type)
Skip the test if using the specified database type.
static setupExternalStoreTestDBs( $testPrefix=null)
Clones the External Store database(s) for testing.
setGroupPermissions( $newPerms, $newKey=null, $newValue=null)
Alters $wgGroupPermissions for the duration of the test.
static getTestSysop()
Convenience method for getting an immutable admin test user.
restoreLoggers()
Restores loggers replaced by setLogger().
overrideMwServices(Config $configOverrides=null, array $services=[])
Stashes the global instance of MediaWikiServices, and installs a new one, allowing test cases to over...
editPage( $pageName, $text, $summary='', $defaultNs=NS_MAIN)
Edits or creates a page/revision.
static MediaWikiServices null $originalServices
The original service locator.
getSchemaOverrides(IMaintainableDatabase $db)
Stub.
setLogger( $channel, LoggerInterface $logger)
Sets the logger for a specified channel, for the duration of the test.
assertFileContains( $fileName, $actualData, $createIfMissing=false, $msg='')
Check whether file contains given data.
static getExternalStoreDatabaseConnections()
Gets master database connections for all of the ExternalStoreDB stores configured in $wgDefaultExtern...
assertType( $type, $actual, $message='')
Asserts the type of the provided value.
run(PHPUnit_Framework_TestResult $result=null)
static installMockMwServices(Config $configOverrides=null)
Creates a new "mock" MediaWikiServices instance, and installs it.
mergeMwGlobalArrayValue( $name, $values)
Merges the given values into a MW global array variable.
array $cliArgs
The CLI arguments passed through from phpunit.php.
static canShallowCopy( $value)
Check if we can back up a value by performing a shallow copy.
setCliArg( $offset, $value)
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown.
setUpSchema(IMaintainableDatabase $db)
Applies the schema overrides returned by getSchemaOverrides(), after undoing any previously applied s...
testMediaWikiTestCaseParentSetupCalled()
Make sure MediaWikiTestCase extending classes have called their parent setUp method.
array $mwGlobals
Holds original values of MediaWiki configuration settings to be restored in tearDown().
array $tmpFiles
Holds the paths of temporary files/directories created through getNewTempFile, and getNewTempDirector...
hideDeprecated( $function)
Don't throw a warning if $function is deprecated and called later.
doStashMwGlobals( $globalKeys)
__construct( $name=null, array $data=[], $dataName='')
checkPHPExtension( $extName)
Check if $extName is a loaded PHP extension, will skip the test whenever it is not loaded.
static listTables(IMaintainableDatabase $db)
resetDB( $db, $tablesUsed)
Empty all tables so they can be repopulated for tests.
array $iniSettings
Holds original values of ini settings to be restored in tearDown().
ensureMockDatabaseConnection(IDatabase $db)
doSetMwGlobals( $pairs, $value=null)
An internal method that allows setService() to set globals that tests are not supposed to touch.
static makeTestConfig(Config $baseConfig=null, Config $customOverrides=null)
Create a config suitable for testing, based on a base config, default overrides, and custom overrides...
resetNamespaces()
Must be called whenever namespaces are changed, e.g., $wgExtraNamespaces is altered.
MediaWikiServices $localServices
The local service locator, created during setUp().
string[] $overriddenServices
Holds a list of services that were overridden with setService().
revisionDelete( $rev, array $value=[Revision::DELETED_TEXT=> 1], $comment='')
Revision-deletes a revision.
static teardownTestDB()
Restores MediaWiki to using the table set (table prefix) it was using before setupTestDB() was called...
static wfResetOutputBuffersBarrier( $buffer)
Used as a marker to prevent wfResetOutputBuffers from breaking PHPUnit.
$called
$called tracks whether the setUp and tearDown method has been called.
static setupDatabaseWithTestPrefix(IMaintainableDatabase $db, $prefix=null)
Setups a database with cloned tables using the given prefix.
setService( $name, $object)
Sets a service, maintaining a stashed version of the previous service to be restored in tearDown.
insertPage( $pageName, $text='Sample page for unit test.', $namespace=null, User $user=null)
Insert a new page.
const DB_PREFIX
Table name prefixes.
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
copyTestData(IDatabase $source, IDatabase $target)
Copy test data from one database connection to another.
static getTestPrefixFor(IDatabase $db)
static makeTestConfigFactoryInstantiator(ConfigFactory $oldFactory, array $configurations)
isWikitextNS( $ns)
Returns true if the given namespace defaults to Wikitext according to $wgNamespaceContentModels.
assertSelect( $table, $fields, $condition, array $expectedRows, array $options=[], array $join_conds=[])
Asserts that the given database query yields the rows given by $expectedRows.
getNewTempDirectory()
obtains a new temporary directory
static setupTestDB(IMaintainableDatabase $db, $prefix)
Creates an empty skeleton of the wiki database by cloning its structure to equivalent tables using th...
static isUsingExternalStoreDB()
Check whether ExternalStoreDB is being used.
array $mwGlobalsToUnset
Holds list of MediaWiki configuration settings to be unset in tearDown().
setIniSetting( $name, $value)
Set an ini setting for the duration of the test.
objectAssociativeSort(array &$array)
Does an associative sort that works for objects.
setTemporaryHook( $hookName, $handler)
Create a temporary hook handler which will be reset by tearDown.
LoggerInterface[] $loggers
Holds original loggers which have been replaced by setLogger()
static prepareServices(Config $bootstrapConfig)
stashMwGlobals( $globalKeys)
Stashes the global, will be restored in tearDown()
static unprefixTable(&$tableName, $ind, $prefix)
undoSchemaOverrides(IMaintainableDatabase $db, $oldOverrides)
Undoes the specified schema overrides.
assertHTMLEquals( $expected, $actual, $msg='')
Put each HTML element on its own line and then equals() the results.
static restoreMwServices()
Restores the original, non-mock MediaWikiServices instance.
arrayWrap(array $elements)
Utility method taking an array of elements and wrapping each element in its own array.
assertArrayEquals(array $expected, array $actual, $ordered=false, $named=false)
Assert that two arrays are equal.
markTestSkippedIfNoDiff3()
Check, if $wgDiff3 is set and ready to merge Will mark the calling test as skipped,...
assertTypeOrValue( $type, $actual, $value=false, $message='')
Asserts that the provided variable is of the specified internal type or equals the $value argument.
static resetNonServiceCaches()
Resets some non-service singleton instances and other static caches.
LoggerFactory service provider that creates LegacyLogger instances.
Definition LegacySpi.php:37
Wraps another spi to capture all logs generated.
PSR-3 logger instance factory.
LoggerFactory service provider that creates loggers implemented by Monolog.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Provides a fallback sequence for Config objects.
static createList( $typeName, IContextSource $context, Title $title, array $ids)
Instantiate the appropriate list class for a given list of IDs.
const DELETED_TEXT
Definition Revision.php:46
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:118
static doPlaceholderInit()
Insert a dummy row with all zeroes if no row is present.
Wraps the user object, so we can also retain full access to properties like password if we log in via...
Definition TestUser.php:9
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
static resetIdByNameCache()
Reset the cache used in idFromName().
Definition User.php:947
static resetGetDefaultOptionsForTestsOnly()
Reset the process cache of default user options.
Definition User.php:1764
Relational database abstraction object.
Definition Database.php:49
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
Content object for wiki text pages.
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
const NS_HELP
Definition Defines.php:85
const NS_USER
Definition Defines.php:75
const NS_FILE
Definition Defines.php:79
const CACHE_NONE
Definition Defines.php:111
const NS_MAIN
Definition Defines.php:73
const NS_MEDIAWIKI
Definition Defines.php:81
const CACHE_ACCEL
Definition Defines.php:114
const EDIT_SUPPRESS_RC
Definition Defines.php:164
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:244
const CACHE_MEMCACHED
Definition Defines.php:113
const CACHE_DB
Definition Defines.php:112
const NS_PROJECT
Definition Defines.php:77
const NS_CATEGORY
Definition Defines.php:87
const EDIT_NEW
Definition Defines.php:161
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\d*-\d*)") will be honored when streaming the file. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition hooks.txt:1991
namespace and then decline to actually register it & $namespaces
Definition hooks.txt:925
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:894
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1266
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:1999
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
this hook is for auditing only RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:996
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition hooks.txt:783
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place or wrap services the preferred way to define a new service is the $wgServiceWiringFiles array $services
Definition hooks.txt:2290
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1779
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2175
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Interface for configuration instances.
Definition Config.php:28
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
lastError()
Get a description of the last error.
getType()
Get the type of the DBMS, as it appears in $wgDBtype.
tablePrefix( $prefix=null)
Get/set the table prefix.
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
Advanced database interface for IDatabase handles that include maintenance methods.
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$source
$buffer
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
if(!isset( $args[0])) $lang