summaryrefslogtreecommitdiff
path: root/tests/phpunit/includes
diff options
context:
space:
mode:
Diffstat (limited to 'tests/phpunit/includes')
-rw-r--r--tests/phpunit/includes/ArticleTablesTest.php34
-rw-r--r--tests/phpunit/includes/ArticleTest.php82
-rw-r--r--tests/phpunit/includes/BlockTest.php124
-rw-r--r--tests/phpunit/includes/CdbTest.php84
-rw-r--r--tests/phpunit/includes/ExternalStoreTest.php32
-rw-r--r--tests/phpunit/includes/ExtraParserTest.php113
-rw-r--r--tests/phpunit/includes/FauxResponseTest.php70
-rw-r--r--tests/phpunit/includes/FormOptionsInitializationTest.php85
-rw-r--r--tests/phpunit/includes/FormOptionsTest.php90
-rw-r--r--tests/phpunit/includes/GlobalFunctions/GlobalTest.php902
-rw-r--r--tests/phpunit/includes/GlobalFunctions/README2
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfExpandUrl.php78
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php120
-rw-r--r--tests/phpunit/includes/HooksTest.php102
-rw-r--r--tests/phpunit/includes/HtmlTest.php90
-rw-r--r--tests/phpunit/includes/HttpTest.php618
-rw-r--r--tests/phpunit/includes/IPTest.php508
-rw-r--r--tests/phpunit/includes/ImageFunctionsTest.php48
-rw-r--r--tests/phpunit/includes/JsonTest.php33
-rw-r--r--tests/phpunit/includes/LanguageConverterTest.php130
-rw-r--r--tests/phpunit/includes/LicensesTest.php22
-rw-r--r--tests/phpunit/includes/LocalFileTest.php99
-rw-r--r--tests/phpunit/includes/MWFunctionTest.php86
-rw-r--r--tests/phpunit/includes/MWNamespaceTest.php460
-rw-r--r--tests/phpunit/includes/MessageTest.php62
-rw-r--r--tests/phpunit/includes/ParserOptionsTest.php36
-rw-r--r--tests/phpunit/includes/Providers.php44
-rw-r--r--tests/phpunit/includes/ResourceLoaderTest.php91
-rw-r--r--tests/phpunit/includes/RevisionTest.php125
-rw-r--r--tests/phpunit/includes/SampleTest.php98
-rw-r--r--tests/phpunit/includes/SanitizerTest.php113
-rw-r--r--tests/phpunit/includes/SeleniumConfigurationTest.php228
-rw-r--r--tests/phpunit/includes/SiteConfigurationTest.php311
-rw-r--r--tests/phpunit/includes/TimeAdjustTest.php51
-rw-r--r--tests/phpunit/includes/TitlePermissionTest.php654
-rw-r--r--tests/phpunit/includes/TitleTest.php79
-rw-r--r--tests/phpunit/includes/UserIsValidEmailAddrTest.php79
-rw-r--r--tests/phpunit/includes/UserTest.php58
-rw-r--r--tests/phpunit/includes/WebRequestTest.php88
-rw-r--r--tests/phpunit/includes/XmlJsTest.php9
-rw-r--r--tests/phpunit/includes/XmlSelectTest.php139
-rw-r--r--tests/phpunit/includes/XmlTest.php304
-rw-r--r--tests/phpunit/includes/ZipDirectoryReaderTest.php79
-rw-r--r--tests/phpunit/includes/api/ApiBlockTest.php62
-rw-r--r--tests/phpunit/includes/api/ApiPurgeTest.php41
-rw-r--r--tests/phpunit/includes/api/ApiQueryTest.php67
-rw-r--r--tests/phpunit/includes/api/ApiTest.php277
-rw-r--r--tests/phpunit/includes/api/ApiTestCase.php139
-rw-r--r--tests/phpunit/includes/api/ApiTestCaseUpload.php114
-rw-r--r--tests/phpunit/includes/api/ApiTestUser.php59
-rw-r--r--tests/phpunit/includes/api/ApiUploadTest.php433
-rw-r--r--tests/phpunit/includes/api/ApiWatchTest.php179
-rw-r--r--tests/phpunit/includes/api/RandomImageGenerator.php473
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatPhpTest.php19
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatTestBase.php22
-rw-r--r--tests/phpunit/includes/api/generateRandomImages.php47
-rw-r--r--tests/phpunit/includes/api/words.txt1000
-rw-r--r--tests/phpunit/includes/db/DatabaseSqliteTest.php312
-rw-r--r--tests/phpunit/includes/db/DatabaseTest.php95
-rw-r--r--tests/phpunit/includes/db/sqlite/tables-1.13.sql342
-rw-r--r--tests/phpunit/includes/db/sqlite/tables-1.15.sql454
-rw-r--r--tests/phpunit/includes/db/sqlite/tables-1.16.sql483
-rw-r--r--tests/phpunit/includes/db/sqlite/tables-1.17.sql516
-rw-r--r--tests/phpunit/includes/installer/InstallDocFormatterTest.php64
-rw-r--r--tests/phpunit/includes/libs/IEUrlExtensionTest.php118
-rw-r--r--tests/phpunit/includes/libs/JavaScriptMinifierTest.php105
-rw-r--r--tests/phpunit/includes/media/BitmapMetadataHandlerTest.php125
-rw-r--r--tests/phpunit/includes/media/BitmapScalingTest.php136
-rw-r--r--tests/phpunit/includes/media/ExifBitmapTest.php122
-rw-r--r--tests/phpunit/includes/media/ExifRotationTest.php249
-rw-r--r--tests/phpunit/includes/media/ExifTest.php51
-rw-r--r--tests/phpunit/includes/media/FormatMetadataTest.php29
-rw-r--r--tests/phpunit/includes/media/GIFMetadataExtractorTest.php95
-rw-r--r--tests/phpunit/includes/media/GIFTest.php85
-rw-r--r--tests/phpunit/includes/media/IPTCTest.php55
-rw-r--r--tests/phpunit/includes/media/JpegMetadataExtractorTest.php94
-rw-r--r--tests/phpunit/includes/media/JpegTest.php29
-rw-r--r--tests/phpunit/includes/media/PNGMetadataExtractorTest.php141
-rw-r--r--tests/phpunit/includes/media/PNGTest.php88
-rw-r--r--tests/phpunit/includes/media/SVGMetadataExtractorTest.php88
-rw-r--r--tests/phpunit/includes/media/TiffTest.php36
-rw-r--r--tests/phpunit/includes/media/XMPTest.php154
-rw-r--r--tests/phpunit/includes/normal/CleanUpTest.php382
-rw-r--r--tests/phpunit/includes/parser/MagicVariableTest.php195
-rw-r--r--tests/phpunit/includes/parser/MediaWikiParserTest.php34
-rw-r--r--tests/phpunit/includes/parser/NewParserTest.php850
-rw-r--r--tests/phpunit/includes/parser/ParserHelpers.php136
-rw-r--r--tests/phpunit/includes/parser/PreprocessorTest.php195
-rw-r--r--tests/phpunit/includes/parser/TagHooks.php77
-rw-r--r--tests/phpunit/includes/search/SearchEngineTest.php163
-rw-r--r--tests/phpunit/includes/search/SearchUpdateTest.php80
-rw-r--r--tests/phpunit/includes/specials/SpecialRecentchanges.php134
-rw-r--r--tests/phpunit/includes/upload/UploadFromUrlTest.php348
-rw-r--r--tests/phpunit/includes/upload/UploadStashTest.php53
-rw-r--r--tests/phpunit/includes/upload/UploadTest.php142
95 files changed, 16447 insertions, 0 deletions
diff --git a/tests/phpunit/includes/ArticleTablesTest.php b/tests/phpunit/includes/ArticleTablesTest.php
new file mode 100644
index 00000000..01776c95
--- /dev/null
+++ b/tests/phpunit/includes/ArticleTablesTest.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @group Database
+ */
+class ArticleTablesTest extends MediaWikiLangTestCase {
+
+ function testbug14404() {
+ global $wgUser, $wgContLang, $wgLanguageCode, $wgLang;
+
+ $title = Title::newFromText("Bug 14404");
+ $article = new Article( $title );
+ $wgUser = new User();
+ $wgUser->mRights = array( 'createpage', 'edit', 'purge' );
+ $wgLanguageCode = 'es';
+ $wgContLang = Language::factory( 'es' );
+
+ $wgLang = Language::factory( 'fr' );
+ $status = $article->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', 0 );
+ $templates1 = $article->getUsedTemplates();
+
+ $wgLang = Language::factory( 'de' );
+ $article->mParserOptions = null; // Let it pick the new user language
+ $article->mPreparedEdit = false; // In order to force the rerendering of the same wikitext
+
+ // We need an edit, a purge is not enough to regenerate the tables
+ $status = $article->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', EDIT_UPDATE );
+ $templates2 = $article->getUsedTemplates();
+
+ $this->assertEquals( $templates1, $templates2 );
+ $this->assertEquals( $templates1[0]->getFullText(), 'Historial' );
+ }
+
+}
diff --git a/tests/phpunit/includes/ArticleTest.php b/tests/phpunit/includes/ArticleTest.php
new file mode 100644
index 00000000..285efee9
--- /dev/null
+++ b/tests/phpunit/includes/ArticleTest.php
@@ -0,0 +1,82 @@
+<?php
+
+class ArticleTest extends MediaWikiTestCase {
+
+ private $title; // holds a Title object
+ private $article; // holds an article
+
+ /** creates a title object and its article object */
+ function setUp() {
+ $this->title = Title::makeTitle( NS_MAIN, 'SomePage' );
+ $this->article = new Article( $this->title );
+
+ }
+
+ /** cleanup title object and its article object */
+ function tearDown() {
+ $this->title = null;
+ $this->article = null;
+
+ }
+
+ function testImplementsGetMagic() {
+ $this->assertEquals( -1, $this->article->mCounter, "Article __get magic" );
+ }
+
+ /**
+ * @depends testImplementsGetMagic
+ */
+ function testImplementsSetMagic() {
+
+ $this->article->mCounter = 2;
+ $this->assertEquals( 2, $this->article->mCounter, "Article __set magic" );
+ }
+
+ /**
+ * @depends testImplementsSetMagic
+ */
+ function testImplementsCallMagic() {
+ $this->article->mCounter = 33;
+ $this->assertEquals( 33, $this->article->getCount(), "Article __call magic" );
+ }
+
+ function testGetOrSetOnNewProperty() {
+ $this->article->ext_someNewProperty = 12;
+ $this->assertEquals( 12, $this->article->ext_someNewProperty,
+ "Article get/set magic on new field" );
+
+ $this->article->ext_someNewProperty = -8;
+ $this->assertEquals( -8, $this->article->ext_someNewProperty,
+ "Article get/set magic on update to new field" );
+ }
+
+ /**
+ * Checks for the existence of the backwards compatibility static functions (forwarders to WikiPage class)
+ */
+ function testStaticFunctions() {
+ $this->assertEquals( WikiPage::selectFields(), Article::selectFields(),
+ "Article static functions" );
+ $this->assertEquals( true, is_callable( "Article::onArticleCreate" ),
+ "Article static functions" );
+ $this->assertEquals( true, is_callable( "Article::onArticleDelete" ),
+ "Article static functions" );
+ $this->assertEquals( true, is_callable( "ImagePage::onArticleEdit" ),
+ "Article static functions" );
+ $this->assertTrue( is_string( CategoryPage::getAutosummary( '', '', 0 ) ),
+ "Article static functions" );
+ }
+
+ function testWikiPageFactory() {
+ $title = Title::makeTitle( NS_FILE, 'Someimage.png' );
+ $page = WikiPage::factory( $title );
+ $this->assertEquals( 'WikiFilePage', get_class( $page ) );
+
+ $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' );
+ $page = WikiPage::factory( $title );
+ $this->assertEquals( 'WikiCategoryPage', get_class( $page ) );
+
+ $title = Title::makeTitle( NS_MAIN, 'SomePage' );
+ $page = WikiPage::factory( $title );
+ $this->assertEquals( 'WikiPage', get_class( $page ) );
+ }
+}
diff --git a/tests/phpunit/includes/BlockTest.php b/tests/phpunit/includes/BlockTest.php
new file mode 100644
index 00000000..2f224ba8
--- /dev/null
+++ b/tests/phpunit/includes/BlockTest.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @group Database
+ */
+class BlockTest extends MediaWikiLangTestCase {
+
+ const REASON = "Some reason";
+
+ private $block, $madeAt;
+
+ /* variable used to save up the blockID we insert in this test suite */
+ private $blockId;
+
+ function setUp() {
+ global $wgContLang;
+ parent::setUp();
+ $wgContLang = Language::factory( 'en' );
+ }
+
+ function addDBData() {
+ //$this->dumpBlocks();
+
+ $user = User::newFromName( 'UTBlockee' );
+ if( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTBlockeePassword' );
+
+ $user->saveSettings();
+ }
+
+ // Delete the last round's block if it's still there
+ $oldBlock = Block::newFromTarget( 'UTBlockee' );
+ if ( $oldBlock ) {
+ // An old block will prevent our new one from saving.
+ $oldBlock->delete();
+ }
+
+ $this->block = new Block( 'UTBlockee', 1, 0,
+ self::REASON
+ );
+ $this->madeAt = wfTimestamp( TS_MW );
+
+ $this->block->insert();
+ // save up ID for use in assertion. Since ID is an autoincrement,
+ // its value might change depending on the order the tests are run.
+ // ApiBlockTest insert its own blocks!
+ $newBlockId = $this->block->getId();
+ if ($newBlockId) {
+ $this->blockId = $newBlockId;
+ } else {
+ throw new MWException( "Failed to insert block for BlockTest; old leftover block remaining?" );
+ }
+ }
+
+ /**
+ * debug function : dump the ipblocks table
+ */
+ function dumpBlocks() {
+ $v = $this->db->query( 'SELECT * FROM unittest_ipblocks' );
+ print "Got " . $v->numRows() . " rows. Full dump follow:\n";
+ foreach( $v as $row ) {
+ print_r( $row );
+ }
+ }
+
+ function testInitializerFunctionsReturnCorrectBlock() {
+ // $this->dumpBlocks();
+
+ $this->assertTrue( $this->block->equals( Block::newFromTarget('UTBlockee') ), "newFromTarget() returns the same block as the one that was made");
+
+ $this->assertTrue( $this->block->equals( Block::newFromID( $this->blockId ) ), "newFromID() returns the same block as the one that was made");
+
+ }
+
+ /**
+ * per bug 26425
+ */
+ function testBug26425BlockTimestampDefaultsToTime() {
+
+ $this->assertEquals( $this->madeAt, $this->block->mTimestamp, "If no timestamp is specified, the block is recorded as time()");
+
+ }
+
+ /**
+ * This is the method previously used to load block info in CheckUser etc
+ * passing an empty value (empty string, null, etc) as the ip parameter bypasses IP lookup checks.
+ *
+ * This stopped working with r84475 and friends: regression being fixed for bug 29116.
+ *
+ * @dataProvider dataBug29116
+ */
+ function testBug29116LoadWithEmptyIp( $vagueTarget ) {
+ $uid = User::idFromName( 'UTBlockee' );
+ $this->assertTrue( ($uid > 0), 'Must be able to look up the target user during tests' );
+
+ $block = new Block();
+ $ok = $block->load( $vagueTarget, $uid );
+ $this->assertTrue( $ok, "Block->load() with empty IP and user ID '$uid' should return a block" );
+
+ $this->assertTrue( $this->block->equals( $block ), "Block->load() returns the same block as the one that was made when given empty ip param " . var_export( $vagueTarget, true ) );
+ }
+
+ /**
+ * CheckUser since being changed to use Block::newFromTarget started failing
+ * because the new function didn't accept empty strings like Block::load()
+ * had. Regression bug 29116.
+ *
+ * @dataProvider dataBug29116
+ */
+ function testBug29116NewFromTargetWithEmptyIp( $vagueTarget ) {
+ $block = Block::newFromTarget('UTBlockee', $vagueTarget);
+ $this->assertTrue( $this->block->equals( $block ), "newFromTarget() returns the same block as the one that was made when given empty vagueTarget param " . var_export( $vagueTarget, true ) );
+ }
+
+ function dataBug29116() {
+ return array(
+ array( null ),
+ array( '' ),
+ array( false )
+ );
+ }
+}
+
diff --git a/tests/phpunit/includes/CdbTest.php b/tests/phpunit/includes/CdbTest.php
new file mode 100644
index 00000000..6c3e6664
--- /dev/null
+++ b/tests/phpunit/includes/CdbTest.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * Test the CDB reader/writer
+ */
+
+class CdbTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ if ( !CdbReader::haveExtension() ) {
+ $this->markTestIncomplete( 'This test requires native CDB support to be present.' );
+ }
+ }
+
+ public function testCdb() {
+ $dir = wfTempDir();
+ if ( !is_writable( $dir ) ) {
+ $this->markTestSkipped( "Temp dir isn't writable" );
+ }
+
+ $w1 = new CdbWriter_PHP( "$dir/php.cdb" );
+ $w2 = new CdbWriter_DBA( "$dir/dba.cdb" );
+
+ $data = array();
+ for ( $i = 0; $i < 1000; $i++ ) {
+ $key = $this->randomString();
+ $value = $this->randomString();
+ $w1->set( $key, $value );
+ $w2->set( $key, $value );
+
+ if ( !isset( $data[$key] ) ) {
+ $data[$key] = $value;
+ }
+ }
+
+ $w1->close();
+ $w2->close();
+
+ $this->assertEquals(
+ md5_file( "$dir/dba.cdb" ),
+ md5_file( "$dir/php.cdb" ),
+ 'same hash'
+ );
+
+ $r1 = new CdbReader_PHP( "$dir/php.cdb" );
+ $r2 = new CdbReader_DBA( "$dir/dba.cdb" );
+
+ foreach ( $data as $key => $value ) {
+ if ( $key === '' ) {
+ // Known bug
+ continue;
+ }
+ $v1 = $r1->get( $key );
+ $v2 = $r2->get( $key );
+
+ $v1 = $v1 === false ? '(not found)' : $v1;
+ $v2 = $v2 === false ? '(not found)' : $v2;
+
+ # cdbAssert( 'Mismatch', $key, $v1, $v2 );
+ $this->cdbAssert( "PHP error", $key, $v1, $value );
+ $this->cdbAssert( "DBA error", $key, $v2, $value );
+ }
+
+ unlink( "$dir/dba.cdb" );
+ unlink( "$dir/php.cdb" );
+ }
+
+ private function randomString() {
+ $len = mt_rand( 0, 10 );
+ $s = '';
+ for ( $j = 0; $j < $len; $j++ ) {
+ $s .= chr( mt_rand( 0, 255 ) );
+ }
+ return $s;
+ }
+
+ private function cdbAssert( $msg, $key, $v1, $v2 ) {
+ $this->assertEquals(
+ $v2,
+ $v1,
+ $msg . ', k=' . bin2hex( $key )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/ExternalStoreTest.php b/tests/phpunit/includes/ExternalStoreTest.php
new file mode 100644
index 00000000..92ec7344
--- /dev/null
+++ b/tests/phpunit/includes/ExternalStoreTest.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * External Store tests
+ */
+
+class ExternalStoreTest extends MediaWikiTestCase {
+ private $saved_wgExternalStores;
+
+ function setUp() {
+ global $wgExternalStores;
+ $this->saved_wgExternalStores = $wgExternalStores ;
+ }
+
+ function tearDown() {
+ global $wgExternalStores;
+ $wgExternalStores = $this->saved_wgExternalStores ;
+ }
+
+ function testExternalStoreDoesNotFetchIncorrectURL() {
+ global $wgExternalStores;
+ $wgExternalStores = true;
+
+ # Assertions for r68900
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'http://' ) );
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'ftp.wikimedia.org' ) );
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( '/super.txt' ) );
+ }
+}
+
diff --git a/tests/phpunit/includes/ExtraParserTest.php b/tests/phpunit/includes/ExtraParserTest.php
new file mode 100644
index 00000000..5b0aa98b
--- /dev/null
+++ b/tests/phpunit/includes/ExtraParserTest.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * Parser-related tests that don't suit for parserTests.txt
+ */
+class ExtraParserTest extends MediaWikiTestCase {
+
+ function setUp() {
+ global $wgMemc;
+ global $wgContLang;
+ global $wgShowDBErrorBacktrace;
+ global $wgLanguageCode;
+
+ $wgShowDBErrorBacktrace = true;
+ $wgLanguageCode = 'en';
+ $wgContLang = new Language( 'en' );
+ $wgMemc = new EmptyBagOStuff;
+
+ $this->options = new ParserOptions;
+ $this->options->setTemplateCallback( array( __CLASS__, 'statelessFetchTemplate' ) );
+ $this->parser = new Parser;
+ }
+
+ // Bug 8689 - Long numeric lines kill the parser
+ function testBug8689() {
+ global $wgLang;
+ global $wgUser;
+ $longLine = '1.' . str_repeat( '1234567890', 100000 ) . "\n";
+
+ if ( $wgLang === null ) $wgLang = new Language;
+
+ $t = Title::newFromText( 'Unit test' );
+ $options = ParserOptions::newFromUser( $wgUser );
+ $this->assertEquals( "<p>$longLine</p>",
+ $this->parser->parse( $longLine, $t, $options )->getText() );
+ }
+
+ /* Test the parser entry points */
+ function testParse() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $parserOutput = $this->parser->parse( "Test\n{{Foo}}\n{{Bar}}" , $title, $this->options );
+ $this->assertEquals( "<p>Test\nContent of <i>Template:Foo</i>\nContent of <i>Template:Bar</i>\n</p>", $parserOutput->getText() );
+ }
+
+ function testPreSaveTransform() {
+ global $wgUser;
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->preSaveTransform( "Test\r\n{{subst:Foo}}\n{{Bar}}", $title, $wgUser, $this->options );
+
+ $this->assertEquals( "Test\nContent of ''Template:Foo''\n{{Bar}}", $outputText );
+ }
+
+ function testPreprocess() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->preprocess( "Test\n{{Foo}}\n{{Bar}}" , $title, $this->options );
+
+ $this->assertEquals( "Test\nContent of ''Template:Foo''\nContent of ''Template:Bar''", $outputText );
+ }
+
+ /**
+ * cleanSig() makes all templates substs and removes tildes
+ */
+ function testCleanSig() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->cleanSig( "{{Foo}} ~~~~" );
+
+ $this->assertEquals( "{{SUBST:Foo}} ", $outputText );
+ }
+
+ /**
+ * cleanSigInSig() just removes tildes
+ */
+ function testCleanSigInSig() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->cleanSigInSig( "{{Foo}} ~~~~" );
+
+ $this->assertEquals( "{{Foo}} ", $outputText );
+ }
+
+ function testGetSection() {
+ $outputText2 = $this->parser->getSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 2 );
+ $outputText1 = $this->parser->getSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 1 );
+
+ $this->assertEquals( "=== Heading 2 ===\nSection 2", $outputText2 );
+ $this->assertEquals( "== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2", $outputText1 );
+ }
+
+ function testReplaceSection() {
+ $outputText = $this->parser->replaceSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 1, "New section 1" );
+
+ $this->assertEquals( "Section 0\nNew section 1\n\n== Heading 3 ==\nSection 3", $outputText );
+ }
+
+ /**
+ * Templates and comments are not affected, but noinclude/onlyinclude is.
+ */
+ function testGetPreloadText() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->getPreloadText( "{{Foo}}<noinclude> censored</noinclude> information <!-- is very secret -->", $title, $this->options );
+
+ $this->assertEquals( "{{Foo}} information <!-- is very secret -->", $outputText );
+ }
+
+ static function statelessFetchTemplate( $title, $parser=false ) {
+ $text = "Content of ''" . $title->getFullText() . "''";
+ $deps = array();
+
+ return array(
+ 'text' => $text,
+ 'finalTitle' => $title,
+ 'deps' => $deps );
+ }
+ }
diff --git a/tests/phpunit/includes/FauxResponseTest.php b/tests/phpunit/includes/FauxResponseTest.php
new file mode 100644
index 00000000..c0420049
--- /dev/null
+++ b/tests/phpunit/includes/FauxResponseTest.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * Tests for the FauxResponse class
+ *
+ * Copyright @ 2011 Alexandre Emsenhuber
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class FauxResponseTest extends MediaWikiTestCase {
+ var $response;
+
+ function setUp() {
+ $this->response = new FauxResponse;
+ }
+
+ function testCookie() {
+ $this->assertEquals( null, $this->response->getcookie( 'key' ), 'Non-existing cookie' );
+ $this->response->setcookie( 'key', 'val' );
+ $this->assertEquals( 'val', $this->response->getcookie( 'key' ), 'Existing cookie' );
+ }
+
+ function testHeader() {
+ $this->assertEquals( null, $this->response->getheader( 'Location' ), 'Non-existing header' );
+
+ $this->response->header( 'Location: http://localhost/' );
+ $this->assertEquals( 'http://localhost/', $this->response->getheader( 'Location' ), 'Set header' );
+
+ $this->response->header( 'Location: http://127.0.0.1/' );
+ $this->assertEquals( 'http://127.0.0.1/', $this->response->getheader( 'Location' ), 'Same header' );
+
+ $this->response->header( 'Location: http://127.0.0.2/', false );
+ $this->assertEquals( 'http://127.0.0.1/', $this->response->getheader( 'Location' ), 'Same header with override disabled' );
+ }
+
+ function testResponseCode() {
+ $this->response->header( 'HTTP/1.1 200' );
+ $this->assertEquals( 200, $this->response->getStatusCode(), 'Header with no message' );
+
+ $this->response->header( 'HTTP/1.x 201' );
+ $this->assertEquals( 201, $this->response->getStatusCode(), 'Header with no message and protocol 1.x' );
+
+ $this->response->header( 'HTTP/1.1 202 OK' );
+ $this->assertEquals( 202, $this->response->getStatusCode(), 'Normal header' );
+
+ $this->response->header( 'HTTP/1.x 203 OK' );
+ $this->assertEquals( 203, $this->response->getStatusCode(), 'Normal header with no message and protocol 1.x' );
+
+ $this->response->header( 'HTTP/1.x 204 OK', false, 205 );
+ $this->assertEquals( 205, $this->response->getStatusCode(), 'Third parameter overrides the HTTP/... header' );
+
+ $this->response->header( 'Location: http://localhost/', false, 206 );
+ $this->assertEquals( 206, $this->response->getStatusCode(), 'Third parameter with another header' );
+ }
+}
diff --git a/tests/phpunit/includes/FormOptionsInitializationTest.php b/tests/phpunit/includes/FormOptionsInitializationTest.php
new file mode 100644
index 00000000..85d76271
--- /dev/null
+++ b/tests/phpunit/includes/FormOptionsInitializationTest.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * This file host two test case classes for the MediaWiki FormOptions class:
+ * - FormOptionsInitializationTest : tests initialization of the class.
+ * - FormOptionsTest : tests methods an on instance
+ *
+ * The split let us take advantage of setting up a fixture for the methods
+ * tests.
+ */
+
+/**
+ * Dummy class to makes FormOptions::$options public.
+ * Used by FormOptionsInitializationTest which need to verify the $options
+ * array is correctly set through the FormOptions::add() function.
+ */
+class FormOptionsExposed extends FormOptions {
+ public function getOptions() {
+ return $this->options;
+ }
+}
+
+/**
+ * Test class for FormOptions initialization
+ * Ensure the FormOptions::add() does what we want it to do.
+ *
+ * Generated by PHPUnit on 2011-02-28 at 20:46:27.
+ *
+ * Copyright © 2011, Ashar Voultoiz
+ *
+ * @author Ashar Voultoiz
+ */
+class FormOptionsInitializationTest extends MediaWikiTestCase {
+ /**
+ * @var FormOptions
+ */
+ protected $object;
+
+
+ /**
+ * A new fresh and empty FormOptions object to test initialization
+ * with.
+ */
+ protected function setUp() {
+ $this->object = new FormOptionsExposed();
+
+ }
+
+ public function testAddStringOption() {
+ $this->object->add( 'foo', 'string value' );
+ $this->assertEquals(
+ array(
+ 'foo' => array(
+ 'default' => 'string value',
+ 'consumed' => false,
+ 'type' => FormOptions::STRING,
+ 'value' => null,
+ )
+ ),
+ $this->object->getOptions()
+ );
+ }
+
+ public function testAddIntegers() {
+ $this->object->add( 'one', 1 );
+ $this->object->add( 'negone', -1 );
+ $this->assertEquals(
+ array(
+ 'negone' => array(
+ 'default' => -1,
+ 'value' => null,
+ 'consumed' => false,
+ 'type' => FormOptions::INT,
+ ),
+ 'one' => array(
+ 'default' => 1,
+ 'value' => null,
+ 'consumed' => false,
+ 'type' => FormOptions::INT,
+ )
+ ),
+ $this->object->getOptions()
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/FormOptionsTest.php b/tests/phpunit/includes/FormOptionsTest.php
new file mode 100644
index 00000000..86618d93
--- /dev/null
+++ b/tests/phpunit/includes/FormOptionsTest.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * This file host two test case classes for the MediaWiki FormOptions class:
+ * - FormOptionsInitializationTest : tests initialization of the class.
+ * - FormOptionsTest : tests methods an on instance
+ *
+ * The split let us take advantage of setting up a fixture for the methods
+ * tests.
+ */
+
+/**
+ * Test class for FormOptions methods.
+ * Generated by PHPUnit on 2011-02-28 at 20:46:27.
+ *
+ * Copyright © 2011, Ashar Voultoiz
+ *
+ * @author Ashar Voultoiz
+ */
+class FormOptionsTest extends MediaWikiTestCase {
+ /**
+ * @var FormOptions
+ */
+ protected $object;
+
+ /**
+ * Instanciates a FormOptions object to play with.
+ * FormOptions::add() is tested by the class FormOptionsInitializationTest
+ * so we assume the function is well tested already an use it to create
+ * the fixture.
+ */
+ protected function setUp() {
+ $this->object = new FormOptions;
+ $this->object->add( 'string1', 'string one' );
+ $this->object->add( 'string2', 'string two' );
+ $this->object->add( 'integer', 0 );
+ $this->object->add( 'intnull', 0, FormOptions::INTNULL );
+ }
+
+ /** Helpers for testGuessType() */
+ /* @{ */
+ private function assertGuessBoolean( $data ) {
+ $this->guess( FormOptions::BOOL, $data );
+ }
+ private function assertGuessInt( $data ) {
+ $this->guess( FormOptions::INT, $data );
+ }
+ private function assertGuessString( $data ) {
+ $this->guess( FormOptions::STRING, $data );
+ }
+
+ /** Generic helper */
+ private function guess( $expected, $data ) {
+ $this->assertEquals(
+ $expected,
+ FormOptions::guessType( $data )
+ );
+ }
+ /* @} */
+
+ /**
+ * Reuse helpers above assertGuessBoolean assertGuessInt assertGuessString
+ */
+ public function testGuessTypeDetection() {
+ $this->assertGuessBoolean( true );
+ $this->assertGuessBoolean( false );
+
+ $this->assertGuessInt( 0 );
+ $this->assertGuessInt( -5 );
+ $this->assertGuessInt( 5 );
+ $this->assertGuessInt( 0x0F );
+
+ $this->assertGuessString( 'true' );
+ $this->assertGuessString( 'false' );
+ $this->assertGuessString( '5' );
+ $this->assertGuessString( '0' );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ public function testGuessTypeOnArrayThrowException() {
+ $this->object->guessType( array( 'foo' ) );
+ }
+ /**
+ * @expectedException MWException
+ */
+ public function testGuessTypeOnNullThrowException() {
+ $this->object->guessType( null );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php
new file mode 100644
index 00000000..3d157d0a
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php
@@ -0,0 +1,902 @@
+<?php
+
+class GlobalTest extends MediaWikiTestCase {
+ function setUp() {
+ global $wgReadOnlyFile, $wgUrlProtocols;
+ $this->originals['wgReadOnlyFile'] = $wgReadOnlyFile;
+ $this->originals['wgUrlProtocols'] = $wgUrlProtocols;
+ $wgReadOnlyFile = tempnam( wfTempDir(), "mwtest_readonly" );
+ $wgUrlProtocols[] = 'file://';
+ unlink( $wgReadOnlyFile );
+ }
+
+ function tearDown() {
+ global $wgReadOnlyFile, $wgUrlProtocols;
+ if ( file_exists( $wgReadOnlyFile ) ) {
+ unlink( $wgReadOnlyFile );
+ }
+ $wgReadOnlyFile = $this->originals['wgReadOnlyFile'];
+ $wgUrlProtocols = $this->originals['wgUrlProtocols'];
+ }
+
+ /** @dataProvider provideForWfArrayDiff2 */
+ public function testWfArrayDiff2( $a, $b, $expected ) {
+ $this->assertEquals(
+ wfArrayDiff2( $a, $b), $expected
+ );
+ }
+
+ // @todo Provide more tests
+ public function provideForWfArrayDiff2() {
+ // $a $b $expected
+ return array(
+ array(
+ array( 'a', 'b'),
+ array( 'a', 'b'),
+ array(),
+ ),
+ array(
+ array( array( 'a'), array( 'a', 'b', 'c' )),
+ array( array( 'a'), array( 'a', 'b' )),
+ array( 1 => array( 'a', 'b', 'c' ) ),
+ ),
+ );
+ }
+
+ function testRandom() {
+ # This could hypothetically fail, but it shouldn't ;)
+ $this->assertFalse(
+ wfRandom() == wfRandom() );
+ }
+
+ function testUrlencode() {
+ $this->assertEquals(
+ "%E7%89%B9%E5%88%A5:Contributions/Foobar",
+ wfUrlencode( "\xE7\x89\xB9\xE5\x88\xA5:Contributions/Foobar" ) );
+ }
+
+ function testReadOnlyEmpty() {
+ global $wgReadOnly;
+ $wgReadOnly = null;
+
+ $this->assertFalse( wfReadOnly() );
+ $this->assertFalse( wfReadOnly() );
+ }
+
+ function testReadOnlySet() {
+ global $wgReadOnly, $wgReadOnlyFile;
+
+ $f = fopen( $wgReadOnlyFile, "wt" );
+ fwrite( $f, 'Message' );
+ fclose( $f );
+ $wgReadOnly = null; # Check on $wgReadOnlyFile
+
+ $this->assertTrue( wfReadOnly() );
+ $this->assertTrue( wfReadOnly() ); # Check cached
+
+ unlink( $wgReadOnlyFile );
+ $wgReadOnly = null; # Clean cache
+
+ $this->assertFalse( wfReadOnly() );
+ $this->assertFalse( wfReadOnly() );
+ }
+
+ function testQuotedPrintable() {
+ $this->assertEquals(
+ "=?UTF-8?Q?=C4=88u=20legebla=3F?=",
+ UserMailer::quotedPrintable( "\xc4\x88u legebla?", "UTF-8" ) );
+ }
+
+ function testTime() {
+ $start = wfTime();
+ $this->assertInternalType( 'float', $start );
+ $end = wfTime();
+ $this->assertTrue( $end > $start, "Time is running backwards!" );
+ }
+
+ function testArrayToCGI() {
+ $this->assertEquals(
+ "baz=AT%26T&foo=bar",
+ wfArrayToCGI(
+ array( 'baz' => 'AT&T', 'ignore' => '' ),
+ array( 'foo' => 'bar', 'baz' => 'overridden value' ) ) );
+ $this->assertEquals(
+ "path%5B0%5D=wiki&path%5B1%5D=test&cfg%5Bservers%5D%5Bhttp%5D=localhost",
+ wfArrayToCGI( array(
+ 'path' => array( 'wiki', 'test' ),
+ 'cfg' => array( 'servers' => array( 'http' => 'localhost' ) ) ) ) );
+ }
+
+ function testCgiToArray() {
+ $this->assertEquals(
+ array( 'path' => array( 'wiki', 'test' ),
+ 'cfg' => array( 'servers' => array( 'http' => 'localhost' ) ) ),
+ wfCgiToArray( 'path%5B0%5D=wiki&path%5B1%5D=test&cfg%5Bservers%5D%5Bhttp%5D=localhost' ) );
+ }
+
+ function testMimeTypeMatch() {
+ $this->assertEquals(
+ 'text/html',
+ mimeTypeMatch( 'text/html',
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.7,
+ 'text/plain' => 0.3 ) ) );
+ $this->assertEquals(
+ 'text/*',
+ mimeTypeMatch( 'text/html',
+ array( 'image/*' => 1.0,
+ 'text/*' => 0.5 ) ) );
+ $this->assertEquals(
+ '*/*',
+ mimeTypeMatch( 'text/html',
+ array( '*/*' => 1.0 ) ) );
+ $this->assertNull(
+ mimeTypeMatch( 'text/html',
+ array( 'image/png' => 1.0,
+ 'image/svg+xml' => 0.5 ) ) );
+ }
+
+ function testNegotiateType() {
+ $this->assertEquals(
+ 'text/html',
+ wfNegotiateType(
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.7,
+ 'text/plain' => 0.5,
+ 'text/*' => 0.2 ),
+ array( 'text/html' => 1.0 ) ) );
+ $this->assertEquals(
+ 'application/xhtml+xml',
+ wfNegotiateType(
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.7,
+ 'text/plain' => 0.5,
+ 'text/*' => 0.2 ),
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.5 ) ) );
+ $this->assertEquals(
+ 'text/html',
+ wfNegotiateType(
+ array( 'text/html' => 1.0,
+ 'text/plain' => 0.5,
+ 'text/*' => 0.5,
+ 'application/xhtml+xml' => 0.2 ),
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.5 ) ) );
+ $this->assertEquals(
+ 'text/html',
+ wfNegotiateType(
+ array( 'text/*' => 1.0,
+ 'image/*' => 0.7,
+ '*/*' => 0.3 ),
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.5 ) ) );
+ $this->assertNull(
+ wfNegotiateType(
+ array( 'text/*' => 1.0 ),
+ array( 'application/xhtml+xml' => 1.0 ) ) );
+ }
+
+ function testTimestamp() {
+ $t = gmmktime( 12, 34, 56, 1, 15, 2001 );
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, $t ),
+ 'TS_UNIX to TS_MW' );
+ $this->assertEquals(
+ '19690115123456',
+ wfTimestamp( TS_MW, -30281104 ),
+ 'Negative TS_UNIX to TS_MW' );
+ $this->assertEquals(
+ 979562096,
+ wfTimestamp( TS_UNIX, $t ),
+ 'TS_UNIX to TS_UNIX' );
+ $this->assertEquals(
+ '2001-01-15 12:34:56',
+ wfTimestamp( TS_DB, $t ),
+ 'TS_UNIX to TS_DB' );
+ $this->assertEquals(
+ '20010115T123456Z',
+ wfTimestamp( TS_ISO_8601_BASIC, $t ),
+ 'TS_ISO_8601_BASIC to TS_DB' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, '20010115123456' ),
+ 'TS_MW to TS_MW' );
+ $this->assertEquals(
+ 979562096,
+ wfTimestamp( TS_UNIX, '20010115123456' ),
+ 'TS_MW to TS_UNIX' );
+ $this->assertEquals(
+ '2001-01-15 12:34:56',
+ wfTimestamp( TS_DB, '20010115123456' ),
+ 'TS_MW to TS_DB' );
+ $this->assertEquals(
+ '20010115T123456Z',
+ wfTimestamp( TS_ISO_8601_BASIC, '20010115123456' ),
+ 'TS_MW to TS_ISO_8601_BASIC' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, '2001-01-15 12:34:56' ),
+ 'TS_DB to TS_MW' );
+ $this->assertEquals(
+ 979562096,
+ wfTimestamp( TS_UNIX, '2001-01-15 12:34:56' ),
+ 'TS_DB to TS_UNIX' );
+ $this->assertEquals(
+ '2001-01-15 12:34:56',
+ wfTimestamp( TS_DB, '2001-01-15 12:34:56' ),
+ 'TS_DB to TS_DB' );
+ $this->assertEquals(
+ '20010115T123456Z',
+ wfTimestamp( TS_ISO_8601_BASIC, '2001-01-15 12:34:56' ),
+ 'TS_DB to TS_ISO_8601_BASIC' );
+
+ # rfc2822 section 3.3
+
+ $this->assertEquals(
+ 'Mon, 15 Jan 2001 12:34:56 GMT',
+ wfTimestamp( TS_RFC2822, '20010115123456' ),
+ 'TS_MW to TS_RFC2822' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, 'Mon, 15 Jan 2001 12:34:56 GMT' ),
+ 'TS_RFC2822 to TS_MW' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, ' Mon, 15 Jan 2001 12:34:56 GMT' ),
+ 'TS_RFC2822 with leading space to TS_MW' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, '15 Jan 2001 12:34:56 GMT' ),
+ 'TS_RFC2822 without optional day-of-week to TS_MW' );
+
+ # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space
+ # obs-FWS = 1*WSP *(CRLF 1*WSP) ; Section 4.2
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, 'Mon, 15 Jan 2001 12:34:56 GMT' ),
+ 'TS_RFC2822 to TS_MW' );
+
+ # WSP = SP / HTAB ; rfc2234
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, "Mon, 15 Jan\x092001 12:34:56 GMT" ),
+ 'TS_RFC2822 with HTAB to TS_MW' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, "Mon, 15 Jan\x09 \x09 2001 12:34:56 GMT" ),
+ 'TS_RFC2822 with HTAB and SP to TS_MW' );
+
+ $this->assertEquals(
+ '19941106084937',
+ wfTimestamp( TS_MW, "Sun, 6 Nov 94 08:49:37 GMT" ),
+ 'TS_RFC2822 with obsolete year to TS_MW' );
+ }
+
+ /**
+ * This test checks wfTimestamp() with values outside.
+ * It needs PHP 64 bits or PHP > 5.1.
+ * See r74778 and bug 25451
+ */
+ function testOldTimestamps() {
+ $this->assertEquals( 'Fri, 13 Dec 1901 20:45:54 GMT',
+ wfTimestamp( TS_RFC2822, '19011213204554' ),
+ 'Earliest time according to php documentation' );
+
+ $this->assertEquals( 'Tue, 19 Jan 2038 03:14:07 GMT',
+ wfTimestamp( TS_RFC2822, '20380119031407' ),
+ 'Latest 32 bit time' );
+
+ $this->assertEquals( '-2147483648',
+ wfTimestamp( TS_UNIX, '19011213204552' ),
+ 'Earliest 32 bit unix time' );
+
+ $this->assertEquals( '2147483647',
+ wfTimestamp( TS_UNIX, '20380119031407' ),
+ 'Latest 32 bit unix time' );
+
+ $this->assertEquals( 'Fri, 13 Dec 1901 20:45:52 GMT',
+ wfTimestamp( TS_RFC2822, '19011213204552' ),
+ 'Earliest 32 bit time' );
+
+ $this->assertEquals( 'Fri, 13 Dec 1901 20:45:51 GMT',
+ wfTimestamp( TS_RFC2822, '19011213204551' ),
+ 'Earliest 32 bit time - 1' );
+
+ $this->assertEquals( 'Tue, 19 Jan 2038 03:14:08 GMT',
+ wfTimestamp( TS_RFC2822, '20380119031408' ),
+ 'Latest 32 bit time + 1' );
+
+ $this->assertEquals( '19011212000000',
+ wfTimestamp(TS_MW, '19011212000000'),
+ 'Convert to itself r74778#c10645' );
+
+ $this->assertEquals( '-2147483649',
+ wfTimestamp( TS_UNIX, '19011213204551' ),
+ 'Earliest 32 bit unix time - 1' );
+
+ $this->assertEquals( '2147483648',
+ wfTimestamp( TS_UNIX, '20380119031408' ),
+ 'Latest 32 bit unix time + 1' );
+
+ $this->assertEquals( '19011213204551',
+ wfTimestamp( TS_MW, '-2147483649' ),
+ '1901 negative unix time to MediaWiki' );
+
+ $this->assertEquals( '18010115123456',
+ wfTimestamp( TS_MW, '-5331871504' ),
+ '1801 negative unix time to MediaWiki' );
+
+ $this->assertEquals( 'Tue, 09 Aug 0117 12:34:56 GMT',
+ wfTimestamp( TS_RFC2822, '0117-08-09 12:34:56'),
+ 'Death of Roman Emperor [[Trajan]]');
+
+ /* @todo FIXME: 00 to 101 years are taken as being in [1970-2069] */
+
+ $this->assertEquals( 'Sun, 01 Jan 0101 00:00:00 GMT',
+ wfTimestamp( TS_RFC2822, '-58979923200'),
+ '1/1/101');
+
+ $this->assertEquals( 'Mon, 01 Jan 0001 00:00:00 GMT',
+ wfTimestamp( TS_RFC2822, '-62135596800'),
+ 'Year 1');
+
+ /* It is not clear if we should generate a year 0 or not
+ * We are completely off RFC2822 requirement of year being
+ * 1900 or later.
+ */
+ $this->assertEquals( 'Wed, 18 Oct 0000 00:00:00 GMT',
+ wfTimestamp( TS_RFC2822, '-62142076800'),
+ 'ISO 8601:2004 [[year 0]], also called [[1 BC]]');
+ }
+
+ function testHttpDate() {
+ # The Resource Loader uses wfTimestamp() to convert timestamps
+ # from If-Modified-Since header.
+ # Thus it must be able to parse all rfc2616 date formats
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
+
+ $this->assertEquals(
+ '19941106084937',
+ wfTimestamp( TS_MW, 'Sun, 06 Nov 1994 08:49:37 GMT' ),
+ 'RFC 822 date' );
+
+ $this->assertEquals(
+ '19941106084937',
+ wfTimestamp( TS_MW, 'Sunday, 06-Nov-94 08:49:37 GMT' ),
+ 'RFC 850 date' );
+
+ $this->assertEquals(
+ '19941106084937',
+ wfTimestamp( TS_MW, 'Sun Nov 6 08:49:37 1994' ),
+ "ANSI C's asctime() format" );
+
+ // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171
+ $this->assertEquals(
+ '20101122141242',
+ wfTimestamp( TS_MW, 'Mon, 22 Nov 2010 14:12:42 GMT; length=52626' ),
+ "Netscape extension to HTTP/1.0" );
+
+ }
+
+ function testTimestampParameter() {
+ // There are a number of assumptions in our codebase where wfTimestamp() should give
+ // the current date but it is not given a 0 there. See r71751 CR
+
+ $now = wfTimestamp( TS_UNIX );
+ // We check that wfTimestamp doesn't return false (error) and use a LessThan assert
+ // for the cases where the test is run in a second boundary.
+
+ $zero = wfTimestamp( TS_UNIX, 0 );
+ $this->assertNotEquals( false, $zero );
+ $this->assertLessThan( 5, $zero - $now );
+
+ $empty = wfTimestamp( TS_UNIX, '' );
+ $this->assertNotEquals( false, $empty );
+ $this->assertLessThan( 5, $empty - $now );
+
+ $null = wfTimestamp( TS_UNIX, null );
+ $this->assertNotEquals( false, $null );
+ $this->assertLessThan( 5, $null - $now );
+ }
+
+ function testBasename() {
+ $sets = array(
+ '' => '',
+ '/' => '',
+ '\\' => '',
+ '//' => '',
+ '\\\\' => '',
+ 'a' => 'a',
+ 'aaaa' => 'aaaa',
+ '/a' => 'a',
+ '\\a' => 'a',
+ '/aaaa' => 'aaaa',
+ '\\aaaa' => 'aaaa',
+ '/aaaa/' => 'aaaa',
+ '\\aaaa\\' => 'aaaa',
+ '\\aaaa\\' => 'aaaa',
+ '/mnt/upload3/wikipedia/en/thumb/8/8b/Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg' => '93px-Zork_Grand_Inquisitor_box_cover.jpg',
+ 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE' => 'VIEWER.EXE',
+ 'Östergötland_coat_of_arms.png' => 'Östergötland_coat_of_arms.png',
+ );
+ foreach ( $sets as $from => $to ) {
+ $this->assertEquals( $to, wfBaseName( $from ),
+ "wfBaseName('$from') => '$to'" );
+ }
+ }
+
+
+ function testFallbackMbstringFunctions() {
+
+ if( !extension_loaded( 'mbstring' ) ) {
+ $this->markTestSkipped( "The mb_string functions must be installed to test the fallback functions" );
+ }
+
+ $sampleUTF = "Östergötland_coat_of_arms.png";
+
+
+ //mb_substr
+ $substr_params = array(
+ array( 0, 0 ),
+ array( 5, -4 ),
+ array( 33 ),
+ array( 100, -5 ),
+ array( -8, 10 ),
+ array( 1, 1 ),
+ array( 2, -1 )
+ );
+
+ foreach( $substr_params as $param_set ) {
+ $old_param_set = $param_set;
+ array_unshift( $param_set, $sampleUTF );
+
+ $this->assertEquals(
+ MWFunction::callArray( 'mb_substr', $param_set ),
+ MWFunction::callArray( 'Fallback::mb_substr', $param_set ),
+ 'Fallback mb_substr with params ' . implode( ', ', $old_param_set )
+ );
+ }
+
+
+ //mb_strlen
+ $this->assertEquals(
+ mb_strlen( $sampleUTF ),
+ Fallback::mb_strlen( $sampleUTF ),
+ 'Fallback mb_strlen'
+ );
+
+
+ //mb_str(r?)pos
+ $strpos_params = array(
+ //array( 'ter' ),
+ //array( 'Ö' ),
+ //array( 'Ö', 3 ),
+ //array( 'oat_', 100 ),
+ //array( 'c', -10 ),
+ //Broken for now
+ );
+
+ foreach( $strpos_params as $param_set ) {
+ $old_param_set = $param_set;
+ array_unshift( $param_set, $sampleUTF );
+
+ $this->assertEquals(
+ MWFunction::callArray( 'mb_strpos', $param_set ),
+ MWFunction::callArray( 'Fallback::mb_strpos', $param_set ),
+ 'Fallback mb_strpos with params ' . implode( ', ', $old_param_set )
+ );
+
+ $this->assertEquals(
+ MWFunction::callArray( 'mb_strrpos', $param_set ),
+ MWFunction::callArray( 'Fallback::mb_strrpos', $param_set ),
+ 'Fallback mb_strrpos with params ' . implode( ', ', $old_param_set )
+ );
+ }
+
+ }
+
+
+ function testDebugFunctionTest() {
+
+ global $wgDebugLogFile, $wgOut, $wgShowDebug, $wgDebugTimestamps;
+
+ $old_log_file = $wgDebugLogFile;
+ $wgDebugLogFile = tempnam( wfTempDir(), 'mw-' );
+ # @todo FIXME: This setting should be tested
+ $wgDebugTimestamps = false;
+
+
+
+ wfDebug( "This is a normal string" );
+ $this->assertEquals( "This is a normal string", file_get_contents( $wgDebugLogFile ) );
+ unlink( $wgDebugLogFile );
+
+
+ wfDebug( "This is nöt an ASCII string" );
+ $this->assertEquals( "This is nöt an ASCII string", file_get_contents( $wgDebugLogFile ) );
+ unlink( $wgDebugLogFile );
+
+
+ wfDebug( "\00305This has böth UTF and control chars\003" );
+ $this->assertEquals( " 05This has böth UTF and control chars ", file_get_contents( $wgDebugLogFile ) );
+ unlink( $wgDebugLogFile );
+
+
+
+ $old_wgOut = $wgOut;
+ $old_wgShowDebug = $wgShowDebug;
+
+ $wgOut = new MockOutputPage;
+
+ $wgShowDebug = true;
+
+ $message = "\00305This has böth UTF and control chars\003";
+
+ wfDebug( $message );
+
+ if( $wgOut->message == "JAJA is a stupid error message. Anyway, here's your message: $message" ) {
+ $this->assertTrue( true, 'MockOutputPage called, set the proper message.' );
+ }
+ else {
+ $this->assertTrue( false, 'MockOutputPage was not called.' );
+ }
+
+ $wgOut = $old_wgOut;
+ $wgShowDebug = $old_wgShowDebug;
+ unlink( $wgDebugLogFile );
+
+
+
+ wfDebugMem();
+ $this->assertGreaterThan( 5000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) );
+ unlink( $wgDebugLogFile );
+
+ wfDebugMem(true);
+ $this->assertGreaterThan( 5000000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) );
+ unlink( $wgDebugLogFile );
+
+
+
+ $wgDebugLogFile = $old_log_file;
+
+ }
+
+ function testClientAcceptsGzipTest() {
+
+ $settings = array(
+ 'gzip' => true,
+ 'bzip' => false,
+ '*' => false,
+ 'compress, gzip' => true,
+ 'gzip;q=1.0' => true,
+ 'foozip' => false,
+ 'foo*zip' => false,
+ 'gzip;q=abcde' => true, //is this REALLY valid?
+ 'gzip;q=12345678.9' => true,
+ ' gzip' => true,
+ );
+
+ if( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) $old_server_setting = $_SERVER['HTTP_ACCEPT_ENCODING'];
+
+ foreach ( $settings as $encoding => $expect ) {
+ $_SERVER['HTTP_ACCEPT_ENCODING'] = $encoding;
+
+ $this->assertEquals( $expect, wfClientAcceptsGzip( true ),
+ "'$encoding' => " . wfBoolToStr( $expect ) );
+ }
+
+ if( isset( $old_server_setting ) ) $_SERVER['HTTP_ACCEPT_ENCODING'] = $old_server_setting;
+
+ }
+
+
+
+ function testSwapVarsTest() {
+
+
+ $var1 = 1;
+ $var2 = 2;
+
+ $this->assertEquals( $var1, 1, 'var1 is set originally' );
+ $this->assertEquals( $var2, 2, 'var1 is set originally' );
+
+ swap( $var1, $var2 );
+
+ $this->assertEquals( $var1, 2, 'var1 is swapped' );
+ $this->assertEquals( $var2, 1, 'var2 is swapped' );
+
+ }
+
+
+ function testWfPercentTest() {
+
+ $pcts = array(
+ array( 6/7, '0.86%', 2, false ),
+ array( 3/3, '1%' ),
+ array( 22/7, '3.14286%', 5 ),
+ array( 3/6, '0.5%' ),
+ array( 1/3, '0%', 0 ),
+ array( 10/3, '0%', -1 ),
+ array( 3/4/5, '0.1%', 1 ),
+ array( 6/7*8, '6.8571428571%', 10 ),
+ );
+
+ foreach( $pcts as $pct ) {
+ if( !isset( $pct[2] ) ) $pct[2] = 2;
+ if( !isset( $pct[3] ) ) $pct[3] = true;
+
+ $this->assertEquals( wfPercent( $pct[0], $pct[2], $pct[3] ), $pct[1], $pct[1] );
+ }
+
+ }
+
+
+ function testInStringTest() {
+
+ $this->assertTrue( in_string( 'foo', 'foobar' ), 'foo is in foobar' );
+ $this->assertFalse( in_string( 'Bar', 'foobar' ), 'Case-sensitive by default' );
+ $this->assertTrue( in_string( 'Foo', 'foobar', true ), 'Case-insensitive when asked' );
+
+ }
+
+ /**
+ * test @see wfShorthandToInteger()
+ * @dataProvider provideShorthand
+ */
+ public function testWfShorthandToInteger( $shorthand, $expected ) {
+ $this->assertEquals( $expected,
+ wfShorthandToInteger( $shorthand )
+ );
+ }
+
+ /** array( shorthand, expected integer ) */
+ public function provideShorthand() {
+ return array(
+ # Null, empty ...
+ array( '', -1),
+ array( ' ', -1),
+ array( null, -1),
+
+ # Failures returns 0 :(
+ array( 'ABCDEFG', 0 ),
+ array( 'Ak', 0 ),
+
+ # Int, strings with spaces
+ array( 1, 1 ),
+ array( ' 1 ', 1 ),
+ array( 1023, 1023 ),
+ array( ' 1023 ', 1023 ),
+
+ # kilo, Mega, Giga
+ array( '1k', 1024 ),
+ array( '1K', 1024 ),
+ array( '1m', 1024 * 1024 ),
+ array( '1M', 1024 * 1024 ),
+ array( '1g', 1024 * 1024 * 1024 ),
+ array( '1G', 1024 * 1024 * 1024 ),
+
+ # Negatives
+ array( -1, -1 ),
+ array( -500, -500 ),
+ array( '-500', -500 ),
+ array( '-1k', -1024 ),
+
+ # Zeroes
+ array( '0', 0 ),
+ array( '0k', 0 ),
+ array( '0M', 0 ),
+ array( '0G', 0 ),
+ array( '-0', 0 ),
+ array( '-0k', 0 ),
+ array( '-0M', 0 ),
+ array( '-0G', 0 ),
+ );
+ }
+
+
+ /**
+ * test @see wfBCP47().
+ * Please note the BCP explicitly state that language codes are case
+ * insensitive, there are some exceptions to the rule :)
+ * This test is used to verify our formatting against all lower and
+ * all upper cases language code.
+ *
+ * @see http://tools.ietf.org/html/bcp47
+ * @dataProvider provideLanguageCodes()
+ */
+ function testBCP47( $code, $expected ) {
+ $code = strtolower( $code );
+ $this->assertEquals( $expected, wfBCP47($code),
+ "Applying BCP47 standard to lower case '$code'"
+ );
+
+ $code = strtoupper( $code );
+ $this->assertEquals( $expected, wfBCP47($code),
+ "Applying BCP47 standard to upper case '$code'"
+ );
+ }
+
+ /**
+ * Array format is ($code, $expected)
+ */
+ function provideLanguageCodes() {
+ return array(
+ // Extracted from BCP47 (list not exhaustive)
+ # 2.1.1
+ array( 'en-ca-x-ca' , 'en-CA-x-ca' ),
+ array( 'sgn-be-fr' , 'sgn-BE-FR' ),
+ array( 'az-latn-x-latn', 'az-Latn-x-latn' ),
+ # 2.2
+ array( 'sr-Latn-RS', 'sr-Latn-RS' ),
+ array( 'az-arab-ir', 'az-Arab-IR' ),
+
+ # 2.2.5
+ array( 'sl-nedis' , 'sl-nedis' ),
+ array( 'de-ch-1996', 'de-CH-1996' ),
+
+ # 2.2.6
+ array(
+ 'en-latn-gb-boont-r-extended-sequence-x-private',
+ 'en-Latn-GB-boont-r-extended-sequence-x-private'
+ ),
+
+ // Examples from BCP47 Appendix A
+ # Simple language subtag:
+ array( 'DE', 'de' ),
+ array( 'fR', 'fr' ),
+ array( 'ja', 'ja' ),
+
+ # Language subtag plus script subtag:
+ array( 'zh-hans', 'zh-Hans'),
+ array( 'sr-cyrl', 'sr-Cyrl'),
+ array( 'sr-latn', 'sr-Latn'),
+
+ # Extended language subtags and their primary language subtag
+ # counterparts:
+ array( 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ),
+ array( 'cmn-hans-cn' , 'cmn-Hans-CN' ),
+ array( 'zh-yue-hk' , 'zh-yue-HK' ),
+ array( 'yue-hk' , 'yue-HK' ),
+
+ # Language-Script-Region:
+ array( 'zh-hans-cn', 'zh-Hans-CN' ),
+ array( 'sr-latn-RS', 'sr-Latn-RS' ),
+
+ # Language-Variant:
+ array( 'sl-rozaj' , 'sl-rozaj' ),
+ array( 'sl-rozaj-biske', 'sl-rozaj-biske' ),
+ array( 'sl-nedis' , 'sl-nedis' ),
+
+ # Language-Region-Variant:
+ array( 'de-ch-1901' , 'de-CH-1901' ),
+ array( 'sl-it-nedis' , 'sl-IT-nedis' ),
+
+ # Language-Script-Region-Variant:
+ array( 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ),
+
+ # Language-Region:
+ array( 'de-de' , 'de-DE' ),
+ array( 'en-us' , 'en-US' ),
+ array( 'es-419', 'es-419'),
+
+ # Private use subtags:
+ array( 'de-ch-x-phonebk' , 'de-CH-x-phonebk' ),
+ array( 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ),
+ /**
+ * Previous test does not reflect the BCP which states:
+ * az-Arab-x-AZE-derbend
+ * AZE being private, it should be lower case, hence the test above
+ * should probably be:
+ #array( 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ),
+ */
+
+ # Private use registry values:
+ array( 'x-whatever', 'x-whatever' ),
+ array( 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ),
+ array( 'de-qaaa' , 'de-Qaaa' ),
+ array( 'sr-latn-qm', 'sr-Latn-QM' ),
+ array( 'sr-qaaa-rs', 'sr-Qaaa-RS' ),
+
+ # Tags that use extensions
+ array( 'en-us-u-islamcal', 'en-US-u-islamcal' ),
+ array( 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ),
+ array( 'en-a-myext-b-another', 'en-a-myext-b-another' ),
+
+ # Invalid:
+ // de-419-DE
+ // a-DE
+ // ar-a-aaa-b-bbb-a-ccc
+
+ /*
+ // ISO 15924 :
+ array( 'sr-Cyrl', 'sr-Cyrl' ),
+ # @todo FIXME: Fix our function?
+ array( 'SR-lATN', 'sr-Latn' ),
+ array( 'fr-latn', 'fr-Latn' ),
+ // Use lowercase for single segment
+ // ISO 3166-1-alpha-2 code
+ array( 'US', 'us' ), # USA
+ array( 'uS', 'us' ), # USA
+ array( 'Fr', 'fr' ), # France
+ array( 'va', 'va' ), # Holy See (Vatican City State)
+ */);
+ }
+
+ /**
+ * @dataProvider provideMakeUrlIndexes()
+ */
+ function testMakeUrlIndexes( $url, $expected ) {
+ $index = wfMakeUrlIndexes( $url );
+ $this->assertEquals( $expected, $index, "wfMakeUrlIndexes(\"$url\")" );
+ }
+
+ function provideMakeUrlIndexes() {
+ return array(
+ array(
+ // just a regular :)
+ 'https://bugzilla.wikimedia.org/show_bug.cgi?id=28627',
+ array( 'https://org.wikimedia.bugzilla./show_bug.cgi?id=28627' )
+ ),
+ array(
+ // mailtos are handled special
+ // is this really right though? that final . probably belongs earlier?
+ 'mailto:wiki@wikimedia.org',
+ array( 'mailto:org.wikimedia@wiki.' )
+ ),
+
+ // file URL cases per bug 28627...
+ array(
+ // three slashes: local filesystem path Unix-style
+ 'file:///whatever/you/like.txt',
+ array( 'file://./whatever/you/like.txt' )
+ ),
+ array(
+ // three slashes: local filesystem path Windows-style
+ 'file:///c:/whatever/you/like.txt',
+ array( 'file://./c:/whatever/you/like.txt' )
+ ),
+ array(
+ // two slashes: UNC filesystem path Windows-style
+ 'file://intranet/whatever/you/like.txt',
+ array( 'file://intranet./whatever/you/like.txt' )
+ ),
+ // Multiple-slash cases that can sorta work on Mozilla
+ // if you hack it just right are kinda pathological,
+ // and unreliable cross-platform or on IE which means they're
+ // unlikely to appear on intranets.
+ //
+ // Those will survive the algorithm but with results that
+ // are less consistent.
+
+ // protocol-relative URL cases per bug 29854...
+ array(
+ '//bugzilla.wikimedia.org/show_bug.cgi?id=28627',
+ array(
+ 'http://org.wikimedia.bugzilla./show_bug.cgi?id=28627',
+ 'https://org.wikimedia.bugzilla./show_bug.cgi?id=28627'
+ )
+ ),
+ );
+ }
+
+ /* TODO: many more! */
+}
+
+
+class MockOutputPage {
+
+ public $message;
+
+ function debug( $message ) {
+ $this->message = "JAJA is a stupid error message. Anyway, here's your message: $message";
+ }
+}
+
diff --git a/tests/phpunit/includes/GlobalFunctions/README b/tests/phpunit/includes/GlobalFunctions/README
new file mode 100644
index 00000000..0042bdac
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/README
@@ -0,0 +1,2 @@
+This directory hold tests for includes/GlobalFunctions.php file
+which is a pile of functions.
diff --git a/tests/phpunit/includes/GlobalFunctions/wfExpandUrl.php b/tests/phpunit/includes/GlobalFunctions/wfExpandUrl.php
new file mode 100644
index 00000000..b388b266
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfExpandUrl.php
@@ -0,0 +1,78 @@
+<?php
+/*
+ * Unit tests for wfExpandUrl()
+ */
+
+class wfExpandUrl extends MediaWikiTestCase {
+ /** @dataProvider provideExpandableUrls */
+ public function testWfExpandUrl( $fullUrl, $shortUrl, $defaultProto, $server, $canServer, $httpsMode, $message ) {
+ // Fake $wgServer and $wgCanonicalServer
+ global $wgServer, $wgCanonicalServer;
+ $oldServer = $wgServer;
+ $oldCanServer = $wgCanonicalServer;
+ $wgServer = $server;
+ $wgCanonicalServer = $canServer;
+
+ // Fake $_SERVER['HTTPS'] if needed
+ if ( $httpsMode ) {
+ $_SERVER['HTTPS'] = 'on';
+ } else {
+ unset( $_SERVER['HTTPS'] );
+ }
+
+ $this->assertEquals( $fullUrl, wfExpandUrl( $shortUrl, $defaultProto ), $message );
+
+ // Restore $wgServer and $wgCanonicalServer
+ $wgServer = $oldServer;
+ $wgCanonicalServer = $oldCanServer;
+ }
+
+ /**
+ * Provider of URL examples for testing wfExpandUrl()
+ */
+ public function provideExpandableUrls() {
+ $modes = array( 'http', 'https' );
+ $servers = array( 'http' => 'http://example.com', 'https' => 'https://example.com', 'protocol-relative' => '//example.com' );
+ $defaultProtos = array( 'http' => PROTO_HTTP, 'https' => PROTO_HTTPS, 'protocol-relative' => PROTO_RELATIVE, 'current' => PROTO_CURRENT, 'canonical' => PROTO_CANONICAL );
+
+ $retval = array();
+ foreach ( $modes as $mode ) {
+ $httpsMode = $mode == 'https';
+ foreach ( $servers as $serverDesc => $server ) {
+ foreach ( $modes as $canServerMode ) {
+ $canServer = "$canServerMode://example2.com";
+ foreach ( $defaultProtos as $protoDesc => $defaultProto ) {
+ $retval[] = array( 'http://example.com', 'http://example.com', $defaultProto, $server, $canServer, $httpsMode, "Testing fully qualified http URLs (no need to expand) (defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" );
+ $retval[] = array( 'https://example.com', 'https://example.com', $defaultProto, $server, $canServer, $httpsMode, "Testing fully qualified https URLs (no need to expand) (defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" );
+ # Would be nice to support this, see fixme on wfExpandUrl()
+ $retval[] = array( "wiki/FooBar", 'wiki/FooBar', $defaultProto, $server, $canServer, $httpsMode, "Test non-expandable relative URLs (defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" );
+
+ // Determine expected protocol
+ $p = $protoDesc . ':'; // default case
+ if ( $protoDesc == 'protocol-relative' ) {
+ $p = '';
+ } else if ( $protoDesc == 'current' ) {
+ $p = "$mode:";
+ } else if ( $protoDesc == 'canonical' ) {
+ $p = "$canServerMode:";
+ } else {
+ $p = $protoDesc . ':';
+ }
+ // Determine expected server name
+ if ( $protoDesc == 'canonical' ) {
+ $srv = $canServer;
+ } else if ( $serverDesc == 'protocol-relative' ) {
+ $srv = $p . $server;
+ } else {
+ $srv = $server;
+ }
+
+ $retval[] = array( "$p//wikipedia.org", '//wikipedia.org', $defaultProto, $server, $canServer, $httpsMode, "Test protocol-relative URL (defaultProto: $protoDesc, wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" );
+ $retval[] = array( "$srv/wiki/FooBar", '/wiki/FooBar', $defaultProto, $server, $canServer, $httpsMode, "Testing expanding URL beginning with / (defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" );
+ }
+ }
+ }
+ }
+ return $retval;
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php
new file mode 100644
index 00000000..cd1a8dbd
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Tests for includes/GlobalFunctions.php -> wfUrlencode()
+ *
+ * The function only need a string parameter and might react to IIS7.0
+ */
+
+class wfUrlencodeTest extends MediaWikiTestCase {
+
+ #### TESTS ##############################################################
+
+ /** @dataProvider provideURLS */
+ public function testEncodingUrlWith( $input, $expected ) {
+ $this->verifyEncodingFor( 'Apache', $input, $expected );
+ }
+
+ /** @dataProvider provideURLS */
+ public function testEncodingUrlWithMicrosoftIis7( $input, $expected ) {
+ $this->verifyEncodingFor( 'Microsoft-IIS/7', $input, $expected );
+ }
+
+ #### HELPERS #############################################################
+
+ /**
+ * Internal helper that actually run the test.
+ * Called by the public methods testEncodingUrlWith...()
+ *
+ */
+ private function verifyEncodingFor( $server, $input, $expectations ) {
+ $expected = $this->extractExpect( $server, $expectations );
+
+ // save up global
+ $old = isset($_SERVER['SERVER_SOFTWARE'])
+ ? $_SERVER['SERVER_SOFTWARE']
+ : null
+ ;
+ $_SERVER['SERVER_SOFTWARE'] = $server;
+ wfUrlencode( null );
+
+ // do the requested test
+ $this->assertEquals(
+ $expected,
+ wfUrlencode( $input ),
+ "Encoding '$input' for server '$server' should be '$expected'"
+ );
+
+ // restore global
+ if( $old === null ) {
+ unset( $_SERVER['SERVER_SOFTWARE'] );
+ } else {
+ $_SERVER['SERVER_SOFTWARE'] = $old;
+ }
+ wfUrlencode( null );
+ }
+
+ /**
+ * Interprets the provider array. Return expected value depending
+ * the HTTP server name.
+ */
+ private function extractExpect( $server, $expectations ) {
+ if( is_string( $expectations ) ) {
+ return $expectations;
+ } elseif( is_array( $expectations ) ) {
+ if( !array_key_exists( $server, $expectations ) ) {
+ throw new MWException( __METHOD__ . " expectation does not have any value for server name $server. Check the provider array.\n" );
+ } else {
+ return $expectations[$server];
+ }
+ } else {
+ throw new MWException( __METHOD__ . " given invalid expectation for '$server'. Should be a string or an array( <http server name> => <string> ).\n" );
+ }
+ }
+
+
+ #### PROVIDERS ###########################################################
+
+ /**
+ * Format is either:
+ * array( 'input', 'expected' );
+ * Or:
+ * array( 'input',
+ * array( 'Apache', 'expected' ),
+ * array( 'Microsoft-IIS/7', 'expected' ),
+ * ),
+ * If you want to add other HTTP server name, you will have to add a new
+ * testing method much like the testEncodingUrlWith() method above.
+ */
+ public function provideURLS() {
+ return array(
+ ### RFC 1738 chars
+ // + is not safe
+ array( '+', '%2B' ),
+ // & and = not safe in queries
+ array( '&', '%26' ),
+ array( '=', '%3D' ),
+
+ array( ':', array(
+ 'Apache' => ':',
+ 'Microsoft-IIS/7' => '%3A',
+ ) ),
+
+ // remaining chars do not need encoding
+ array(
+ ';@$-_.!*',
+ ';@$-_.!*',
+ ),
+
+ ### Other tests
+ // slash remain unchanged. %2F seems to break things
+ array( '/', '/' ),
+
+ // Other 'funnies' chars
+ array( '[]', '%5B%5D' ),
+ array( '<>', '%3C%3E' ),
+
+ // Apostrophe is encoded
+ array( '\'', '%27' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/HooksTest.php b/tests/phpunit/includes/HooksTest.php
new file mode 100644
index 00000000..2f9d9f8d
--- /dev/null
+++ b/tests/phpunit/includes/HooksTest.php
@@ -0,0 +1,102 @@
+<?php
+
+class HooksTest extends MediaWikiTestCase {
+
+ public function testOldStyleHooks() {
+ $foo = 'Foo';
+ $bar = 'Bar';
+
+ $i = new NothingClass();
+
+ global $wgHooks;
+
+ $wgHooks['MediaWikiHooksTest001'][] = array( $i, 'someNonStatic' );
+
+ wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertEquals( 'fOO', $foo, 'Standard method' );
+ $foo = 'Foo';
+
+ $wgHooks['MediaWikiHooksTest001'][] = $i;
+
+ wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertEquals( 'foo', $foo, 'onEventName style' );
+ $foo = 'Foo';
+
+ $wgHooks['MediaWikiHooksTest001'][] = array( $i, 'someNonStaticWithData', 'baz' );
+
+ wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertEquals( 'baz', $foo, 'Data included' );
+ $foo = 'Foo';
+
+ $wgHooks['MediaWikiHooksTest001'][] = array( $i, 'someStatic' );
+
+ wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertEquals( 'bah', $foo, 'Standard static method' );
+ //$foo = 'Foo';
+
+ unset( $wgHooks['MediaWikiHooksTest001'] );
+
+ }
+
+ public function testNewStyleHooks() {
+ $foo = 'Foo';
+ $bar = 'Bar';
+
+ $i = new NothingClass();
+
+ Hooks::register( 'MediaWikiHooksTest001', array( $i, 'someNonStatic' ) );
+
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertEquals( 'fOO', $foo, 'Standard method' );
+ $foo = 'Foo';
+
+ Hooks::register( 'MediaWikiHooksTest001', $i );
+
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertEquals( 'foo', $foo, 'onEventName style' );
+ $foo = 'Foo';
+
+ Hooks::register( 'MediaWikiHooksTest001', array( $i, 'someNonStaticWithData', 'baz' ) );
+
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertEquals( 'baz', $foo, 'Data included' );
+ $foo = 'Foo';
+
+ Hooks::register( 'MediaWikiHooksTest001', array( $i, 'someStatic' ) );
+
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertEquals( 'bah', $foo, 'Standard static method' );
+ $foo = 'Foo';
+ }
+}
+
+class NothingClass {
+ static public function someStatic( &$foo, &$bar ) {
+ $foo = 'bah';
+ return true;
+ }
+
+ public function someNonStatic( &$foo, &$bar ) {
+ $foo = 'fOO';
+ $bar = 'bAR';
+ return true;
+ }
+
+ public function onMediaWikiHooksTest001( &$foo, &$bar ) {
+ $foo = 'foo';
+ return true;
+ }
+
+ public function someNonStaticWithData( $foo, &$bar ) {
+ $bar = $foo;
+ return true;
+ }
+}
diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php
new file mode 100644
index 00000000..96bb1803
--- /dev/null
+++ b/tests/phpunit/includes/HtmlTest.php
@@ -0,0 +1,90 @@
+<?php
+/** tests for includes/Html.php */
+
+class HtmlTest extends MediaWikiTestCase {
+ private static $oldLang;
+
+ public function setUp() {
+ global $wgLang, $wgLanguageCode;
+
+ self::$oldLang = $wgLang;
+ $wgLanguageCode = 'en';
+ $wgLang = Language::factory( $wgLanguageCode );
+ }
+
+ public function tearDown() {
+ global $wgLang, $wgLanguageCode;
+ $wgLang = self::$oldLang;
+ $wgLanguageCode = $wgLang->getCode();
+ }
+
+ public function testExpandAttributesSkipsNullAndFalse() {
+
+ ### EMPTY ########
+ $this->AssertEmpty(
+ Html::expandAttributes( array( 'foo'=>null) ),
+ 'skip keys with null value'
+ );
+ $this->AssertEmpty(
+ Html::expandAttributes( array( 'foo'=>false) ),
+ 'skip keys with false value'
+ );
+ $this->AssertNotEmpty(
+ Html::expandAttributes( array( 'foo'=>'') ),
+ 'keep keys with an empty string'
+ );
+ }
+
+ public function testExpandAttributesForBooleans() {
+ $this->AssertEquals(
+ '',
+ Html::expandAttributes( array( 'selected'=>false) ),
+ 'Boolean attributes do not generates output when value is false'
+ );
+ $this->AssertEquals(
+ '',
+ Html::expandAttributes( array( 'selected'=>null) ),
+ 'Boolean attributes do not generates output when value is null'
+ );
+
+ ### FIXME: maybe they should just output 'selected'
+ $this->AssertEquals(
+ ' selected=""',
+ Html::expandAttributes( array( 'selected'=>true ) ),
+ 'Boolean attributes skip value output'
+ );
+ $this->AssertEquals(
+ ' selected=""',
+ Html::expandAttributes( array( 'selected' ) ),
+ 'Boolean attributes (ex: selected) do not need a value'
+ );
+ }
+
+ /**
+ * Test for Html::expandAttributes()
+ * Please note it output a string prefixed with a space!
+ */
+ public function testExpandAttributesVariousExpansions() {
+ ### NOT EMPTY ####
+ $this->AssertEquals(
+ ' empty_string=""',
+ Html::expandAttributes( array( 'empty_string'=>'') ),
+ 'Value with an empty string'
+ );
+ $this->AssertEquals(
+ ' key="value"',
+ Html::expandAttributes( array( 'key'=>'value') ),
+ 'Value is a string'
+ );
+ $this->AssertEquals(
+ ' one="1"',
+ Html::expandAttributes( array( 'one'=>1) ),
+ 'Value is a numeric one'
+ );
+ $this->AssertEquals(
+ ' zero="0"',
+ Html::expandAttributes( array( 'zero'=>0) ),
+ 'Value is a numeric zero'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/HttpTest.php b/tests/phpunit/includes/HttpTest.php
new file mode 100644
index 00000000..1a99af7d
--- /dev/null
+++ b/tests/phpunit/includes/HttpTest.php
@@ -0,0 +1,618 @@
+<?php
+
+class MockCookie extends Cookie {
+ public function canServeDomain( $arg ) { return parent::canServeDomain( $arg ); }
+ public function canServePath( $arg ) { return parent::canServePath( $arg ); }
+ public function isUnExpired() { return parent::isUnExpired(); }
+}
+
+/**
+ * @group Broken
+ */
+class HttpTest extends MediaWikiTestCase {
+ static $content;
+ static $headers;
+ static $has_curl;
+ static $has_fopen;
+ static $has_proxy = false;
+ static $proxy = "http://hulk:8080/";
+ var $test_geturl = array(
+ "http://en.wikipedia.org/robots.txt",
+ "https://secure.wikimedia.org/",
+ "http://pecl.php.net/feeds/pkg_apc.rss",
+ "http://meta.wikimedia.org/w/index.php?title=Interwiki_map&action=raw",
+ "http://www.mediawiki.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:MediaWiki_hooks&format=php",
+ );
+ var $test_requesturl = array( "http://en.wikipedia.org/wiki/Special:Export/User:MarkAHershberger" );
+
+ var $test_posturl = array( "http://www.comp.leeds.ac.uk/cgi-bin/Perl/environment-example" => "review=test" );
+
+ function setUp() {
+ putenv( "http_proxy" ); /* Remove any proxy env var, so curl doesn't get confused */
+ if ( is_array( self::$content ) ) {
+ return;
+ }
+ self::$has_curl = function_exists( 'curl_init' );
+ self::$has_fopen = wfIniGetBool( 'allow_url_fopen' );
+
+ if ( !file_exists( "/usr/bin/curl" ) ) {
+ $this->markTestIncomplete( "This test requires the curl binary at /usr/bin/curl. If you have curl, please file a bug on this test, or, better yet, provide a patch." );
+ }
+
+ $content = tempnam( wfTempDir(), "" );
+ $headers = tempnam( wfTempDir(), "" );
+ if ( !$content && !$headers ) {
+ die( "Couldn't create temp file!" );
+ }
+
+ // This probably isn't the best test for a proxy, but it works on my system!
+ system( "curl -0 -o $content -s " . self::$proxy );
+ $out = file_get_contents( $content );
+ if ( $out ) {
+ self::$has_proxy = true;
+ }
+
+ /* Maybe use wget instead of curl here ... just to use a different codebase? */
+ foreach ( $this->test_geturl as $u ) {
+ system( "curl -0 -s -D $headers '$u' -o $content" );
+ self::$content["GET $u"] = file_get_contents( $content );
+ self::$headers["GET $u"] = file_get_contents( $headers );
+ }
+ foreach ( $this->test_requesturl as $u ) {
+ system( "curl -0 -s -X POST -H 'Content-Length: 0' -D $headers '$u' -o $content" );
+ self::$content["POST $u"] = file_get_contents( $content );
+ self::$headers["POST $u"] = file_get_contents( $headers );
+ }
+ foreach ( $this->test_posturl as $u => $postData ) {
+ system( "curl -0 -s -X POST -d '$postData' -D $headers '$u' -o $content" );
+ self::$content["POST $u => $postData"] = file_get_contents( $content );
+ self::$headers["POST $u => $postData"] = file_get_contents( $headers );
+ }
+ unlink( $content );
+ unlink( $headers );
+ }
+
+
+ function testInstantiation() {
+ Http::$httpEngine = false;
+
+ $r = MWHttpRequest::factory( "http://www.example.com/" );
+ if ( self::$has_curl ) {
+ $this->assertThat( $r, $this->isInstanceOf( 'CurlHttpRequest' ) );
+ } else {
+ $this->assertThat( $r, $this->isInstanceOf( 'PhpHttpRequest' ) );
+ }
+ unset( $r );
+
+ if ( !self::$has_fopen ) {
+ $this->setExpectedException( 'MWException' );
+ }
+ Http::$httpEngine = 'php';
+ $r = MWHttpRequest::factory( "http://www.example.com/" );
+ $this->assertThat( $r, $this->isInstanceOf( 'PhpHttpRequest' ) );
+ unset( $r );
+
+ if ( !self::$has_curl ) {
+ $this->setExpectedException( 'MWException' );
+ }
+ Http::$httpEngine = 'curl';
+ $r = MWHttpRequest::factory( "http://www.example.com/" );
+ if ( self::$has_curl ) {
+ $this->assertThat( $r, $this->isInstanceOf( 'CurlHttpRequest' ) );
+ }
+ }
+
+ function runHTTPFailureChecks() {
+ // Each of the following requests should result in a failure.
+
+ $timeout = 1;
+ $start_time = time();
+ $r = Http::get( "http://www.example.com:1/", $timeout );
+ $end_time = time();
+ $this->assertLessThan( $timeout + 2, $end_time - $start_time,
+ "Request took less than {$timeout}s via " . Http::$httpEngine );
+ $this->assertEquals( $r, false, "false -- what we get on error from Http::get()" );
+
+ $r = Http::get( "http://www.mediawiki.org/xml/made-up-url", $timeout );
+ $this->assertFalse( $r, "False on 404s" );
+
+
+ $r = MWHttpRequest::factory( "http://www.mediawiki.org/xml/made-up-url" );
+ $er = $r->execute();
+ if ( $r instanceof PhpHttpRequest && version_compare( '5.2.10', phpversion(), '>' ) ) {
+ $this->assertRegexp( "/HTTP request failed/", $er->getWikiText() );
+ } else {
+ $this->assertRegexp( "/404 Not Found/", $er->getWikiText() );
+ }
+ }
+
+ function testFailureDefault() {
+ Http::$httpEngine = false;
+ $this->runHTTPFailureChecks();
+ }
+
+ function testFailurePhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = "php";
+ $this->runHTTPFailureChecks();
+ }
+
+ function testFailureCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = "curl";
+ $this->runHTTPFailureChecks();
+ }
+
+ /* ./phase3/includes/Import.php:1108: $data = Http::request( $method, $url ); */
+ /* ./includes/Import.php:1124: $link = Title::newFromText( "$interwiki:Special:Export/$page" ); */
+ /* ./includes/Import.php:1134: return ImportStreamSource::newFromURL( $url, "POST" ); */
+ function runHTTPRequests( $proxy = null ) {
+ $opt = array();
+
+ if ( $proxy ) {
+ $opt['proxy'] = $proxy;
+ } elseif ( $proxy === false ) {
+ $opt['noProxy'] = true;
+ }
+
+ /* no postData here because the only request I could find in code so far didn't have any */
+ foreach ( $this->test_requesturl as $u ) {
+ $r = Http::request( "POST", $u, $opt );
+ $this->assertEquals( self::$content["POST $u"], "$r", "POST $u with " . Http::$httpEngine );
+ }
+ }
+
+ function testRequestDefault() {
+ Http::$httpEngine = false;
+ $this->runHTTPRequests();
+ }
+
+ function testRequestPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = "php";
+ $this->runHTTPRequests();
+ }
+
+ function testRequestCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = "curl";
+ $this->runHTTPRequests();
+ }
+
+ function runHTTPGets( $proxy = null ) {
+ $opt = array();
+
+ if ( $proxy ) {
+ $opt['proxy'] = $proxy;
+ } elseif ( $proxy === false ) {
+ $opt['noProxy'] = true;
+ }
+
+ foreach ( $this->test_geturl as $u ) {
+ $r = Http::get( $u, 30, $opt ); /* timeout of 30s */
+ $this->assertEquals( self::$content["GET $u"], "$r", "Get $u with " . Http::$httpEngine );
+ }
+ }
+
+ function testGetDefault() {
+ Http::$httpEngine = false;
+ $this->runHTTPGets();
+ }
+
+ function testGetPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = "php";
+ $this->runHTTPGets();
+ }
+
+ function testGetCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = "curl";
+ $this->runHTTPGets();
+ }
+
+ function runHTTPPosts( $proxy = null ) {
+ $opt = array();
+
+ if ( $proxy ) {
+ $opt['proxy'] = $proxy;
+ } elseif ( $proxy === false ) {
+ $opt['noProxy'] = true;
+ }
+
+ foreach ( $this->test_posturl as $u => $postData ) {
+ $opt['postData'] = $postData;
+ $r = Http::post( $u, $opt );
+ $this->assertEquals( self::$content["POST $u => $postData"], "$r",
+ "POST $u (postData=$postData) with " . Http::$httpEngine );
+ }
+ }
+
+ function testPostDefault() {
+ Http::$httpEngine = false;
+ $this->runHTTPPosts();
+ }
+
+ function testPostPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = "php";
+ $this->runHTTPPosts();
+ }
+
+ function testPostCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = "curl";
+ $this->runHTTPPosts();
+ }
+
+ function runProxyRequests() {
+ if ( !self::$has_proxy ) {
+ $this->markTestIncomplete( "This test requires a proxy." );
+ }
+ $this->runHTTPGets( self::$proxy );
+ $this->runHTTPPosts( self::$proxy );
+ $this->runHTTPRequests( self::$proxy );
+
+ // Set false here to do noProxy
+ $this->runHTTPGets( false );
+ $this->runHTTPPosts( false );
+ $this->runHTTPRequests( false );
+ }
+
+ function testProxyDefault() {
+ Http::$httpEngine = false;
+ $this->runProxyRequests();
+ }
+
+ function testProxyPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = 'php';
+ $this->runProxyRequests();
+ }
+
+ function testProxyCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = 'curl';
+ $this->runProxyRequests();
+ }
+
+ function testIsLocalUrl() {
+ }
+
+ /* ./extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.body.php:559: $user_agent = Http::userAgent(); */
+ function testUserAgent() {
+ }
+
+ function testIsValidUrl() {
+ }
+
+ /**
+ * @dataProvider cookieDomains
+ */
+ function testValidateCookieDomain( $expected, $domain, $origin=null ) {
+ if ( $origin ) {
+ $ok = Cookie::validateCookieDomain( $domain, $origin );
+ $msg = "$domain against origin $origin";
+ } else {
+ $ok = Cookie::validateCookieDomain( $domain );
+ $msg = "$domain";
+ }
+ $this->assertEquals( $expected, $ok, $msg );
+ }
+
+ function cookieDomains() {
+ return array(
+ array( false, "org"),
+ array( false, ".org"),
+ array( true, "wikipedia.org"),
+ array( true, ".wikipedia.org"),
+ array( false, "co.uk" ),
+ array( false, ".co.uk" ),
+ array( false, "gov.uk" ),
+ array( false, ".gov.uk" ),
+ array( true, "supermarket.uk" ),
+ array( false, "uk" ),
+ array( false, ".uk" ),
+ array( false, "127.0.0." ),
+ array( false, "127." ),
+ array( false, "127.0.0.1." ),
+ array( true, "127.0.0.1" ),
+ array( false, "333.0.0.1" ),
+ array( true, "example.com" ),
+ array( false, "example.com." ),
+ array( true, ".example.com" ),
+
+ array( true, ".example.com", "www.example.com" ),
+ array( false, "example.com", "www.example.com" ),
+ array( true, "127.0.0.1", "127.0.0.1" ),
+ array( false, "127.0.0.1", "localhost" ),
+ );
+ }
+
+ function testSetCooke() {
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => "ac.th",
+ "path" => "/path/",
+ ) );
+ $this->assertFalse( $c->canServeDomain( "ac.th" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => "example.com",
+ "path" => "/path/",
+ ) );
+
+ $this->assertTrue( $c->canServeDomain( "example.com" ) );
+ $this->assertFalse( $c->canServeDomain( "www.example.com" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ ) );
+
+ $this->assertFalse( $c->canServeDomain( "www.example.net" ) );
+ $this->assertFalse( $c->canServeDomain( "example.com" ) );
+ $this->assertTrue( $c->canServeDomain( "www.example.com" ) );
+
+ $this->assertFalse( $c->canServePath( "/" ) );
+ $this->assertFalse( $c->canServePath( "/bogus/path/" ) );
+ $this->assertFalse( $c->canServePath( "/path" ) );
+ $this->assertTrue( $c->canServePath( "/path/" ) );
+
+ $this->assertTrue( $c->isUnExpired() );
+
+ $this->assertEquals( "", $c->serializeToHttpRequest( "/path/", "www.example.net" ) );
+ $this->assertEquals( "", $c->serializeToHttpRequest( "/", "www.example.com" ) );
+ $this->assertEquals( "name=value", $c->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => "www.example.com",
+ "path" => "/path/",
+ ) );
+ $this->assertFalse( $c->canServeDomain( "example.com" ) );
+ $this->assertFalse( $c->canServeDomain( "www.example.net" ) );
+ $this->assertTrue( $c->canServeDomain( "www.example.com" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ "expires" => "-1 day",
+ ) );
+ $this->assertFalse( $c->isUnExpired() );
+ $this->assertEquals( "", $c->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ "expires" => "+1 day",
+ ) );
+ $this->assertTrue( $c->isUnExpired() );
+ $this->assertEquals( "name=value", $c->serializeToHttpRequest( "/path/", "www.example.com" ) );
+ }
+
+ function testCookieJarSetCookie() {
+ $cj = new CookieJar;
+ $cj->setCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ ) );
+ $cj->setCookie( "name2", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/sub",
+ ) );
+ $cj->setCookie( "name3", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/",
+ ) );
+ $cj->setCookie( "name4", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ ) );
+ $cj->setCookie( "name5", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ "expires" => "-1 day",
+ ) );
+
+ $this->assertEquals( "name4=value", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+ $this->assertEquals( "name3=value", $cj->serializeToHttpRequest( "/", "www.example.com" ) );
+ $this->assertEquals( "name=value; name3=value", $cj->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $cj->setCookie( "name5", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ "expires" => "+1 day",
+ ) );
+ $this->assertEquals( "name4=value; name5=value", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+
+ $cj->setCookie( "name4", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ "expires" => "-1 day",
+ ) );
+ $this->assertEquals( "name5=value", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+ }
+
+ function testParseResponseHeader() {
+ $cj = new CookieJar;
+
+ $h[] = "Set-Cookie: name4=value; domain=.example.com; path=/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[0], "www.example.com" );
+ $this->assertEquals( "name4=value", $cj->serializeToHttpRequest( "/", "www.example.com" ) );
+
+ $h[] = "name4=value2; domain=.example.com; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[1], "www.example.com" );
+ $this->assertEquals( "", $cj->serializeToHttpRequest( "/", "www.example.com" ) );
+ $this->assertEquals( "name4=value2", $cj->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $h[] = "name5=value3; domain=.example.com; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[2], "www.example.com" );
+ $this->assertEquals( "name4=value2; name5=value3", $cj->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $h[] = "name6=value3; domain=.example.net; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[3], "www.example.com" );
+ $this->assertEquals( "", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+
+ $h[] = "name6=value0; domain=.example.net; path=/path/; expires=Mon, 09-Dec-1999 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[4], "www.example.net" );
+ $this->assertEquals( "", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+
+ $h[] = "name6=value4; domain=.example.net; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[5], "www.example.net" );
+ $this->assertEquals( "name6=value4", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+ }
+
+ function runCookieRequests() {
+ $r = MWHttpRequest::factory( "http://www.php.net/manual", array( 'followRedirects' => true ) );
+ $r->execute();
+
+ $jar = $r->getCookieJar();
+ $this->assertThat( $jar, $this->isInstanceOf( 'CookieJar' ) );
+
+ $serialized = $jar->serializeToHttpRequest( "/search?q=test", "www.php.net" );
+ $this->assertRegExp( '/\bCOUNTRY=[^=;]+/', $serialized );
+ $this->assertRegExp( '/\bLAST_LANG=[^=;]+/', $serialized );
+ $this->assertEquals( '', $jar->serializeToHttpRequest( "/search?q=test", "www.php.com" ) );
+ }
+
+ function testCookieRequestDefault() {
+ Http::$httpEngine = false;
+ $this->runCookieRequests();
+ }
+ function testCookieRequestPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = 'php';
+ $this->runCookieRequests();
+ }
+ function testCookieRequestCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = 'curl';
+ $this->runCookieRequests();
+ }
+
+ /**
+ * Test Http::isValidURI()
+ * @bug 27854 : Http::isValidURI is to lax
+ *@dataProvider provideURI */
+ function testIsValidUri( $expect, $URI, $message = '' ) {
+ $this->assertEquals(
+ $expect,
+ (bool) Http::isValidURI( $URI ),
+ $message
+ );
+ }
+
+ /**
+ * Feeds URI to test a long regular expression in Http::isValidURI
+ */
+ function provideURI() {
+ /** Format: 'boolean expectation', 'URI to test', 'Optional message' */
+ return array(
+ array( false, '¿non sens before!! http://a', 'Allow anything before URI' ),
+
+ # (http|https) - only two schemes allowed
+ array( true, 'http://www.example.org/' ),
+ array( true, 'https://www.example.org/' ),
+ array( true, 'http://www.example.org', 'URI without directory' ),
+ array( true, 'http://a', 'Short name' ),
+ array( true, 'http://étoile', 'Allow UTF-8 in hostname' ), # 'étoile' is french for 'star'
+ array( false, '\\host\directory', 'CIFS share' ),
+ array( false, 'gopher://host/dir', 'Reject gopher scheme' ),
+ array( false, 'telnet://host', 'Reject telnet scheme' ),
+
+ # :\/\/ - double slashes
+ array( false, 'http//example.org', 'Reject missing colon in protocol' ),
+ array( false, 'http:/example.org', 'Reject missing slash in protocol' ),
+ array( false, 'http:example.org', 'Must have two slashes' ),
+ # Following fail since hostname can be made of anything
+ array( false, 'http:///example.org', 'Must have exactly two slashes, not three' ),
+
+ # (\w+:{0,1}\w*@)? - optional user:pass
+ array( true, 'http://user@host', 'Username provided' ),
+ array( true, 'http://user:@host', 'Username provided, no password' ),
+ array( true, 'http://user:pass@host', 'Username and password provided' ),
+
+ # (\S+) - host part is made of anything not whitespaces
+ array( false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ),
+ array( false, 'http://exam:ple.org/', 'hostname can not use colons!' ),
+
+ # (:[0-9]+)? - port number
+ array( true, 'http://example.org:80/' ),
+ array( true, 'https://example.org:80/' ),
+ array( true, 'http://example.org:443/' ),
+ array( true, 'https://example.org:443/' ),
+
+ # Part after the hostname is / or / with something else
+ array( true, 'http://example/#' ),
+ array( true, 'http://example/!' ),
+ array( true, 'http://example/:' ),
+ array( true, 'http://example/.' ),
+ array( true, 'http://example/?' ),
+ array( true, 'http://example/+' ),
+ array( true, 'http://example/=' ),
+ array( true, 'http://example/&' ),
+ array( true, 'http://example/%' ),
+ array( true, 'http://example/@' ),
+ array( true, 'http://example/-' ),
+ array( true, 'http://example//' ),
+ array( true, 'http://example/&' ),
+
+ # Fragment
+ array( true, 'http://exam#ple.org', ), # This one is valid, really!
+ array( true, 'http://example.org:80#anchor' ),
+ array( true, 'http://example.org/?id#anchor' ),
+ array( true, 'http://example.org/?#anchor' ),
+
+ array( false, 'http://a ¿non !!sens after', 'Allow anything after URI' ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/IPTest.php b/tests/phpunit/includes/IPTest.php
new file mode 100644
index 00000000..c77dd852
--- /dev/null
+++ b/tests/phpunit/includes/IPTest.php
@@ -0,0 +1,508 @@
+<?php
+/*
+ * Tests for IP validity functions. Ported from /t/inc/IP.t by avar.
+ */
+
+class IPTest extends MediaWikiTestCase {
+ /**
+ * not sure it should be tested with boolean false. hashar 20100924
+ * @covers IP::isIPAddress
+ */
+ public function testisIPAddress() {
+ $this->assertFalse( IP::isIPAddress( false ), 'Boolean false is not an IP' );
+ $this->assertFalse( IP::isIPAddress( true ), 'Boolean true is not an IP' );
+ $this->assertFalse( IP::isIPAddress( "" ), 'Empty string is not an IP' );
+ $this->assertFalse( IP::isIPAddress( 'abc' ), 'Garbage IP string' );
+ $this->assertFalse( IP::isIPAddress( ':' ), 'Single ":" is not an IP' );
+ $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::1'), 'IPv6 with a double :: occurence' );
+ $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::'), 'IPv6 with a double :: occurence, last at end' );
+ $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1'), 'IPv6 with a double :: occurence, firt at beginning' );
+ $this->assertFalse( IP::isIPAddress( '124.24.52' ), 'IPv4 not enough quads' );
+ $this->assertFalse( IP::isIPAddress( '24.324.52.13' ), 'IPv4 out of range' );
+ $this->assertFalse( IP::isIPAddress( '.24.52.13' ), 'IPv4 starts with period' );
+ $this->assertFalse( IP::isIPAddress( 'fc:100:300' ), 'IPv6 with only 3 words' );
+
+ $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' );
+ $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' );
+ $this->assertTrue( IP::isIPAddress( '74.24.52.13/20', 'IPv4 range' ) );
+ $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' );
+ $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' );
+
+ $validIPs = array( 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac',
+ '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' );
+ foreach ( $validIPs as $ip ) {
+ $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" );
+ }
+ }
+
+ /**
+ * @covers IP::isIPv6
+ */
+ public function testisIPv6() {
+ $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' );
+ $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
+ $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' );
+ $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' );
+
+ $this->assertTrue( IP::isIPv6( 'fc:100::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac::' ) );
+
+ $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' );
+ $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ), 'IPv6 with 9 words ending with "::"' );
+
+ $this->assertFalse( IP::isIPv6( ':::' ) );
+ $this->assertFalse( IP::isIPv6( '::0:' ), 'IPv6 ending in a lone ":"' );
+
+ $this->assertTrue( IP::isIPv6( '::' ), 'IPv6 zero address' );
+ $this->assertTrue( IP::isIPv6( '::0' ) );
+ $this->assertTrue( IP::isIPv6( '::fc' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a:d' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e:ac' ) );
+
+ $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
+ $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
+
+ $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' );
+ $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' );
+ $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' );
+
+ $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a:d', 'IPv6 with "::" and 4 words' ) );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
+ $this->assertTrue( IP::isIPv6( '2001::df'), 'IPv6 with "::" and 2 words' );
+ $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df'), 'IPv6 with "::" and 5 words' );
+ $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2'), 'IPv6 with "::" and 6 words' );
+
+ $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
+ $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
+
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) );
+ }
+
+ /**
+ * @covers IP::isIPv4
+ */
+ public function testisIPv4() {
+ $this->assertFalse( IP::isIPv4( false ), 'Boolean false is not an IP' );
+ $this->assertFalse( IP::isIPv4( true ), 'Boolean true is not an IP' );
+ $this->assertFalse( IP::isIPv4( "" ), 'Empty string is not an IP' );
+ $this->assertFalse( IP::isIPv4( 'abc' ) );
+ $this->assertFalse( IP::isIPv4( ':' ) );
+ $this->assertFalse( IP::isIPv4( '124.24.52' ), 'IPv4 not enough quads' );
+ $this->assertFalse( IP::isIPv4( '24.324.52.13' ), 'IPv4 out of range' );
+ $this->assertFalse( IP::isIPv4( '.24.52.13' ), 'IPv4 starts with period' );
+
+ $this->assertTrue( IP::isIPv4( '124.24.52.13' ) );
+ $this->assertTrue( IP::isIPv4( '1.24.52.13' ) );
+ $this->assertTrue( IP::isIPv4( '74.24.52.13/20', 'IPv4 range' ) );
+ }
+
+ /**
+ * @covers IP::isValid
+ */
+ public function testValidIPs() {
+ foreach ( range( 0, 255 ) as $i ) {
+ $a = sprintf( "%03d", $i );
+ $b = sprintf( "%02d", $i );
+ $c = sprintf( "%01d", $i );
+ foreach ( array_unique( array( $a, $b, $c ) ) as $f ) {
+ $ip = "$f.$f.$f.$f";
+ $this->assertTrue( IP::isValid( $ip ) , "$ip is a valid IPv4 address" );
+ }
+ }
+ foreach ( range( 0x0, 0xFFFF, 0xF ) as $i ) {
+ $a = sprintf( "%04x", $i );
+ $b = sprintf( "%03x", $i );
+ $c = sprintf( "%02x", $i );
+ foreach ( array_unique( array( $a, $b, $c ) ) as $f ) {
+ $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
+ $this->assertTrue( IP::isValid( $ip ) , "$ip is a valid IPv6 address" );
+ }
+ }
+ // test with some abbreviations
+ $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' );
+ $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
+ $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' );
+ $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' );
+
+ $this->assertTrue( IP::isValid( 'fc:100::' ) );
+ $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) );
+ $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) );
+
+ $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' );
+ $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
+ $this->assertTrue( IP::isValid( '2001::df'), 'IPv6 with "::" and 2 words' );
+ $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df'), 'IPv6 with "::" and 5 words' );
+ $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2'), 'IPv6 with "::" and 6 words' );
+ $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
+ $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
+
+ $this->assertFalse( IP::isValid( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' );
+ $this->assertFalse( IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ), 'IPv6 with 9 words ending with "::"' );
+ }
+
+ /**
+ * @covers IP::isValid
+ */
+ public function testInvalidIPs() {
+ // Out of range...
+ foreach ( range( 256, 999 ) as $i ) {
+ $a = sprintf( "%03d", $i );
+ $b = sprintf( "%02d", $i );
+ $c = sprintf( "%01d", $i );
+ foreach ( array_unique( array( $a, $b, $c ) ) as $f ) {
+ $ip = "$f.$f.$f.$f";
+ $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" );
+ }
+ }
+ foreach ( range( 'g', 'z' ) as $i ) {
+ $a = sprintf( "%04s", $i );
+ $b = sprintf( "%03s", $i );
+ $c = sprintf( "%02s", $i );
+ foreach ( array_unique( array( $a, $b, $c ) ) as $f ) {
+ $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
+ $this->assertFalse( IP::isValid( $ip ) , "$ip is not a valid IPv6 address" );
+ }
+ }
+ // Have CIDR
+ $ipCIDRs = array(
+ '212.35.31.121/32',
+ '212.35.31.121/18',
+ '212.35.31.121/24',
+ '::ff:d:321:5/96',
+ 'ff::d3:321:5/116',
+ 'c:ff:12:1:ea:d:321:5/120',
+ );
+ foreach ( $ipCIDRs as $i ) {
+ $this->assertFalse( IP::isValid( $i ),
+ "$i is an invalid IP address because it is a block" );
+ }
+ // Incomplete/garbage
+ $invalid = array(
+ 'www.xn--var-xla.net',
+ '216.17.184.G',
+ '216.17.184.1.',
+ '216.17.184',
+ '216.17.184.',
+ '256.17.184.1'
+ );
+ foreach ( $invalid as $i ) {
+ $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" );
+ }
+ }
+
+ /**
+ * @covers IP::isValidBlock
+ */
+ public function testValidBlocks() {
+ $valid = array(
+ '116.17.184.5/32',
+ '0.17.184.5/30',
+ '16.17.184.1/24',
+ '30.242.52.14/1',
+ '10.232.52.13/8',
+ '30.242.52.14/0',
+ '::e:f:2001/96',
+ '::c:f:2001/128',
+ '::10:f:2001/70',
+ '::fe:f:2001/1',
+ '::6d:f:2001/8',
+ '::fe:f:2001/0',
+ );
+ foreach ( $valid as $i ) {
+ $this->assertTrue( IP::isValidBlock( $i ), "$i is a valid IP block" );
+ }
+ }
+
+ /**
+ * @covers IP::isValidBlock
+ */
+ public function testInvalidBlocks() {
+ $invalid = array(
+ '116.17.184.5/33',
+ '0.17.184.5/130',
+ '16.17.184.1/-1',
+ '10.232.52.13/*',
+ '7.232.52.13/ab',
+ '11.232.52.13/',
+ '::e:f:2001/129',
+ '::c:f:2001/228',
+ '::10:f:2001/-1',
+ '::6d:f:2001/*',
+ '::86:f:2001/ab',
+ '::23:f:2001/',
+ );
+ foreach ( $invalid as $i ) {
+ $this->assertFalse( IP::isValidBlock( $i ), "$i is not a valid IP block" );
+ }
+ }
+
+ /**
+ * Improve IP::sanitizeIP() code coverage
+ * @todo Most probably incomplete
+ */
+ public function testSanitizeIP() {
+ $this->assertNull( IP::sanitizeIP('') );
+ $this->assertNull( IP::sanitizeIP(' ') );
+ }
+
+ /**
+ * test wrapper around ip2long which might return -1 or false depending on PHP version
+ * @covers IP::toUnsigned
+ */
+ public function testip2longWrapper() {
+ // @todo FIXME: Add more tests ?
+ $this->assertEquals( pow(2,32) - 1, IP::toUnsigned( '255.255.255.255' ));
+ $i = 'IN.VA.LI.D';
+ $this->assertFalse( IP::toUnSigned( $i ) );
+ }
+
+ /**
+ * @covers IP::isPublic
+ */
+ public function testPrivateIPs() {
+ $private = array( 'fc00::3', 'fc00::ff', '::1', '10.0.0.1', '172.16.0.1', '192.168.0.1' );
+ foreach ( $private as $p ) {
+ $this->assertFalse( IP::isPublic( $p ), "$p is not a public IP address" );
+ }
+ $public = array( '2001:5c0:1000:a::133', 'fc::3' );
+ foreach ( $public as $p ) {
+ $this->assertTrue( IP::isPublic( $p ), "$p is a public IP address" );
+ }
+ }
+
+ // Private wrapper used to test CIDR Parsing.
+ private function assertFalseCIDR( $CIDR, $msg='' ) {
+ $ff = array( false, false );
+ $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg );
+ }
+
+ // Private wrapper to test network shifting using only dot notation
+ private function assertNet( $expected, $CIDR ) {
+ $parse = IP::parseCIDR( $CIDR );
+ $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" );
+ }
+
+ /**
+ * @covers IP::hexToQuad
+ */
+ public function testHexToQuad() {
+ $this->assertEquals( '0.0.0.1' , IP::hexToQuad( '00000001' ) );
+ $this->assertEquals( '255.0.0.0' , IP::hexToQuad( 'FF000000' ) );
+ $this->assertEquals( '255.255.255.255', IP::hexToQuad( 'FFFFFFFF' ) );
+ $this->assertEquals( '10.188.222.255' , IP::hexToQuad( '0ABCDEFF' ) );
+ // hex not left-padded...
+ $this->assertEquals( '0.0.0.0' , IP::hexToQuad( '0' ) );
+ $this->assertEquals( '0.0.0.1' , IP::hexToQuad( '1' ) );
+ $this->assertEquals( '0.0.0.255' , IP::hexToQuad( 'FF' ) );
+ $this->assertEquals( '0.0.255.0' , IP::hexToQuad( 'FF00' ) );
+ }
+
+ /**
+ * @covers IP::hexToOctet
+ */
+ public function testHexToOctet() {
+ $this->assertEquals( '0:0:0:0:0:0:0:1',
+ IP::hexToOctet( '00000000000000000000000000000001' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FF:3',
+ IP::hexToOctet( '00000000000000000000000000FF0003' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FF00:6',
+ IP::hexToOctet( '000000000000000000000000FF000006' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF',
+ IP::hexToOctet( '000000000000000000000000FCCFFAFF' ) );
+ $this->assertEquals( 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF',
+ IP::hexToOctet( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ) );
+ // hex not left-padded...
+ $this->assertEquals( '0:0:0:0:0:0:0:0' , IP::hexToOctet( '0' ) );
+ $this->assertEquals( '0:0:0:0:0:0:0:1' , IP::hexToOctet( '1' ) );
+ $this->assertEquals( '0:0:0:0:0:0:0:FF' , IP::hexToOctet( 'FF' ) );
+ $this->assertEquals( '0:0:0:0:0:0:0:FFD0' , IP::hexToOctet( 'FFD0' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FA00:0' , IP::hexToOctet( 'FA000000' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF', IP::hexToOctet( 'FCCFFAFF' ) );
+ }
+
+ /*
+ * IP::parseCIDR() returns an array containing a signed IP address
+ * representing the network mask and the bit mask.
+ * @covers IP::parseCIDR
+ */
+ function testCIDRParsing() {
+ $this->assertFalseCIDR( '192.0.2.0' , "missing mask" );
+ $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" );
+
+ // Verify if statement
+ $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" );
+ $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" );
+ $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" );
+ $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" );
+
+ // Check internal logic
+ # 0 mask always result in array(0,0)
+ $this->assertEquals( array( 0, 0 ), IP::parseCIDR('192.0.0.2/0') );
+ $this->assertEquals( array( 0, 0 ), IP::parseCIDR('0.0.0.0/0') );
+ $this->assertEquals( array( 0, 0 ), IP::parseCIDR('255.255.255.255/0') );
+
+ // @todo FIXME: Add more tests.
+
+ # This part test network shifting
+ $this->assertNet( '192.0.0.0' , '192.0.0.2/24' );
+ $this->assertNet( '192.168.5.0', '192.168.5.13/24');
+ $this->assertNet( '10.0.0.160' , '10.0.0.161/28' );
+ $this->assertNet( '10.0.0.0' , '10.0.0.3/28' );
+ $this->assertNet( '10.0.0.0' , '10.0.0.3/30' );
+ $this->assertNet( '10.0.0.4' , '10.0.0.4/30' );
+ $this->assertNet( '172.17.32.0', '172.17.35.48/21' );
+ $this->assertNet( '10.128.0.0' , '10.135.0.0/9' );
+ $this->assertNet( '134.0.0.0' , '134.0.5.1/8' );
+ }
+
+
+ /**
+ * @covers IP::canonicalize
+ */
+ public function testIPCanonicalizeOnValidIp() {
+ $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ),
+ 'Canonicalization of a valid IP returns it unchanged' );
+ }
+
+ /**
+ * @covers IP::canonicalize
+ */
+ public function testIPCanonicalizeMappedAddress() {
+ $this->assertEquals(
+ '192.0.2.152',
+ IP::canonicalize( '::ffff:192.0.2.152' )
+ );
+ $this->assertEquals(
+ '192.0.2.152',
+ IP::canonicalize( '::192.0.2.152' )
+ );
+ }
+
+ /**
+ * Issues there are most probably from IP::toHex() or IP::parseRange()
+ * @covers IP::isInRange
+ * @dataProvider provideIPsAndRanges
+ */
+ public function testIPIsInRange( $expected, $addr, $range, $message = '' ) {
+ $this->assertEquals(
+ $expected,
+ IP::isInRange( $addr, $range ),
+ $message
+ );
+ }
+
+ /** Provider for testIPIsInRange() */
+ function provideIPsAndRanges() {
+ # Format: (expected boolean, address, range, optional message)
+ return array(
+ # IPv4
+ array( true , '192.0.2.0' , '192.0.2.0/24', 'Network address' ),
+ array( true , '192.0.2.77' , '192.0.2.0/24', 'Simple address' ),
+ array( true , '192.0.2.255' , '192.0.2.0/24', 'Broadcast address' ),
+
+ array( false, '0.0.0.0' , '192.0.2.0/24' ),
+ array( false, '255.255.255' , '192.0.2.0/24' ),
+
+ # IPv6
+ array( false, '::1' , '2001:DB8::/32' ),
+ array( false, '::' , '2001:DB8::/32' ),
+ array( false, 'FE80::1', '2001:DB8::/32' ),
+
+ array( true , '2001:DB8::' , '2001:DB8::/32' ),
+ array( true , '2001:0DB8::' , '2001:DB8::/32' ),
+ array( true , '2001:DB8::1' , '2001:DB8::/32' ),
+ array( true , '2001:0DB8::1', '2001:DB8::/32' ),
+ array( true , '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF',
+ '2001:DB8::/32' ),
+
+ array( false, '2001:0DB8:F::', '2001:DB8::/96' ),
+ );
+ }
+
+ /**
+ * Test for IP::splitHostAndPort().
+ * @dataProvider provideSplitHostAndPort
+ */
+ function testSplitHostAndPort( $expected, $input, $description ) {
+ $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description );
+ }
+
+ /**
+ * Provider for IP::splitHostAndPort()
+ */
+ function provideSplitHostAndPort() {
+ return array(
+ array( false, '[', 'Unclosed square bracket' ),
+ array( false, '[::', 'Unclosed square bracket 2' ),
+ array( array( '::', false ), '::', 'Bare IPv6 0' ),
+ array( array( '::1', false ), '::1', 'Bare IPv6 1' ),
+ array( array( '::', false ), '[::]', 'Bracketed IPv6 0' ),
+ array( array( '::1', false ), '[::1]', 'Bracketed IPv6 1' ),
+ array( array( '::1', 80 ), '[::1]:80', 'Bracketed IPv6 with port' ),
+ array( false, '::x', 'Double colon but no IPv6' ),
+ array( array( 'x', 80 ), 'x:80', 'Hostname and port' ),
+ array( false, 'x:x', 'Hostname and invalid port' ),
+ array( array( 'x', false ), 'x', 'Plain hostname' )
+ );
+ }
+
+ /**
+ * Test for IP::combineHostAndPort()
+ * @dataProvider provideCombineHostAndPort
+ */
+ function testCombineHostAndPort( $expected, $input, $description ) {
+ list( $host, $port, $defaultPort ) = $input;
+ $this->assertEquals(
+ $expected,
+ IP::combineHostAndPort( $host, $port, $defaultPort ),
+ $description );
+ }
+
+ /**
+ * Provider for IP::combineHostAndPort()
+ */
+ function provideCombineHostAndPort() {
+ return array(
+ array( '[::1]', array( '::1', 2, 2 ), 'IPv6 default port' ),
+ array( '[::1]:2', array( '::1', 2, 3 ), 'IPv6 non-default port' ),
+ array( 'x', array( 'x', 2, 2 ), 'Normal default port' ),
+ array( 'x:2', array( 'x', 2, 3 ), 'Normal non-default port' ),
+ );
+ }
+
+ /**
+ * Test for IP::sanitizeRange()
+ * @dataProvider provideIPCIDRs
+ */
+ function testSanitizeRange( $input, $expected, $description ) {
+ $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description );
+ }
+
+ /**
+ * Provider for IP::testSanitizeRange()
+ */
+ function provideIPCIDRs() {
+ return array(
+ array( '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ),
+ array( '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ),
+ array( '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ),
+ array( '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ),
+ array( '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ),
+ array( '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ),
+ array( '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ),
+ array( '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/ImageFunctionsTest.php b/tests/phpunit/includes/ImageFunctionsTest.php
new file mode 100644
index 00000000..cb7e67f3
--- /dev/null
+++ b/tests/phpunit/includes/ImageFunctionsTest.php
@@ -0,0 +1,48 @@
+<?php
+
+class ImageFunctionsTest extends MediaWikiTestCase {
+ function testFitBoxWidth() {
+ $vals = array(
+ array(
+ 'width' => 50,
+ 'height' => 50,
+ 'tests' => array(
+ 50 => 50,
+ 17 => 17,
+ 18 => 18 ) ),
+ array(
+ 'width' => 366,
+ 'height' => 300,
+ 'tests' => array(
+ 50 => 61,
+ 17 => 21,
+ 18 => 22 ) ),
+ array(
+ 'width' => 300,
+ 'height' => 366,
+ 'tests' => array(
+ 50 => 41,
+ 17 => 14,
+ 18 => 15 ) ),
+ array(
+ 'width' => 100,
+ 'height' => 400,
+ 'tests' => array(
+ 50 => 12,
+ 17 => 4,
+ 18 => 4 ) ) );
+ foreach ( $vals as $row ) {
+ extract( $row );
+ foreach ( $tests as $max => $expected ) {
+ $y = round( $expected * $height / $width );
+ $result = wfFitBoxWidth( $width, $height, $max );
+ $y2 = round( $result * $height / $width );
+ $this->assertEquals( $expected,
+ $result,
+ "($width, $height, $max) wanted: {$expected}x$y, got: {$result}x$y2" );
+ }
+ }
+ }
+}
+
+
diff --git a/tests/phpunit/includes/JsonTest.php b/tests/phpunit/includes/JsonTest.php
new file mode 100644
index 00000000..75dd18d5
--- /dev/null
+++ b/tests/phpunit/includes/JsonTest.php
@@ -0,0 +1,33 @@
+<?php
+
+class JsonTest extends MediaWikiTestCase {
+
+ function testPhpBug46944Test() {
+
+ $this->assertNotEquals(
+ '\ud840\udc00',
+ strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ),
+ 'Test encoding an broken json_encode character (U+20000)'
+ );
+
+
+ }
+
+ function testDecodeVarTypes() {
+
+ $this->assertInternalType(
+ 'object',
+ FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ),
+ 'Default to object'
+ );
+
+ $this->assertInternalType(
+ 'array',
+ FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ),
+ 'Optional array'
+ );
+
+ }
+
+}
+
diff --git a/tests/phpunit/includes/LanguageConverterTest.php b/tests/phpunit/includes/LanguageConverterTest.php
new file mode 100644
index 00000000..baf28b07
--- /dev/null
+++ b/tests/phpunit/includes/LanguageConverterTest.php
@@ -0,0 +1,130 @@
+<?php
+
+class LanguageConverterTest extends MediaWikiLangTestCase {
+ protected $lang = null;
+ protected $lc = null;
+
+ function setUp() {
+ parent::setUp();
+ global $wgMemc, $wgRequest, $wgUser, $wgContLang;
+
+ $wgUser = new User;
+ $wgRequest = new FauxRequest( array() );
+ $wgMemc = new EmptyBagOStuff;
+ $wgContLang = Language::factory( 'tg' );
+ $this->lang = new LanguageToTest();
+ $this->lc = new TestConverter( $this->lang, 'tg',
+ array( 'tg', 'tg-latn' ) );
+ }
+
+ function tearDown() {
+ global $wgMemc;
+ unset( $wgMemc );
+ unset( $this->lc );
+ unset( $this->lang );
+ parent::tearDown();
+ }
+
+ function testGetPreferredVariantDefaults() {
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaders() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaderWeight() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg;q=1' );
+
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaderWeight2() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg-latn;q=1' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaderMulti() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'en, tg-latn;q=1' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantUserOption() {
+ global $wgUser;
+
+ $wgUser = new User;
+ $wgUser->load(); // from 'defaults'
+ $wgUser->mId = 1;
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaderUserVsUrl() {
+ global $wgRequest, $wgUser, $wgContLang;
+
+ $wgContLang = Language::factory( 'tg-latn' );
+ $wgRequest->setVal( 'variant', 'tg' );
+ $wgUser = User::newFromId( "admin" );
+ $wgUser->setId( 1 );
+ $wgUser->mFrom = 'defaults';
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant', 'tg-latn' ); // The user's data is ignored
+ // because the variant is set in the URL.
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+
+ function testGetPreferredVariantDefaultLanguageVariant() {
+ global $wgDefaultLanguageVariant;
+
+ $wgDefaultLanguageVariant = 'tg-latn';
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantDefaultLanguageVsUrlVariant() {
+ global $wgDefaultLanguageVariant, $wgRequest, $wgContLang;
+
+ $wgContLang = Language::factory( 'tg-latn' );
+ $wgDefaultLanguageVariant = 'tg';
+ $wgRequest->setVal( 'variant', null );
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+}
+
+/**
+ * Test converter (from Tajiki to latin orthography)
+ */
+class TestConverter extends LanguageConverter {
+ private $table = array(
+ 'б' => 'b',
+ 'в' => 'v',
+ 'г' => 'g',
+ );
+
+ function loadDefaultTables() {
+ $this->mTables = array(
+ 'tg-latn' => new ReplacementArray( $this->table ),
+ 'tg' => new ReplacementArray()
+ );
+ }
+
+}
+
+class LanguageToTest extends Language {
+ function __construct() {
+ parent::__construct();
+ $variants = array( 'tg', 'tg-latn' );
+ $this->mConverter = new TestConverter( $this, 'tg', $variants );
+ }
+}
diff --git a/tests/phpunit/includes/LicensesTest.php b/tests/phpunit/includes/LicensesTest.php
new file mode 100644
index 00000000..e467f3cd
--- /dev/null
+++ b/tests/phpunit/includes/LicensesTest.php
@@ -0,0 +1,22 @@
+<?php
+
+class LicensesTest extends MediaWikiTestCase {
+
+ function testLicenses() {
+ $str = "
+* Free licenses:
+** GFDL|Debian disagrees
+";
+
+ $lc = new Licenses( array(
+ 'fieldname' => 'FooField',
+ 'type' => 'select',
+ 'section' => 'description',
+ 'id' => 'wpLicense',
+ 'label' => 'A label text', # Note can't test label-message because $wgOut is not defined
+ 'name' => 'AnotherName',
+ 'licenses' => $str,
+ ) );
+ $this->assertThat( $lc, $this->isInstanceOf( 'Licenses' ) );
+ }
+}
diff --git a/tests/phpunit/includes/LocalFileTest.php b/tests/phpunit/includes/LocalFileTest.php
new file mode 100644
index 00000000..e08d4d7e
--- /dev/null
+++ b/tests/phpunit/includes/LocalFileTest.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * These tests should work regardless of $wgCapitalLinks
+ * @group Database
+ */
+
+class LocalFileTest extends MediaWikiTestCase {
+ function setUp() {
+ global $wgCapitalLinks;
+
+ $wgCapitalLinks = true;
+ $info = array(
+ 'name' => 'test',
+ 'directory' => '/testdir',
+ 'url' => '/testurl',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ );
+ $this->repo_hl0 = new LocalRepo( array( 'hashLevels' => 0 ) + $info );
+ $this->repo_hl2 = new LocalRepo( array( 'hashLevels' => 2 ) + $info );
+ $this->repo_lc = new LocalRepo( array( 'initialCapital' => false ) + $info );
+ $this->file_hl0 = $this->repo_hl0->newFile( 'test!' );
+ $this->file_hl2 = $this->repo_hl2->newFile( 'test!' );
+ $this->file_lc = $this->repo_lc->newFile( 'test!' );
+ }
+
+ function testGetHashPath() {
+ $this->assertEquals( '', $this->file_hl0->getHashPath() );
+ $this->assertEquals( 'a/a2/', $this->file_hl2->getHashPath() );
+ $this->assertEquals( 'c/c4/', $this->file_lc->getHashPath() );
+ }
+
+ function testGetRel() {
+ $this->assertEquals( 'Test!', $this->file_hl0->getRel() );
+ $this->assertEquals( 'a/a2/Test!', $this->file_hl2->getRel() );
+ $this->assertEquals( 'c/c4/test!', $this->file_lc->getRel() );
+ }
+
+ function testGetUrlRel() {
+ $this->assertEquals( 'Test%21', $this->file_hl0->getUrlRel() );
+ $this->assertEquals( 'a/a2/Test%21', $this->file_hl2->getUrlRel() );
+ $this->assertEquals( 'c/c4/test%21', $this->file_lc->getUrlRel() );
+ }
+
+ function testGetArchivePath() {
+ $this->assertEquals( '/testdir/archive', $this->file_hl0->getArchivePath() );
+ $this->assertEquals( '/testdir/archive/a/a2', $this->file_hl2->getArchivePath() );
+ $this->assertEquals( '/testdir/archive/!', $this->file_hl0->getArchivePath( '!' ) );
+ $this->assertEquals( '/testdir/archive/a/a2/!', $this->file_hl2->getArchivePath( '!' ) );
+ }
+
+ function testGetThumbPath() {
+ $this->assertEquals( '/testdir/thumb/Test!', $this->file_hl0->getThumbPath() );
+ $this->assertEquals( '/testdir/thumb/a/a2/Test!', $this->file_hl2->getThumbPath() );
+ $this->assertEquals( '/testdir/thumb/Test!/x', $this->file_hl0->getThumbPath( 'x' ) );
+ $this->assertEquals( '/testdir/thumb/a/a2/Test!/x', $this->file_hl2->getThumbPath( 'x' ) );
+ }
+
+ function testGetArchiveUrl() {
+ $this->assertEquals( '/testurl/archive', $this->file_hl0->getArchiveUrl() );
+ $this->assertEquals( '/testurl/archive/a/a2', $this->file_hl2->getArchiveUrl() );
+ $this->assertEquals( '/testurl/archive/%21', $this->file_hl0->getArchiveUrl( '!' ) );
+ $this->assertEquals( '/testurl/archive/a/a2/%21', $this->file_hl2->getArchiveUrl( '!' ) );
+ }
+
+ function testGetThumbUrl() {
+ $this->assertEquals( '/testurl/thumb/Test%21', $this->file_hl0->getThumbUrl() );
+ $this->assertEquals( '/testurl/thumb/a/a2/Test%21', $this->file_hl2->getThumbUrl() );
+ $this->assertEquals( '/testurl/thumb/Test%21/x', $this->file_hl0->getThumbUrl( 'x' ) );
+ $this->assertEquals( '/testurl/thumb/a/a2/Test%21/x', $this->file_hl2->getThumbUrl( 'x' ) );
+ }
+
+ function testGetArchiveVirtualUrl() {
+ $this->assertEquals( 'mwrepo://test/public/archive', $this->file_hl0->getArchiveVirtualUrl() );
+ $this->assertEquals( 'mwrepo://test/public/archive/a/a2', $this->file_hl2->getArchiveVirtualUrl() );
+ $this->assertEquals( 'mwrepo://test/public/archive/%21', $this->file_hl0->getArchiveVirtualUrl( '!' ) );
+ $this->assertEquals( 'mwrepo://test/public/archive/a/a2/%21', $this->file_hl2->getArchiveVirtualUrl( '!' ) );
+ }
+
+ function testGetThumbVirtualUrl() {
+ $this->assertEquals( 'mwrepo://test/thumb/Test%21', $this->file_hl0->getThumbVirtualUrl() );
+ $this->assertEquals( 'mwrepo://test/thumb/a/a2/Test%21', $this->file_hl2->getThumbVirtualUrl() );
+ $this->assertEquals( 'mwrepo://test/thumb/Test%21/%21', $this->file_hl0->getThumbVirtualUrl( '!' ) );
+ $this->assertEquals( 'mwrepo://test/thumb/a/a2/Test%21/%21', $this->file_hl2->getThumbVirtualUrl( '!' ) );
+ }
+
+ function testGetUrl() {
+ $this->assertEquals( '/testurl/Test%21', $this->file_hl0->getUrl() );
+ $this->assertEquals( '/testurl/a/a2/Test%21', $this->file_hl2->getUrl() );
+ }
+
+ function testWfLocalFile() {
+ $file = wfLocalFile( "File:Some_file_that_probably_doesn't exist.png" );
+ $this->assertThat( $file, $this->isInstanceOf( 'LocalFile' ), 'wfLocalFile() returns LocalFile for valid Titles' );
+ }
+}
+
+
diff --git a/tests/phpunit/includes/MWFunctionTest.php b/tests/phpunit/includes/MWFunctionTest.php
new file mode 100644
index 00000000..ed5e7602
--- /dev/null
+++ b/tests/phpunit/includes/MWFunctionTest.php
@@ -0,0 +1,86 @@
+<?php
+
+class MWFunctionTest extends MediaWikiTestCase {
+
+ function testCallUserFuncWorkarounds() {
+
+ $this->assertEquals(
+ call_user_func( array( 'MWFunctionTest', 'someMethod' ) ),
+ MWFunction::call( 'MWFunctionTest::someMethod' )
+ );
+ $this->assertEquals(
+ call_user_func( array( 'MWFunctionTest', 'someMethod' ), 'foo', 'bar', 'baz' ),
+ MWFunction::call( 'MWFunctionTest::someMethod', 'foo', 'bar', 'baz' )
+ );
+
+
+
+ $this->assertEquals(
+ call_user_func_array( array( 'MWFunctionTest', 'someMethod' ), array() ),
+ MWFunction::callArray( 'MWFunctionTest::someMethod', array() )
+ );
+ $this->assertEquals(
+ call_user_func_array( array( 'MWFunctionTest', 'someMethod' ), array( 'foo', 'bar', 'baz' ) ),
+ MWFunction::callArray( 'MWFunctionTest::someMethod', array( 'foo', 'bar', 'baz' ) )
+ );
+
+ }
+
+ function testNewObjFunction() {
+
+ $arg1 = 'Foo';
+ $arg2 = 'Bar';
+ $arg3 = array( 'Baz' );
+ $arg4 = new ExampleObject;
+
+ $args = array( $arg1, $arg2, $arg3, $arg4 );
+
+ $newObject = new MWBlankClass( $arg1, $arg2, $arg3, $arg4 );
+
+ $this->assertEquals(
+ MWFunction::newObj( 'MWBlankClass', $args )->args,
+ $newObject->args
+ );
+
+ $this->assertEquals(
+ MWFunction::newObj( 'MWBlankClass', $args, true )->args,
+ $newObject->args,
+ 'Works even with PHP version < 5.1.3'
+ );
+
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ function testCallingParentFails() {
+
+ MWFunction::call( 'parent::foo' );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ function testCallingSelfFails() {
+
+ MWFunction::call( 'self::foo' );
+ }
+
+ public static function someMethod() {
+ return func_get_args();
+ }
+
+}
+
+class MWBlankClass {
+
+ public $args = array();
+
+ function __construct( $arg1, $arg2, $arg3, $arg4 ) {
+ $this->args = array( $arg1, $arg2, $arg3, $arg4 );
+ }
+
+}
+
+class ExampleObject {
+}
diff --git a/tests/phpunit/includes/MWNamespaceTest.php b/tests/phpunit/includes/MWNamespaceTest.php
new file mode 100644
index 00000000..462afc24
--- /dev/null
+++ b/tests/phpunit/includes/MWNamespaceTest.php
@@ -0,0 +1,460 @@
+<?php
+/**
+ * @author Ashar Voultoiz
+ * @copyright Copyright © 2011, Ashar Voultoiz
+ * @file
+ */
+
+/**
+ * Test class for MWNamespace.
+ * Generated by PHPUnit on 2011-02-20 at 21:01:55.
+ *
+ */
+class MWNamespaceTest extends MediaWikiTestCase {
+ /**
+ * Sets up the fixture, for example, opens a network connection.
+ * This method is called before a test is executed.
+ */
+ protected function setUp() {
+ }
+
+ /**
+ * Tears down the fixture, for example, closes a network connection.
+ * This method is called after a test is executed.
+ */
+ protected function tearDown() {
+ }
+
+
+#### START OF TESTS #########################################################
+
+ /**
+ * @todo Write more texts, handle $wgAllowImageMoving setting
+ */
+ public function testIsMovable() {
+ $this->assertFalse( MWNamespace::isMovable( NS_CATEGORY ) );
+ # @todo FIXME: Write more tests!!
+ }
+
+ /**
+ * Please make sure to change testIsTalk() if you change the assertions below
+ */
+ public function testIsMain() {
+ // Special namespaces
+ $this->assertTrue( MWNamespace::isMain( NS_MEDIA ) );
+ $this->assertTrue( MWNamespace::isMain( NS_SPECIAL ) );
+
+ // Subject pages
+ $this->assertTrue( MWNamespace::isMain( NS_MAIN ) );
+ $this->assertTrue( MWNamespace::isMain( NS_USER ) );
+ $this->assertTrue( MWNamespace::isMain( 100 ) ); # user defined
+
+ // Talk pages
+ $this->assertFalse( MWNamespace::isMain( NS_TALK ) );
+ $this->assertFalse( MWNamespace::isMain( NS_USER_TALK ) );
+ $this->assertFalse( MWNamespace::isMain( 101 ) ); # user defined
+ }
+
+ /**
+ * Reverse of testIsMain().
+ * Please update testIsMain() if you change assertions below
+ */
+ public function testIsTalk() {
+ // Special namespaces
+ $this->assertFalse( MWNamespace::isTalk( NS_MEDIA ) );
+ $this->assertFalse( MWNamespace::isTalk( NS_SPECIAL ) );
+
+ // Subject pages
+ $this->assertFalse( MWNamespace::isTalk( NS_MAIN ) );
+ $this->assertFalse( MWNamespace::isTalk( NS_USER ) );
+ $this->assertFalse( MWNamespace::isTalk( 100 ) ); # user defined
+
+ // Talk pages
+ $this->assertTrue( MWNamespace::isTalk( NS_TALK ) );
+ $this->assertTrue( MWNamespace::isTalk( NS_USER_TALK ) );
+ $this->assertTrue( MWNamespace::isTalk( 101 ) ); # user defined
+ }
+
+ /**
+ * Regular getTalk() calls
+ * Namespaces without a talk page (NS_MEDIA, NS_SPECIAL) are tested in
+ * the function testGetTalkExceptions()
+ */
+ public function testGetTalk() {
+ $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_MAIN ) );
+ }
+
+ /**
+ * Exceptions with getTalk()
+ * NS_MEDIA does not have talk pages. MediaWiki raise an exception for them.
+ * @expectedException MWException
+ */
+ public function testGetTalkExceptionsForNsMedia() {
+ $this->assertNull( MWNamespace::getTalk( NS_MEDIA ) );
+ }
+
+ /**
+ * Exceptions with getTalk()
+ * NS_SPECIAL does not have talk pages. MediaWiki raise an exception for them.
+ * @expectedException MWException
+ */
+ public function testGetTalkExceptionsForNsSpecial() {
+ $this->assertNull( MWNamespace::getTalk( NS_SPECIAL ) );
+ }
+
+ /**
+ * Regular getAssociated() calls
+ * Namespaces without an associated page (NS_MEDIA, NS_SPECIAL) are tested in
+ * the function testGetAssociatedExceptions()
+ */
+ public function testGetAssociated() {
+ $this->assertEquals( NS_TALK, MWNamespace::getAssociated( NS_MAIN ) );
+ $this->assertEquals( NS_MAIN, MWNamespace::getAssociated( NS_TALK ) );
+
+ }
+
+ ### Exceptions with getAssociated()
+ ### NS_MEDIA and NS_SPECIAL do not have talk pages. MediaWiki raises
+ ### an exception for them.
+ /**
+ * @expectedException MWException
+ */
+ public function testGetAssociatedExceptionsForNsMedia() {
+ $this->assertNull( MWNamespace::getAssociated( NS_MEDIA ) );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ public function testGetAssociatedExceptionsForNsSpecial() {
+ $this->assertNull( MWNamespace::getAssociated( NS_SPECIAL ) );
+ }
+
+ /**
+ */
+ public function testGetSubject() {
+ // Special namespaces are their own subjects
+ $this->assertEquals( NS_MEDIA, MWNamespace::getSubject( NS_MEDIA ) );
+ $this->assertEquals( NS_SPECIAL, MWNamespace::getSubject( NS_SPECIAL ) );
+
+ $this->assertEquals( NS_MAIN, MWNamespace::getSubject( NS_TALK ) );
+ $this->assertEquals( NS_USER, MWNamespace::getSubject( NS_USER_TALK ) );
+ }
+
+ /**
+ * @todo Implement testExists().
+ */
+ public function testExists() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+
+ /**
+ * @todo Implement testGetCanonicalNamespaces().
+ */
+ public function testGetCanonicalNamespaces() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+
+ /**
+ * @todo Implement testGetCanonicalName().
+ */
+ public function testGetCanonicalName() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+
+ /**
+ * @todo Implement testGetCanonicalIndex().
+ */
+ public function testGetCanonicalIndex() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+
+ /**
+ * @todo Implement testGetValidNamespaces().
+ */
+ public function testGetValidNamespaces() {
+ // Remove the following lines when you implement this test.
+ $this->markTestIncomplete(
+ 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.'
+ );
+ }
+
+ /**
+ */
+ public function testCanTalk() {
+ $this->assertFalse( MWNamespace::canTalk( NS_MEDIA ) );
+ $this->assertFalse( MWNamespace::canTalk( NS_SPECIAL ) );
+
+ $this->assertTrue( MWNamespace::canTalk( NS_MAIN ) );
+ $this->assertTrue( MWNamespace::canTalk( NS_TALK ) );
+ $this->assertTrue( MWNamespace::canTalk( NS_USER ) );
+ $this->assertTrue( MWNamespace::canTalk( NS_USER_TALK ) );
+
+ // User defined namespaces
+ $this->assertTrue( MWNamespace::canTalk( 100 ) );
+ $this->assertTrue( MWNamespace::canTalk( 101 ) );
+ }
+
+ /**
+ */
+ public function testIsContent() {
+ // NS_MAIN is a content namespace per DefaultSettings.php
+ // and per function definition.
+ $this->assertTrue( MWNamespace::isContent( NS_MAIN ) );
+
+ // Other namespaces which are not expected to be content
+ $this->assertFalse( MWNamespace::isContent( NS_MEDIA ) );
+ $this->assertFalse( MWNamespace::isContent( NS_SPECIAL ) );
+ $this->assertFalse( MWNamespace::isContent( NS_TALK ) );
+ $this->assertFalse( MWNamespace::isContent( NS_USER ) );
+ $this->assertFalse( MWNamespace::isContent( NS_CATEGORY ) );
+ // User defined namespace:
+ $this->assertFalse( MWNamespace::isContent( 100 ) );
+ }
+
+ /**
+ * Similar to testIsContent() but alters the $wgContentNamespaces
+ * global variable.
+ */
+ public function testIsContentWithAdditionsInWgContentNamespaces() {
+ // NS_MAIN is a content namespace per DefaultSettings.php
+ // and per function definition.
+ $this->assertTrue( MWNamespace::isContent( NS_MAIN ) );
+
+ // Tests that user defined namespace #252 is not content:
+ $this->assertFalse( MWNamespace::isContent( 252 ) );
+
+ # @todo FIXME: Is global saving really required for PHPUnit?
+ // Bless namespace # 252 as a content namespace
+ global $wgContentNamespaces;
+ $savedGlobal = $wgContentNamespaces;
+ $wgContentNamespaces[] = 252;
+ $this->assertTrue( MWNamespace::isContent( 252 ) );
+
+ // Makes sure NS_MAIN was not impacted
+ $this->assertTrue( MWNamespace::isContent( NS_MAIN ) );
+
+ // Restore global
+ $wgContentNamespaces = $savedGlobal;
+
+ // Verify namespaces after global restauration
+ $this->assertTrue( MWNamespace::isContent( NS_MAIN ) );
+ $this->assertFalse( MWNamespace::isContent( 252 ) );
+ }
+
+ public function testIsWatchable() {
+ // Specials namespaces are not watchable
+ $this->assertFalse( MWNamespace::isWatchable( NS_MEDIA ) );
+ $this->assertFalse( MWNamespace::isWatchable( NS_SPECIAL ) );
+
+ // Core defined namespaces are watchables
+ $this->assertTrue( MWNamespace::isWatchable( NS_MAIN ) );
+ $this->assertTrue( MWNamespace::isWatchable( NS_TALK ) );
+
+ // Additional, user defined namespaces are watchables
+ $this->assertTrue( MWNamespace::isWatchable( 100 ) );
+ $this->assertTrue( MWNamespace::isWatchable( 101 ) );
+ }
+
+ public function testHasSubpages() {
+ // Special namespaces:
+ $this->assertFalse( MWNamespace::hasSubpages( NS_MEDIA ) );
+ $this->assertFalse( MWNamespace::hasSubpages( NS_SPECIAL ) );
+
+ // namespaces without subpages
+ # save up global
+ global $wgNamespacesWithSubpages;
+ $saved = null;
+ if( array_key_exists( NS_MAIN, $wgNamespacesWithSubpages ) ) {
+ $saved = $wgNamespacesWithSubpages[NS_MAIN];
+ unset( $wgNamespacesWithSubpages[NS_MAIN] );
+ }
+
+ $this->assertFalse( MWNamespace::hasSubpages( NS_MAIN ) );
+
+ $wgNamespacesWithSubpages[NS_MAIN] = true;
+ $this->assertTrue( MWNamespace::hasSubpages( NS_MAIN ) );
+ $wgNamespacesWithSubpages[NS_MAIN] = false;
+ $this->assertFalse( MWNamespace::hasSubpages( NS_MAIN ) );
+
+ # restore global
+ if( $saved !== null ) {
+ $wgNamespacesWithSubpages[NS_MAIN] = $saved;
+ }
+
+ // Some namespaces with subpages
+ $this->assertTrue( MWNamespace::hasSubpages( NS_TALK ) );
+ $this->assertTrue( MWNamespace::hasSubpages( NS_USER ) );
+ $this->assertTrue( MWNamespace::hasSubpages( NS_USER_TALK ) );
+ }
+
+ /**
+ */
+ public function testGetContentNamespaces() {
+ $this->assertEquals(
+ array( NS_MAIN ),
+ MWNamespace::getcontentNamespaces(),
+ '$wgContentNamespaces is an array with only NS_MAIN by default'
+ );
+
+ global $wgContentNamespaces;
+
+ # test !is_array( $wgcontentNamespaces )
+ $wgContentNamespaces = '';
+ $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() );
+ $wgContentNamespaces = false;
+ $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() );
+ $wgContentNamespaces = null;
+ $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() );
+ $wgContentNamespaces = 5;
+ $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() );
+
+ # test $wgContentNamespaces === array()
+ $wgContentNamespaces = array();
+ $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() );
+
+ # test !in_array( NS_MAIN, $wgContentNamespaces )
+ $wgContentNamespaces = array( NS_USER, NS_CATEGORY );
+ $this->assertEquals(
+ array( NS_MAIN, NS_USER, NS_CATEGORY ),
+ MWNamespace::getcontentNamespaces(),
+ 'NS_MAIN is forced in wgContentNamespaces even if unwanted'
+ );
+
+ # test other cases, return $wgcontentNamespaces as is
+ $wgContentNamespaces = array( NS_MAIN );
+ $this->assertEquals(
+ array( NS_MAIN ),
+ MWNamespace::getcontentNamespaces()
+ );
+
+ $wgContentNamespaces = array( NS_MAIN, NS_USER, NS_CATEGORY );
+ $this->assertEquals(
+ array( NS_MAIN, NS_USER, NS_CATEGORY ),
+ MWNamespace::getcontentNamespaces()
+ );
+
+ }
+
+ /**
+ * Some namespaces are always capitalized per code definition
+ * in MWNamespace::$alwaysCapitalizedNamespaces
+ */
+ public function testIsCapitalizedHardcodedAssertions() {
+ // NS_MEDIA and NS_FILE are treated the same
+ $this->assertEquals(
+ MWNamespace::isCapitalized( NS_MEDIA ),
+ MWNamespace::isCapitalized( NS_FILE ),
+ 'NS_MEDIA and NS_FILE have same capitalization rendering'
+ );
+
+ // Boths are capitalized by default
+ $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIA ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_FILE ) );
+
+ // Always capitalized namespaces
+ // @see MWNamespace::$alwaysCapitalizedNamespaces
+ $this->assertTrue( MWNamespace::isCapitalized( NS_SPECIAL ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_USER ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIAWIKI ) );
+ }
+
+ /**
+ * Follows up for testIsCapitalizedHardcodedAssertions() but alter the
+ * global $wgCapitalLink setting to have extended coverage.
+ *
+ * MWNamespace::isCapitalized() rely on two global settings:
+ * $wgCapitalLinkOverrides = array(); by default
+ * $wgCapitalLinks = true; by default
+ * This function test $wgCapitalLinks
+ *
+ * Global setting correctness is tested against the NS_PROJECT and
+ * NS_PROJECT_TALK namespaces since they are not hardcoded nor specials
+ */
+ public function testIsCapitalizedWithWgCapitalLinks() {
+ global $wgCapitalLinks;
+ // Save the global to easily reset to MediaWiki default settings
+ $savedGlobal = $wgCapitalLinks;
+
+ $wgCapitalLinks = true;
+ $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT_TALK ) );
+
+ $wgCapitalLinks = false;
+ // hardcoded namespaces (see above function) are still capitalized:
+ $this->assertTrue( MWNamespace::isCapitalized( NS_SPECIAL ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_USER ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIAWIKI ) );
+ // setting is correctly applied
+ $this->assertFalse( MWNamespace::isCapitalized( NS_PROJECT ) );
+ $this->assertFalse( MWNamespace::isCapitalized( NS_PROJECT_TALK ) );
+
+ // reset global state:
+ $wgCapitalLinks = $savedGlobal;
+ }
+
+ /**
+ * Counter part for MWNamespace::testIsCapitalizedWithWgCapitalLinks() now
+ * testing the $wgCapitalLinkOverrides global.
+ *
+ * @todo split groups of assertions in autonomous testing functions
+ */
+ public function testIsCapitalizedWithWgCapitalLinkOverrides() {
+ global $wgCapitalLinkOverrides;
+ // Save the global to easily reset to MediaWiki default settings
+ $savedGlobal = $wgCapitalLinkOverrides;
+
+ // Test default settings
+ $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT_TALK ) );
+ // hardcoded namespaces (see above function) are capitalized:
+ $this->assertTrue( MWNamespace::isCapitalized( NS_SPECIAL ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_USER ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIAWIKI ) );
+
+ // Hardcoded namespaces remains capitalized
+ $wgCapitalLinkOverrides[NS_SPECIAL] = false;
+ $wgCapitalLinkOverrides[NS_USER] = false;
+ $wgCapitalLinkOverrides[NS_MEDIAWIKI] = false;
+ $this->assertTrue( MWNamespace::isCapitalized( NS_SPECIAL ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_USER ) );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIAWIKI ) );
+
+ $wgCapitalLinkOverrides = $savedGlobal;
+ $wgCapitalLinkOverrides[NS_PROJECT] = false;
+ $this->assertFalse( MWNamespace::isCapitalized( NS_PROJECT ) );
+ $wgCapitalLinkOverrides[NS_PROJECT] = true ;
+ $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT ) );
+ unset( $wgCapitalLinkOverrides[NS_PROJECT] );
+ $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT ) );
+
+ // reset global state:
+ $wgCapitalLinkOverrides = $savedGlobal;
+ }
+
+ public function testHasGenderDistinction() {
+ // Namespaces with gender distinctions
+ $this->assertTrue( MWNamespace::hasGenderDistinction( NS_USER ) );
+ $this->assertTrue( MWNamespace::hasGenderDistinction( NS_USER_TALK ) );
+
+ // Other ones, "genderless"
+ $this->assertFalse( MWNamespace::hasGenderDistinction( NS_MEDIA ) );
+ $this->assertFalse( MWNamespace::hasGenderDistinction( NS_SPECIAL ) );
+ $this->assertFalse( MWNamespace::hasGenderDistinction( NS_MAIN ) );
+ $this->assertFalse( MWNamespace::hasGenderDistinction( NS_TALK ) );
+
+ }
+}
+
diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php
new file mode 100644
index 00000000..45c02bbe
--- /dev/null
+++ b/tests/phpunit/includes/MessageTest.php
@@ -0,0 +1,62 @@
+<?php
+
+class MessageTest extends MediaWikiLangTestCase {
+
+ function testExists() {
+ $this->assertTrue( wfMessage( 'mainpage' )->exists() );
+ $this->assertTrue( wfMessage( 'mainpage' )->params( array() )->exists() );
+ $this->assertTrue( wfMessage( 'mainpage' )->rawParams( 'foo', 123 )->exists() );
+ $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->exists() );
+ $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->params( array() )->exists() );
+ $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->rawParams( 'foo', 123 )->exists() );
+ }
+
+ function testKey() {
+ $this->assertInstanceOf( 'Message', wfMessage( 'mainpage' ) );
+ $this->assertInstanceOf( 'Message', wfMessage( 'i-dont-exist-evar' ) );
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->text() );
+ $this->assertEquals( '&lt;i-dont-exist-evar&gt;', wfMessage( 'i-dont-exist-evar' )->text() );
+ }
+
+ function testInLanguage() {
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inLanguage( 'en' )->text() );
+ $this->assertEquals( 'Заглавная страница', wfMessage( 'mainpage' )->inLanguage( 'ru' )->text() );
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inLanguage( Language::factory( 'en' ) )->text() );
+ $this->assertEquals( 'Заглавная страница', wfMessage( 'mainpage' )->inLanguage( Language::factory( 'ru' ) )->text() );
+ }
+
+ function testMessageParams() {
+ $this->assertEquals( 'Return to $1.', wfMessage( 'returnto' )->text() );
+ $this->assertEquals( 'Return to $1.', wfMessage( 'returnto', array() )->text() );
+ $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', 'foo', 'bar' )->text() );
+ $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', array( 'foo', 'bar' ) )->text() );
+ }
+
+ function testMessageParamSubstitution() {
+ $this->assertEquals( '(Заглавная страница)', wfMessage( 'parentheses', 'Заглавная страница' )->plain() );
+ $this->assertEquals( '(Заглавная страница $1)', wfMessage( 'parentheses', 'Заглавная страница $1' )->plain() );
+ $this->assertEquals( '(Заглавная страница)', wfMessage( 'parentheses' )->rawParams( 'Заглавная страница' )->plain() );
+ $this->assertEquals( '(Заглавная страница $1)', wfMessage( 'parentheses' )->rawParams( 'Заглавная страница $1' )->plain() );
+ }
+
+ function testInContentLanguage() {
+ global $wgLang, $wgForceUIMsgAsContentMsg;
+ $oldLang = $wgLang;
+ $wgLang = Language::factory( 'fr' );
+
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inContentLanguage()->plain(), 'ForceUIMsg disabled' );
+ $wgForceUIMsgAsContentMsg['testInContentLanguage'] = 'mainpage';
+ $this->assertEquals( 'Accueil', wfMessage( 'mainpage' )->inContentLanguage()->plain(), 'ForceUIMsg enabled' );
+
+ /* Restore globals */
+ $wgLang = $oldLang;
+ unset( $wgForceUIMsgAsContentMsg['testInContentLanguage'] );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ function testInLanguageThrows() {
+ wfMessage( 'foo' )->inLanguage( 123 );
+ }
+}
diff --git a/tests/phpunit/includes/ParserOptionsTest.php b/tests/phpunit/includes/ParserOptionsTest.php
new file mode 100644
index 00000000..58c89146
--- /dev/null
+++ b/tests/phpunit/includes/ParserOptionsTest.php
@@ -0,0 +1,36 @@
+<?php
+
+class ParserOptionsTest extends MediaWikiTestCase {
+
+ private $popts;
+ private $pcache;
+
+ function setUp() {
+ ParserTest::setUp(); //reuse setup from parser tests
+ global $wgContLang, $wgUser, $wgLanguageCode;
+ $wgContLang = Language::factory( $wgLanguageCode );
+ $this->popts = new ParserOptions( $wgUser );
+ $this->pcache = ParserCache::singleton();
+ }
+
+ function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * ParserOptions::optionsHash was not giving consistent results when $wgUseDynamicDates was set
+ * @group Database
+ */
+ function testGetParserCacheKeyWithDynamicDates() {
+ global $wgUseDynamicDates;
+ $wgUseDynamicDates = true;
+
+ $title = Title::newFromText( "Some test article" );
+ $article = new Article( $title );
+
+ $pcacheKeyBefore = $this->pcache->getKey( $article, $this->popts );
+ $this->assertNotNull( $this->popts->getDateFormat() );
+ $pcacheKeyAfter = $this->pcache->getKey( $article, $this->popts );
+ $this->assertEquals( $pcacheKeyBefore, $pcacheKeyAfter );
+ }
+}
diff --git a/tests/phpunit/includes/Providers.php b/tests/phpunit/includes/Providers.php
new file mode 100644
index 00000000..02898673
--- /dev/null
+++ b/tests/phpunit/includes/Providers.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Generic providers for the MediaWiki PHPUnit test suite
+ *
+ * @author Ashar Voultoiz
+ * @copyright Copyright © 2011, Ashar Voultoiz
+ * @file
+ */
+
+/** */
+class MediaWikiProvide {
+
+ /* provide an array of numbers from 1 up to @param $num */
+ private static function createProviderUpTo( $num ) {
+ $ret = array();
+ for( $i=1; $i<=$num;$i++ ) {
+ $ret[] = array( $i );
+ }
+ return $ret;
+ }
+
+ /* array of months numbers (as an integer) */
+ public static function Months() {
+ return self::createProviderUpTo( 12 );
+ }
+
+ /* array of days numbers (as an integer) */
+ public static function Days() {
+ return self::createProviderUpTo( 31 );
+ }
+
+ public static function DaysMonths() {
+ $ret = array();
+
+ $months = self::Months();
+ $days = self::Days();
+ foreach( $months as $month) {
+ foreach( $days as $day ) {
+ $ret[] = array( $day[0], $month[0] );
+ }
+ }
+ return $ret;
+ }
+}
diff --git a/tests/phpunit/includes/ResourceLoaderTest.php b/tests/phpunit/includes/ResourceLoaderTest.php
new file mode 100644
index 00000000..30a69c5e
--- /dev/null
+++ b/tests/phpunit/includes/ResourceLoaderTest.php
@@ -0,0 +1,91 @@
+<?php
+
+class ResourceLoaderTest extends PHPUnit_Framework_TestCase {
+
+ protected static $resourceLoaderRegisterModulesHook;
+
+ /* Hook Methods */
+
+ /**
+ * ResourceLoaderRegisterModules hook
+ */
+ public static function resourceLoaderRegisterModules( &$resourceLoader ) {
+ self::$resourceLoaderRegisterModulesHook = true;
+ return true;
+ }
+
+ /* Provider Methods */
+ public function provideValidModules() {
+ return array(
+ array( 'TEST.validModule1', new ResourceLoaderTestModule() ),
+ );
+ }
+
+ /* Test Methods */
+
+ /**
+ * Ensures that the ResourceLoaderRegisterModules hook is called when a new ResourceLoader object is constructed
+ * @covers ResourceLoader::__construct
+ */
+ public function testCreatingNewResourceLoaderCallsRegistrationHook() {
+ self::$resourceLoaderRegisterModulesHook = false;
+ $resourceLoader = new ResourceLoader();
+ $this->assertTrue( self::$resourceLoaderRegisterModulesHook );
+ return $resourceLoader;
+ }
+
+ /**
+ * @dataProvider provideValidModules
+ * @depends testCreatingNewResourceLoaderCallsRegistrationHook
+ * @covers ResourceLoader::register
+ * @covers ResourceLoader::getModule
+ */
+ public function testRegisteredValidModulesAreAccessible(
+ $name, ResourceLoaderModule $module, ResourceLoader $resourceLoader
+ ) {
+ $resourceLoader->register( $name, $module );
+ $this->assertEquals( $module, $resourceLoader->getModule( $name ) );
+ }
+
+ /**
+ * @dataProvider providePackedModules
+ */
+ public function testMakePackedModulesString( $desc, $modules, $packed ) {
+ $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc );
+ }
+
+ /**
+ * @dataProvider providePackedModules
+ */
+ public function testexpandModuleNames( $desc, $modules, $packed ) {
+ $this->assertEquals( $modules, ResourceLoaderContext::expandModuleNames( $packed ), $desc );
+ }
+
+ public function providePackedModules() {
+ return array(
+ array(
+ 'Example from makePackedModulesString doc comment',
+ array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ),
+ 'foo.bar,baz|bar.baz,quux',
+ ),
+ array(
+ 'Example from expandModuleNames doc comment',
+ array( 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ),
+ 'jquery.foo,bar|jquery.ui.baz,quux',
+ ),
+ array(
+ 'Regression fixed in r88706 with dotless names',
+ array( 'foo', 'bar', 'baz' ),
+ 'foo,bar,baz',
+ )
+ );
+ }
+}
+
+/* Stubs */
+
+class ResourceLoaderTestModule extends ResourceLoaderModule { }
+
+/* Hooks */
+global $wgHooks;
+$wgHooks['ResourceLoaderRegisterModules'][] = 'ResourceLoaderTest::resourceLoaderRegisterModules';
diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php
new file mode 100644
index 00000000..d7654db9
--- /dev/null
+++ b/tests/phpunit/includes/RevisionTest.php
@@ -0,0 +1,125 @@
+<?php
+
+class RevisionTest extends MediaWikiTestCase {
+ var $saveGlobals = array();
+
+ function setUp() {
+ global $wgContLang;
+ $wgContLang = Language::factory( 'en' );
+ $globalSet = array(
+ 'wgLegacyEncoding' => false,
+ 'wgCompressRevisions' => false,
+ );
+ foreach ( $globalSet as $var => $data ) {
+ $this->saveGlobals[$var] = $GLOBALS[$var];
+ $GLOBALS[$var] = $data;
+ }
+ }
+
+ function tearDown() {
+ foreach ( $this->saveGlobals as $var => $data ) {
+ $GLOBALS[$var] = $data;
+ }
+ }
+
+ function testGetRevisionText() {
+ $row = new stdClass;
+ $row->old_flags = '';
+ $row->old_text = 'This is a bunch of revision text.';
+ $this->assertEquals(
+ 'This is a bunch of revision text.',
+ Revision::getRevisionText( $row ) );
+ }
+
+ function testGetRevisionTextGzip() {
+ if ( !function_exists( 'gzdeflate' ) ) {
+ $this->markTestSkipped( 'Gzip compression is not enabled (requires zlib).' );
+ } else {
+ $row = new stdClass;
+ $row->old_flags = 'gzip';
+ $row->old_text = gzdeflate( 'This is a bunch of revision text.' );
+ $this->assertEquals(
+ 'This is a bunch of revision text.',
+ Revision::getRevisionText( $row ) );
+ }
+ }
+
+ function testGetRevisionTextUtf8Native() {
+ $row = new stdClass;
+ $row->old_flags = 'utf-8';
+ $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
+ $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1';
+ $this->assertEquals(
+ "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ) );
+ }
+
+ function testGetRevisionTextUtf8Legacy() {
+ $row = new stdClass;
+ $row->old_flags = '';
+ $row->old_text = "Wiki est l'\xe9cole superieur !";
+ $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1';
+ $this->assertEquals(
+ "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ) );
+ }
+
+ function testGetRevisionTextUtf8NativeGzip() {
+ if ( !function_exists( 'gzdeflate' ) ) {
+ $this->markTestSkipped( 'Gzip compression is not enabled (requires zlib).' );
+ } else {
+ $row = new stdClass;
+ $row->old_flags = 'gzip,utf-8';
+ $row->old_text = gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" );
+ $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1';
+ $this->assertEquals(
+ "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ) );
+ }
+ }
+
+ function testGetRevisionTextUtf8LegacyGzip() {
+ if ( !function_exists( 'gzdeflate' ) ) {
+ $this->markTestSkipped( 'Gzip compression is not enabled (requires zlib).' );
+ } else {
+ $row = new stdClass;
+ $row->old_flags = 'gzip';
+ $row->old_text = gzdeflate( "Wiki est l'\xe9cole superieur !" );
+ $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1';
+ $this->assertEquals(
+ "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ) );
+ }
+ }
+
+ function testCompressRevisionTextUtf8() {
+ $row = new stdClass;
+ $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
+ $row->old_flags = Revision::compressRevisionText( $row->old_text );
+ $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
+ "Flags should contain 'utf-8'" );
+ $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ),
+ "Flags should not contain 'gzip'" );
+ $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+ $row->old_text, "Direct check" );
+ $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ), "getRevisionText" );
+ }
+
+ function testCompressRevisionTextUtf8Gzip() {
+ $GLOBALS['wgCompressRevisions'] = true;
+ $row = new stdClass;
+ $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
+ $row->old_flags = Revision::compressRevisionText( $row->old_text );
+ $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
+ "Flags should contain 'utf-8'" );
+ $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ),
+ "Flags should contain 'gzip'" );
+ $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+ gzinflate( $row->old_text ), "Direct check" );
+ $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ), "getRevisionText" );
+ }
+}
+
+
diff --git a/tests/phpunit/includes/SampleTest.php b/tests/phpunit/includes/SampleTest.php
new file mode 100644
index 00000000..77a371d5
--- /dev/null
+++ b/tests/phpunit/includes/SampleTest.php
@@ -0,0 +1,98 @@
+<?php
+
+class TestSample extends MediaWikiLangTestCase {
+
+ /**
+ * Anything that needs to happen before your tests should go here.
+ */
+ function setUp() {
+ global $wgContLang;
+ parent::setUp();
+
+ /* For example, we need to set $wgContLang for creating a new Title */
+ $wgContLang = Language::factory( 'en' );
+ }
+
+ /**
+ * Anything cleanup you need to do should go here.
+ */
+ function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * Name tests so that PHPUnit can turn them into sentances when
+ * they run. While MediaWiki isn't strictly an Agile Programming
+ * project, you are encouraged to use the naming described under
+ * "Agile Documentation" at
+ * http://www.phpunit.de/manual/3.4/en/other-uses-for-tests.html
+ */
+ function testTitleObjectStringConversion() {
+ $title = Title::newFromText("text");
+ $this->assertEquals("Text", $title->__toString(), "Title creation");
+ $this->assertEquals("Text", "Text", "Automatic string conversion");
+
+ $title = Title::newFromText("text", NS_MEDIA);
+ $this->assertEquals("Media:Text", $title->__toString(), "Title creation with namespace");
+
+ }
+
+ /**
+ * If you want to run a the same test with a variety of data. use a data provider.
+ * see: http://www.phpunit.de/manual/3.4/en/writing-tests-for-phpunit.html
+ */
+ public function provideTitles() {
+ return array(
+ array( 'Text', NS_MEDIA, 'Media:Text' ),
+ array( 'Text', null, 'Text' ),
+ array( 'text', null, 'Text' ),
+ array( 'Text', NS_USER, 'User:Text' ),
+ array( 'Photo.jpg', NS_IMAGE, 'File:Photo.jpg' )
+ );
+ }
+
+ /**
+ * @dataProvider provideTitles
+ * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.dataProvider
+ */
+ public function testCreateBasicListOfTitles($titleName, $ns, $text) {
+ $title = Title::newFromText($titleName, $ns);
+ $this->assertEquals($text, "$title", "see if '$titleName' matches '$text'");
+ }
+
+ public function testSetUpMainPageTitleForNextTest() {
+ $title = Title::newMainPage();
+ $this->assertEquals("Main Page", "$title", "Test initial creation of a title");
+
+ return $title;
+ }
+
+ /**
+ * Instead of putting a bunch of tests in a single test method,
+ * you should put only one or two tests in each test method. This
+ * way, the test method names can remain descriptive.
+ *
+ * If you want to make tests depend on data created in another
+ * method, you can create dependencies feed whatever you return
+ * from the dependant method (e.g. testInitialCreation in this
+ * example) as arguments to the next method (e.g. $title in
+ * testTitleDepends is whatever testInitialCreatiion returned.)
+ */
+ /**
+ * @depends testSetUpMainPageTitleForNextTest
+ * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.depends
+ */
+ public function testCheckMainPageTitleIsConsideredLocal( $title ) {
+ $this->assertTrue( $title->isLocal() );
+ }
+
+ /**
+ * @expectedException MWException object
+ * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.expectedException
+ */
+ function testTitleObjectFromObject() {
+ $title = Title::newFromText( Title::newFromText( "test" ) );
+ $this->assertEquals( "Test", $title->isLocal() );
+ }
+}
+
diff --git a/tests/phpunit/includes/SanitizerTest.php b/tests/phpunit/includes/SanitizerTest.php
new file mode 100644
index 00000000..40d6cf77
--- /dev/null
+++ b/tests/phpunit/includes/SanitizerTest.php
@@ -0,0 +1,113 @@
+<?php
+
+class SanitizerTest extends MediaWikiTestCase {
+
+ function setUp() {
+ AutoLoader::loadClass( 'Sanitizer' );
+ }
+
+ function testDecodeNamedEntities() {
+ $this->assertEquals(
+ "\xc3\xa9cole",
+ Sanitizer::decodeCharReferences( '&eacute;cole' ),
+ 'decode named entities'
+ );
+ }
+
+ function testDecodeNumericEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole!",
+ Sanitizer::decodeCharReferences( "&#x108;io bonas dans l'&#233;cole!" ),
+ 'decode numeric entities'
+ );
+ }
+
+ function testDecodeMixedEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole!",
+ Sanitizer::decodeCharReferences( "&#x108;io bonas dans l'&eacute;cole!" ),
+ 'decode mixed numeric/named entities'
+ );
+ }
+
+ function testDecodeMixedComplexEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole! (mais pas &#x108;io dans l'&eacute;cole)",
+ Sanitizer::decodeCharReferences(
+ "&#x108;io bonas dans l'&eacute;cole! (mais pas &amp;#x108;io dans l'&#38;eacute;cole)"
+ ),
+ 'decode mixed complex entities'
+ );
+ }
+
+ function testInvalidAmpersand() {
+ $this->assertEquals(
+ 'a & b',
+ Sanitizer::decodeCharReferences( 'a & b' ),
+ 'Invalid ampersand'
+ );
+ }
+
+ function testInvalidEntities() {
+ $this->assertEquals(
+ '&foo;',
+ Sanitizer::decodeCharReferences( '&foo;' ),
+ 'Invalid named entity'
+ );
+ }
+
+ function testInvalidNumberedEntities() {
+ $this->assertEquals( UTF8_REPLACEMENT, Sanitizer::decodeCharReferences( "&#88888888888888;" ), 'Invalid numbered entity' );
+ }
+
+ function testSelfClosingTag() {
+ $GLOBALS['wgUseTidy'] = false;
+ $this->assertEquals(
+ '<div>Hello world</div>',
+ Sanitizer::removeHTMLtags( '<div>Hello world</div />' ),
+ 'Self-closing closing div'
+ );
+ }
+
+ function testDecodeTagAttributes() {
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=bar' ), array( 'foo' => 'bar' ), 'Unquoted attribute' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( ' foo = bar ' ), array( 'foo' => 'bar' ), 'Spaced attribute' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo="bar"' ), array( 'foo' => 'bar' ), 'Double-quoted attribute' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'bar\'' ), array( 'foo' => 'bar' ), 'Single-quoted attribute' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'bar\' baz="foo"' ), array( 'foo' => 'bar', 'baz' => 'foo' ), 'Several attributes' );
+
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'bar\' baz="foo"' ), array( 'foo' => 'bar', 'baz' => 'foo' ), 'Several attributes' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'bar\' baz="foo"' ), array( 'foo' => 'bar', 'baz' => 'foo' ), 'Several attributes' );
+
+ $this->assertEquals( Sanitizer::decodeTagAttributes( ':foo=\'bar\'' ), array( ':foo' => 'bar' ), 'Leading :' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( '_foo=\'bar\'' ), array( '_foo' => 'bar' ), 'Leading _' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'Foo=\'bar\'' ), array( 'foo' => 'bar' ), 'Leading capital' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'FOO=BAR' ), array( 'foo' => 'BAR' ), 'Attribute keys are normalized to lowercase' );
+
+ # Invalid beginning
+ $this->assertEquals( Sanitizer::decodeTagAttributes( '-foo=bar' ), array(), 'Leading - is forbidden' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( '.foo=bar' ), array(), 'Leading . is forbidden' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo-bar=bar' ), array( 'foo-bar' => 'bar' ), 'A - is allowed inside the attribute' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo-=bar' ), array( 'foo-' => 'bar' ), 'A - is allowed inside the attribute' );
+
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo.bar=baz' ), array( 'foo.bar' => 'baz' ), 'A . is allowed inside the attribute' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo.=baz' ), array( 'foo.' => 'baz' ), 'A . is allowed as last character' );
+
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo6=baz' ), array( 'foo6' => 'baz' ), 'Numbers are allowed' );
+
+ # This bit is more relaxed than XML rules, but some extensions use it, like ProofreadPage (see bug 27539)
+ $this->assertEquals( Sanitizer::decodeTagAttributes( '1foo=baz' ), array( '1foo' => 'baz' ), 'Leading numbers are allowed' );
+
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo$=baz' ), array(), 'Symbols are not allowed' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo@=baz' ), array(), 'Symbols are not allowed' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo~=baz' ), array(), 'Symbols are not allowed' );
+
+
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=1[#^`*%w/(' ), array( 'foo' => '1[#^`*%w/(' ), 'All kind of characters are allowed as values' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo="1[#^`*%\'w/("' ), array( 'foo' => '1[#^`*%\'w/(' ), 'Double quotes are allowed if quoted by single quotes' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'1[#^`*%"w/(\'' ), array( 'foo' => '1[#^`*%"w/(' ), 'Single quotes are allowed if quoted by double quotes' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=&amp;&quot;' ), array( 'foo' => '&"' ), 'Special chars can be provided as entities' );
+ $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=&foobar;' ), array( 'foo' => '&foobar;' ), 'Entity-like items are accepted' );
+ }
+}
+
diff --git a/tests/phpunit/includes/SeleniumConfigurationTest.php b/tests/phpunit/includes/SeleniumConfigurationTest.php
new file mode 100644
index 00000000..750524eb
--- /dev/null
+++ b/tests/phpunit/includes/SeleniumConfigurationTest.php
@@ -0,0 +1,228 @@
+<?php
+
+class SeleniumConfigurationTest extends MediaWikiTestCase {
+
+ /*
+ * The file where the test temporarity stores the selenium config.
+ * This should be cleaned up as part of teardown.
+ */
+ private $tempFileName;
+
+ /*
+ * String containing the a sample selenium settings
+ */
+ private $testConfig0 =
+'
+[SeleniumSettings]
+browsers[firefox] = "*firefox"
+browsers[iexplorer] = "*iexploreproxy"
+browsers[chrome] = "*chrome"
+host = "localhost"
+port = "foobarr"
+wikiUrl = "http://localhost/deployment"
+username = "xxxxxxx"
+userPassword = ""
+testBrowser = "chrome"
+startserver =
+stopserver =
+jUnitLogFile =
+runAgainstGrid = false
+
+[SeleniumTests]
+testSuite[SimpleSeleniumTestSuite] = "tests/selenium/SimpleSeleniumTestSuite.php"
+testSuite[TestSuiteName] = "testSuitePath"
+';
+ /*
+ * Array of expected browsers from $testConfig0
+ */
+ private $testBrowsers0 = array( 'firefox' => '*firefox',
+ 'iexplorer' => '*iexploreproxy',
+ 'chrome' => '*chrome'
+ );
+ /*
+ * Array of expected selenium settings from $testConfig0
+ */
+ private $testSettings0 = array(
+ 'host' => 'localhost',
+ 'port' => 'foobarr',
+ 'wikiUrl' => 'http://localhost/deployment',
+ 'username' => 'xxxxxxx',
+ 'userPassword' => '',
+ 'testBrowser' => 'chrome',
+ 'startserver' => null,
+ 'stopserver' => null,
+ 'seleniumserverexecpath' => null,
+ 'jUnitLogFile' => null,
+ 'runAgainstGrid' => null
+ );
+ /*
+ * Array of expected testSuites from $testConfig0
+ */
+ private $testSuites0 = array(
+ 'SimpleSeleniumTestSuite' => 'tests/selenium/SimpleSeleniumTestSuite.php',
+ 'TestSuiteName' => 'testSuitePath'
+ );
+
+
+ /*
+ * Another sample selenium settings file contents
+ */
+ private $testConfig1 =
+'
+[SeleniumSettings]
+host = "localhost"
+testBrowser = "firefox"
+';
+ /*
+ * Expected browsers from $testConfig1
+ */
+ private $testBrowsers1 = null;
+ /*
+ * Expected selenium settings from $testConfig1
+ */
+ private $testSettings1 = array(
+ 'host' => 'localhost',
+ 'port' => null,
+ 'wikiUrl' => null,
+ 'username' => null,
+ 'userPassword' => null,
+ 'testBrowser' => 'firefox',
+ 'startserver' => null,
+ 'stopserver' => null,
+ 'seleniumserverexecpath' => null,
+ 'jUnitLogFile' => null,
+ 'runAgainstGrid' => null
+ );
+ /*
+ * Expected test suites from $testConfig1
+ */
+ private $testSuites1 = null;
+
+
+ public function setUp() {
+ if ( !defined( 'SELENIUMTEST' ) ) {
+ define( 'SELENIUMTEST', true );
+ }
+ }
+
+ /*
+ * Clean up the temporary file used to store the selenium settings.
+ */
+ public function tearDown() {
+ if ( strlen( $this->tempFileName ) > 0 ) {
+ unlink( $this->tempFileName );
+ unset( $this->tempFileName );
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * @expectedException MWException
+ * @group SeleniumFramework
+ */
+ public function testErrorOnIncorrectConfigFile() {
+ $seleniumSettings = array();
+ $seleniumBrowsers = array();
+ $seleniumTestSuites = array();
+
+ SeleniumConfig::getSeleniumSettings($seleniumSettings,
+ $seleniumBrowsers,
+ $seleniumTestSuites,
+ "Some_fake_settings_file.ini" );
+
+ }
+
+ /**
+ * @expectedException MWException
+ * @group SeleniumFramework
+ */
+ public function testErrorOnMissingConfigFile() {
+ $seleniumSettings = array();
+ $seleniumBrowsers = array();
+ $seleniumTestSuites = array();
+ global $wgSeleniumConfigFile;
+ $wgSeleniumConfigFile = '';
+ SeleniumConfig::getSeleniumSettings($seleniumSettings,
+ $seleniumBrowsers,
+ $seleniumTestSuites);
+ }
+
+ /**
+ * @group SeleniumFramework
+ */
+ public function testUsesGlobalVarForConfigFile() {
+ $seleniumSettings = array();
+ $seleniumBrowsers = array();
+ $seleniumTestSuites = array();
+ global $wgSeleniumConfigFile;
+ $this->writeToTempFile( $this->testConfig0 );
+ $wgSeleniumConfigFile = $this->tempFileName;
+ SeleniumConfig::getSeleniumSettings($seleniumSettings,
+ $seleniumBrowsers,
+ $seleniumTestSuites);
+ $this->assertEquals($seleniumSettings, $this->testSettings0 ,
+ 'The selenium settings should have been read from the file defined in $wgSeleniumConfigFile'
+ );
+ $this->assertEquals($seleniumBrowsers, $this->testBrowsers0,
+ 'The available browsers should have been read from the file defined in $wgSeleniumConfigFile'
+ );
+ $this->assertEquals($seleniumTestSuites, $this->testSuites0,
+ 'The test suites should have been read from the file defined in $wgSeleniumConfigFile'
+ );
+ }
+
+ /**
+ * @group SeleniumFramework
+ * @dataProvider sampleConfigs
+ */
+ public function testgetSeleniumSettings($sampleConfig, $expectedSettings, $expectedBrowsers, $expectedSuites ) {
+ $this->writeToTempFile( $sampleConfig );
+ $seleniumSettings = array();
+ $seleniumBrowsers = array();
+ $seleniumTestSuites = null;
+
+ SeleniumConfig::getSeleniumSettings($seleniumSettings,
+ $seleniumBrowsers,
+ $seleniumTestSuites,
+ $this->tempFileName );
+
+ $this->assertEquals($seleniumSettings, $expectedSettings,
+ "The selenium settings for the following test configuration was not retrieved correctly" . $sampleConfig
+ );
+ $this->assertEquals($seleniumBrowsers, $expectedBrowsers,
+ "The available browsers for the following test configuration was not retrieved correctly" . $sampleConfig
+ );
+ $this->assertEquals($seleniumTestSuites, $expectedSuites,
+ "The test suites for the following test configuration was not retrieved correctly" . $sampleConfig
+ );
+
+
+ }
+
+ /*
+ * create a temp file and write text to it.
+ * @param $testToWrite the text to write to the temp file
+ */
+ private function writeToTempFile($textToWrite) {
+ $this->tempFileName = tempnam(sys_get_temp_dir(), 'test_settings.');
+ $tempFile = fopen( $this->tempFileName, "w" );
+ fwrite($tempFile , $textToWrite);
+ fclose($tempFile);
+ }
+
+ /*
+ * Returns an array containing:
+ * The contents of the selenium cingiguration ini file
+ * The expected selenium configuration array that getSeleniumSettings should return
+ * The expected available browsers array that getSeleniumSettings should return
+ * The expected test suites arrya that getSeleniumSettings should return
+ */
+ public function sampleConfigs() {
+ return array(
+ array($this->testConfig0, $this->testSettings0, $this->testBrowsers0, $this->testSuites0 ),
+ array($this->testConfig1, $this->testSettings1, $this->testBrowsers1, $this->testSuites1 )
+ );
+ }
+
+
+}
diff --git a/tests/phpunit/includes/SiteConfigurationTest.php b/tests/phpunit/includes/SiteConfigurationTest.php
new file mode 100644
index 00000000..57d3532a
--- /dev/null
+++ b/tests/phpunit/includes/SiteConfigurationTest.php
@@ -0,0 +1,311 @@
+<?php
+
+function getSiteParams( $conf, $wiki ) {
+ $site = null;
+ $lang = null;
+ foreach ( $conf->suffixes as $suffix ) {
+ if ( substr( $wiki, -strlen( $suffix ) ) == $suffix ) {
+ $site = $suffix;
+ $lang = substr( $wiki, 0, -strlen( $suffix ) );
+ break;
+ }
+ }
+ return array(
+ 'suffix' => $site,
+ 'lang' => $lang,
+ 'params' => array(
+ 'lang' => $lang,
+ 'site' => $site,
+ 'wiki' => $wiki,
+ ),
+ 'tags' => array( 'tag' ),
+ );
+}
+
+class SiteConfigurationTest extends MediaWikiTestCase {
+ var $mConf;
+
+ function setUp() {
+ $this->mConf = new SiteConfiguration;
+
+ $this->mConf->suffixes = array( 'wiki' );
+ $this->mConf->wikis = array( 'enwiki', 'dewiki', 'frwiki' );
+ $this->mConf->settings = array(
+ 'simple' => array(
+ 'wiki' => 'wiki',
+ 'tag' => 'tag',
+ 'enwiki' => 'enwiki',
+ 'dewiki' => 'dewiki',
+ 'frwiki' => 'frwiki',
+ ),
+
+ 'fallback' => array(
+ 'default' => 'default',
+ 'wiki' => 'wiki',
+ 'tag' => 'tag',
+ ),
+
+ 'params' => array(
+ 'default' => '$lang $site $wiki',
+ ),
+
+ '+global' => array(
+ 'wiki' => array(
+ 'wiki' => 'wiki',
+ ),
+ 'tag' => array(
+ 'tag' => 'tag',
+ ),
+ 'enwiki' => array(
+ 'enwiki' => 'enwiki',
+ ),
+ 'dewiki' => array(
+ 'dewiki' => 'dewiki',
+ ),
+ 'frwiki' => array(
+ 'frwiki' => 'frwiki',
+ ),
+ ),
+
+ 'merge' => array(
+ '+wiki' => array(
+ 'wiki' => 'wiki',
+ ),
+ '+tag' => array(
+ 'tag' => 'tag',
+ ),
+ 'default' => array(
+ 'default' => 'default',
+ ),
+ '+enwiki' => array(
+ 'enwiki' => 'enwiki',
+ ),
+ '+dewiki' => array(
+ 'dewiki' => 'dewiki',
+ ),
+ '+frwiki' => array(
+ 'frwiki' => 'frwiki',
+ ),
+ ),
+ );
+
+ $GLOBALS['global'] = array( 'global' => 'global' );
+ }
+
+
+ function testSiteFromDb() {
+ $this->assertEquals(
+ array( 'wikipedia', 'en' ),
+ $this->mConf->siteFromDB( 'enwiki' ),
+ 'siteFromDB()'
+ );
+ $this->assertEquals(
+ array( 'wikipedia', '' ),
+ $this->mConf->siteFromDB( 'wiki' ),
+ 'siteFromDB() on a suffix'
+ );
+ $this->assertEquals(
+ array( null, null ),
+ $this->mConf->siteFromDB( 'wikien' ),
+ 'siteFromDB() on a non-existing wiki'
+ );
+
+ $this->mConf->suffixes = array( 'wiki', '' );
+ $this->assertEquals(
+ array( '', 'wikien' ),
+ $this->mConf->siteFromDB( 'wikien' ),
+ 'siteFromDB() on a non-existing wiki (2)'
+ );
+ }
+
+ function testGetLocalDatabases() {
+ $this->assertEquals(
+ array( 'enwiki', 'dewiki', 'frwiki' ),
+ $this->mConf->getLocalDatabases(),
+ 'getLocalDatabases()'
+ );
+ }
+
+ function testGetConfVariables() {
+ $this->assertEquals(
+ 'enwiki',
+ $this->mConf->get( 'simple', 'enwiki', 'wiki' ),
+ 'get(): simple setting on an existing wiki'
+ );
+ $this->assertEquals(
+ 'dewiki',
+ $this->mConf->get( 'simple', 'dewiki', 'wiki' ),
+ 'get(): simple setting on an existing wiki (2)'
+ );
+ $this->assertEquals(
+ 'frwiki',
+ $this->mConf->get( 'simple', 'frwiki', 'wiki' ),
+ 'get(): simple setting on an existing wiki (3)'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'simple', 'wiki', 'wiki' ),
+ 'get(): simple setting on an suffix'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'simple', 'eswiki', 'wiki' ),
+ 'get(): simple setting on an non-existing wiki'
+ );
+
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'fallback', 'enwiki', 'wiki' ),
+ 'get(): fallback setting on an existing wiki'
+ );
+ $this->assertEquals(
+ 'tag',
+ $this->mConf->get( 'fallback', 'dewiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): fallback setting on an existing wiki (with wiki tag)'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'fallback', 'wiki', 'wiki' ),
+ 'get(): fallback setting on an suffix'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'fallback', 'wiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): fallback setting on an suffix (with wiki tag)'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'fallback', 'eswiki', 'wiki' ),
+ 'get(): fallback setting on an non-existing wiki'
+ );
+ $this->assertEquals(
+ 'tag',
+ $this->mConf->get( 'fallback', 'eswiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): fallback setting on an non-existing wiki (with wiki tag)'
+ );
+
+ $common = array( 'wiki' => 'wiki', 'default' => 'default' );
+ $commonTag = array( 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' );
+ $this->assertEquals(
+ array( 'enwiki' => 'enwiki' ) + $common,
+ $this->mConf->get( 'merge', 'enwiki', 'wiki' ),
+ 'get(): merging setting on an existing wiki'
+ );
+ $this->assertEquals(
+ array( 'enwiki' => 'enwiki' ) + $commonTag,
+ $this->mConf->get( 'merge', 'enwiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an existing wiki (with tag)'
+ );
+ $this->assertEquals(
+ array( 'dewiki' => 'dewiki' ) + $common,
+ $this->mConf->get( 'merge', 'dewiki', 'wiki' ),
+ 'get(): merging setting on an existing wiki (2)'
+ );
+ $this->assertEquals(
+ array( 'dewiki' => 'dewiki' ) + $commonTag,
+ $this->mConf->get( 'merge', 'dewiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an existing wiki (2) (with tag)'
+ );
+ $this->assertEquals(
+ array( 'frwiki' => 'frwiki' ) + $common,
+ $this->mConf->get( 'merge', 'frwiki', 'wiki' ),
+ 'get(): merging setting on an existing wiki (3)'
+ );
+ $this->assertEquals(
+ array( 'frwiki' => 'frwiki' ) + $commonTag,
+ $this->mConf->get( 'merge', 'frwiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an existing wiki (3) (with tag)'
+ );
+ $this->assertEquals(
+ array( 'wiki' => 'wiki' ) + $common,
+ $this->mConf->get( 'merge', 'wiki', 'wiki' ),
+ 'get(): merging setting on an suffix'
+ );
+ $this->assertEquals(
+ array( 'wiki' => 'wiki' ) + $commonTag,
+ $this->mConf->get( 'merge', 'wiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an suffix (with tag)'
+ );
+ $this->assertEquals(
+ $common,
+ $this->mConf->get( 'merge', 'eswiki', 'wiki' ),
+ 'get(): merging setting on an non-existing wiki'
+ );
+ $this->assertEquals(
+ $commonTag,
+ $this->mConf->get( 'merge', 'eswiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an non-existing wiki (with tag)'
+ );
+ }
+
+ function testSiteFromDbWithCallback() {
+ $this->mConf->siteParamsCallback = 'getSiteParams';
+
+ $this->assertEquals(
+ array( 'wiki', 'en' ),
+ $this->mConf->siteFromDB( 'enwiki' ),
+ 'siteFromDB() with callback'
+ );
+ $this->assertEquals(
+ array( 'wiki', '' ),
+ $this->mConf->siteFromDB( 'wiki' ),
+ 'siteFromDB() with callback on a suffix'
+ );
+ $this->assertEquals(
+ array( null, null ),
+ $this->mConf->siteFromDB( 'wikien' ),
+ 'siteFromDB() with callback on a non-existing wiki'
+ );
+ }
+
+ function testParameterReplacement() {
+ $this->mConf->siteParamsCallback = 'getSiteParams';
+
+ $this->assertEquals(
+ 'en wiki enwiki',
+ $this->mConf->get( 'params', 'enwiki', 'wiki' ),
+ 'get(): parameter replacement on an existing wiki'
+ );
+ $this->assertEquals(
+ 'de wiki dewiki',
+ $this->mConf->get( 'params', 'dewiki', 'wiki' ),
+ 'get(): parameter replacement on an existing wiki (2)'
+ );
+ $this->assertEquals(
+ 'fr wiki frwiki',
+ $this->mConf->get( 'params', 'frwiki', 'wiki' ),
+ 'get(): parameter replacement on an existing wiki (3)'
+ );
+ $this->assertEquals(
+ ' wiki wiki',
+ $this->mConf->get( 'params', 'wiki', 'wiki' ),
+ 'get(): parameter replacement on an suffix'
+ );
+ $this->assertEquals(
+ 'es wiki eswiki',
+ $this->mConf->get( 'params', 'eswiki', 'wiki' ),
+ 'get(): parameter replacement on an non-existing wiki'
+ );
+ }
+
+ function testGetAllGlobals() {
+ $this->mConf->siteParamsCallback = 'getSiteParams';
+
+ $getall = array(
+ 'simple' => 'enwiki',
+ 'fallback' => 'tag',
+ 'params' => 'en wiki enwiki',
+ 'global' => array( 'enwiki' => 'enwiki' ) + $GLOBALS['global'],
+ 'merge' => array( 'enwiki' => 'enwiki', 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ),
+ );
+ $this->assertEquals( $getall, $this->mConf->getAll( 'enwiki' ), 'getAll()' );
+
+ $this->mConf->extractAllGlobals( 'enwiki', 'wiki' );
+
+ $this->assertEquals( $getall['simple'], $GLOBALS['simple'], 'extractAllGlobals(): simple setting' );
+ $this->assertEquals( $getall['fallback'], $GLOBALS['fallback'], 'extractAllGlobals(): fallback setting' );
+ $this->assertEquals( $getall['params'], $GLOBALS['params'], 'extractAllGlobals(): parameter replacement' );
+ $this->assertEquals( $getall['global'], $GLOBALS['global'], 'extractAllGlobals(): merging with global' );
+ $this->assertEquals( $getall['merge'], $GLOBALS['merge'], 'extractAllGlobals(): merging setting' );
+ }
+}
diff --git a/tests/phpunit/includes/TimeAdjustTest.php b/tests/phpunit/includes/TimeAdjustTest.php
new file mode 100644
index 00000000..cd027c5b
--- /dev/null
+++ b/tests/phpunit/includes/TimeAdjustTest.php
@@ -0,0 +1,51 @@
+<?php
+
+class TimeAdjustTest extends MediaWikiLangTestCase {
+ static $offset;
+
+ public function setUp() {
+ parent::setUp();
+ global $wgLocalTZoffset;
+ self::$offset = $wgLocalTZoffset;
+
+ $this->iniSet( 'precision', 15 );
+ }
+
+ public function tearDown() {
+ global $wgLocalTZoffset;
+ $wgLocalTZoffset = self::$offset;
+ parent::tearDown();
+ }
+
+ # Test offset usage for a given language::userAdjust
+ function testUserAdjust() {
+ global $wgLocalTZoffset, $wgContLang;
+
+ $wgContLang = $en = Language::factory( 'en' );
+
+ #  Collection of parameters for Language_t_Offset.
+ # Format: date to be formatted, localTZoffset value, expected date
+ $userAdjust_tests = array(
+ array( 20061231235959, 0, 20061231235959 ),
+ array( 20061231235959, 5, 20070101000459 ),
+ array( 20061231235959, 15, 20070101001459 ),
+ array( 20061231235959, 60, 20070101005959 ),
+ array( 20061231235959, 90, 20070101012959 ),
+ array( 20061231235959, 120, 20070101015959 ),
+ array( 20061231235959, 540, 20070101085959 ),
+ array( 20061231235959, -5, 20061231235459 ),
+ array( 20061231235959, -30, 20061231232959 ),
+ array( 20061231235959, -60, 20061231225959 ),
+ );
+
+ foreach ( $userAdjust_tests as $data ) {
+ $wgLocalTZoffset = $data[1];
+
+ $this->assertEquals(
+ strval( $data[2] ),
+ strval( $en->userAdjust( $data[0], '' ) ),
+ "User adjust {$data[0]} by {$data[1]} minutes should give {$data[2]}"
+ );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/TitlePermissionTest.php b/tests/phpunit/includes/TitlePermissionTest.php
new file mode 100644
index 00000000..1b179686
--- /dev/null
+++ b/tests/phpunit/includes/TitlePermissionTest.php
@@ -0,0 +1,654 @@
+<?php
+
+/**
+ * @group Database
+ */
+class TitlePermissionTest extends MediaWikiLangTestCase {
+ protected $title;
+ protected $user;
+ protected $anonUser;
+ protected $userUser;
+ protected $altUser;
+ protected $userName;
+ protected $altUserName;
+
+ function setUp() {
+ global $wgLocaltimezone, $wgLocalTZoffset, $wgMemc, $wgContLang, $wgLang;
+ parent::setUp();
+
+ if(!$wgMemc) {
+ $wgMemc = new EmptyBagOStuff;
+ }
+ $wgContLang = $wgLang = Language::factory( 'en' );
+
+ $this->userName = "Useruser";
+ $this->altUserName = "Altuseruser";
+ date_default_timezone_set( $wgLocaltimezone );
+ $wgLocalTZoffset = date( "Z" ) / 60;
+
+ $this->title = Title::makeTitle( NS_MAIN, "Main Page" );
+ if ( !isset( $this->userUser ) || !( $this->userUser instanceOf User ) ) {
+ $this->userUser = User::newFromName( $this->userName );
+
+ if ( !$this->userUser->getID() ) {
+ $this->userUser = User::createNew( $this->userName, array(
+ "email" => "test@example.com",
+ "real_name" => "Test User" ) );
+ $this->userUser->load();
+ }
+
+ $this->altUser = User::newFromName( $this->altUserName );
+ if ( !$this->altUser->getID() ) {
+ $this->altUser = User::createNew( $this->altUserName, array(
+ "email" => "alttest@example.com",
+ "real_name" => "Test User Alt" ) );
+ $this->altUser->load();
+ }
+
+ $this->anonUser = User::newFromId( 0 );
+
+ $this->user = $this->userUser;
+ }
+ }
+
+ function tearDown() {
+ parent::tearDown();
+ }
+
+ function setUserPerm( $perm ) {
+ if ( is_array( $perm ) ) {
+ $this->user->mRights = $perm;
+ } else {
+ $this->user->mRights = array( $perm );
+ }
+ }
+
+ function setTitle( $ns, $title = "Main_Page" ) {
+ $this->title = Title::makeTitle( $ns, $title );
+ }
+
+ function setUser( $userName = null ) {
+ if ( $userName === 'anon' ) {
+ $this->user = $this->anonUser;
+ } elseif ( $userName === null || $userName === $this->userName ) {
+ $this->user = $this->userUser;
+ } else {
+ $this->user = $this->altUser;
+ }
+
+ global $wgUser;
+ $wgUser = $this->user;
+ }
+
+ function testQuickPermissions() {
+ global $wgContLang;
+ $prefix = $wgContLang->getFormattedNsText( NS_PROJECT );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array(), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createpage" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( "nocreatetext" ) ), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreatetext' ) ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createpage" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreatetext' ) ), $res );
+
+ $this->setUser( $this->userName );
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createpage" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createpage" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createtalk" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'create', $this->user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, $this->userName . '/subpage' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setUser( $this->userName );
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ), $res );
+
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "movefile" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenotallowed' ) ), $res );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "movefile" );
+ $res = $this->title->getUserPermissionsErrors( 'move', $this->user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setUser( $this->userName );
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ) ) );
+
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ) );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ) ) );
+
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ),
+ array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUser( 'anon' );
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', array( ) );
+
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ),
+ array( array( 'movenologintext' ) ) );
+
+ $this->setUser( $this->userName );
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ) );
+
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', array( ) );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( 'move' );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setUserPerm( '' );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array( array( 'movenotallowed' ) ), $res );
+
+ $this->setTitle( NS_USER );
+ $this->setUser( $this->userName );
+ $this->setUserPerm( array( "move", "move-rootuserpages" ) );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setUserPerm( "move" );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array( array( 'cant-move-to-user-page' ) ), $res );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( array( "move", "move-rootuserpages" ) );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setTitle( NS_USER, "User/subpage" );
+ $this->setUserPerm( array( "move", "move-rootuserpages" ) );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setUserPerm( "move" );
+ $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setUser( 'anon' );
+ $check = array( 'edit' => array( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( ), true ),
+ 'protect' => array( array( array( 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ), array( 'protect-cantedit' ) ),
+ array( array( 'badaccess-group0' ), array( 'protect-cantedit' ) ),
+ array( array( 'protect-cantedit' ) ), false ),
+ '' => array( array( ), array( ), array( ), true ) );
+ global $wgUser;
+ $wgUser = $this->user;
+ foreach ( array( "edit", "protect", "" ) as $action ) {
+ $this->setUserPerm( null );
+ $this->assertEquals( $check[$action][0],
+ $this->title->getUserPermissionsErrors( $action, $this->user, true ) );
+
+ global $wgGroupPermissions;
+ $old = $wgGroupPermissions;
+ $wgGroupPermissions = array();
+
+ $this->assertEquals( $check[$action][1],
+ $this->title->getUserPermissionsErrors( $action, $this->user, true ) );
+ $wgGroupPermissions = $old;
+
+ $this->setUserPerm( $action );
+ $this->assertEquals( $check[$action][2],
+ $this->title->getUserPermissionsErrors( $action, $this->user, true ) );
+
+ $this->setUserPerm( $action );
+ $this->assertEquals( $check[$action][3],
+ $this->title->userCan( $action, true ) );
+ $this->assertEquals( $check[$action][3],
+ $this->title->quickUserCan( $action, false ) );
+
+ # count( User::getGroupsWithPermissions( $action ) ) < 1
+ }
+ }
+
+ function runGroupPermissions( $action, $result, $result2 = null ) {
+ global $wgGroupPermissions;
+
+ if ( $result2 === null ) $result2 = $result;
+
+ $wgGroupPermissions['autoconfirmed']['move'] = false;
+ $wgGroupPermissions['user']['move'] = false;
+ $res = $this->title->getUserPermissionsErrors( $action, $this->user );
+ $this->assertEquals( $result, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = true;
+ $wgGroupPermissions['user']['move'] = false;
+ $res = $this->title->getUserPermissionsErrors( $action, $this->user );
+ $this->assertEquals( $result2, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = true;
+ $wgGroupPermissions['user']['move'] = true;
+ $res = $this->title->getUserPermissionsErrors( $action, $this->user );
+ $this->assertEquals( $result2, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = false;
+ $wgGroupPermissions['user']['move'] = true;
+ $res = $this->title->getUserPermissionsErrors( $action, $this->user );
+ $this->assertEquals( $result2, $res );
+ }
+
+ function testSpecialsAndNSPermissions() {
+ $this->setUser( $this->userName );
+ global $wgUser;
+ $wgUser = $this->user;
+
+ $this->setTitle( NS_SPECIAL );
+
+ $this->assertEquals( array( array( 'badaccess-group0' ), array( 'ns-specialprotected' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'execute', $this->user ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ global $wgNamespaceProtection;
+ $wgNamespaceProtection[NS_USER] = array ( 'bogus' );
+ $this->setTitle( NS_USER );
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ), array( 'namespaceprotected', 'User' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( array( 'protectedinterface' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( array( 'protectedinterface' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $wgNamespaceProtection = null;
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'bogus' ) );
+
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'bogus' ) );
+ }
+
+ function testCssAndJavascriptPermissions() {
+ $this->setUser( $this->userName );
+ global $wgUser;
+ $wgUser = $this->user;
+
+ $this->setTitle( NS_USER, $this->altUserName . '/test.js' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ),
+ array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ),
+ array( array( 'badaccess-group0' ) ) );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/test.css' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ) );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/tempo' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ) );
+ }
+
+ function runCSSandJSPermissions( $result0, $result1, $result2 ) {
+ $this->setUserPerm( '' );
+ $this->assertEquals( $result0,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'editusercss' );
+ $this->assertEquals( $result1,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'edituserjs' );
+ $this->assertEquals( $result2,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'editusercssjs' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( array( 'edituserjs', 'editusercss' ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ }
+
+ function testPageRestrictions() {
+ global $wgUser, $wgContLang;
+
+ $prefix = $wgContLang->getFormattedNsText( NS_PROJECT );
+
+ $wgUser = $this->user;
+ $this->setTitle( NS_MAIN );
+ $this->title->mRestrictionsLoaded = true;
+ $this->setUserPerm( "edit" );
+ $this->title->mRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( array( ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+
+ $this->assertEquals( true,
+ $this->title->quickUserCan( 'edit', false ) );
+ $this->title->mRestrictions = array( "edit" => array( 'bogus', "sysop", "protect", "" ),
+ "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ $this->setUserPerm( "" );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ $this->setUserPerm( array( "edit", "editprotected" ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ $this->title->mCascadeRestriction = true;
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'bogus', false ) );
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'edit', false ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ }
+
+ function testCascadingSourcesRestrictions() {
+ global $wgUser;
+ $wgUser = $this->user;
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->setUserPerm( array( "edit", "bogus" ) );
+
+ $this->title->mCascadeSources = array( Title::makeTitle( NS_MAIN, "Bogus" ), Title::makeTitle( NS_MAIN, "UnBogus" ) );
+ $this->title->mCascadingRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( false,
+ $this->title->userCan( 'bogus' ) );
+ $this->assertEquals( array( array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ),
+ array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->assertEquals( true,
+ $this->title->userCan( 'edit' ) );
+ $this->assertEquals( array( ),
+ $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
+
+ }
+
+ function testActionPermissions() {
+ global $wgUser;
+ $wgUser = $this->user;
+
+ $this->setUserPerm( array( "createpage" ) );
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->title->mTitleProtection['pt_create_perm'] = '';
+ $this->title->mTitleProtection['pt_user'] = $this->user->getID();
+ $this->title->mTitleProtection['pt_expiry'] = Block::infinity();
+ $this->title->mTitleProtection['pt_reason'] = 'test';
+ $this->title->mCascadeRestriction = false;
+
+ $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ),
+ $this->title->getUserPermissionsErrors( 'create', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'create' ) );
+
+ $this->title->mTitleProtection['pt_create_perm'] = 'sysop';
+ $this->setUserPerm( array( 'createpage', 'protect' ) );
+ $this->assertEquals( array( ),
+ $this->title->getUserPermissionsErrors( 'create', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'create' ) );
+
+
+ $this->setUserPerm( array( 'createpage' ) );
+ $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ),
+ $this->title->getUserPermissionsErrors( 'create', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'create' ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->setUserPerm( array( "move" ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move' ) );
+ $this->assertEquals( array( array( 'immobile-source-namespace', 'Media' ) ),
+ $this->title->getUserPermissionsErrors( 'move', $this->user ) );
+
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->assertEquals( array( ),
+ $this->title->getUserPermissionsErrors( 'move', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'move' ) );
+
+ $this->title->mInterwiki = "no";
+ $this->assertEquals( array( array( 'immobile-page' ) ),
+ $this->title->getUserPermissionsErrors( 'move', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move' ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move-target' ) );
+ $this->assertEquals( array( array( 'immobile-target-namespace', 'Media' ) ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->assertEquals( array( ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'move-target' ) );
+
+ $this->title->mInterwiki = "no";
+ $this->assertEquals( array( array( 'immobile-target-page' ) ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move-target' ) );
+
+ }
+
+ function testUserBlock() {
+ global $wgUser, $wgEmailConfirmToEdit, $wgEmailAuthentication;
+ $wgEmailConfirmToEdit = true;
+ $wgEmailAuthentication = true;
+ $wgUser = $this->user;
+
+ $this->setUserPerm( array( "createpage", "move" ) );
+ $this->setTitle( NS_MAIN, "test page" );
+
+ # $short
+ $this->assertEquals( array( array( 'confirmedittext' ) ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+ $wgEmailConfirmToEdit = false;
+ $this->assertEquals( true, $this->title->userCan( 'move-target' ) );
+
+ # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount'
+ $this->assertEquals( array( ),
+ $this->title->getUserPermissionsErrors( 'move-target',
+ $this->user ) );
+
+ global $wgLang;
+ $prev = time();
+ $now = time() + 120;
+ $this->user->mBlockedby = $this->user->getId();
+ $this->user->mBlock = new Block( '127.0.8.1', $this->user->getId(), $this->user->getId(),
+ 'no reason given', $prev + 3600, 1, 0 );
+ $this->user->mBlock->mTimestamp = 0;
+ $this->assertEquals( array( array( 'autoblockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', null, 'infinite', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $prev ), true ) ) ),
+ $this->title->getUserPermissionsErrors( 'move-target',
+ $this->user ) );
+
+ $this->assertEquals( false, $this->title->userCan( 'move-target' ) );
+ // quickUserCan should ignore user blocks
+ $this->assertEquals( true, $this->title->quickUserCan( 'move-target' ) );
+
+ global $wgLocalTZoffset;
+ $wgLocalTZoffset = -60;
+ $this->user->mBlockedby = $this->user->getName();
+ $this->user->mBlock = new Block( '127.0.8.1', 2, 1, 'no reason given', $now, 0, 10 );
+ $this->assertEquals( array( array( 'blockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', null, '23:00, 31 December 1969', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ) ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+
+ # $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this )
+ # $user->blockedFor() == ''
+ # $user->mBlock->mExpiry == 'infinity'
+ }
+}
diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php
new file mode 100644
index 00000000..51b36160
--- /dev/null
+++ b/tests/phpunit/includes/TitleTest.php
@@ -0,0 +1,79 @@
+<?php
+
+class TitleTest extends MediaWikiTestCase {
+
+ function testLegalChars() {
+ $titlechars = Title::legalChars();
+
+ foreach ( range( 1, 255 ) as $num ) {
+ $chr = chr( $num );
+ if ( strpos( "#[]{}<>|", $chr ) !== false || preg_match( "/[\\x00-\\x1f\\x7f]/", $chr ) ) {
+ $this->assertFalse( (bool)preg_match( "/[$titlechars]/", $chr ), "chr($num) = $chr is not a valid titlechar" );
+ } else {
+ $this->assertTrue( (bool)preg_match( "/[$titlechars]/", $chr ), "chr($num) = $chr is a valid titlechar" );
+ }
+ }
+ }
+
+ /**
+ * @dataProvider dataBug31100
+ */
+ function testBug31100FixSpecialName( $text, $expectedParam ) {
+ $title = Title::newFromText( $text );
+ $fixed = $title->fixSpecialName();
+ $stuff = explode( '/', $fixed->getDbKey(), 2 );
+ if ( count( $stuff ) == 2 ) {
+ $par = $stuff[1];
+ } else {
+ $par = null;
+ }
+ $this->assertEquals( $expectedParam, $par, "Bug 31100 regression check: Title->fixSpecialName() should preserve parameter" );
+ }
+
+ function dataBug31100() {
+ return array(
+ array( 'Special:Version', null ),
+ array( 'Special:Version/', '' ),
+ array( 'Special:Version/param', 'param' ),
+ );
+ }
+
+ /**
+ * Auth-less test of Title::isValidMoveOperation
+ *
+ * @param string $source
+ * @param string $target
+ * @param array|string|true $requiredErrors
+ * @dataProvider dataTestIsValidMoveOperation
+ */
+ function testIsValidMoveOperation( $source, $target, $expected ) {
+ $title = Title::newFromText( $source );
+ $nt = Title::newFromText( $target );
+ $errors = $title->isValidMoveOperation( $nt, false );
+ if ( $expected === true ) {
+ $this->assertTrue( $errors );
+ } else {
+ $errors = $this->flattenErrorsArray( $errors );
+ foreach ( (array)$expected as $error ) {
+ $this->assertContains( $error, $errors );
+ }
+ }
+ }
+
+ function flattenErrorsArray( $errors ) {
+ $result = array();
+ foreach ( $errors as $error ) {
+ $result[] = $error[0];
+ }
+ return $result;
+ }
+
+ function dataTestIsValidMoveOperation() {
+ return array(
+ array( 'Test', 'Test', 'selfmove' ),
+ array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' )
+ );
+ }
+
+
+}
diff --git a/tests/phpunit/includes/UserIsValidEmailAddrTest.php b/tests/phpunit/includes/UserIsValidEmailAddrTest.php
new file mode 100644
index 00000000..99bf718e
--- /dev/null
+++ b/tests/phpunit/includes/UserIsValidEmailAddrTest.php
@@ -0,0 +1,79 @@
+<?php
+
+class UserIsValidEmailAddrTest extends MediaWikiTestCase {
+
+ private function checkEmail( $addr, $expected = true, $msg = '') {
+ if( $msg == '' ) { $msg = "Testing $addr"; }
+ $this->assertEquals(
+ $expected,
+ User::isValidEmailAddr( $addr ),
+ $msg
+ );
+ }
+ private function valid( $addr, $msg = '' ) {
+ $this->checkEmail( $addr, true, $msg );
+ }
+ private function invalid( $addr, $msg = '' ) {
+ $this->checkEmail( $addr, false, $msg );
+ }
+
+ function testEmailWellKnownUserAtHostDotTldAreValid() {
+ $this->valid( 'user@example.com' );
+ $this->valid( 'user@example.museum' );
+ }
+ function testEmailWithUpperCaseCharactersAreValid() {
+ $this->valid( 'USER@example.com' );
+ $this->valid( 'user@EXAMPLE.COM' );
+ $this->valid( 'user@Example.com' );
+ $this->valid( 'USER@eXAMPLE.com' );
+ }
+ function testEmailWithAPlusInUserName() {
+ $this->valid( 'user+sub@example.com' );
+ $this->valid( 'user+@example.com' );
+ }
+ function testEmailDoesNotNeedATopLevelDomain() {
+ $this->valid( "user@localhost" );
+ $this->valid( "FooBar@localdomain" );
+ $this->valid( "nobody@mycompany" );
+ }
+ function testEmailWithWhiteSpacesBeforeOrAfterAreInvalids() {
+ $this->invalid( " user@host.com" );
+ $this->invalid( "user@host.com " );
+ $this->invalid( "\tuser@host.com" );
+ $this->invalid( "user@host.com\t" );
+ }
+ function testEmailWithWhiteSpacesAreInvalids() {
+ $this->invalid( "User user@host" );
+ $this->invalid( "first last@mycompany" );
+ $this->invalid( "firstlast@my company" );
+ }
+ // bug 26948 : comma were matched by an incorrect regexp range
+ function testEmailWithCommasAreInvalids() {
+ $this->invalid( "user,foo@example.org" );
+ $this->invalid( "userfoo@ex,ample.org" );
+ }
+ function testEmailWithHyphens() {
+ $this->valid( "user-foo@example.org" );
+ $this->valid( "userfoo@ex-ample.org" );
+ }
+ function testEmailDomainCanNotBeginWithDot() {
+ $this->invalid( "user@." );
+ $this->invalid( "user@.localdomain" );
+ $this->invalid( "user@localdomain." );
+ $this->valid( "user.@localdomain" );
+ $this->valid( ".@localdomain" );
+ $this->invalid( ".@a............" );
+ }
+ function testEmailWithFunnyCharacters() {
+ $this->valid( "\$user!ex{this}@123.com" );
+ }
+ function testEmailTopLevelDomainCanBeNumerical() {
+ $this->valid( "user@example.1234" );
+ }
+ function testEmailWithoutAtSignIsInvalid() {
+ $this->invalid( 'useràexample.com' );
+ }
+ function testEmailWithOneCharacterDomainIsValid() {
+ $this->valid( 'user@a' );
+ }
+}
diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/UserTest.php
new file mode 100644
index 00000000..df91aca8
--- /dev/null
+++ b/tests/phpunit/includes/UserTest.php
@@ -0,0 +1,58 @@
+<?php
+
+class UserTest extends MediaWikiTestCase {
+ protected $savedGroupPermissions, $savedRevokedPermissions;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->savedGroupPermissions = $GLOBALS['wgGroupPermissions'];
+ $this->savedRevokedPermissions = $GLOBALS['wgRevokePermissions'];
+
+ $this->setUpPermissionGlobals();
+ }
+ private function setUpPermissionGlobals() {
+ global $wgGroupPermissions, $wgRevokePermissions;
+
+ $wgGroupPermissions['unittesters'] = array(
+ 'runtest' => true,
+ 'writetest' => false,
+ 'nukeworld' => false,
+ );
+ $wgGroupPermissions['testwriters'] = array(
+ 'writetest' => true,
+ 'modifytest' => true,
+ );
+
+ $wgRevokePermissions['formertesters'] = array(
+ 'runtest' => true,
+ );
+ }
+ public function tearDown() {
+ parent::tearDown();
+
+ $GLOBALS['wgGroupPermissions'] = $this->savedGroupPermissions;
+ $GLOBALS['wgRevokePermissions'] = $this->savedRevokedPermissions;
+ }
+
+ public function testGroupPermissions() {
+ $rights = User::getGroupPermissions( array( 'unittesters' ) );
+ $this->assertContains( 'runtest', $rights );
+ $this->assertNotContains( 'writetest', $rights );
+ $this->assertNotContains( 'modifytest', $rights );
+ $this->assertNotContains( 'nukeworld', $rights );
+
+ $rights = User::getGroupPermissions( array( 'unittesters', 'testwriters' ) );
+ $this->assertContains( 'runtest', $rights );
+ $this->assertContains( 'writetest', $rights );
+ $this->assertContains( 'modifytest', $rights );
+ $this->assertNotContains( 'nukeworld', $rights );
+ }
+ public function testRevokePermissions() {
+ $rights = User::getGroupPermissions( array( 'unittesters', 'formertesters' ) );
+ $this->assertNotContains( 'runtest', $rights );
+ $this->assertNotContains( 'writetest', $rights );
+ $this->assertNotContains( 'modifytest', $rights );
+ $this->assertNotContains( 'nukeworld', $rights );
+ }
+} \ No newline at end of file
diff --git a/tests/phpunit/includes/WebRequestTest.php b/tests/phpunit/includes/WebRequestTest.php
new file mode 100644
index 00000000..1cfbd3fc
--- /dev/null
+++ b/tests/phpunit/includes/WebRequestTest.php
@@ -0,0 +1,88 @@
+<?php
+
+class WebRequestTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideDetectServer
+ */
+ function testDetectServer( $expected, $input, $description ) {
+ $oldServer = $_SERVER;
+ $_SERVER = $input;
+ $result = WebRequest::detectServer();
+ $_SERVER = $oldServer;
+ $this->assertEquals( $expected, $result, $description );
+ }
+
+ function provideDetectServer() {
+ return array(
+ array(
+ 'http://x',
+ array(
+ 'HTTP_HOST' => 'x'
+ ),
+ 'Host header'
+ ),
+ array(
+ 'https://x',
+ array(
+ 'HTTP_HOST' => 'x',
+ 'HTTPS' => 'on',
+ ),
+ 'Host header with secure'
+ ),
+ array(
+ 'http://x',
+ array(
+ 'HTTP_HOST' => 'x',
+ 'SERVER_PORT' => 80,
+ ),
+ 'Default SERVER_PORT',
+ ),
+ array(
+ 'http://x',
+ array(
+ 'HTTP_HOST' => 'x',
+ 'HTTPS' => 'off',
+ ),
+ 'Secure off'
+ ),
+ array(
+ 'http://y',
+ array(
+ 'SERVER_NAME' => 'y',
+ ),
+ 'Server name'
+ ),
+ array(
+ 'http://x',
+ array(
+ 'HTTP_HOST' => 'x',
+ 'SERVER_NAME' => 'y',
+ ),
+ 'Host server name precedence'
+ ),
+ array(
+ 'http://[::1]:81',
+ array(
+ 'HTTP_HOST' => '[::1]',
+ 'SERVER_NAME' => '::1',
+ 'SERVER_PORT' => '81',
+ ),
+ 'Apache bug 26005'
+ ),
+ array(
+ 'http://localhost',
+ array(
+ 'SERVER_NAME' => '[2001'
+ ),
+ 'Kind of like lighttpd per commit message in MW r83847',
+ ),
+ array(
+ 'http://[2a01:e35:2eb4:1::2]:777',
+ array(
+ 'SERVER_NAME' => '[2a01:e35:2eb4:1::2]:777'
+ ),
+ 'Possible lighttpd environment per bug 14977 comment 13',
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/XmlJsTest.php b/tests/phpunit/includes/XmlJsTest.php
new file mode 100644
index 00000000..c5b411fb
--- /dev/null
+++ b/tests/phpunit/includes/XmlJsTest.php
@@ -0,0 +1,9 @@
+<?php
+class XmlJs extends MediaWikiTestCase {
+ public function testConstruction() {
+ $obj = new XmlJsCode( null );
+ $this->assertNull( $obj->value );
+ $obj = new XmlJsCode( '' );
+ $this->assertSame( $obj->value, '' );
+ }
+}
diff --git a/tests/phpunit/includes/XmlSelectTest.php b/tests/phpunit/includes/XmlSelectTest.php
new file mode 100644
index 00000000..bf761e3d
--- /dev/null
+++ b/tests/phpunit/includes/XmlSelectTest.php
@@ -0,0 +1,139 @@
+<?php
+
+// TODO
+class XmlSelectTest extends MediaWikiTestCase {
+ protected $select;
+
+ protected function setUp() {
+ $this->select = new XmlSelect();
+ }
+ protected function tearDown() {
+ $this->select = null;
+ }
+
+ ### START OF TESTS ###
+
+ public function testConstructWithoutParameters() {
+ $this->assertEquals( '<select></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * Parameters are $name (false), $id (false), $default (false)
+ * @dataProvider provideConstructionParameters
+ */
+ public function testConstructParameters( $name, $id, $default, $expected ) {
+ $this->select = new XmlSelect( $name, $id, $default );
+ $this->assertEquals( $expected, $this->select->getHTML() );
+ }
+
+ /**
+ * Provide parameters for testConstructParameters() which use three
+ * parameters:
+ * - $name (default: false)
+ * - $id (default: false)
+ * - $default (default: false)
+ * Provides a fourth parameters representing the expected HTML output
+ *
+ */
+ public function provideConstructionParameters() {
+ return array(
+ /**
+ * Values are set following a 3-bit Gray code where two successive
+ * values differ by only one value.
+ * See http://en.wikipedia.org/wiki/Gray_code
+ */
+ # $name $id $default
+ array( false , false, false, '<select></select>' ),
+ array( false , false, 'foo', '<select></select>' ),
+ array( false , 'id' , 'foo', '<select id="id"></select>' ),
+ array( false , 'id' , false, '<select id="id"></select>' ),
+ array( 'name', 'id' , false, '<select name="name" id="id"></select>' ),
+ array( 'name', 'id' , 'foo', '<select name="name" id="id"></select>' ),
+ array( 'name', false, 'foo', '<select name="name"></select>' ),
+ array( 'name', false, false, '<select name="name"></select>' ),
+ );
+ }
+
+ # Begin XmlSelect::addOption() similar to Xml::option
+ public function testAddOption() {
+ $this->select->addOption( 'foo' );
+ $this->assertEquals( '<select><option value="foo">foo</option></select>', $this->select->getHTML() );
+ }
+ public function testAddOptionWithDefault() {
+ $this->select->addOption( 'foo', true );
+ $this->assertEquals( '<select><option value="1">foo</option></select>', $this->select->getHTML() );
+ }
+ public function testAddOptionWithFalse() {
+ $this->select->addOption( 'foo', false );
+ $this->assertEquals( '<select><option value="foo">foo</option></select>', $this->select->getHTML() );
+ }
+ public function testAddOptionWithValueZero() {
+ $this->select->addOption( 'foo', 0 );
+ $this->assertEquals( '<select><option value="0">foo</option></select>', $this->select->getHTML() );
+ }
+ # End XmlSelect::addOption() similar to Xml::option
+
+ public function testSetDefault() {
+ $this->select->setDefault( 'bar1' );
+ $this->select->addOption( 'foo1' );
+ $this->select->addOption( 'bar1' );
+ $this->select->addOption( 'foo2' );
+ $this->assertEquals(
+'<select><option value="foo1">foo1</option>' . "\n" .
+'<option value="bar1" selected="selected">bar1</option>' . "\n" .
+'<option value="foo2">foo2</option></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * Adding default later on should set the correct selection or
+ * raise an exception.
+ * To handle this, we need to render the options in getHtml()
+ */
+ public function testSetDefaultAfterAddingOptions() {
+ $this->select->addOption( 'foo1' );
+ $this->select->addOption( 'bar1' );
+ $this->select->addOption( 'foo2' );
+ $this->select->setDefault( 'bar1' ); # setting default after adding options
+ $this->assertEquals(
+'<select><option value="foo1">foo1</option>' . "\n" .
+'<option value="bar1" selected="selected">bar1</option>' . "\n" .
+'<option value="foo2">foo2</option></select>', $this->select->getHTML() );
+ }
+
+ public function testGetAttributes() {
+ # create some attributes
+ $this->select->setAttribute( 'dummy', 0x777 );
+ $this->select->setAttribute( 'string', 'euro €' );
+ $this->select->setAttribute( 1911, 'razor' );
+
+ # verify we can retrieve them
+ $this->assertEquals(
+ $this->select->getAttribute( 'dummy' ),
+ 0x777
+ );
+ $this->assertEquals(
+ $this->select->getAttribute( 'string' ),
+ 'euro €'
+ );
+ $this->assertEquals(
+ $this->select->getAttribute( 1911 ),
+ 'razor'
+ );
+
+ # inexistant keys should give us 'null'
+ $this->assertEquals(
+ $this->select->getAttribute( 'I DO NOT EXIT' ),
+ null
+ );
+
+ # verify string / integer
+ $this->assertEquals(
+ $this->select->getAttribute( '1911' ),
+ 'razor'
+ );
+ $this->assertEquals(
+ $this->select->getAttribute( 'dummy' ),
+ 0x777
+ );
+ }
+}
diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php
new file mode 100644
index 00000000..fbb498d8
--- /dev/null
+++ b/tests/phpunit/includes/XmlTest.php
@@ -0,0 +1,304 @@
+<?php
+
+class XmlTest extends MediaWikiTestCase {
+ private static $oldLang;
+
+ public function setUp() {
+ global $wgLang, $wgLanguageCode;
+
+ self::$oldLang = $wgLang;
+ $wgLanguageCode = 'en';
+ $wgLang = Language::factory( $wgLanguageCode );
+ }
+
+ public function tearDown() {
+ global $wgLang, $wgLanguageCode;
+ $wgLang = self::$oldLang;
+ $wgLanguageCode = $wgLang->getCode();
+ }
+
+ public function testExpandAttributes() {
+ $this->assertNull( Xml::expandAttributes(null),
+ 'Converting a null list of attributes'
+ );
+ $this->assertEquals( '', Xml::expandAttributes( array() ),
+ 'Converting an empty list of attributes'
+ );
+ }
+
+ public function testExpandAttributesException() {
+ $this->setExpectedException('MWException');
+ Xml::expandAttributes('string');
+ }
+
+ function testElementOpen() {
+ $this->assertEquals(
+ '<element>',
+ Xml::element( 'element', null, null ),
+ 'Opening element with no attributes'
+ );
+ }
+
+ function testElementEmpty() {
+ $this->assertEquals(
+ '<element />',
+ Xml::element( 'element', null, '' ),
+ 'Terminated empty element'
+ );
+ }
+
+ function testElementInputCanHaveAValueOfZero() {
+ $this->assertEquals(
+ '<input name="name" value="0" />',
+ Xml::input( 'name', false, 0 ),
+ 'Input with a value of 0 (bug 23797)'
+ );
+ }
+ function testElementEscaping() {
+ $this->assertEquals(
+ '<element>hello &lt;there&gt; you &amp; you</element>',
+ Xml::element( 'element', null, 'hello <there> you & you' ),
+ 'Element with no attributes and content that needs escaping'
+ );
+ }
+
+ public function testEscapeTagsOnly() {
+ $this->assertEquals( '&quot;&gt;&lt;', Xml::escapeTagsOnly( '"><' ),
+ 'replace " > and < with their HTML entitites'
+ );
+ }
+
+ function testElementAttributes() {
+ $this->assertEquals(
+ '<element key="value" <>="&lt;&gt;">',
+ Xml::element( 'element', array( 'key' => 'value', '<>' => '<>' ), null ),
+ 'Element attributes, keys are not escaped'
+ );
+ }
+
+ function testOpenElement() {
+ $this->assertEquals(
+ '<element k="v">',
+ Xml::openElement( 'element', array( 'k' => 'v' ) ),
+ 'openElement() shortcut'
+ );
+ }
+
+ function testCloseElement() {
+ $this->assertEquals( '</element>', Xml::closeElement( 'element' ), 'closeElement() shortcut' );
+ }
+
+ public function testDateMenu( ) {
+ $curYear = intval(gmdate('Y'));
+ $prevYear = $curYear - 1;
+
+ $curMonth = intval(gmdate('n'));
+ $prevMonth = $curMonth - 1;
+ if( $prevMonth == 0 ) { $prevMonth = 12; }
+ $nextMonth = $curMonth + 1;
+ if( $nextMonth == 13 ) { $nextMonth = 1; }
+
+
+ $this->assertEquals(
+ '<label for="year">From year (and earlier):</label> <input name="year" size="4" value="2011" id="year" maxlength="4" /> <label for="month">From month (and earlier):</label> <select id="month" name="month" class="mw-month-selector"><option value="-1">all</option>' . "\n" .
+'<option value="1">January</option>' . "\n" .
+'<option value="2" selected="selected">February</option>' . "\n" .
+'<option value="3">March</option>' . "\n" .
+'<option value="4">April</option>' . "\n" .
+'<option value="5">May</option>' . "\n" .
+'<option value="6">June</option>' . "\n" .
+'<option value="7">July</option>' . "\n" .
+'<option value="8">August</option>' . "\n" .
+'<option value="9">September</option>' . "\n" .
+'<option value="10">October</option>' . "\n" .
+'<option value="11">November</option>' . "\n" .
+'<option value="12">December</option></select>',
+ Xml::dateMenu( 2011, 02 ),
+ "Date menu for february 2011"
+ );
+ $this->assertEquals(
+ '<label for="year">From year (and earlier):</label> <input name="year" size="4" value="2011" id="year" maxlength="4" /> <label for="month">From month (and earlier):</label> <select id="month" name="month" class="mw-month-selector"><option value="-1">all</option>' . "\n" .
+'<option value="1">January</option>' . "\n" .
+'<option value="2">February</option>' . "\n" .
+'<option value="3">March</option>' . "\n" .
+'<option value="4">April</option>' . "\n" .
+'<option value="5">May</option>' . "\n" .
+'<option value="6">June</option>' . "\n" .
+'<option value="7">July</option>' . "\n" .
+'<option value="8">August</option>' . "\n" .
+'<option value="9">September</option>' . "\n" .
+'<option value="10">October</option>' . "\n" .
+'<option value="11">November</option>' . "\n" .
+'<option value="12">December</option></select>',
+ Xml::dateMenu( 2011, -1),
+ "Date menu with negative month for 'All'"
+ );
+ $this->assertEquals(
+ Xml::dateMenu( $curYear, $curMonth ),
+ Xml::dateMenu( '' , $curMonth ),
+ "Date menu year is the current one when not specified"
+ );
+ $this->assertEquals(
+ Xml::dateMenu( $prevYear, $nextMonth ),
+ Xml::dateMenu( '', $nextMonth ),
+ "Date menu next month is 11 months ago"
+ );
+
+ # @todo FIXME: Please note there is no year there!
+ $this->assertEquals(
+ '<label for="year">From year (and earlier):</label> <input name="year" size="4" value="" id="year" maxlength="4" /> <label for="month">From month (and earlier):</label> <select id="month" name="month" class="mw-month-selector"><option value="-1">all</option>' . "\n" .
+'<option value="1">January</option>' . "\n" .
+'<option value="2">February</option>' . "\n" .
+'<option value="3">March</option>' . "\n" .
+'<option value="4">April</option>' . "\n" .
+'<option value="5">May</option>' . "\n" .
+'<option value="6">June</option>' . "\n" .
+'<option value="7">July</option>' . "\n" .
+'<option value="8">August</option>' . "\n" .
+'<option value="9">September</option>' . "\n" .
+'<option value="10">October</option>' . "\n" .
+'<option value="11">November</option>' . "\n" .
+'<option value="12">December</option></select>',
+ Xml::dateMenu( '', ''),
+ "Date menu with neither year or month"
+ );
+ }
+
+ #
+ # textarea
+ #
+ function testTextareaNoContent() {
+ $this->assertEquals(
+ '<textarea name="name" id="name" cols="40" rows="5"></textarea>',
+ Xml::textarea( 'name', '' ),
+ 'textarea() with not content'
+ );
+ }
+
+ function testTextareaAttribs() {
+ $this->assertEquals(
+ '<textarea name="name" id="name" cols="20" rows="10">&lt;txt&gt;</textarea>',
+ Xml::textarea( 'name', '<txt>', 20, 10 ),
+ 'textarea() with custom attribs'
+ );
+ }
+
+ #
+ # input and label
+ #
+ function testLabelCreation() {
+ $this->assertEquals(
+ '<label for="id">name</label>',
+ Xml::label( 'name', 'id' ),
+ 'label() with no attribs'
+ );
+ }
+ function testLabelAttributeCanOnlyBeClassOrTitle() {
+ $this->assertEquals(
+ '<label for="id">name</label>',
+ Xml::label( 'name', 'id', array( 'generated' => true ) ),
+ 'label() can not be given a generated attribute'
+ );
+ $this->assertEquals(
+ '<label for="id" class="nice">name</label>',
+ Xml::label( 'name', 'id', array( 'class' => 'nice' ) ),
+ 'label() can get a class attribute'
+ );
+ $this->assertEquals(
+ '<label for="id" title="nice tooltip">name</label>',
+ Xml::label( 'name', 'id', array( 'title' => 'nice tooltip' ) ),
+ 'label() can get a title attribute'
+ );
+ $this->assertEquals(
+ '<label for="id" class="nice" title="nice tooltip">name</label>',
+ Xml::label( 'name', 'id', array(
+ 'generated' => true,
+ 'class' => 'nice',
+ 'title' => 'nice tooltip',
+ 'anotherattr' => 'value',
+ )
+ ),
+ 'label() skip all attributes but "class" and "title"'
+ );
+ }
+
+ #
+ # JS
+ #
+ function testEscapeJsStringSpecialChars() {
+ $this->assertEquals(
+ '\\\\\r\n',
+ Xml::escapeJsString( "\\\r\n" ),
+ 'escapeJsString() with special characters'
+ );
+ }
+
+ function testEncodeJsVarBoolean() {
+ $this->assertEquals(
+ 'true',
+ Xml::encodeJsVar( true ),
+ 'encodeJsVar() with boolean'
+ );
+ }
+
+ function testEncodeJsVarNull() {
+ $this->assertEquals(
+ 'null',
+ Xml::encodeJsVar( null ),
+ 'encodeJsVar() with null'
+ );
+ }
+
+ function testEncodeJsVarArray() {
+ $this->assertEquals(
+ '["a", 1]',
+ Xml::encodeJsVar( array( 'a', 1 ) ),
+ 'encodeJsVar() with array'
+ );
+ $this->assertEquals(
+ '{"a": "a", "b": 1}',
+ Xml::encodeJsVar( array( 'a' => 'a', 'b' => 1 ) ),
+ 'encodeJsVar() with associative array'
+ );
+ }
+
+ function testEncodeJsVarObject() {
+ $this->assertEquals(
+ '{"a": "a", "b": 1}',
+ Xml::encodeJsVar( (object)array( 'a' => 'a', 'b' => 1 ) ),
+ 'encodeJsVar() with object'
+ );
+ }
+
+ function testEncodeJsVarInt() {
+ $this->assertEquals(
+ '123456',
+ Xml::encodeJsVar( 123456 ),
+ 'encodeJsVar() with int'
+ );
+ }
+
+ function testEncodeJsVarFloat() {
+ $this->assertEquals(
+ '1.23456',
+ Xml::encodeJsVar( 1.23456 ),
+ 'encodeJsVar() with float'
+ );
+ }
+
+ function testEncodeJsVarIntString() {
+ $this->assertEquals(
+ '"123456"',
+ Xml::encodeJsVar( '123456' ),
+ 'encodeJsVar() with int-like string'
+ );
+ }
+
+ function testEncodeJsVarFloatString() {
+ $this->assertEquals(
+ '"1.23456"',
+ Xml::encodeJsVar( '1.23456' ),
+ 'encodeJsVar() with float-like string'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/ZipDirectoryReaderTest.php b/tests/phpunit/includes/ZipDirectoryReaderTest.php
new file mode 100644
index 00000000..f7ca59e2
--- /dev/null
+++ b/tests/phpunit/includes/ZipDirectoryReaderTest.php
@@ -0,0 +1,79 @@
+<?php
+
+class ZipDirectoryReaderTest extends MediaWikiTestCase {
+ var $zipDir, $entries;
+
+ function setUp() {
+ $this->zipDir = dirname( __FILE__ ) . '/../data/zip';
+ }
+
+ function zipCallback( $entry ) {
+ $this->entries[] = $entry;
+ }
+
+ function readZipAssertError( $file, $error, $assertMessage ) {
+ $this->entries = array();
+ $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", array( $this, 'zipCallback' ) );
+ $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
+ }
+
+ function readZipAssertSuccess( $file, $assertMessage ) {
+ $this->entries = array();
+ $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", array( $this, 'zipCallback' ) );
+ $this->assertTrue( $status->isOK(), $assertMessage );
+ }
+
+ function testEmpty() {
+ $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
+ }
+
+ function testMultiDisk0() {
+ $this->readZipAssertError( 'split.zip', 'zip-unsupported',
+ 'Split zip error' );
+ }
+
+ function testNoSignature() {
+ $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
+ 'No signature should give "wrong format" error' );
+ }
+
+ function testSimple() {
+ $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
+ $this->assertEquals( $this->entries, array( array(
+ 'name' => 'Class.class',
+ 'mtime' => '20010115000000',
+ 'size' => 1,
+ ) ) );
+ }
+
+ function testBadCentralEntrySignature() {
+ $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
+ 'Bad central entry error' );
+ }
+
+ function testTrailingBytes() {
+ $this->readZipAssertError( 'trail.zip', 'zip-bad',
+ 'Trailing bytes error' );
+ }
+
+ function testWrongCDStart() {
+ $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
+ 'Wrong CD start disk error' );
+ }
+
+
+ function testCentralDirectoryGap() {
+ $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
+ 'CD gap error' );
+ }
+
+ function testCentralDirectoryTruncated() {
+ $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
+ 'CD truncated error (should hit unpack() overrun)' );
+ }
+
+ function testLooksLikeZip64() {
+ $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
+ 'A file which looks like ZIP64 but isn\'t, should give error' );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php
new file mode 100644
index 00000000..227555eb
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiBlockTest.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @group Database
+ */
+class ApiBlockTest extends ApiTestCase {
+
+ function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ function getTokens() {
+ return $this->getTokenList( self::$users['sysop'] );
+ }
+
+ function addDBData() {
+ $user = User::newFromName( 'UTBlockee' );
+
+ if ( $user->getId() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTBlockeePassword' );
+
+ $user->saveSettings();
+ }
+ }
+
+ function testMakeNormalBlock() {
+
+ $data = $this->getTokens();
+
+ $user = User::newFromName( 'UTBlockee' );
+
+ if ( !$user->getId() ) {
+ $this->markTestIncomplete( "The user UTBlockee does not exist" );
+ }
+
+ if( !isset( $data[0]['query']['pages'] ) ) {
+ $this->markTestIncomplete( "No block token found" );
+ }
+
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'block',
+ 'user' => 'UTBlockee',
+ 'reason' => BlockTest::REASON,
+ 'token' => $pageinfo['blocktoken'] ), $data );
+
+ $block = Block::newFromTarget('UTBlockee');
+
+ $this->assertTrue( !is_null( $block ), 'Block is valid' );
+
+ $this->assertEquals( 'UTBlockee', (string)$block->getTarget() );
+ $this->assertEquals( 'Some reason', $block->mReason );
+ $this->assertEquals( 'infinity', $block->mExpiry );
+
+ }
+
+}
diff --git a/tests/phpunit/includes/api/ApiPurgeTest.php b/tests/phpunit/includes/api/ApiPurgeTest.php
new file mode 100644
index 00000000..db1563e9
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiPurgeTest.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @group Database
+ */
+class ApiPurgeTest extends ApiTestCase {
+
+ function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ function testPurgeMainPage() {
+
+ if ( !Title::newFromText( 'UTPage' )->exists() ) {
+ $this->markTestIncomplete( "The article [[UTPage]] does not exist" );
+ }
+
+ $somePage = mt_rand();
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'purge',
+ 'titles' => 'UTPage|' . $somePage . '|%5D' ) );
+
+ $this->assertArrayHasKey( 'purge', $data[0] );
+
+ $this->assertArrayHasKey( 0, $data[0]['purge'] );
+ $this->assertArrayHasKey( 'purged', $data[0]['purge'][0] );
+ $this->assertEquals( 'UTPage', $data[0]['purge'][0]['title'] );
+
+ $this->assertArrayHasKey( 1, $data[0]['purge'] );
+ $this->assertArrayHasKey( 'missing', $data[0]['purge'][1] );
+ $this->assertEquals( $somePage, $data[0]['purge'][1]['title'] );
+
+ $this->assertArrayHasKey( 2, $data[0]['purge'] );
+ $this->assertArrayHasKey( 'invalid', $data[0]['purge'][2] );
+ $this->assertEquals( '%5D', $data[0]['purge'][2]['title'] );
+
+ }
+
+}
diff --git a/tests/phpunit/includes/api/ApiQueryTest.php b/tests/phpunit/includes/api/ApiQueryTest.php
new file mode 100644
index 00000000..114eadf3
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiQueryTest.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @group Database
+ */
+class ApiQueryTest extends ApiTestCase {
+
+ function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ function testTitlesGetNormalized() {
+
+ global $wgMetaNamespace;
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'titles' => 'Project:articleA|article_B' ) );
+
+
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'normalized', $data[0]['query'] );
+
+ $this->assertEquals(
+ array(
+ 'from' => 'Project:articleA',
+ 'to' => $wgMetaNamespace . ':ArticleA'
+ ),
+ $data[0]['query']['normalized'][0]
+ );
+
+ $this->assertEquals(
+ array(
+ 'from' => 'article_B',
+ 'to' => 'Article B'
+ ),
+ $data[0]['query']['normalized'][1]
+ );
+
+ }
+
+ function testTitlesAreRejectedIfInvalid() {
+ $title = false;
+ while( !$title || Title::newFromText( $title )->exists() ) {
+ $title = md5( mt_rand( 0, 10000 ) + rand( 0, 999000 ) );
+ }
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'titles' => $title . '|Talk:' ) );
+
+
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'pages', $data[0]['query'] );
+ $this->assertEquals( 2, count( $data[0]['query']['pages'] ) );
+
+ $this->assertArrayHasKey( -2, $data[0]['query']['pages'] );
+ $this->assertArrayHasKey( -1, $data[0]['query']['pages'] );
+
+ $this->assertArrayHasKey( 'missing', $data[0]['query']['pages'][-2] );
+ $this->assertArrayHasKey( 'invalid', $data[0]['query']['pages'][-1] );
+
+
+ }
+
+}
diff --git a/tests/phpunit/includes/api/ApiTest.php b/tests/phpunit/includes/api/ApiTest.php
new file mode 100644
index 00000000..a587e6b1
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTest.php
@@ -0,0 +1,277 @@
+<?php
+
+/**
+ * @group Database
+ */
+class ApiTest extends ApiTestCase {
+
+ function testRequireOnlyOneParameterDefault() {
+ $mock = new MockApi();
+
+ $this->assertEquals(
+ null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt",
+ "enablechunks" => false ), "filename", "enablechunks" ) );
+ }
+
+ /**
+ * @expectedException UsageException
+ */
+ function testRequireOnlyOneParameterZero() {
+ $mock = new MockApi();
+
+ $this->assertEquals(
+ null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt",
+ "enablechunks" => 0 ), "filename", "enablechunks" ) );
+ }
+
+ /**
+ * @expectedException UsageException
+ */
+ function testRequireOnlyOneParameterTrue() {
+ $mock = new MockApi();
+
+ $this->assertEquals(
+ null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt",
+ "enablechunks" => true ), "filename", "enablechunks" ) );
+ }
+
+ /**
+ * Test that the API will accept a FauxRequest and execute. The help action
+ * (default) throws a UsageException. Just validate we're getting proper XML
+ *
+ * @expectedException UsageException
+ */
+ function testApi() {
+
+ $api = new ApiMain(
+ new FauxRequest( array( 'action' => 'help', 'format' => 'xml' ) )
+ );
+ $api->execute();
+ $api->getPrinter()->setBufferResult( true );
+ $api->printResult( false );
+ $resp = $api->getPrinter()->getBuffer();
+
+ libxml_use_internal_errors( true );
+ $sxe = simplexml_load_string( $resp );
+ $this->assertNotInternalType( "bool", $sxe );
+ $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) );
+ }
+
+ /**
+ * Test result of attempted login with an empty username
+ */
+ function testApiLoginNoName() {
+ $data = $this->doApiRequest( array( 'action' => 'login',
+ 'lgname' => '', 'lgpassword' => self::$users['sysop']->password,
+ ) );
+ $this->assertEquals( 'NoName', $data[0]['login']['result'] );
+ }
+
+ function testApiLoginBadPass() {
+ global $wgServer;
+
+ $user = self::$users['sysop'];
+ $user->user->logOut();
+
+ if ( !isset( $wgServer ) ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+ $ret = $this->doApiRequest( array(
+ "action" => "login",
+ "lgname" => $user->username,
+ "lgpassword" => "bad",
+ )
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotInternalType( "bool", $result );
+ $a = $result["login"]["result"];
+ $this->assertEquals( "NeedToken", $a );
+
+ $token = $result["login"]["token"];
+
+ $ret = $this->doApiRequest( array(
+ "action" => "login",
+ "lgtoken" => $token,
+ "lgname" => $user->username,
+ "lgpassword" => "badnowayinhell",
+ )
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotInternalType( "bool", $result );
+ $a = $result["login"]["result"];
+
+ $this->assertEquals( "WrongPass", $a );
+ }
+
+ function testApiLoginGoodPass() {
+ global $wgServer;
+
+ if ( !isset( $wgServer ) ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+
+ $user = self::$users['sysop'];
+ $user->user->logOut();
+
+ $ret = $this->doApiRequest( array(
+ "action" => "login",
+ "lgname" => $user->username,
+ "lgpassword" => $user->password,
+ )
+ );
+
+ $result = $ret[0];
+ $this->assertNotInternalType( "bool", $result );
+ $this->assertNotInternalType( "null", $result["login"] );
+
+ $a = $result["login"]["result"];
+ $this->assertEquals( "NeedToken", $a );
+ $token = $result["login"]["token"];
+
+ $ret = $this->doApiRequest( array(
+ "action" => "login",
+ "lgtoken" => $token,
+ "lgname" => $user->username,
+ "lgpassword" => $user->password,
+ )
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotInternalType( "bool", $result );
+ $a = $result["login"]["result"];
+
+ $this->assertEquals( "Success", $a );
+ }
+
+ function testApiGotCookie() {
+ $this->markTestIncomplete( "The server can't do external HTTP requests, and the internal one won't give cookies" );
+
+ global $wgServer, $wgScriptPath;
+
+ if ( !isset( $wgServer ) ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+ $user = self::$users['sysop'];
+
+ $req = MWHttpRequest::factory( self::$apiUrl . "?action=login&format=xml",
+ array( "method" => "POST",
+ "postData" => array(
+ "lgname" => $user->username,
+ "lgpassword" => $user->password ) ) );
+ $req->execute();
+
+ libxml_use_internal_errors( true );
+ $sxe = simplexml_load_string( $req->getContent() );
+ $this->assertNotInternalType( "bool", $sxe );
+ $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) );
+ $this->assertNotInternalType( "null", $sxe->login[0] );
+
+ $a = $sxe->login[0]->attributes()->result[0];
+ $this->assertEquals( ' result="NeedToken"', $a->asXML() );
+ $token = (string)$sxe->login[0]->attributes()->token;
+
+ $req->setData( array(
+ "lgtoken" => $token,
+ "lgname" => $user->username,
+ "lgpassword" => $user->password ) );
+ $req->execute();
+
+ $cj = $req->getCookieJar();
+ $serverName = parse_url( $wgServer, PHP_URL_HOST );
+ $this->assertNotEquals( false, $serverName );
+ $serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName );
+ $this->assertNotEquals( '', $serializedCookie );
+ $this->assertRegexp( '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $user->userName . '; .*Token=/', $serializedCookie );
+
+ return $cj;
+ }
+
+ /**
+ * @depends testApiGotCookie
+ */
+ function testApiListPages( CookieJar $cj ) {
+ $this->markTestIncomplete( "Not done with this yet" );
+ global $wgServer;
+
+ if ( $wgServer == "http://localhost" ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+ $req = MWHttpRequest::factory( self::$apiUrl . "?action=query&format=xml&prop=revisions&" .
+ "titles=Main%20Page&rvprop=timestamp|user|comment|content" );
+ $req->setCookieJar( $cj );
+ $req->execute();
+ libxml_use_internal_errors( true );
+ $sxe = simplexml_load_string( $req->getContent() );
+ $this->assertNotInternalType( "bool", $sxe );
+ $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) );
+ $a = $sxe->query[0]->pages[0]->page[0]->attributes();
+ }
+
+ function testRunLogin() {
+ $sysopUser = self::$users['sysop'];
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => $sysopUser->username,
+ 'lgpassword' => $sysopUser->password ) );
+
+ $this->assertArrayHasKey( "login", $data[0] );
+ $this->assertArrayHasKey( "result", $data[0]['login'] );
+ $this->assertEquals( "NeedToken", $data[0]['login']['result'] );
+ $token = $data[0]['login']['token'];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ "lgtoken" => $token,
+ "lgname" => $sysopUser->username,
+ "lgpassword" => $sysopUser->password ), $data );
+
+ $this->assertArrayHasKey( "login", $data[0] );
+ $this->assertArrayHasKey( "result", $data[0]['login'] );
+ $this->assertEquals( "Success", $data[0]['login']['result'] );
+ $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] );
+
+ return $data;
+ }
+
+ function testGettingToken() {
+ foreach ( self::$users as $user ) {
+ $this->runTokenTest( $user );
+ }
+ }
+
+ function runTokenTest( $user ) {
+
+ $data = $this->getTokenList( $user );
+
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'pages', $data[0]['query'] );
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+
+ $rights = $user->user->getRights();
+
+ $this->assertArrayHasKey( $key, $data[0]['query']['pages'] );
+ $this->assertArrayHasKey( 'edittoken', $data[0]['query']['pages'][$key] );
+ $this->assertArrayHasKey( 'movetoken', $data[0]['query']['pages'][$key] );
+
+ if ( isset( $rights['delete'] ) ) {
+ $this->assertArrayHasKey( 'deletetoken', $data[0]['query']['pages'][$key] );
+ }
+
+ if ( isset( $rights['block'] ) ) {
+ $this->assertArrayHasKey( 'blocktoken', $data[0]['query']['pages'][$key] );
+ $this->assertArrayHasKey( 'unblocktoken', $data[0]['query']['pages'][$key] );
+ }
+
+ if ( isset( $rights['protect'] ) ) {
+ $this->assertArrayHasKey( 'protecttoken', $data[0]['query']['pages'][$key] );
+ }
+
+ return $data;
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php
new file mode 100644
index 00000000..2917c880
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTestCase.php
@@ -0,0 +1,139 @@
+<?php
+
+abstract class ApiTestCase extends MediaWikiLangTestCase {
+ /**
+ * @var Array of ApiTestUser
+ */
+ public static $users;
+ protected static $apiUrl;
+
+ function setUp() {
+ global $wgContLang, $wgAuth, $wgMemc, $wgRequest, $wgUser, $wgServer;
+
+ parent::setUp();
+ self::$apiUrl = $wgServer . wfScript( 'api' );
+ $wgMemc = new EmptyBagOStuff();
+ $wgContLang = Language::factory( 'en' );
+ $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' );
+ $wgRequest = new FauxRequest( array() );
+
+ self::$users = array(
+ 'sysop' => new ApiTestUser(
+ 'Apitestsysop',
+ 'Api Test Sysop',
+ 'api_test_sysop@sample.com',
+ array( 'sysop' )
+ ),
+ 'uploader' => new ApiTestUser(
+ 'Apitestuser',
+ 'Api Test User',
+ 'api_test_user@sample.com',
+ array()
+ )
+ );
+
+ $wgUser = self::$users['sysop']->user;
+
+ }
+
+ protected function doApiRequest( $params, $session = null, $appendModule = false ) {
+ if ( is_null( $session ) ) {
+ $session = array();
+ }
+
+ $request = new FauxRequest( $params, true, $session );
+ $module = new ApiMain( $request, true );
+ $module->execute();
+
+ $results = array( $module->getResultData(), $request, $request->getSessionArray() );
+ if( $appendModule ) {
+ $results[] = $module;
+ }
+
+ return $results;
+ }
+
+ /**
+ * Add an edit token to the API request
+ * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the
+ * request, without actually requesting a "real" edit token
+ * @param $params: key-value API params
+ * @param $session: session array
+ */
+ protected function doApiRequestWithToken( $params, $session ) {
+ if ( $session['wsToken'] ) {
+ // add edit token to fake session
+ $session['wsEditToken'] = $session['wsToken'];
+ // add token to request parameters
+ $params['token'] = md5( $session['wsToken'] ) . User::EDIT_TOKEN_SUFFIX;
+ return $this->doApiRequest( $params, $session );
+ } else {
+ throw new Exception( "request data not in right format" );
+ }
+ }
+
+ protected function doLogin() {
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => self::$users['sysop']->username,
+ 'lgpassword' => self::$users['sysop']->password ) );
+
+ $token = $data[0]['login']['token'];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgtoken' => $token,
+ 'lgname' => self::$users['sysop']->username,
+ 'lgpassword' => self::$users['sysop']->password
+ ), $data );
+
+ return $data;
+ }
+
+ protected function getTokenList( $user ) {
+ $GLOBALS['wgUser'] = $user->user;
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'titles' => 'Main Page',
+ 'intoken' => 'edit|delete|protect|move|block|unblock',
+ 'prop' => 'info' ) );
+ return $data;
+ }
+}
+
+class UserWrapper {
+ public $userName, $password, $user;
+
+ public function __construct( $userName, $password, $group = '' ) {
+ $this->userName = $userName;
+ $this->password = $password;
+
+ $this->user = User::newFromName( $this->userName );
+ if ( !$this->user->getID() ) {
+ $this->user = User::createNew( $this->userName, array(
+ "email" => "test@example.com",
+ "real_name" => "Test User" ) );
+ }
+ $this->user->setPassword( $this->password );
+
+ if ( $group !== '' ) {
+ $this->user->addGroup( $group );
+ }
+ $this->user->saveSettings();
+ }
+}
+
+class MockApi extends ApiBase {
+ public function execute() { }
+ public function getVersion() { }
+
+ public function __construct() { }
+
+ public function getAllowedParams() {
+ return array(
+ 'filename' => null,
+ 'enablechunks' => false,
+ 'sessionkey' => null,
+ );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiTestCaseUpload.php b/tests/phpunit/includes/api/ApiTestCaseUpload.php
new file mode 100644
index 00000000..e51e7214
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTestCaseUpload.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * * Abstract class to support upload tests
+ */
+
+abstract class ApiTestCaseUpload extends ApiTestCase {
+ /**
+ * Fixture -- run before every test
+ */
+ public function setUp() {
+ global $wgEnableUploads, $wgEnableAPI;
+ parent::setUp();
+
+ $wgEnableUploads = true;
+ $wgEnableAPI = true;
+ wfSetupSession();
+
+ $this->clearFakeUploads();
+ }
+
+ /**
+ * Helper function -- remove files and associated articles by Title
+ * @param $title Title: title to be removed
+ */
+ public function deleteFileByTitle( $title ) {
+ if ( $title->exists() ) {
+ $file = wfFindFile( $title, array( 'ignoreRedirect' => true ) );
+ $noOldArchive = ""; // yes this really needs to be set this way
+ $comment = "removing for test";
+ $restrictDeletedVersions = false;
+ $status = FileDeleteForm::doDelete( $title, $file, $noOldArchive, $comment, $restrictDeletedVersions );
+ if ( !$status->isGood() ) {
+ return false;
+ }
+ $article = new Article( $title );
+ $article->doDeleteArticle( "removing for test" );
+
+ // see if it now doesn't exist; reload
+ $title = Title::newFromText( $title->getText(), NS_FILE );
+ }
+ return ! ( $title && $title instanceof Title && $title->exists() );
+ }
+
+ /**
+ * Helper function -- remove files and associated articles with a particular filename
+ * @param $fileName String: filename to be removed
+ */
+ public function deleteFileByFileName( $fileName ) {
+ return $this->deleteFileByTitle( Title::newFromText( $fileName, NS_FILE ) );
+ }
+
+
+ /**
+ * Helper function -- given a file on the filesystem, find matching content in the db (and associated articles) and remove them.
+ * @param $filePath String: path to file on the filesystem
+ */
+ public function deleteFileByContent( $filePath ) {
+ $hash = File::sha1Base36( $filePath );
+ $dupes = RepoGroup::singleton()->findBySha1( $hash );
+ $success = true;
+ foreach ( $dupes as $dupe ) {
+ $success &= $this->deleteFileByTitle( $dupe->getTitle() );
+ }
+ return $success;
+ }
+
+ /**
+ * Fake an upload by dumping the file into temp space, and adding info to $_FILES.
+ * (This is what PHP would normally do).
+ * @param $fieldName String: name this would have in the upload form
+ * @param $fileName String: name to title this
+ * @param $type String: mime type
+ * @param $filePath String: path where to find file contents
+ */
+ function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) {
+ $tmpName = tempnam( wfTempDir(), "" );
+ if ( !file_exists( $filePath ) ) {
+ throw new Exception( "$filePath doesn't exist!" );
+ };
+
+ if ( !copy( $filePath, $tmpName ) ) {
+ throw new Exception( "couldn't copy $filePath to $tmpName" );
+ }
+
+ clearstatcache();
+ $size = filesize( $tmpName );
+ if ( $size === false ) {
+ throw new Exception( "couldn't stat $tmpName" );
+ }
+
+ $_FILES[ $fieldName ] = array(
+ 'name' => $fileName,
+ 'type' => $type,
+ 'tmp_name' => $tmpName,
+ 'size' => $size,
+ 'error' => null
+ );
+
+ return true;
+
+ }
+
+ /**
+ * Remove traces of previous fake uploads
+ */
+ function clearFakeUploads() {
+ $_FILES = array();
+ }
+
+
+
+
+}
diff --git a/tests/phpunit/includes/api/ApiTestUser.php b/tests/phpunit/includes/api/ApiTestUser.php
new file mode 100644
index 00000000..df60682f
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTestUser.php
@@ -0,0 +1,59 @@
+<?php
+
+/* Wraps the user object, so we can also retain full access to properties like password if we log in via the API */
+class ApiTestUser {
+ public $username;
+ public $password;
+ public $email;
+ public $groups;
+ public $user;
+
+ function __construct( $username, $realname = 'Real Name', $email = 'sample@sample.com', $groups = array() ) {
+ $this->username = $username;
+ $this->realname = $realname;
+ $this->email = $email;
+ $this->groups = $groups;
+
+ // don't allow user to hardcode or select passwords -- people sometimes run tests
+ // on live wikis. Sometimes we create sysop users in these tests. A sysop user with
+ // a known password would be a Bad Thing.
+ $this->password = User::randomPassword();
+
+ $this->user = User::newFromName( $this->username );
+ $this->user->load();
+
+ // In an ideal world we'd have a new wiki (or mock data store) for every single test.
+ // But for now, we just need to create or update the user with the desired properties.
+ // we particularly need the new password, since we just generated it randomly.
+ // In core MediaWiki, there is no functionality to delete users, so this is the best we can do.
+ if ( !$this->user->getID() ) {
+ // create the user
+ $this->user = User::createNew(
+ $this->username, array(
+ "email" => $this->email,
+ "real_name" => $this->realname
+ )
+ );
+ if ( !$this->user ) {
+ throw new Exception( "error creating user" );
+ }
+ }
+
+ // update the user to use the new random password and other details
+ $this->user->setPassword( $this->password );
+ $this->user->setEmail( $this->email );
+ $this->user->setRealName( $this->realname );
+ // remove all groups, replace with any groups specified
+ foreach ( $this->user->getGroups() as $group ) {
+ $this->user->removeGroup( $group );
+ }
+ if ( count( $this->groups ) ) {
+ foreach ( $this->groups as $group ) {
+ $this->user->addGroup( $group );
+ }
+ }
+ $this->user->saveSettings();
+
+ }
+
+}
diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php
new file mode 100644
index 00000000..5c929784
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiUploadTest.php
@@ -0,0 +1,433 @@
+<?php
+
+/**
+ * @group Database
+ */
+
+/**
+ * n.b. Ensure that you can write to the images/ directory as the
+ * user that will run tests.
+ */
+
+// Note for reviewers: this intentionally duplicates functionality already in "ApiSetup" and so on.
+// This framework works better IMO and has less strangeness (such as test cases inheriting from "ApiSetup"...)
+// (and in the case of the other Upload tests, this flat out just actually works... )
+
+// TODO: port the other Upload tests, and other API tests to this framework
+
+require_once( 'ApiTestCaseUpload.php' );
+
+/**
+ * @group Database
+ *
+ * This is pretty sucky... needs to be prettified.
+ */
+class ApiUploadTest extends ApiTestCaseUpload {
+
+ /**
+ * Testing login
+ * XXX this is a funny way of getting session context
+ */
+ function testLogin() {
+ $user = self::$users['uploader'];
+
+ $params = array(
+ 'action' => 'login',
+ 'lgname' => $user->username,
+ 'lgpassword' => $user->password
+ );
+ list( $result, , $session ) = $this->doApiRequest( $params );
+ $this->assertArrayHasKey( "login", $result );
+ $this->assertArrayHasKey( "result", $result['login'] );
+ $this->assertEquals( "NeedToken", $result['login']['result'] );
+ $token = $result['login']['token'];
+
+ $params = array(
+ 'action' => 'login',
+ 'lgtoken' => $token,
+ 'lgname' => $user->username,
+ 'lgpassword' => $user->password
+ );
+ list( $result, , $session ) = $this->doApiRequest( $params, $session );
+ $this->assertArrayHasKey( "login", $result );
+ $this->assertArrayHasKey( "result", $result['login'] );
+ $this->assertEquals( "Success", $result['login']['result'] );
+ $this->assertArrayHasKey( 'lgtoken', $result['login'] );
+
+ return $session;
+
+ }
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadRequiresToken( $session ) {
+ $exception = false;
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload'
+ ) );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "The token parameter must be set", $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+ }
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadMissingParams( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $exception = false;
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'upload',
+ ), $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "One of the parameters filekey, file, url, statuskey is required",
+ $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUpload( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ }
+ catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ $filePath = $filePaths[0];
+ $fileSize = filesize( $filePath );
+ $fileName = basename( $filePath );
+
+ $this->deleteFileByFileName( $fileName );
+ $this->deleteFileByContent( $filePath );
+
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileName,
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName",
+ );
+
+ $exception = false;
+ try {
+ list( $result, , ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] );
+ $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePath );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadZeroLength( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $mimeType = 'image/png';
+
+ $filePath = tempnam( wfTempDir(), "" );
+ $fileName = "apiTestUploadZeroLength.png";
+
+ $this->deleteFileByFileName( $fileName );
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileName,
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName",
+ );
+
+ $exception = false;
+ try {
+ $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $this->assertContains( 'The file you submitted was empty', $e->getMessage() );
+ $exception = true;
+ }
+ $this->assertTrue( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePath );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadSameFileName( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ }
+ catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $filePaths = $randomImageGenerator->writeImages( 2, $extension, wfTempDir() );
+ // we'll reuse this filename
+ $fileName = basename( $filePaths[0] );
+
+ // clear any other files with the same name
+ $this->deleteFileByFileName( $fileName );
+
+ // we reuse these params
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileName,
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName",
+ );
+
+ // first upload .... should succeed
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $exception = false;
+ try {
+ list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception );
+
+ // second upload with the same name (but different content)
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $exception = false;
+ try {
+ list( $result, , ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Warning', $result['upload']['result'] );
+ $this->assertTrue( isset( $result['upload']['warnings'] ) );
+ $this->assertTrue( isset( $result['upload']['warnings']['exists'] ) );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePaths[0] );
+ unlink( $filePaths[1] );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadSameContent( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ }
+ catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ $fileNames[0] = basename( $filePaths[0] );
+ $fileNames[1] = "SameContentAs" . $fileNames[0];
+
+ // clear any other files with the same name or content
+ $this->deleteFileByContent( $filePaths[0] );
+ $this->deleteFileByFileName( $fileNames[0] );
+ $this->deleteFileByFileName( $fileNames[1] );
+
+ // first upload .... should succeed
+
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileNames[0],
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for " . $fileNames[0],
+ );
+
+ if (! $this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePaths[0] ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $exception = false;
+ try {
+ list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception );
+
+
+ // second upload with the same content (but different name)
+
+ if (! $this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePaths[0] ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileNames[1],
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for " . $fileNames[1],
+ );
+
+ $exception = false;
+ try {
+ list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Warning', $result['upload']['result'] );
+ $this->assertTrue( isset( $result['upload']['warnings'] ) );
+ $this->assertTrue( isset( $result['upload']['warnings']['duplicate'] ) );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileNames[0] );
+ $this->deleteFileByFilename( $fileNames[1] );
+ unlink( $filePaths[0] );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadStash( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ }
+ catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ $filePath = $filePaths[0];
+ $fileSize = filesize( $filePath );
+ $fileName = basename( $filePath );
+
+ $this->deleteFileByFileName( $fileName );
+ $this->deleteFileByContent( $filePath );
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $params = array(
+ 'action' => 'upload',
+ 'stash' => 1,
+ 'filename' => $fileName,
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName",
+ );
+
+ $exception = false;
+ try {
+ list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertFalse( $exception );
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] );
+ $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
+ $this->assertTrue( isset( $result['upload']['filekey'] ) );
+ $this->assertEquals( $result['upload']['sessionkey'], $result['upload']['filekey'] );
+ $filekey = $result['upload']['filekey'];
+
+ // it should be visible from Special:UploadStash
+ // XXX ...but how to test this, with a fake WebRequest with the session?
+
+ // now we should try to release the file from stash
+ $params = array(
+ 'action' => 'upload',
+ 'filekey' => $filekey,
+ 'filename' => $fileName,
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName, altered",
+ );
+
+ $this->clearFakeUploads();
+ $exception = false;
+ try {
+ list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePath );
+ }
+}
+
diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php
new file mode 100644
index 00000000..3c7ff304
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiWatchTest.php
@@ -0,0 +1,179 @@
+<?php
+
+/**
+ * @group Database
+ * @todo This test suite is severly broken and need a full review
+ */
+class ApiWatchTest extends ApiTestCase {
+
+ function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ function getTokens() {
+ return $this->getTokenList( self::$users['sysop'] );
+ }
+
+ /**
+ * @group Broken
+ */
+ function testWatchEdit() {
+
+ $data = $this->getTokens();
+
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'edit',
+ 'title' => 'UTPage',
+ 'text' => 'new text',
+ 'token' => $pageinfo['edittoken'],
+ 'watchlist' => 'watch' ), $data );
+ $this->assertArrayHasKey( 'edit', $data[0] );
+ $this->assertArrayHasKey( 'result', $data[0]['edit'] );
+ $this->assertEquals( 'Success', $data[0]['edit']['result'] );
+
+ return $data;
+ }
+
+ /**
+ * @depends testWatchEdit
+ */
+ function testWatchClear() {
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'list' => 'watchlist' ), $data );
+
+ if ( isset( $data[0]['query']['watchlist'] ) ) {
+ $wl = $data[0]['query']['watchlist'];
+
+ foreach ( $wl as $page ) {
+ $data = $this->doApiRequest( array(
+ 'action' => 'watch',
+ 'title' => $page['title'],
+ 'unwatch' => true ), $data );
+ }
+ }
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'list' => 'watchlist' ), $data );
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'watchlist', $data[0]['query'] );
+ $this->assertEquals( 0, count( $data[0]['query']['watchlist'] ) );
+
+ return $data;
+ }
+
+ /**
+ * @group Broken
+ */
+ function testWatchProtect() {
+
+ $data = $this->getTokens();
+
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'protect',
+ 'token' => $pageinfo['protecttoken'],
+ 'title' => 'UTPage',
+ 'protections' => 'edit=sysop',
+ 'watchlist' => 'unwatch' ), $data );
+
+ $this->assertArrayHasKey( 'protect', $data[0] );
+ $this->assertArrayHasKey( 'protections', $data[0]['protect'] );
+ $this->assertEquals( 1, count( $data[0]['protect']['protections'] ) );
+ $this->assertArrayHasKey( 'edit', $data[0]['protect']['protections'][0] );
+ }
+
+
+ function testGetRollbackToken() {
+
+ $data = $this->getTokens();
+
+ if ( !Title::newFromText( 'UTPage' )->exists() ) {
+ $this->markTestIncomplete( "The article [[UTPage]] does not exist" );
+ }
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'prop' => 'revisions',
+ 'titles' => 'UTPage',
+ 'rvtoken' => 'rollback' ), $data );
+
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'pages', $data[0]['query'] );
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+
+ if ( isset( $data[0]['query']['pages'][$key]['missing'] ) ) {
+ $this->markTestIncomplete( "Target page (UTPage) doesn't exist" );
+ }
+
+ $this->assertArrayHasKey( 'pageid', $data[0]['query']['pages'][$key] );
+ $this->assertArrayHasKey( 'revisions', $data[0]['query']['pages'][$key] );
+ $this->assertArrayHasKey( 0, $data[0]['query']['pages'][$key]['revisions'] );
+ $this->assertArrayHasKey( 'rollbacktoken', $data[0]['query']['pages'][$key]['revisions'][0] );
+
+ return $data;
+ }
+
+ /**
+ * @depends testGetRollbackToken
+ * @group Broken
+ */
+ function testWatchRollback( $data ) {
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key]['revisions'][0];
+
+ try {
+ $data = $this->doApiRequest( array(
+ 'action' => 'rollback',
+ 'title' => 'UTPage',
+ 'user' => $pageinfo['user'],
+ 'token' => $pageinfo['rollbacktoken'],
+ 'watchlist' => 'watch' ), $data );
+ } catch( UsageException $ue ) {
+ if( $ue->getCodeString() == 'onlyauthor' ) {
+ $this->markTestIncomplete( "Only one author to 'UTPage', cannot test rollback" );
+ } else {
+ $this->fail( "Received error '" . $ue->getCodeString() . "'" );
+ }
+ }
+
+ $this->assertArrayHasKey( 'rollback', $data[0] );
+ $this->assertArrayHasKey( 'title', $data[0]['rollback'] );
+ }
+
+ /**
+ * @group Broken
+ */
+ function testWatchDelete() {
+
+ $data = $this->getTokens();
+
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'delete',
+ 'token' => $pageinfo['deletetoken'],
+ 'title' => 'UTPage' ), $data );
+ $this->assertArrayHasKey( 'delete', $data[0] );
+ $this->assertArrayHasKey( 'title', $data[0]['delete'] );
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'list' => 'watchlist' ), $data );
+
+ $this->markTestIncomplete( 'This test needs to verify the deleted article was added to the users watchlist' );
+ }
+}
diff --git a/tests/phpunit/includes/api/RandomImageGenerator.php b/tests/phpunit/includes/api/RandomImageGenerator.php
new file mode 100644
index 00000000..ae349978
--- /dev/null
+++ b/tests/phpunit/includes/api/RandomImageGenerator.php
@@ -0,0 +1,473 @@
+<?php
+
+/*
+ * RandomImageGenerator -- does what it says on the tin.
+ * Requires Imagick, the ImageMagick library for PHP, or the command line equivalent (usually 'convert').
+ *
+ * Because MediaWiki tests the uniqueness of media upload content, and filenames, it is sometimes useful to generate
+ * files that are guaranteed (or at least very likely) to be unique in both those ways.
+ * This generates a number of filenames with random names and random content (colored triangles)
+ *
+ * It is also useful to have fresh content because our tests currently run in a "destructive" mode, and don't create a fresh new wiki for each
+ * test run.
+ * Consequently, if we just had a few static files we kept re-uploading, we'd get lots of warnings about matching content or filenames,
+ * and even if we deleted those files, we'd get warnings about archived files.
+ *
+ * This can also be used with a cronjob to generate random files all the time -- I use it to have a constant, never ending supply when I'm
+ * testing interactively.
+ *
+ * @file
+ * @author Neil Kandalgaonkar <neilk@wikimedia.org>
+ */
+
+/**
+ * RandomImageGenerator: does what it says on the tin.
+ * Can fetch a random image, or also write a number of them to disk with random filenames.
+ */
+class RandomImageGenerator {
+
+ private $dictionaryFile;
+ private $minWidth = 400;
+ private $maxWidth = 800;
+ private $minHeight = 400;
+ private $maxHeight = 800;
+ private $shapesToDraw = 5;
+ private $imageWriteMethod;
+
+ /**
+ * Orientations: 0th row, 0th column, EXIF orientation code, rotation 2x2 matrix that is opposite of orientation
+ * n.b. we do not handle the 'flipped' orientations, which is why there is no entry for 2, 4, 5, or 7. Those
+ * seem to be rare in real images anyway
+ * (we also would need a non-symmetric shape for the images to test those, like a letter F)
+ */
+ private static $orientations = array(
+ array(
+ '0thRow' => 'top',
+ '0thCol' => 'left',
+ 'exifCode' => 1,
+ 'counterRotation' => array( array( 1, 0 ), array( 0, 1 ) )
+ ),
+ array(
+ '0thRow' => 'bottom',
+ '0thCol' => 'right',
+ 'exifCode' => 3,
+ 'counterRotation' => array( array( -1, 0 ), array( 0, -1 ) )
+ ),
+ array(
+ '0thRow' => 'right',
+ '0thCol' => 'top',
+ 'exifCode' => 6,
+ 'counterRotation' => array( array( 0, 1 ), array( 1, 0 ) )
+ ),
+ array(
+ '0thRow' => 'left',
+ '0thCol' => 'bottom',
+ 'exifCode' => 8,
+ 'counterRotation' => array( array( 0, -1 ), array( -1, 0 ) )
+ )
+ );
+
+
+ public function __construct( $options = array() ) {
+ foreach ( array( 'dictionaryFile', 'minWidth', 'minHeight', 'maxHeight', 'shapesToDraw' ) as $property ) {
+ if ( isset( $options[$property] ) ) {
+ $this->$property = $options[$property];
+ }
+ }
+
+ // find the dictionary file, to generate random names
+ if ( !isset( $this->dictionaryFile ) ) {
+ foreach ( array(
+ '/usr/share/dict/words',
+ '/usr/dict/words',
+ dirname( __FILE__ ) . '/words.txt' )
+ as $dictionaryFile ) {
+ if ( is_file( $dictionaryFile ) and is_readable( $dictionaryFile ) ) {
+ $this->dictionaryFile = $dictionaryFile;
+ break;
+ }
+ }
+ }
+ if ( !isset( $this->dictionaryFile ) ) {
+ throw new Exception( "RandomImageGenerator: dictionary file not found or not specified properly" );
+ }
+
+ if ( !class_exists( 'Imagick' ) ) {
+ throw new Exception( 'No Imagick extension' );
+ }
+ global $wgExiv2Command;
+ if ( !$wgExiv2Command || !is_executable( $wgExiv2Command ) ) {
+ throw new Exception( 'exiv2 not executable or $wgExiv2Command not set' );
+ }
+ }
+
+ /**
+ * Writes random images with random filenames to disk in the directory you specify, or current working directory
+ *
+ * @param $number Integer: number of filenames to write
+ * @param $format String: optional, must be understood by ImageMagick, such as 'jpg' or 'gif'
+ * @param $dir String: directory, optional (will default to current working directory)
+ * @return Array: filenames we just wrote
+ */
+ function writeImages( $number, $format = 'jpg', $dir = null ) {
+ $filenames = $this->getRandomFilenames( $number, $format, $dir );
+ $imageWriteMethod = $this->getImageWriteMethod( $format );
+ foreach( $filenames as $filename ) {
+ $this->{$imageWriteMethod}( $this->getImageSpec(), $format, $filename );
+ }
+ return $filenames;
+ }
+
+
+ /**
+ * Figure out how we write images. This is a factor of both format and the local system
+ * @param $format (a typical extension like 'svg', 'jpg', etc.)
+ */
+ function getImageWriteMethod( $format ) {
+ global $wgUseImageMagick, $wgImageMagickConvertCommand;
+ if ( $format === 'svg' ) {
+ return 'writeSvg';
+ } else {
+ // figure out how to write images
+ if ( class_exists( 'Imagick' ) ) {
+ return 'writeImageWithApi';
+ } elseif ( $wgUseImageMagick && $wgImageMagickConvertCommand && is_executable( $wgImageMagickConvertCommand ) ) {
+ return 'writeImageWithCommandLine';
+ }
+ }
+ throw new Exception( "RandomImageGenerator: could not find a suitable method to write images in '$format' format" );
+ }
+
+ /**
+ * Return a number of randomly-generated filenames
+ * Each filename uses two words randomly drawn from the dictionary, like elephantine_spatula.jpg
+ *
+ * @param $number Integer: of filenames to generate
+ * @param $extension String: optional, defaults to 'jpg'
+ * @param $dir String: optional, defaults to current working directory
+ * @return Array: of filenames
+ */
+ private function getRandomFilenames( $number, $extension = 'jpg', $dir = null ) {
+ if ( is_null( $dir ) ) {
+ $dir = getcwd();
+ }
+ $filenames = array();
+ foreach( $this->getRandomWordPairs( $number ) as $pair ) {
+ $basename = $pair[0] . '_' . $pair[1];
+ if ( !is_null( $extension ) ) {
+ $basename .= '.' . $extension;
+ }
+ $basename = preg_replace( '/\s+/', '', $basename );
+ $filenames[] = "$dir/$basename";
+ }
+
+ return $filenames;
+
+ }
+
+
+ /**
+ * Generate data representing an image of random size (within limits),
+ * consisting of randomly colored and sized upward pointing triangles against a random background color
+ * (This data is used in the writeImage* methods).
+ * @return {Mixed}
+ */
+ public function getImageSpec() {
+ $spec = array();
+
+ $spec['width'] = mt_rand( $this->minWidth, $this->maxWidth );
+ $spec['height'] = mt_rand( $this->minHeight, $this->maxHeight );
+ $spec['fill'] = $this->getRandomColor();
+
+ $diagonalLength = sqrt( pow( $spec['width'], 2 ) + pow( $spec['height'], 2 ) );
+
+ $draws = array();
+ for ( $i = 0; $i <= $this->shapesToDraw; $i++ ) {
+ $radius = mt_rand( 0, $diagonalLength / 4 );
+ if ( $radius == 0 ) {
+ continue;
+ }
+ $originX = mt_rand( -1 * $radius, $spec['width'] + $radius );
+ $originY = mt_rand( -1 * $radius, $spec['height'] + $radius );
+ $angle = mt_rand( 0, ( 3.141592/2 ) * $radius ) / $radius;
+ $legDeltaX = round( $radius * sin( $angle ) );
+ $legDeltaY = round( $radius * cos( $angle ) );
+
+ $draw = array();
+ $draw['fill'] = $this->getRandomColor();
+ $draw['shape'] = array(
+ array( 'x' => $originX, 'y' => $originY - $radius ),
+ array( 'x' => $originX + $legDeltaX, 'y' => $originY + $legDeltaY ),
+ array( 'x' => $originX - $legDeltaX, 'y' => $originY + $legDeltaY ),
+ array( 'x' => $originX, 'y' => $originY - $radius )
+ );
+ $draws[] = $draw;
+
+ }
+
+ $spec['draws'] = $draws;
+
+ return $spec;
+ }
+
+ /**
+ * Given array( array('x' => 10, 'y' => 20), array( 'x' => 30, y=> 5 ) )
+ * returns "10,20 30,5"
+ * Useful for SVG and imagemagick command line arguments
+ * @param $shape: Array of arrays, each array containing x & y keys mapped to numeric values
+ * @return string
+ */
+ static function shapePointsToString( $shape ) {
+ $points = array();
+ foreach ( $shape as $point ) {
+ $points[] = $point['x'] . ',' . $point['y'];
+ }
+ return join( " ", $points );
+ }
+
+ /**
+ * Based on image specification, write a very simple SVG file to disk.
+ * Ignores the background spec because transparency is cool. :)
+ * @param $spec: spec describing background and shapes to draw
+ * @param $format: file format to write (which is obviously always svg here)
+ * @param $filename: filename to write to
+ */
+ public function writeSvg( $spec, $format, $filename ) {
+ $svg = new SimpleXmlElement( '<svg/>' );
+ $svg->addAttribute( 'xmlns', 'http://www.w3.org/2000/svg' );
+ $svg->addAttribute( 'version', '1.1' );
+ $svg->addAttribute( 'width', $spec['width'] );
+ $svg->addAttribute( 'height', $spec['height'] );
+ $g = $svg->addChild( 'g' );
+ foreach ( $spec['draws'] as $drawSpec ) {
+ $shape = $g->addChild( 'polygon' );
+ $shape->addAttribute( 'fill', $drawSpec['fill'] );
+ $shape->addAttribute( 'points', self::shapePointsToString( $drawSpec['shape'] ) );
+ };
+ if ( ! $fh = fopen( $filename, 'w' ) ) {
+ throw new Exception( "couldn't open $filename for writing" );
+ }
+ fwrite( $fh, $svg->asXML() );
+ if ( !fclose($fh) ) {
+ throw new Exception( "couldn't close $filename" );
+ }
+ }
+
+ /**
+ * Based on an image specification, write such an image to disk, using Imagick PHP extension
+ * @param $spec: spec describing background and circles to draw
+ * @param $format: file format to write
+ * @param $filename: filename to write to
+ */
+ public function writeImageWithApi( $spec, $format, $filename ) {
+ // this is a hack because I can't get setImageOrientation() to work. See below.
+ global $wgExiv2Command;
+
+ $image = new Imagick();
+ /**
+ * If the format is 'jpg', will also add a random orientation -- the image will be drawn rotated with triangle points
+ * facing in some direction (0, 90, 180 or 270 degrees) and a countering rotation should turn the triangle points upward again
+ */
+ $orientation = self::$orientations[0]; // default is normal orientation
+ if ( $format == 'jpg' ) {
+ $orientation = self::$orientations[ array_rand( self::$orientations ) ];
+ $spec = self::rotateImageSpec( $spec, $orientation['counterRotation'] );
+ }
+
+ $image->newImage( $spec['width'], $spec['height'], new ImagickPixel( $spec['fill'] ) );
+
+ foreach ( $spec['draws'] as $drawSpec ) {
+ $draw = new ImagickDraw();
+ $draw->setFillColor( $drawSpec['fill'] );
+ $draw->polygon( $drawSpec['shape'] );
+ $image->drawImage( $draw );
+ }
+
+ $image->setImageFormat( $format );
+
+ // this doesn't work, even though it's documented to do so...
+ // $image->setImageOrientation( $orientation['exifCode'] );
+
+ $image->writeImage( $filename );
+
+ // because the above setImageOrientation call doesn't work... nor can I get an external imagemagick binary to do this either...
+ // hacking this for now (only works if you have exiv2 installed, a program to read and manipulate exif)
+ if ( $wgExiv2Command ) {
+ $cmd = wfEscapeShellArg( $wgExiv2Command )
+ . " -M "
+ . wfEscapeShellArg( "set Exif.Image.Orientation " . $orientation['exifCode'] )
+ . " "
+ . wfEscapeShellArg( $filename );
+
+ $retval = 0;
+ $err = wfShellExec( $cmd, $retval );
+ if ( $retval !== 0 ) {
+ print "Error with $cmd: $retval, $err\n";
+ }
+ }
+
+
+ }
+
+ /**
+ * Given an image specification, produce rotated version
+ * This is used when simulating a rotated image capture with EXIF orientation
+ * @param $spec Object returned by getImageSpec
+ * @param $matrix 2x2 transformation matrix
+ * @return transformed Spec
+ */
+ private static function rotateImageSpec( &$spec, $matrix ) {
+ $tSpec = array();
+ $dims = self::matrixMultiply2x2( $matrix, $spec['width'], $spec['height'] );
+ $correctionX = 0;
+ $correctionY = 0;
+ if ( $dims['x'] < 0 ) {
+ $correctionX = abs( $dims['x'] );
+ }
+ if ( $dims['y'] < 0 ) {
+ $correctionY = abs( $dims['y'] );
+ }
+ $tSpec['width'] = abs( $dims['x'] );
+ $tSpec['height'] = abs( $dims['y'] );
+ $tSpec['fill'] = $spec['fill'];
+ $tSpec['draws'] = array();
+ foreach( $spec['draws'] as $draw ) {
+ $tDraw = array(
+ 'fill' => $draw['fill'],
+ 'shape' => array()
+ );
+ foreach( $draw['shape'] as $point ) {
+ $tPoint = self::matrixMultiply2x2( $matrix, $point['x'], $point['y'] );
+ $tPoint['x'] += $correctionX;
+ $tPoint['y'] += $correctionY;
+ $tDraw['shape'][] = $tPoint;
+ }
+ $tSpec['draws'][] = $tDraw;
+ }
+ return $tSpec;
+ }
+
+ /**
+ * Given a matrix and a pair of images, return new position
+ * @param $matrix: 2x2 rotation matrix
+ * @param $x: x-coordinate number
+ * @param $y: y-coordinate number
+ * @return Array transformed with properties x, y
+ */
+ private static function matrixMultiply2x2( $matrix, $x, $y ) {
+ return array(
+ 'x' => $x * $matrix[0][0] + $y * $matrix[0][1],
+ 'y' => $x * $matrix[1][0] + $y * $matrix[1][1]
+ );
+ }
+
+
+ /**
+ * Based on an image specification, write such an image to disk, using the command line ImageMagick program ('convert').
+ *
+ * Sample command line:
+ * $ convert -size 100x60 xc:rgb(90,87,45) \
+ * -draw 'fill rgb(12,34,56) polygon 41,39 44,57 50,57 41,39' \
+ * -draw 'fill rgb(99,123,231) circle 59,39 56,57' \
+ * -draw 'fill rgb(240,12,32) circle 50,21 50,3' filename.png
+ *
+ * @param $spec: spec describing background and shapes to draw
+ * @param $format: file format to write (unused by this method but kept so it has the same signature as writeImageWithApi)
+ * @param $filename: filename to write to
+ */
+ public function writeImageWithCommandLine( $spec, $format, $filename ) {
+ global $wgImageMagickConvertCommand;
+ $args = array();
+ $args[] = "-size " . wfEscapeShellArg( $spec['width'] . 'x' . $spec['height'] );
+ $args[] = wfEscapeShellArg( "xc:" . $spec['fill'] );
+ foreach( $spec['draws'] as $draw ) {
+ $fill = $draw['fill'];
+ $polygon = self::shapePointsToString( $draw['shape'] );
+ $drawCommand = "fill $fill polygon $polygon";
+ $args[] = '-draw ' . wfEscapeShellArg( $drawCommand );
+ }
+ $args[] = wfEscapeShellArg( $filename );
+
+ $command = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " . implode( " ", $args );
+ $retval = null;
+ wfShellExec( $command, $retval );
+ return ( $retval === 0 );
+ }
+
+ /**
+ * Generate a string of random colors for ImageMagick or SVG, like "rgb(12, 37, 98)"
+ *
+ * @return {String}
+ */
+ public function getRandomColor() {
+ $components = array();
+ for ($i = 0; $i <= 2; $i++ ) {
+ $components[] = mt_rand( 0, 255 );
+ }
+ return 'rgb(' . join(', ', $components) . ')';
+ }
+
+ /**
+ * Get an array of random pairs of random words, like array( array( 'foo', 'bar' ), array( 'quux', 'baz' ) );
+ *
+ * @param $number Integer: number of pairs
+ * @return Array: of two-element arrays
+ */
+ private function getRandomWordPairs( $number ) {
+ $lines = $this->getRandomLines( $number * 2 );
+ // construct pairs of words
+ $pairs = array();
+ $count = count( $lines );
+ for( $i = 0; $i < $count; $i += 2 ) {
+ $pairs[] = array( $lines[$i], $lines[$i+1] );
+ }
+ return $pairs;
+ }
+
+
+ /**
+ * Return N random lines from a file
+ *
+ * Will throw exception if the file could not be read or if it had fewer lines than requested.
+ *
+ * @param $number_desired Integer: number of lines desired
+ * @return Array: of exactly n elements, drawn randomly from lines the file
+ */
+ private function getRandomLines( $number_desired ) {
+ $filepath = $this->dictionaryFile;
+
+ // initialize array of lines
+ $lines = array();
+ for ( $i = 0; $i < $number_desired; $i++ ) {
+ $lines[] = null;
+ }
+
+ /*
+ * This algorithm obtains N random lines from a file in one single pass. It does this by replacing elements of
+ * a fixed-size array of lines, less and less frequently as it reads the file.
+ */
+ $fh = fopen( $filepath, "r" );
+ if ( !$fh ) {
+ throw new Exception( "couldn't open $filepath" );
+ }
+ $line_number = 0;
+ $max_index = $number_desired - 1;
+ while( !feof( $fh ) ) {
+ $line = fgets( $fh );
+ if ( $line !== false ) {
+ $line_number++;
+ $line = trim( $line );
+ if ( mt_rand( 0, $line_number ) <= $max_index ) {
+ $lines[ mt_rand( 0, $max_index ) ] = $line;
+ }
+ }
+ }
+ fclose( $fh );
+ if ( $line_number < $number_desired ) {
+ throw new Exception( "not enough lines in $filepath" );
+ }
+
+ return $lines;
+ }
+
+}
diff --git a/tests/phpunit/includes/api/format/ApiFormatPhpTest.php b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
new file mode 100644
index 00000000..8209f591
--- /dev/null
+++ b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ */
+class ApiFormatPhpTest extends ApiFormatTestBase {
+
+ function testValidPhpSyntax() {
+
+ $data = $this->apiRequest( 'php', array( 'action' => 'query', 'meta' => 'siteinfo' ) );
+
+ $this->assertInternalType( 'array', unserialize( $data ) );
+ $this->assertGreaterThan( 0, count( (array) $data ) );
+
+
+ }
+
+}
diff --git a/tests/phpunit/includes/api/format/ApiFormatTestBase.php b/tests/phpunit/includes/api/format/ApiFormatTestBase.php
new file mode 100644
index 00000000..a0b7b020
--- /dev/null
+++ b/tests/phpunit/includes/api/format/ApiFormatTestBase.php
@@ -0,0 +1,22 @@
+<?php
+
+abstract class ApiFormatTestBase extends ApiTestCase {
+ protected function apiRequest( $format, $params, $data = null ) {
+ $data = parent::doApiRequest( $params, $data, true );
+
+ $module = $data[3];
+
+ $printer = $module->createPrinterByName( $format );
+ $printer->setUnescapeAmps( false );
+
+ $printer->initPrinter( false );
+
+ ob_start();
+ $printer->execute();
+ $out = ob_get_clean();
+
+ $printer->closePrinter();
+
+ return $out;
+ }
+}
diff --git a/tests/phpunit/includes/api/generateRandomImages.php b/tests/phpunit/includes/api/generateRandomImages.php
new file mode 100644
index 00000000..f3a14e5b
--- /dev/null
+++ b/tests/phpunit/includes/api/generateRandomImages.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Bootstrapping for test image file generation
+ *
+ * @file
+ */
+
+// Evaluate the include path relative to this file
+$IP = dirname( dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) );
+
+// Start up MediaWiki in command-line mode
+require_once( "$IP/maintenance/Maintenance.php" );
+require("RandomImageGenerator.php");
+
+class GenerateRandomImages extends Maintenance {
+
+ public function execute() {
+
+ $getOptSpec = array(
+ 'dictionaryFile::',
+ 'minWidth::',
+ 'maxWidth::',
+ 'minHeight::',
+ 'maxHeight::',
+ 'shapesToDraw::',
+ 'shape::',
+
+ 'number::',
+ 'format::'
+ );
+ $options = getopt( null, $getOptSpec );
+
+ $format = isset( $options['format'] ) ? $options['format'] : 'jpg';
+ unset( $options['format'] );
+
+ $number = isset( $options['number'] ) ? intval( $options['number'] ) : 10;
+ unset( $options['number'] );
+
+ $randomImageGenerator = new RandomImageGenerator( $options );
+ $randomImageGenerator->writeImages( $number, $format );
+ }
+}
+
+$maintClass = 'GenerateRandomImages';
+require( RUN_MAINTENANCE_IF_MAIN );
+
+
diff --git a/tests/phpunit/includes/api/words.txt b/tests/phpunit/includes/api/words.txt
new file mode 100644
index 00000000..7ce23ee3
--- /dev/null
+++ b/tests/phpunit/includes/api/words.txt
@@ -0,0 +1,1000 @@
+Andaquian
+Anoplanthus
+Araquaju
+Astrophyton
+Avarish
+Batonga
+Bdellidae
+Betoyan
+Bismarck
+Britishness
+Carmen
+Chatillon
+Clement
+Coryphaena
+Croton
+Cyrillianism
+Dagomba
+Decimus
+Dichorisandra
+Duculinae
+Empusa
+Escallonia
+Fathometer
+Fon
+Fundulinae
+Gadswoons
+Gederathite
+Gemini
+Gerbera
+Gregarinida
+Gyracanthus
+Halopsychidae
+Hasidim
+Hemerobius
+Ichthyosauridae
+Iscariot
+Jeames
+Jesuitry
+Jovian
+Judaization
+Katie
+Ladin
+Langhian
+Lapithaean
+Lisette
+Macrochira
+Malaxis
+Malvastrum
+Maranhao
+Marxian
+Maurist
+Metrosideros
+Micky
+Microsporon
+Odacidae
+Ophiuchid
+Osmorhiza
+Paguma
+Palesman
+Papayaceae
+Pastinaca
+Philoxenian
+Pleurostigma
+Rarotongan
+Rhodoraceae
+Rong
+Saho
+Sanyakoan
+Sardanapalian
+Sauropoda
+Sedentaria
+Shambu
+Shukulumbwe
+Solonian
+Spaniardization
+Spirochaetaceae
+Stomatopoda
+Stratiotes
+Taiwanhemp
+Titanically
+Venetianed
+Victrola
+Yuman
+abatis
+abaton
+abjoint
+acanthoma
+acari
+acceptance
+actinography
+acuteness
+addiment
+adelite
+adelomorphic
+adelphogamy
+adipocele
+aelurophobia
+affined
+aflaunt
+agathokakological
+aischrolatreia
+alarmedly
+alebench
+aleurone
+allelotropic
+allerion
+alloplastic
+allowable
+alternacy
+alternariose
+altricial
+ambitionist
+amendment
+amiableness
+amicableness
+ammo
+amortizable
+anchorate
+anemometrically
+angelocracy
+angelological
+anodal
+anomalure
+antedate
+antiagglutinin
+antirationalist
+antiscorbutic
+antisplasher
+antithesize
+antiunionist
+antoecian
+apolegamic
+appropriation
+archididascalian
+archival
+arteriophlebotomy
+articulable
+asseveration
+assignation
+atelo
+atrienses
+atrophy
+atterminement
+atypic
+automower
+aveloz
+awrist
+azteca
+bairnteam
+balsamweed
+bannerman
+beardy
+becry
+beek
+beggarwise
+bescab
+bestness
+bethel
+bewildering
+bibliophilism
+bitterblain
+blakeberyed
+boccarella
+bocedization
+boobyalla
+bourbon
+bowbent
+bowerbird
+brachygnathous
+brail
+branchiferous
+brelaw
+brew
+brideweed
+bridgeable
+brombenzamide
+buddler
+burbankian
+burr
+buskin
+cacochymical
+calefactory
+caliper
+canaliculus
+candidature
+canellaceous
+canniness
+canning
+cantilene
+carbonatation
+carthamic
+caseum
+caudated
+causationist
+ceruleite
+chalder
+chalta
+charmel
+chekan
+chillness
+chirogymnast
+chirpling
+chlorinous
+cholanthrene
+chondroblast
+chromatography
+chromophilous
+chronical
+cicatrice
+cinchonine
+city
+clubbing
+coastal
+coaxially
+coercible
+coeternity
+coff
+coinventor
+collyba
+combinator
+complanation
+comprehensibility
+conchuela
+congenital
+context
+contranatural
+corallum
+cordately
+cornupete
+corolliferous
+coroneted
+corticosterone
+coseat
+cottage
+crocetin
+crossleted
+crottels
+curvedness
+cycadeous
+cyclism
+cylindrically
+cynanche
+cyrtoceratitic
+cystospasm
+danceress
+dancette
+dawny
+daydreamy
+debar
+decarburization
+decorousness
+decrepitness
+delirious
+deozonizer
+dermatosis
+desma
+deutencephalic
+diacetate
+diarthrodial
+diathermy
+dicolic
+dimastigate
+dimidiation
+dipetto
+disavowable
+disintrench
+disman
+dismay
+disorder
+disoxygenation
+dithionous
+dogman
+dragonfly
+dramatical
+drawspan
+drubbly
+drunk
+duskly
+ecderonic
+ectocuniform
+ectocyst
+ehrwaldite
+electrocute
+elemicin
+embracing
+emotionality
+enactment
+enamor
+enclave
+endameba
+endochylous
+endocrinologist
+endolymph
+endothecal
+entasia
+epigeous
+episcopicide
+epitrichial
+erminee
+erraticalness
+eruptivity
+erythrocytoschisis
+esperance
+estuous
+eucrystalline
+eugeny
+evacuant
+everbloomer
+evocation
+exarchateship
+exasperate
+excorticate
+excrementary
+exile
+expandedly
+exponency
+expressionist
+expulsion
+extemporary
+extollation
+extortive
+extrabulbar
+extraprostatic
+facticide
+fairer
+fakery
+fasibitikite
+fatiscent
+fearless
+febrifuge
+ferie
+fibrousness
+fingered
+fisheye
+flagpole
+flagrantness
+fleche
+fluidism
+folliculin
+footbreadth
+forceps
+forecontrive
+forthbring
+foveated
+fuchsin
+fungicidal
+funori
+gamelang
+gametically
+garvanzo
+gasoliner
+gastrophile
+germproof
+gerontism
+gigantical
+glaciology
+godmotherhood
+gooseherd
+gordunite
+gove
+gracilis
+greathead
+grieveship
+guidable
+gyromancy
+gyrostat
+habitus
+hailweed
+handhole
+hangalai
+haznadar
+heliced
+hemihypertrophy
+hemimorphic
+hemistrumectomy
+heptavalent
+heptite
+herbalist
+herpetology
+hesperid
+hexacarbon
+hieromnemon
+hobbyless
+holodactylic
+homoeoarchy
+hopperings
+hospitable
+houseboat
+huh
+huntedly
+hydroponics
+hydrosomal
+hyperdactylia
+hyperperistalsis
+hypogeocarpous
+ideogram
+idiopathical
+illegitimate
+imambarah
+impotently
+improvise
+impuberal
+inaccurately
+incarnant
+inchoation
+incliner
+incredulous
+indiscriminateness
+indulgenced
+inebriation
+inexpressiveness
+infibulate
+inflectedness
+iniome
+ink
+inquietly
+insaturable
+insinuative
+instiller
+institutive
+insultproof
+interactionist
+intercensal
+interpenetrable
+intertranspicuous
+intrinsicality
+inwards
+iridiocyte
+iridoparalysis
+irreportable
+isoprene
+isosmotic
+izard
+jacuaru
+jaculative
+jerkined
+joe
+joyous
+julienne
+justicehood
+kali
+kalidium
+katha
+kathal
+keelage
+keratomycosis
+khaki
+khedival
+kinkily
+knife
+kolo
+kraken
+kwarta
+labba
+labber
+laboress
+lacunar
+latch
+lauric
+lawter
+lectotype
+leeches
+legible
+lepidosteoid
+leucobasalt
+leverer
+libellate
+limnimeter
+lithography
+lithotypic
+locomotor
+logarithmetically
+logistician
+lyncine
+lysogenesis
+machan
+macromyelon
+maharana
+mandibulate
+manganapatite
+marchpane
+mas
+masochistic
+mastaba
+matching
+meditatively
+megalopolitan
+melaniline
+mentum
+mercaptides
+mestome
+metasomatism
+meterless
+micronuclear
+micropetalous
+microreaction
+microsporophore
+mileway
+milliarium
+millisecond
+misbind
+miscollocation
+misreader
+modernicide
+modification
+modulant
+monkfish
+monoamino
+monocarbide
+monographical
+morphinomaniac
+mullein
+munge
+mutilate
+mycophagist
+myelosarcoma
+myospasm
+myriadly
+nagaika
+naphthionate
+natant
+naviculaeform
+nayward
+neallotype
+necrophilia
+nectared
+neigher
+neogamous
+neurodynia
+neurorthopteran
+nidation
+nieceship
+nitrobacteria
+nitrosification
+nogheaded
+nonassertive
+noneuphonious
+nonextant
+nonincrease
+nonintermittent
+nonmetallic
+nonprehensile
+nonremunerative
+nonsocial
+nonvesting
+noontime
+noreaster
+nounal
+nub
+nucleoplasm
+nullisome
+numero
+numerous
+oblongatal
+observe
+obtusilingual
+obvert
+occipitoatlantal
+oceanside
+ochlophobist
+odontiasis
+opalescence
+opticon
+oraculousness
+orarium
+organically
+orthopedically
+ostosis
+overadvance
+overbuilt
+overdiscouragement
+overdoer
+overhardy
+overjocular
+overmagnify
+overofficered
+overpotent
+overprizer
+overrunner
+overshrink
+oversimply
+oversplash
+ovology
+oxskin
+oxychloride
+oxygenant
+ozokerite
+pactional
+palaeoanthropography
+palaeographical
+palaeopsychology
+palliasse
+palpebral
+pandaric
+pantelegraph
+papicolist
+papulate
+parakinetic
+parasitism
+parochialic
+parochialize
+passionlike
+patch
+paucidentate
+pawnbrokeress
+pecite
+pecky
+pedipulation
+pellitory
+perfilograph
+periblast
+perigemmal
+periost
+periplus
+perishable
+periwig
+permansive
+persistingly
+persymmetrical
+phantom
+phasmatrope
+philocaly
+philogyny
+philosophister
+philotherianism
+phorology
+phototrophic
+phrator
+phratral
+phthisipneumony
+physogastry
+phytologic
+phytoptid
+pianograph
+picqueter
+piculet
+pigeoner
+pimaric
+pinesap
+pist
+planometer
+platano
+playful
+plea
+pleuropneumonic
+plowwoman
+plump
+pluviographical
+pneumocele
+podophthalmate
+polyad
+polythalamian
+poppyhead
+portamento
+portmanteau
+portraitlike
+possible
+potassamide
+powderer
+praepubis
+preanesthetic
+prebarbaric
+predealer
+predomination
+prefactory
+preirrigational
+prelector
+presbytership
+presecure
+preservable
+prespecialist
+preventionism
+prewound
+princely
+priorship
+proannexationist
+proanthropos
+probeable
+probouleutic
+profitless
+proplasma
+prosectorial
+protecting
+protochemistry
+protosulphate
+pseudoataxia
+psilology
+psychoneurotic
+pterygial
+publicist
+purgation
+purplishness
+putatively
+pyracene
+pyrenomycete
+pyromancy
+pyrophone
+quadroon
+quailhead
+qualifier
+quaternal
+rabblelike
+rambunctious
+rapidness
+ratably
+rationalism
+razor
+reannoy
+recultivation
+regulable
+reimplant
+reimposition
+reimprison
+reinjure
+reinspiration
+reintroduce
+remantle
+reprehensibility
+reptant
+require
+resteal
+restful
+returnability
+revisableness
+rewash
+rewhirl
+reyield
+rhizotomy
+rhodamine
+rigwiddie
+rimester
+ripper
+rippet
+rockish
+rockwards
+rollicky
+roosters
+rooted
+rosal
+rozum
+saccharated
+sagamore
+sagy
+salesmanship
+salivous
+sallet
+salta
+saprostomous
+satiation
+sauropsid
+sawarra
+sawback
+scabish
+scabrate
+scampavia
+scientificophilosophical
+scirrosity
+scoliometer
+scolopendrelloid
+secantly
+seignioral
+semibull
+semic
+seminarianism
+semiped
+semiprivate
+semispherical
+semispontaneous
+seneschal
+septendecimal
+serotherapist
+servation
+sesquisulphuret
+severish
+sextipartite
+sextubercular
+shipyard
+shuckpen
+siderosis
+silex
+sillyhow
+silverbelly
+silverbelly
+simulacrum
+sisham
+sixte
+skeiner
+skiapod
+slopped
+slubby
+smalts
+sockmaker
+solute
+somethingness
+somnify
+southwester
+spathilla
+spectrochemical
+sphagnology
+spinales
+spiriting
+spirling
+spirochetemia
+spreadboard
+spurflower
+squawdom
+squeezing
+staircase
+staker
+stamphead
+statolith
+stekan
+stellulate
+stinker
+stomodaea
+streamingly
+strikingness
+strouthocamelian
+stuprum
+subacutely
+subboreal
+subcontractor
+subendorsement
+subprofitable
+subserviate
+subsneer
+subungual
+sucuruju
+sugan
+sulphocarbolate
+summerwood
+superficialist
+superinference
+superregenerative
+supplicate
+suspendible
+synchronizer
+syntectic
+tachyglossate
+tailless
+taintment
+takingly
+taletelling
+tarpon
+tasteful
+taxeater
+taxy
+teache
+teachless
+teg
+tegmen
+teletyper
+temperable
+ten
+tenent
+teskere
+testes
+thallogen
+thapsia
+thewness
+thickety
+thiobacteria
+thorniness
+throwing
+thyroprivic
+tinnitus
+tocalote
+tolerationist
+tonalamatl
+torvous
+totality
+tottering
+toug
+tracheopathia
+tragedical
+translucent
+trifoveolate
+trilaurin
+trophoplasmatic
+trunkless
+turbanless
+turnpiker
+twangle
+twitterboned
+ultraornate
+umbilication
+unabatingly
+unabjured
+unadequateness
+unaffectedness
+unarriving
+unassorted
+unattacked
+unbenumbed
+unboasted
+unburning
+uncensorious
+uncongested
+uncontemnedly
+uncontemporary
+uncrook
+uncrystallizability
+uncurb
+uncustomariness
+underbillow
+undercanopy
+underestimation
+underhanging
+underpetticoated
+underpropped
+undersole
+understocking
+underworld
+undevout
+undisappointing
+undistinctive
+unfiscal
+unfluted
+unfreckled
+ungentilize
+unglobe
+unhelped
+unhomogeneously
+unifoliate
+uninflammable
+uninterrogated
+unisonal
+unkindled
+unlikeableness
+unlisty
+unlocked
+unmoving
+unmultipliable
+unnestled
+unnoticed
+unobservable
+unobviated
+unoffensively
+unofficerlike
+unpoetic
+unpractically
+unquestionableness
+unrehearsed
+unrevised
+unrhetorical
+unsadden
+unsaluting
+unscriptural
+unseeking
+unshowed
+unsolicitous
+unsprouted
+unsubjective
+unsubsidized
+unsymbolic
+untenant
+unterrified
+untranquil
+untraversed
+untrusty
+untying
+unwillful
+unwinding
+upspring
+uptwist
+urachovesical
+uropygial
+vagabondism
+varicoid
+varletess
+vasal
+ventrocaudal
+verisimilitude
+vermigerous
+vibrometer
+viminal
+virus
+vocationalism
+voguey
+vulnerability
+waggle
+wamblingly
+warmus
+waxer
+waying
+wedgeable
+wellmaker
+whomever
+wigged
+witchlike
+wokas
+woodrowel
+woodsman
+woolding
+xanthelasmic
+xiphosternum
+yachtman
+yachtsmanlike
+yelp
+zoophytal \ No newline at end of file
diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php
new file mode 100644
index 00000000..914ab27c
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php
@@ -0,0 +1,312 @@
+<?php
+
+class MockDatabaseSqlite extends DatabaseSqliteStandalone {
+ var $lastQuery;
+
+ function __construct( ) {
+ parent::__construct( ':memory:' );
+ }
+
+ function query( $sql, $fname = '', $tempIgnore = false ) {
+ $this->lastQuery = $sql;
+ return true;
+ }
+
+ function replaceVars( $s ) {
+ return parent::replaceVars( $s );
+ }
+}
+
+/**
+ * @group sqlite
+ */
+class DatabaseSqliteTest extends MediaWikiTestCase {
+ var $db;
+
+ public function setUp() {
+ if ( !Sqlite::isPresent() ) {
+ $this->markTestSkipped( 'No SQLite support detected' );
+ }
+ $this->db = new MockDatabaseSqlite();
+ if ( version_compare( $this->db->getServerVersion(), '3.6.0', '<' ) ) {
+ $this->markTestSkipped( "SQLite at least 3.6 required, {$this->db->getServerVersion()} found" );
+ }
+ }
+
+ private function replaceVars( $sql ) {
+ // normalize spacing to hide implementation details
+ return preg_replace( '/\s+/', ' ', $this->db->replaceVars( $sql ) );
+ }
+
+ private function assertResultIs( $expected, $res ) {
+ $this->assertNotNull( $res );
+ $i = 0;
+ foreach( $res as $row ) {
+ foreach( $expected[$i] as $key => $value ) {
+ $this->assertTrue( isset( $row->$key ) );
+ $this->assertEquals( $value, $row->$key );
+ }
+ $i++;
+ }
+ $this->assertEquals( count( $expected ), $i, 'Unexpected number of rows' );
+ }
+
+ public function testReplaceVars() {
+ $this->assertEquals( 'foo', $this->replaceVars( 'foo' ), "Don't break anything accidentally" );
+
+ $this->assertEquals( "CREATE TABLE /**/foo (foo_key INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+ . "foo_bar TEXT, foo_name TEXT NOT NULL DEFAULT '', foo_int INTEGER, foo_int2 INTEGER );",
+ $this->replaceVars( "CREATE TABLE /**/foo (foo_key int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ foo_bar char(13), foo_name varchar(255) binary NOT NULL DEFAULT '', foo_int tinyint ( 8 ), foo_int2 int(16) ) ENGINE=MyISAM;" )
+ );
+
+ $this->assertEquals( "CREATE TABLE foo ( foo1 REAL, foo2 REAL, foo3 REAL );",
+ $this->replaceVars( "CREATE TABLE foo ( foo1 FLOAT, foo2 DOUBLE( 1,10), foo3 DOUBLE PRECISION );" )
+ );
+
+ $this->assertEquals( "CREATE TABLE foo ( foo_binary1 BLOB, foo_binary2 BLOB );",
+ $this->replaceVars( "CREATE TABLE foo ( foo_binary1 binary(16), foo_binary2 varbinary(32) );" )
+ );
+
+ $this->assertEquals( "CREATE TABLE text ( text_foo TEXT );",
+ $this->replaceVars( "CREATE TABLE text ( text_foo tinytext );" ),
+ 'Table name changed'
+ );
+
+ $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );",
+ $this->replaceVars("CREATE TABLE foo ( foobar INT PRIMARY KEY NOT NULL AUTO_INCREMENT );" )
+ );
+ $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );",
+ $this->replaceVars("CREATE TABLE foo ( foobar INT PRIMARY KEY AUTO_INCREMENT NOT NULL );" )
+ );
+
+ $this->assertEquals( "CREATE TABLE enums( enum1 TEXT, myenum TEXT)",
+ $this->replaceVars( "CREATE TABLE enums( enum1 ENUM('A', 'B'), myenum ENUM ('X', 'Y'))" )
+ );
+
+ $this->assertEquals( "ALTER TABLE foo ADD COLUMN foo_bar INTEGER DEFAULT 42",
+ $this->replaceVars( "ALTER TABLE foo\nADD COLUMN foo_bar int(10) unsigned DEFAULT 42" )
+ );
+ }
+
+ public function testTableName() {
+ // @todo Moar!
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $this->assertEquals( 'foo', $db->tableName( 'foo' ) );
+ $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) );
+ $db->tablePrefix( 'foo' );
+ $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) );
+ $this->assertEquals( 'foobar', $db->tableName( 'bar' ) );
+ }
+
+ public function testDuplicateTableStructure() {
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $db->query( 'CREATE TABLE foo(foo, barfoo)' );
+
+ $db->duplicateTableStructure( 'foo', 'bar' );
+ $this->assertEquals( 'CREATE TABLE "bar"(foo, barfoo)',
+ $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ),
+ 'Normal table duplication'
+ );
+
+ $db->duplicateTableStructure( 'foo', 'baz', true );
+ $this->assertEquals( 'CREATE TABLE "baz"(foo, barfoo)',
+ $db->selectField( 'sqlite_temp_master', 'sql', array( 'name' => 'baz' ) ),
+ 'Creation of temporary duplicate'
+ );
+ $this->assertEquals( 0,
+ $db->selectField( 'sqlite_master', 'COUNT(*)', array( 'name' => 'baz' ) ),
+ 'Create a temporary duplicate only'
+ );
+ }
+
+ public function testDuplicateTableStructureVirtual() {
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ if ( $db->getFulltextSearchModule() != 'FTS3' ) {
+ $this->markTestSkipped( 'FTS3 not supported, cannot create virtual tables' );
+ }
+ $db->query( 'CREATE VIRTUAL TABLE "foo" USING FTS3(foobar)' );
+
+ $db->duplicateTableStructure( 'foo', 'bar' );
+ $this->assertEquals( 'CREATE VIRTUAL TABLE "bar" USING FTS3(foobar)',
+ $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ),
+ 'Duplication of virtual tables'
+ );
+
+ $db->duplicateTableStructure( 'foo', 'baz', true );
+ $this->assertEquals( 'CREATE VIRTUAL TABLE "baz" USING FTS3(foobar)',
+ $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'baz' ) ),
+ "Can't create temporary virtual tables, should fall back to non-temporary duplication"
+ );
+ }
+
+ public function testDeleteJoin() {
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $db->query( 'CREATE TABLE a (a_1)', __METHOD__ );
+ $db->query( 'CREATE TABLE b (b_1, b_2)', __METHOD__ );
+ $db->insert( 'a', array(
+ array( 'a_1' => 1 ),
+ array( 'a_1' => 2 ),
+ array( 'a_1' => 3 ),
+ ),
+ __METHOD__
+ );
+ $db->insert( 'b', array(
+ array( 'b_1' => 2, 'b_2' => 'a' ),
+ array( 'b_1' => 3, 'b_2' => 'b' ),
+ ),
+ __METHOD__
+ );
+ $db->deleteJoin( 'a', 'b', 'a_1', 'b_1', array( 'b_2' => 'a' ), __METHOD__ );
+ $res = $db->query( "SELECT * FROM a", __METHOD__ );
+ $this->assertResultIs( array(
+ array( 'a_1' => 1 ),
+ array( 'a_1' => 3 ),
+ ),
+ $res
+ );
+ }
+
+ public function testEntireSchema() {
+ global $IP;
+
+ $result = Sqlite::checkSqlSyntax( "$IP/maintenance/tables.sql" );
+ if ( $result !== true ) {
+ $this->fail( $result );
+ }
+ $this->assertTrue( true ); // avoid test being marked as incomplete due to lack of assertions
+ }
+
+ /**
+ * Runs upgrades of older databases and compares results with current schema
+ * @todo: currently only checks list of tables
+ */
+ public function testUpgrades() {
+ global $IP, $wgVersion;
+
+ // Versions tested
+ $versions = array(
+ //'1.13', disabled for now, was totally screwed up
+ // SQLite wasn't included in 1.14
+ '1.15',
+ '1.16',
+ '1.17',
+ );
+
+ // Mismatches for these columns we can safely ignore
+ $ignoredColumns = array(
+ 'user_newtalk.user_last_timestamp', // r84185
+ );
+
+ $currentDB = new DatabaseSqliteStandalone( ':memory:' );
+ $currentDB->sourceFile( "$IP/maintenance/tables.sql" );
+ $currentTables = $this->getTables( $currentDB );
+ sort( $currentTables );
+
+ foreach ( $versions as $version ) {
+ $versions = "upgrading from $version to $wgVersion";
+ $db = $this->prepareDB( $version );
+ $tables = $this->getTables( $db );
+ $this->assertEquals( $currentTables, $tables, "Different tables $versions" );
+ foreach ( $tables as $table ) {
+ $currentCols = $this->getColumns( $currentDB, $table );
+ $cols = $this->getColumns( $db, $table );
+ $this->assertEquals(
+ array_keys( $currentCols ),
+ array_keys( $cols ),
+ "Mismatching columns for table \"$table\" $versions"
+ );
+ foreach ( $currentCols as $name => $column ) {
+ $fullName = "$table.$name";
+ $this->assertEquals(
+ (bool)$column->pk,
+ (bool)$cols[$name]->pk,
+ "PRIMARY KEY status does not match for column $fullName $versions"
+ );
+ if ( !in_array( $fullName, $ignoredColumns ) ) {
+ $this->assertEquals(
+ (bool)$column->notnull,
+ (bool)$cols[$name]->notnull,
+ "NOT NULL status does not match for column $fullName $versions"
+ );
+ $this->assertEquals(
+ $column->dflt_value,
+ $cols[$name]->dflt_value,
+ "Default values does not match for column $fullName $versions"
+ );
+ }
+ }
+ $currentIndexes = $this->getIndexes( $currentDB, $table );
+ $indexes = $this->getIndexes( $db, $table );
+ $this->assertEquals(
+ array_keys( $currentIndexes ),
+ array_keys( $indexes ),
+ "mismatching indexes for table \"$table\" $versions"
+ );
+ }
+ $db->close();
+ }
+ }
+
+ private function prepareDB( $version ) {
+ static $maint = null;
+ if ( $maint === null ) {
+ $maint = new FakeMaintenance();
+ $maint->loadParamsAndArgs( null, array( 'quiet' => 1 ) );
+ }
+
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $db->sourceFile( dirname( __FILE__ ) . "/sqlite/tables-$version.sql" );
+ $updater = DatabaseUpdater::newForDB( $db, false, $maint );
+ $updater->doUpdates( array( 'core' ) );
+ return $db;
+ }
+
+ private function getTables( $db ) {
+ $list = array_flip( $db->listTables() );
+ $excluded = array(
+ 'math', // moved out of core in 1.18
+ 'searchindex',
+ 'searchindex_content',
+ 'searchindex_segments',
+ 'searchindex_segdir',
+ // FTS4 ready!!1
+ 'searchindex_docsize',
+ 'searchindex_stat',
+ );
+ foreach ( $excluded as $t ) {
+ unset( $list[$t] );
+ }
+ $list = array_flip( $list );
+ sort( $list );
+ return $list;
+ }
+
+ private function getColumns( $db, $table ) {
+ $cols = array();
+ $res = $db->query( "PRAGMA table_info($table)" );
+ $this->assertNotNull( $res );
+ foreach ( $res as $col ) {
+ $cols[$col->name] = $col;
+ }
+ ksort( $cols );
+ return $cols;
+ }
+
+ private function getIndexes( $db, $table ) {
+ $indexes = array();
+ $res = $db->query( "PRAGMA index_list($table)" );
+ $this->assertNotNull( $res );
+ foreach ( $res as $index ) {
+ $res2 = $db->query( "PRAGMA index_info({$index->name})" );
+ $this->assertNotNull( $res2 );
+ $index->columns = array();
+ foreach ( $res2 as $col ) {
+ $index->columns[] = $col;
+ }
+ $indexes[$index->name] = $index;
+ }
+ ksort( $indexes );
+ return $indexes;
+ }
+}
diff --git a/tests/phpunit/includes/db/DatabaseTest.php b/tests/phpunit/includes/db/DatabaseTest.php
new file mode 100644
index 00000000..d480ac6e
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseTest.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * @group Database
+ */
+class DatabaseTest extends MediaWikiTestCase {
+ var $db;
+
+ function setUp() {
+ $this->db = wfGetDB( DB_SLAVE );
+ }
+
+ function testAddQuotesNull() {
+ $check = "NULL";
+ if ( $this->db->getType() === 'sqlite' || $this->db->getType() === 'oracle' ) {
+ $check = "''";
+ }
+ $this->assertEquals( $check, $this->db->addQuotes( null ) );
+ }
+
+ function testAddQuotesInt() {
+ # returning just "1234" should be ok too, though...
+ # maybe
+ $this->assertEquals(
+ "'1234'",
+ $this->db->addQuotes( 1234 ) );
+ }
+
+ function testAddQuotesFloat() {
+ # returning just "1234.5678" would be ok too, though
+ $this->assertEquals(
+ "'1234.5678'",
+ $this->db->addQuotes( 1234.5678 ) );
+ }
+
+ function testAddQuotesString() {
+ $this->assertEquals(
+ "'string'",
+ $this->db->addQuotes( 'string' ) );
+ }
+
+ function testAddQuotesStringQuote() {
+ $check = "'string''s cause trouble'";
+ if ( $this->db->getType() === 'mysql' ) {
+ $check = "'string\'s cause trouble'";
+ }
+ $this->assertEquals(
+ $check,
+ $this->db->addQuotes( "string's cause trouble" ) );
+ }
+
+ function testFillPreparedEmpty() {
+ $sql = $this->db->fillPrepared(
+ 'SELECT * FROM interwiki', array() );
+ $this->assertEquals(
+ "SELECT * FROM interwiki",
+ $sql );
+ }
+
+ function testFillPreparedQuestion() {
+ $sql = $this->db->fillPrepared(
+ 'SELECT * FROM cur WHERE cur_namespace=? AND cur_title=?',
+ array( 4, "Snicker's_paradox" ) );
+
+ $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker''s_paradox'";
+ if ( $this->db->getType() === 'mysql' ) {
+ $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker\'s_paradox'";
+ }
+ $this->assertEquals( $check, $sql );
+ }
+
+ function testFillPreparedBang() {
+ $sql = $this->db->fillPrepared(
+ 'SELECT user_id FROM ! WHERE user_name=?',
+ array( '"user"', "Slash's Dot" ) );
+
+ $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash''s Dot'";
+ if ( $this->db->getType() === 'mysql' ) {
+ $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash\'s Dot'";
+ }
+ $this->assertEquals( $check, $sql );
+ }
+
+ function testFillPreparedRaw() {
+ $sql = $this->db->fillPrepared(
+ "SELECT * FROM cur WHERE cur_title='This_\\&_that,_WTF\\?\\!'",
+ array( '"user"', "Slash's Dot" ) );
+ $this->assertEquals(
+ "SELECT * FROM cur WHERE cur_title='This_&_that,_WTF?!'",
+ $sql );
+ }
+
+}
+
+
diff --git a/tests/phpunit/includes/db/sqlite/tables-1.13.sql b/tests/phpunit/includes/db/sqlite/tables-1.13.sql
new file mode 100644
index 00000000..a0dcb553
--- /dev/null
+++ b/tests/phpunit/includes/db/sqlite/tables-1.13.sql
@@ -0,0 +1,342 @@
+-- This is a copy of SQLite schema from MediaWiki 1.13 used for updater testing
+
+CREATE TABLE /*$wgDBprefix*/user (
+ user_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_name varchar(255) default '',
+ user_real_name varchar(255) default '',
+ user_password tinyblob ,
+ user_newpassword tinyblob ,
+ user_newpass_time BLOB,
+ user_email tinytext ,
+ user_options blob ,
+ user_touched BLOB default '',
+ user_token BLOB default '',
+ user_email_authenticated BLOB,
+ user_email_token BLOB,
+ user_email_token_expires BLOB,
+ user_registration BLOB,
+ user_editcount int) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/user_groups (
+ ug_user INTEGER default '0',
+ ug_group varBLOB default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/user_newtalk (
+ user_id INTEGER default '0',
+ user_ip varBLOB default '',
+ user_last_timestamp BLOB default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/page (
+ page_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ page_namespace INTEGER ,
+ page_title varchar(255) ,
+ page_restrictions tinyblob ,
+ page_counter bigint default '0',
+ page_is_redirect tinyint default '0',
+ page_is_new tinyint default '0',
+ page_random real ,
+ page_touched BLOB default '',
+ page_latest INTEGER ,
+ page_len INTEGER ) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/revision (
+ rev_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ rev_page INTEGER ,
+ rev_text_id INTEGER ,
+ rev_comment tinyblob ,
+ rev_user INTEGER default '0',
+ rev_user_text varchar(255) default '',
+ rev_timestamp BLOB default '',
+ rev_minor_edit tinyint default '0',
+ rev_deleted tinyint default '0',
+ rev_len int,
+ rev_parent_id INTEGER default NULL) /*$wgDBTableOptions*/ ;
+
+CREATE TABLE /*$wgDBprefix*/text (
+ old_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ old_text mediumblob ,
+ old_flags tinyblob ) /*$wgDBTableOptions*/ ;
+
+CREATE TABLE /*$wgDBprefix*/archive (
+ ar_namespace INTEGER default '0',
+ ar_title varchar(255) default '',
+ ar_text mediumblob ,
+ ar_comment tinyblob ,
+ ar_user INTEGER default '0',
+ ar_user_text varchar(255) ,
+ ar_timestamp BLOB default '',
+ ar_minor_edit tinyint default '0',
+ ar_flags tinyblob ,
+ ar_rev_id int,
+ ar_text_id int,
+ ar_deleted tinyint default '0',
+ ar_len int,
+ ar_page_id int,
+ ar_parent_id INTEGER default NULL) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/pagelinks (
+ pl_from INTEGER default '0',
+ pl_namespace INTEGER default '0',
+ pl_title varchar(255) default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/templatelinks (
+ tl_from INTEGER default '0',
+ tl_namespace INTEGER default '0',
+ tl_title varchar(255) default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/imagelinks (
+ il_from INTEGER default '0',
+ il_to varchar(255) default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/categorylinks (
+ cl_from INTEGER default '0',
+ cl_to varchar(255) default '',
+ cl_sortkey varchar(70) default '',
+ cl_timestamp timestamp ) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/category (
+ cat_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ cat_title varchar(255) ,
+ cat_pages INTEGER signed default 0,
+ cat_subcats INTEGER signed default 0,
+ cat_files INTEGER signed default 0,
+ cat_hidden tinyint default 0) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/externallinks (
+ el_from INTEGER default '0',
+ el_to blob ,
+ el_index blob ) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/langlinks (
+ ll_from INTEGER default '0',
+ ll_lang varBLOB default '',
+ ll_title varchar(255) default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/site_stats (
+ ss_row_id INTEGER ,
+ ss_total_views bigint default '0',
+ ss_total_edits bigint default '0',
+ ss_good_articles bigint default '0',
+ ss_total_pages bigint default '-1',
+ ss_users bigint default '-1',
+ ss_admins INTEGER default '-1',
+ ss_images INTEGER default '0') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/hitcounter (
+ hc_id INTEGER
+) ;
+
+CREATE TABLE /*$wgDBprefix*/ipblocks (
+ ipb_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ ipb_address tinyblob ,
+ ipb_user INTEGER default '0',
+ ipb_by INTEGER default '0',
+ ipb_by_text varchar(255) default '',
+ ipb_reason tinyblob ,
+ ipb_timestamp BLOB default '',
+ ipb_auto bool default 0,
+ ipb_anon_only bool default 0,
+ ipb_create_account bool default 1,
+ ipb_enable_autoblock bool default '1',
+ ipb_expiry varBLOB default '',
+ ipb_range_start tinyblob ,
+ ipb_range_end tinyblob ,
+ ipb_deleted bool default 0,
+ ipb_block_email bool default 0) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/image (
+ img_name varchar(255) default '',
+ img_size INTEGER default '0',
+ img_width INTEGER default '0',
+ img_height INTEGER default '0',
+ img_metadata mediumblob ,
+ img_bits INTEGER default '0',
+ img_media_type TEXT default NULL,
+ img_major_mime TEXT default "unknown",
+ img_minor_mime varBLOB default "unknown",
+ img_description tinyblob ,
+ img_user INTEGER default '0',
+ img_user_text varchar(255) ,
+ img_timestamp varBLOB default '',
+ img_sha1 varBLOB default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/oldimage (
+ oi_name varchar(255) default '',
+ oi_archive_name varchar(255) default '',
+ oi_size INTEGER default 0,
+ oi_width INTEGER default 0,
+ oi_height INTEGER default 0,
+ oi_bits INTEGER default 0,
+ oi_description tinyblob ,
+ oi_user INTEGER default '0',
+ oi_user_text varchar(255) ,
+ oi_timestamp BLOB default '',
+ oi_metadata mediumblob ,
+ oi_media_type TEXT default NULL,
+ oi_major_mime TEXT default "unknown",
+ oi_minor_mime varBLOB default "unknown",
+ oi_deleted tinyint default '0',
+ oi_sha1 varBLOB default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/filearchive (
+ fa_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ fa_name varchar(255) default '',
+ fa_archive_name varchar(255) default '',
+ fa_storage_group varBLOB,
+ fa_storage_key varBLOB default '',
+ fa_deleted_user int,
+ fa_deleted_timestamp BLOB default '',
+ fa_deleted_reason text,
+ fa_size INTEGER default '0',
+ fa_width INTEGER default '0',
+ fa_height INTEGER default '0',
+ fa_metadata mediumblob,
+ fa_bits INTEGER default '0',
+ fa_media_type TEXT default NULL,
+ fa_major_mime TEXT default "unknown",
+ fa_minor_mime varBLOB default "unknown",
+ fa_description tinyblob,
+ fa_user INTEGER default '0',
+ fa_user_text varchar(255) ,
+ fa_timestamp BLOB default '',
+ fa_deleted tinyint default '0') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/recentchanges (
+ rc_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ rc_timestamp varBLOB default '',
+ rc_cur_time varBLOB default '',
+ rc_user INTEGER default '0',
+ rc_user_text varchar(255) ,
+ rc_namespace INTEGER default '0',
+ rc_title varchar(255) default '',
+ rc_comment varchar(255) default '',
+ rc_minor tinyint default '0',
+ rc_bot tinyint default '0',
+ rc_new tinyint default '0',
+ rc_cur_id INTEGER default '0',
+ rc_this_oldid INTEGER default '0',
+ rc_last_oldid INTEGER default '0',
+ rc_type tinyint default '0',
+ rc_moved_to_ns tinyint default '0',
+ rc_moved_to_title varchar(255) default '',
+ rc_patrolled tinyint default '0',
+ rc_ip varBLOB default '',
+ rc_old_len int,
+ rc_new_len int,
+ rc_deleted tinyint default '0',
+ rc_logid INTEGER default '0',
+ rc_log_type varBLOB NULL default NULL,
+ rc_log_action varBLOB NULL default NULL,
+ rc_params blob NULL) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/watchlist (
+ wl_user INTEGER ,
+ wl_namespace INTEGER default '0',
+ wl_title varchar(255) default '',
+ wl_notificationtimestamp varBLOB) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/math (
+ math_inputhash varBLOB ,
+ math_outputhash varBLOB ,
+ math_html_conservativeness tinyint ,
+ math_html text,
+ math_mathml text) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/searchindex (
+ si_page INTEGER ,
+ si_title varchar(255) default '',
+ si_text mediumtext ) ;
+
+CREATE TABLE /*$wgDBprefix*/interwiki (
+ iw_prefix varchar(32) ,
+ iw_url blob ,
+ iw_local bool ,
+ iw_trans tinyint default 0) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/querycache (
+ qc_type varBLOB ,
+ qc_value INTEGER default '0',
+ qc_namespace INTEGER default '0',
+ qc_title varchar(255) default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/objectcache (
+ keyname varBLOB default '',
+ value mediumblob,
+ exptime datetime) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/transcache (
+ tc_url varBLOB ,
+ tc_contents text,
+ tc_time INTEGER ) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/logging (
+ log_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ log_type varBLOB default '',
+ log_action varBLOB default '',
+ log_timestamp BLOB default '19700101000000',
+ log_user INTEGER default 0,
+ log_namespace INTEGER default 0,
+ log_title varchar(255) default '',
+ log_comment varchar(255) default '',
+ log_params blob ,
+ log_deleted tinyint default '0') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/trackbacks (
+ tb_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ tb_page INTEGER REFERENCES /*$wgDBprefix*/page(page_id) ON DELETE CASCADE,
+ tb_title varchar(255) ,
+ tb_url blob ,
+ tb_ex text,
+ tb_name varchar(255)) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/job (
+ job_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ job_cmd varBLOB default '',
+ job_namespace INTEGER ,
+ job_title varchar(255) ,
+ job_params blob ) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/querycache_info (
+ qci_type varBLOB default '',
+ qci_timestamp BLOB default '19700101000000') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/redirect (
+ rd_from INTEGER default '0',
+ rd_namespace INTEGER default '0',
+ rd_title varchar(255) default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/querycachetwo (
+ qcc_type varBLOB ,
+ qcc_value INTEGER default '0',
+ qcc_namespace INTEGER default '0',
+ qcc_title varchar(255) default '',
+ qcc_namespacetwo INTEGER default '0',
+ qcc_titletwo varchar(255) default '') /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/page_restrictions (
+ pr_page INTEGER ,
+ pr_type varBLOB ,
+ pr_level varBLOB ,
+ pr_cascade tinyint ,
+ pr_user INTEGER NULL,
+ pr_expiry varBLOB NULL,
+ pr_id INTEGER PRIMARY KEY AUTOINCREMENT) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/protected_titles (
+ pt_namespace INTEGER ,
+ pt_title varchar(255) ,
+ pt_user INTEGER ,
+ pt_reason tinyblob,
+ pt_timestamp BLOB ,
+ pt_expiry varBLOB default '',
+ pt_create_perm varBLOB ) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/page_props (
+ pp_page INTEGER ,
+ pp_propname varBLOB ,
+ pp_value blob ) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*$wgDBprefix*/updatelog (
+ ul_key varchar(255) ) /*$wgDBTableOptions*/;
+
+
diff --git a/tests/phpunit/includes/db/sqlite/tables-1.15.sql b/tests/phpunit/includes/db/sqlite/tables-1.15.sql
new file mode 100644
index 00000000..901bac52
--- /dev/null
+++ b/tests/phpunit/includes/db/sqlite/tables-1.15.sql
@@ -0,0 +1,454 @@
+-- This is a copy of MediaWiki 1.15 schema shared by MySQL and SQLite.
+-- It is used for updater testing. Comments are stripped to decrease
+-- file size, as we don't need to maintain it.
+
+CREATE TABLE /*_*/user (
+ user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ user_name varchar(255) binary NOT NULL default '',
+ user_real_name varchar(255) binary NOT NULL default '',
+ user_password tinyblob NOT NULL,
+ user_newpassword tinyblob NOT NULL,
+ user_newpass_time binary(14),
+ user_email tinytext NOT NULL,
+ user_options blob NOT NULL,
+ user_touched binary(14) NOT NULL default '',
+ user_token binary(32) NOT NULL default '',
+ user_email_authenticated binary(14),
+ user_email_token binary(32),
+ user_email_token_expires binary(14),
+ user_registration binary(14),
+ user_editcount int
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name);
+CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token);
+CREATE TABLE /*_*/user_groups (
+ ug_user int unsigned NOT NULL default 0,
+ ug_group varbinary(16) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group);
+CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group);
+CREATE TABLE /*_*/user_newtalk (
+ user_id int NOT NULL default 0,
+ user_ip varbinary(40) NOT NULL default '',
+ user_last_timestamp binary(14) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id);
+CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip);
+CREATE TABLE /*_*/page (
+ page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ page_namespace int NOT NULL,
+ page_title varchar(255) binary NOT NULL,
+ page_restrictions tinyblob NOT NULL,
+ page_counter bigint unsigned NOT NULL default 0,
+ page_is_redirect tinyint unsigned NOT NULL default 0,
+ page_is_new tinyint unsigned NOT NULL default 0,
+ page_random real unsigned NOT NULL,
+ page_touched binary(14) NOT NULL default '',
+ page_latest int unsigned NOT NULL,
+ page_len int unsigned NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title);
+CREATE INDEX /*i*/page_random ON /*_*/page (page_random);
+CREATE INDEX /*i*/page_len ON /*_*/page (page_len);
+CREATE TABLE /*_*/revision (
+ rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ rev_page int unsigned NOT NULL,
+ rev_text_id int unsigned NOT NULL,
+ rev_comment tinyblob NOT NULL,
+ rev_user int unsigned NOT NULL default 0,
+ rev_user_text varchar(255) binary NOT NULL default '',
+ rev_timestamp binary(14) NOT NULL default '',
+ rev_minor_edit tinyint unsigned NOT NULL default 0,
+ rev_deleted tinyint unsigned NOT NULL default 0,
+ rev_len int unsigned,
+ rev_parent_id int unsigned default NULL
+) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024;
+CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id);
+CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp);
+CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp);
+CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp);
+CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp);
+CREATE TABLE /*_*/text (
+ old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ old_text mediumblob NOT NULL,
+ old_flags tinyblob NOT NULL
+) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240;
+CREATE TABLE /*_*/archive (
+ ar_namespace int NOT NULL default 0,
+ ar_title varchar(255) binary NOT NULL default '',
+ ar_text mediumblob NOT NULL,
+ ar_comment tinyblob NOT NULL,
+ ar_user int unsigned NOT NULL default 0,
+ ar_user_text varchar(255) binary NOT NULL,
+ ar_timestamp binary(14) NOT NULL default '',
+ ar_minor_edit tinyint NOT NULL default 0,
+ ar_flags tinyblob NOT NULL,
+ ar_rev_id int unsigned,
+ ar_text_id int unsigned,
+ ar_deleted tinyint unsigned NOT NULL default 0,
+ ar_len int unsigned,
+ ar_page_id int unsigned,
+ ar_parent_id int unsigned default NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
+CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp);
+CREATE TABLE /*_*/pagelinks (
+ pl_from int unsigned NOT NULL default 0,
+ pl_namespace int NOT NULL default 0,
+ pl_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title);
+CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from);
+CREATE TABLE /*_*/templatelinks (
+ tl_from int unsigned NOT NULL default 0,
+ tl_namespace int NOT NULL default 0,
+ tl_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title);
+CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from);
+CREATE TABLE /*_*/imagelinks (
+ il_from int unsigned NOT NULL default 0,
+ il_to varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to);
+CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from);
+CREATE TABLE /*_*/categorylinks (
+ cl_from int unsigned NOT NULL default 0,
+ cl_to varchar(255) binary NOT NULL default '',
+ cl_sortkey varchar(70) binary NOT NULL default '',
+ cl_timestamp timestamp NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to);
+CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_sortkey,cl_from);
+CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp);
+CREATE TABLE /*_*/category (
+ cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ cat_title varchar(255) binary NOT NULL,
+ cat_pages int signed NOT NULL default 0,
+ cat_subcats int signed NOT NULL default 0,
+ cat_files int signed NOT NULL default 0,
+ cat_hidden tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title);
+CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages);
+CREATE TABLE /*_*/externallinks (
+ el_from int unsigned NOT NULL default 0,
+ el_to blob NOT NULL,
+ el_index blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40));
+CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from);
+CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60));
+CREATE TABLE /*_*/langlinks (
+ ll_from int unsigned NOT NULL default 0,
+
+ ll_lang varbinary(20) NOT NULL default '',
+ ll_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang);
+CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title);
+CREATE TABLE /*_*/site_stats (
+ ss_row_id int unsigned NOT NULL,
+ ss_total_views bigint unsigned default 0,
+ ss_total_edits bigint unsigned default 0,
+ ss_good_articles bigint unsigned default 0,
+ ss_total_pages bigint default '-1',
+ ss_users bigint default '-1',
+ ss_active_users bigint default '-1',
+ ss_admins int default '-1',
+ ss_images int default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id);
+CREATE TABLE /*_*/hitcounter (
+ hc_id int unsigned NOT NULL
+) ENGINE=HEAP MAX_ROWS=25000;
+CREATE TABLE /*_*/ipblocks (
+ ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ ipb_address tinyblob NOT NULL,
+ ipb_user int unsigned NOT NULL default 0,
+ ipb_by int unsigned NOT NULL default 0,
+ ipb_by_text varchar(255) binary NOT NULL default '',
+ ipb_reason tinyblob NOT NULL,
+ ipb_timestamp binary(14) NOT NULL default '',
+ ipb_auto bool NOT NULL default 0,
+ ipb_anon_only bool NOT NULL default 0,
+ ipb_create_account bool NOT NULL default 1,
+ ipb_enable_autoblock bool NOT NULL default '1',
+ ipb_expiry varbinary(14) NOT NULL default '',
+ ipb_range_start tinyblob NOT NULL,
+ ipb_range_end tinyblob NOT NULL,
+ ipb_deleted bool NOT NULL default 0,
+ ipb_block_email bool NOT NULL default 0,
+ ipb_allow_usertalk bool NOT NULL default 0
+) /*$wgDBTableOptions*/;
+
+CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only);
+CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user);
+CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8));
+CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp);
+CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry);
+CREATE TABLE /*_*/image (
+ img_name varchar(255) binary NOT NULL default '' PRIMARY KEY,
+ img_size int unsigned NOT NULL default 0,
+ img_width int NOT NULL default 0,
+ img_height int NOT NULL default 0,
+ img_metadata mediumblob NOT NULL,
+ img_bits int NOT NULL default 0,
+ img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown",
+ img_minor_mime varbinary(32) NOT NULL default "unknown",
+ img_description tinyblob NOT NULL,
+ img_user int unsigned NOT NULL default 0,
+ img_user_text varchar(255) binary NOT NULL,
+ img_timestamp varbinary(14) NOT NULL default '',
+ img_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
+CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
+CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1);
+CREATE TABLE /*_*/oldimage (
+ oi_name varchar(255) binary NOT NULL default '',
+ oi_archive_name varchar(255) binary NOT NULL default '',
+ oi_size int unsigned NOT NULL default 0,
+ oi_width int NOT NULL default 0,
+ oi_height int NOT NULL default 0,
+ oi_bits int NOT NULL default 0,
+ oi_description tinyblob NOT NULL,
+ oi_user int unsigned NOT NULL default 0,
+ oi_user_text varchar(255) binary NOT NULL,
+ oi_timestamp binary(14) NOT NULL default '',
+ oi_metadata mediumblob NOT NULL,
+ oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown",
+ oi_minor_mime varbinary(32) NOT NULL default "unknown",
+ oi_deleted tinyint unsigned NOT NULL default 0,
+ oi_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
+CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
+CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1);
+CREATE TABLE /*_*/filearchive (
+ fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ fa_name varchar(255) binary NOT NULL default '',
+ fa_archive_name varchar(255) binary default '',
+ fa_storage_group varbinary(16),
+ fa_storage_key varbinary(64) default '',
+ fa_deleted_user int,
+ fa_deleted_timestamp binary(14) default '',
+ fa_deleted_reason text,
+ fa_size int unsigned default 0,
+ fa_width int default 0,
+ fa_height int default 0,
+ fa_metadata mediumblob,
+ fa_bits int default 0,
+ fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown",
+ fa_minor_mime varbinary(32) default "unknown",
+ fa_description tinyblob,
+ fa_user int unsigned default 0,
+ fa_user_text varchar(255) binary,
+ fa_timestamp binary(14) default '',
+ fa_deleted tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp);
+CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key);
+CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
+CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+CREATE TABLE /*_*/recentchanges (
+ rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ rc_timestamp varbinary(14) NOT NULL default '',
+ rc_cur_time varbinary(14) NOT NULL default '',
+ rc_user int unsigned NOT NULL default 0,
+ rc_user_text varchar(255) binary NOT NULL,
+ rc_namespace int NOT NULL default 0,
+ rc_title varchar(255) binary NOT NULL default '',
+ rc_comment varchar(255) binary NOT NULL default '',
+ rc_minor tinyint unsigned NOT NULL default 0,
+ rc_bot tinyint unsigned NOT NULL default 0,
+ rc_new tinyint unsigned NOT NULL default 0,
+ rc_cur_id int unsigned NOT NULL default 0,
+ rc_this_oldid int unsigned NOT NULL default 0,
+ rc_last_oldid int unsigned NOT NULL default 0,
+ rc_type tinyint unsigned NOT NULL default 0,
+ rc_moved_to_ns tinyint unsigned NOT NULL default 0,
+ rc_moved_to_title varchar(255) binary NOT NULL default '',
+ rc_patrolled tinyint unsigned NOT NULL default 0,
+ rc_ip varbinary(40) NOT NULL default '',
+ rc_old_len int,
+ rc_new_len int,
+ rc_deleted tinyint unsigned NOT NULL default 0,
+ rc_logid int unsigned NOT NULL default 0,
+ rc_log_type varbinary(255) NULL default NULL,
+ rc_log_action varbinary(255) NULL default NULL,
+ rc_params blob NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp);
+CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title);
+CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id);
+CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp);
+CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip);
+CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text);
+CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp);
+CREATE TABLE /*_*/watchlist (
+ wl_user int unsigned NOT NULL,
+ wl_namespace int NOT NULL default 0,
+ wl_title varchar(255) binary NOT NULL default '',
+ wl_notificationtimestamp varbinary(14)
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title);
+CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title);
+CREATE TABLE /*_*/math (
+ math_inputhash varbinary(16) NOT NULL,
+ math_outputhash varbinary(16) NOT NULL,
+ math_html_conservativeness tinyint NOT NULL,
+ math_html text,
+ math_mathml text
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/math_inputhash ON /*_*/math (math_inputhash);
+CREATE TABLE /*_*/searchindex (
+ si_page int unsigned NOT NULL,
+ si_title varchar(255) NOT NULL default '',
+ si_text mediumtext NOT NULL
+) ENGINE=MyISAM;
+CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page);
+CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title);
+CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text);
+CREATE TABLE /*_*/interwiki (
+ iw_prefix varchar(32) NOT NULL,
+ iw_url blob NOT NULL,
+ iw_local bool NOT NULL,
+ iw_trans tinyint NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix);
+CREATE TABLE /*_*/querycache (
+ qc_type varbinary(32) NOT NULL,
+ qc_value int unsigned NOT NULL default 0,
+ qc_namespace int NOT NULL default 0,
+ qc_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value);
+CREATE TABLE /*_*/objectcache (
+ keyname varbinary(255) NOT NULL default '' PRIMARY KEY,
+ value mediumblob,
+ exptime datetime
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime);
+CREATE TABLE /*_*/transcache (
+ tc_url varbinary(255) NOT NULL,
+ tc_contents text,
+ tc_time int NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url);
+CREATE TABLE /*_*/logging (
+ log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ log_type varbinary(10) NOT NULL default '',
+ log_action varbinary(10) NOT NULL default '',
+ log_timestamp binary(14) NOT NULL default '19700101000000',
+ log_user int unsigned NOT NULL default 0,
+ log_namespace int NOT NULL default 0,
+ log_title varchar(255) binary NOT NULL default '',
+ log_comment varchar(255) NOT NULL default '',
+ log_params blob NOT NULL,
+ log_deleted tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp);
+CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp);
+CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp);
+CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp);
+CREATE TABLE /*_*/trackbacks (
+ tb_id int PRIMARY KEY AUTO_INCREMENT,
+ tb_page int REFERENCES /*_*/page(page_id) ON DELETE CASCADE,
+ tb_title varchar(255) NOT NULL,
+ tb_url blob NOT NULL,
+ tb_ex text,
+ tb_name varchar(255)
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/tb_page ON /*_*/trackbacks (tb_page);
+CREATE TABLE /*_*/job (
+ job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ job_cmd varbinary(60) NOT NULL default '',
+ job_namespace int NOT NULL,
+ job_title varchar(255) binary NOT NULL,
+ job_params blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title);
+CREATE TABLE /*_*/querycache_info (
+ qci_type varbinary(32) NOT NULL default '',
+ qci_timestamp binary(14) NOT NULL default '19700101000000'
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type);
+CREATE TABLE /*_*/redirect (
+ rd_from int unsigned NOT NULL default 0 PRIMARY KEY,
+ rd_namespace int NOT NULL default 0,
+ rd_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from);
+CREATE TABLE /*_*/querycachetwo (
+ qcc_type varbinary(32) NOT NULL,
+ qcc_value int unsigned NOT NULL default 0,
+ qcc_namespace int NOT NULL default 0,
+ qcc_title varchar(255) binary NOT NULL default '',
+ qcc_namespacetwo int NOT NULL default 0,
+ qcc_titletwo varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value);
+CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title);
+CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo);
+CREATE TABLE /*_*/page_restrictions (
+ pr_page int NOT NULL,
+ pr_type varbinary(60) NOT NULL,
+ pr_level varbinary(60) NOT NULL,
+ pr_cascade tinyint NOT NULL,
+ pr_user int NULL,
+ pr_expiry varbinary(14) NULL,
+ pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type);
+CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level);
+CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level);
+CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade);
+CREATE TABLE /*_*/protected_titles (
+ pt_namespace int NOT NULL,
+ pt_title varchar(255) binary NOT NULL,
+ pt_user int unsigned NOT NULL,
+ pt_reason tinyblob,
+ pt_timestamp binary(14) NOT NULL,
+ pt_expiry varbinary(14) NOT NULL default '',
+ pt_create_perm varbinary(60) NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title);
+CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp);
+CREATE TABLE /*_*/page_props (
+ pp_page int NOT NULL,
+ pp_propname varbinary(60) NOT NULL,
+ pp_value blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname);
+CREATE TABLE /*_*/updatelog (
+ ul_key varchar(255) NOT NULL PRIMARY KEY
+) /*$wgDBTableOptions*/;
+CREATE TABLE /*_*/change_tag (
+ ct_rc_id int NULL,
+ ct_log_id int NULL,
+ ct_rev_id int NULL,
+ ct_tag varchar(255) NOT NULL,
+ ct_params blob NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag);
+CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag);
+CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag);
+CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id);
+CREATE TABLE /*_*/tag_summary (
+ ts_rc_id int NULL,
+ ts_log_id int NULL,
+ ts_rev_id int NULL,
+ ts_tags blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id);
+CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id);
+CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id);
+CREATE TABLE /*_*/valid_tag (
+ vt_tag varchar(255) NOT NULL PRIMARY KEY
+) /*$wgDBTableOptions*/;
diff --git a/tests/phpunit/includes/db/sqlite/tables-1.16.sql b/tests/phpunit/includes/db/sqlite/tables-1.16.sql
new file mode 100644
index 00000000..6e56add2
--- /dev/null
+++ b/tests/phpunit/includes/db/sqlite/tables-1.16.sql
@@ -0,0 +1,483 @@
+-- This is a copy of MediaWiki 1.16 schema shared by MySQL and SQLite.
+-- It is used for updater testing. Comments are stripped to decrease
+-- file size, as we don't need to maintain it.
+
+CREATE TABLE /*_*/user (
+ user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ user_name varchar(255) binary NOT NULL default '',
+ user_real_name varchar(255) binary NOT NULL default '',
+ user_password tinyblob NOT NULL,
+ user_newpassword tinyblob NOT NULL,
+ user_newpass_time binary(14),
+ user_email tinytext NOT NULL,
+ user_options blob NOT NULL,
+ user_touched binary(14) NOT NULL default '',
+ user_token binary(32) NOT NULL default '',
+ user_email_authenticated binary(14),
+ user_email_token binary(32),
+ user_email_token_expires binary(14),
+ user_registration binary(14),
+ user_editcount int
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name);
+CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token);
+CREATE TABLE /*_*/user_groups (
+ ug_user int unsigned NOT NULL default 0,
+ ug_group varbinary(16) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group);
+CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group);
+CREATE TABLE /*_*/user_newtalk (
+ user_id int NOT NULL default 0,
+ user_ip varbinary(40) NOT NULL default '',
+ user_last_timestamp binary(14) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id);
+CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip);
+CREATE TABLE /*_*/user_properties (
+ up_user int NOT NULL,
+ up_property varbinary(32) NOT NULL,
+ up_value blob
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property);
+CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property);
+CREATE TABLE /*_*/page (
+ page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ page_namespace int NOT NULL,
+ page_title varchar(255) binary NOT NULL,
+ page_restrictions tinyblob NOT NULL,
+ page_counter bigint unsigned NOT NULL default 0,
+ page_is_redirect tinyint unsigned NOT NULL default 0,
+ page_is_new tinyint unsigned NOT NULL default 0,
+ page_random real unsigned NOT NULL,
+ page_touched binary(14) NOT NULL default '',
+ page_latest int unsigned NOT NULL,
+ page_len int unsigned NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title);
+CREATE INDEX /*i*/page_random ON /*_*/page (page_random);
+CREATE INDEX /*i*/page_len ON /*_*/page (page_len);
+CREATE TABLE /*_*/revision (
+ rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ rev_page int unsigned NOT NULL,
+ rev_text_id int unsigned NOT NULL,
+ rev_comment tinyblob NOT NULL,
+ rev_user int unsigned NOT NULL default 0,
+ rev_user_text varchar(255) binary NOT NULL default '',
+ rev_timestamp binary(14) NOT NULL default '',
+ rev_minor_edit tinyint unsigned NOT NULL default 0,
+ rev_deleted tinyint unsigned NOT NULL default 0,
+ rev_len int unsigned,
+ rev_parent_id int unsigned default NULL
+) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024;
+CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id);
+CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp);
+CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp);
+CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp);
+CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp);
+CREATE TABLE /*_*/text (
+ old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ old_text mediumblob NOT NULL,
+ old_flags tinyblob NOT NULL
+) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240;
+CREATE TABLE /*_*/archive (
+ ar_namespace int NOT NULL default 0,
+ ar_title varchar(255) binary NOT NULL default '',
+ ar_text mediumblob NOT NULL,
+ ar_comment tinyblob NOT NULL,
+ ar_user int unsigned NOT NULL default 0,
+ ar_user_text varchar(255) binary NOT NULL,
+ ar_timestamp binary(14) NOT NULL default '',
+ ar_minor_edit tinyint NOT NULL default 0,
+ ar_flags tinyblob NOT NULL,
+ ar_rev_id int unsigned,
+ ar_text_id int unsigned,
+ ar_deleted tinyint unsigned NOT NULL default 0,
+ ar_len int unsigned,
+ ar_page_id int unsigned,
+ ar_parent_id int unsigned default NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
+CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp);
+CREATE TABLE /*_*/pagelinks (
+ pl_from int unsigned NOT NULL default 0,
+ pl_namespace int NOT NULL default 0,
+ pl_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title);
+CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from);
+CREATE TABLE /*_*/templatelinks (
+ tl_from int unsigned NOT NULL default 0,
+ tl_namespace int NOT NULL default 0,
+ tl_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title);
+CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from);
+CREATE TABLE /*_*/imagelinks (
+ il_from int unsigned NOT NULL default 0,
+ il_to varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to);
+CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from);
+CREATE TABLE /*_*/categorylinks (
+ cl_from int unsigned NOT NULL default 0,
+ cl_to varchar(255) binary NOT NULL default '',
+ cl_sortkey varchar(70) binary NOT NULL default '',
+ cl_timestamp timestamp NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to);
+CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_sortkey,cl_from);
+CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp);
+CREATE TABLE /*_*/category (
+ cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ cat_title varchar(255) binary NOT NULL,
+ cat_pages int signed NOT NULL default 0,
+ cat_subcats int signed NOT NULL default 0,
+ cat_files int signed NOT NULL default 0,
+ cat_hidden tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title);
+CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages);
+CREATE TABLE /*_*/externallinks (
+ el_from int unsigned NOT NULL default 0,
+ el_to blob NOT NULL,
+ el_index blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40));
+CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from);
+CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60));
+CREATE TABLE /*_*/external_user (
+ eu_local_id int unsigned NOT NULL PRIMARY KEY,
+ eu_external_id varchar(255) binary NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/eu_external_id ON /*_*/external_user (eu_external_id);
+CREATE TABLE /*_*/langlinks (
+ ll_from int unsigned NOT NULL default 0,
+ ll_lang varbinary(20) NOT NULL default '',
+ ll_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang);
+CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title);
+CREATE TABLE /*_*/site_stats (
+ ss_row_id int unsigned NOT NULL,
+ ss_total_views bigint unsigned default 0,
+ ss_total_edits bigint unsigned default 0,
+ ss_good_articles bigint unsigned default 0,
+ ss_total_pages bigint default '-1',
+ ss_users bigint default '-1',
+ ss_active_users bigint default '-1',
+ ss_admins int default '-1',
+ ss_images int default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id);
+CREATE TABLE /*_*/hitcounter (
+ hc_id int unsigned NOT NULL
+) ENGINE=HEAP MAX_ROWS=25000;
+CREATE TABLE /*_*/ipblocks (
+ ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ ipb_address tinyblob NOT NULL,
+ ipb_user int unsigned NOT NULL default 0,
+ ipb_by int unsigned NOT NULL default 0,
+ ipb_by_text varchar(255) binary NOT NULL default '',
+ ipb_reason tinyblob NOT NULL,
+ ipb_timestamp binary(14) NOT NULL default '',
+ ipb_auto bool NOT NULL default 0,
+ ipb_anon_only bool NOT NULL default 0,
+ ipb_create_account bool NOT NULL default 1,
+ ipb_enable_autoblock bool NOT NULL default '1',
+ ipb_expiry varbinary(14) NOT NULL default '',
+ ipb_range_start tinyblob NOT NULL,
+ ipb_range_end tinyblob NOT NULL,
+ ipb_deleted bool NOT NULL default 0,
+ ipb_block_email bool NOT NULL default 0,
+ ipb_allow_usertalk bool NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only);
+CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user);
+CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8));
+CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp);
+CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry);
+CREATE TABLE /*_*/image (
+ img_name varchar(255) binary NOT NULL default '' PRIMARY KEY,
+ img_size int unsigned NOT NULL default 0,
+ img_width int NOT NULL default 0,
+ img_height int NOT NULL default 0,
+ img_metadata mediumblob NOT NULL,
+ img_bits int NOT NULL default 0,
+ img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown",
+ img_minor_mime varbinary(100) NOT NULL default "unknown",
+ img_description tinyblob NOT NULL,
+ img_user int unsigned NOT NULL default 0,
+ img_user_text varchar(255) binary NOT NULL,
+ img_timestamp varbinary(14) NOT NULL default '',
+ img_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
+CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
+CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1);
+CREATE TABLE /*_*/oldimage (
+ oi_name varchar(255) binary NOT NULL default '',
+ oi_archive_name varchar(255) binary NOT NULL default '',
+ oi_size int unsigned NOT NULL default 0,
+ oi_width int NOT NULL default 0,
+ oi_height int NOT NULL default 0,
+ oi_bits int NOT NULL default 0,
+ oi_description tinyblob NOT NULL,
+ oi_user int unsigned NOT NULL default 0,
+ oi_user_text varchar(255) binary NOT NULL,
+ oi_timestamp binary(14) NOT NULL default '',
+ oi_metadata mediumblob NOT NULL,
+ oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown",
+ oi_minor_mime varbinary(100) NOT NULL default "unknown",
+ oi_deleted tinyint unsigned NOT NULL default 0,
+ oi_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
+CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
+CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1);
+CREATE TABLE /*_*/filearchive (
+ fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ fa_name varchar(255) binary NOT NULL default '',
+ fa_archive_name varchar(255) binary default '',
+ fa_storage_group varbinary(16),
+ fa_storage_key varbinary(64) default '',
+ fa_deleted_user int,
+ fa_deleted_timestamp binary(14) default '',
+ fa_deleted_reason text,
+ fa_size int unsigned default 0,
+ fa_width int default 0,
+ fa_height int default 0,
+ fa_metadata mediumblob,
+ fa_bits int default 0,
+ fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown",
+ fa_minor_mime varbinary(100) default "unknown",
+ fa_description tinyblob,
+ fa_user int unsigned default 0,
+ fa_user_text varchar(255) binary,
+ fa_timestamp binary(14) default '',
+ fa_deleted tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp);
+CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key);
+CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
+CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+CREATE TABLE /*_*/recentchanges (
+ rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ rc_timestamp varbinary(14) NOT NULL default '',
+ rc_cur_time varbinary(14) NOT NULL default '',
+ rc_user int unsigned NOT NULL default 0,
+ rc_user_text varchar(255) binary NOT NULL,
+ rc_namespace int NOT NULL default 0,
+ rc_title varchar(255) binary NOT NULL default '',
+ rc_comment varchar(255) binary NOT NULL default '',
+ rc_minor tinyint unsigned NOT NULL default 0,
+ rc_bot tinyint unsigned NOT NULL default 0,
+ rc_new tinyint unsigned NOT NULL default 0,
+ rc_cur_id int unsigned NOT NULL default 0,
+ rc_this_oldid int unsigned NOT NULL default 0,
+ rc_last_oldid int unsigned NOT NULL default 0,
+ rc_type tinyint unsigned NOT NULL default 0,
+ rc_moved_to_ns tinyint unsigned NOT NULL default 0,
+ rc_moved_to_title varchar(255) binary NOT NULL default '',
+ rc_patrolled tinyint unsigned NOT NULL default 0,
+ rc_ip varbinary(40) NOT NULL default '',
+ rc_old_len int,
+ rc_new_len int,
+ rc_deleted tinyint unsigned NOT NULL default 0,
+ rc_logid int unsigned NOT NULL default 0,
+ rc_log_type varbinary(255) NULL default NULL,
+ rc_log_action varbinary(255) NULL default NULL,
+ rc_params blob NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp);
+CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title);
+CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id);
+CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp);
+CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip);
+CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text);
+CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp);
+CREATE TABLE /*_*/watchlist (
+ wl_user int unsigned NOT NULL,
+ wl_namespace int NOT NULL default 0,
+ wl_title varchar(255) binary NOT NULL default '',
+ wl_notificationtimestamp varbinary(14)
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title);
+CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title);
+CREATE TABLE /*_*/math (
+ math_inputhash varbinary(16) NOT NULL,
+ math_outputhash varbinary(16) NOT NULL,
+ math_html_conservativeness tinyint NOT NULL,
+ math_html text,
+ math_mathml text
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/math_inputhash ON /*_*/math (math_inputhash);
+CREATE TABLE /*_*/searchindex (
+ si_page int unsigned NOT NULL,
+ si_title varchar(255) NOT NULL default '',
+ si_text mediumtext NOT NULL
+) ENGINE=MyISAM;
+CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page);
+CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title);
+CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text);
+CREATE TABLE /*_*/interwiki (
+ iw_prefix varchar(32) NOT NULL,
+ iw_url blob NOT NULL,
+ iw_local bool NOT NULL,
+ iw_trans tinyint NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix);
+CREATE TABLE /*_*/querycache (
+ qc_type varbinary(32) NOT NULL,
+ qc_value int unsigned NOT NULL default 0,
+ qc_namespace int NOT NULL default 0,
+ qc_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value);
+CREATE TABLE /*_*/objectcache (
+ keyname varbinary(255) NOT NULL default '' PRIMARY KEY,
+ value mediumblob,
+ exptime datetime
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime);
+CREATE TABLE /*_*/transcache (
+ tc_url varbinary(255) NOT NULL,
+ tc_contents text,
+ tc_time binary(14) NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url);
+CREATE TABLE /*_*/logging (
+ log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ log_type varbinary(32) NOT NULL default '',
+ log_action varbinary(32) NOT NULL default '',
+ log_timestamp binary(14) NOT NULL default '19700101000000',
+ log_user int unsigned NOT NULL default 0,
+ log_user_text varchar(255) binary NOT NULL default '',
+ log_namespace int NOT NULL default 0,
+ log_title varchar(255) binary NOT NULL default '',
+ log_page int unsigned NULL,
+ log_comment varchar(255) NOT NULL default '',
+ log_params blob NOT NULL,
+ log_deleted tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp);
+CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp);
+CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp);
+CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp);
+CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp);
+CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp);
+CREATE TABLE /*_*/log_search (
+ ls_field varbinary(32) NOT NULL,
+ ls_value varchar(255) NOT NULL,
+ ls_log_id int unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id);
+CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id);
+CREATE TABLE /*_*/trackbacks (
+ tb_id int PRIMARY KEY AUTO_INCREMENT,
+ tb_page int REFERENCES /*_*/page(page_id) ON DELETE CASCADE,
+ tb_title varchar(255) NOT NULL,
+ tb_url blob NOT NULL,
+ tb_ex text,
+ tb_name varchar(255)
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/tb_page ON /*_*/trackbacks (tb_page);
+CREATE TABLE /*_*/job (
+ job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ job_cmd varbinary(60) NOT NULL default '',
+ job_namespace int NOT NULL,
+ job_title varchar(255) binary NOT NULL,
+ job_params blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128));
+CREATE TABLE /*_*/querycache_info (
+ qci_type varbinary(32) NOT NULL default '',
+ qci_timestamp binary(14) NOT NULL default '19700101000000'
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type);
+CREATE TABLE /*_*/redirect (
+ rd_from int unsigned NOT NULL default 0 PRIMARY KEY,
+ rd_namespace int NOT NULL default 0,
+ rd_title varchar(255) binary NOT NULL default '',
+ rd_interwiki varchar(32) default NULL,
+ rd_fragment varchar(255) binary default NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from);
+CREATE TABLE /*_*/querycachetwo (
+ qcc_type varbinary(32) NOT NULL,
+ qcc_value int unsigned NOT NULL default 0,
+ qcc_namespace int NOT NULL default 0,
+ qcc_title varchar(255) binary NOT NULL default '',
+ qcc_namespacetwo int NOT NULL default 0,
+ qcc_titletwo varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value);
+CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title);
+CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo);
+CREATE TABLE /*_*/page_restrictions (
+ pr_page int NOT NULL,
+ pr_type varbinary(60) NOT NULL,
+ pr_level varbinary(60) NOT NULL,
+ pr_cascade tinyint NOT NULL,
+ pr_user int NULL,
+ pr_expiry varbinary(14) NULL,
+ pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type);
+CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level);
+CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level);
+CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade);
+CREATE TABLE /*_*/protected_titles (
+ pt_namespace int NOT NULL,
+ pt_title varchar(255) binary NOT NULL,
+ pt_user int unsigned NOT NULL,
+ pt_reason tinyblob,
+ pt_timestamp binary(14) NOT NULL,
+ pt_expiry varbinary(14) NOT NULL default '',
+ pt_create_perm varbinary(60) NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title);
+CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp);
+CREATE TABLE /*_*/page_props (
+ pp_page int NOT NULL,
+ pp_propname varbinary(60) NOT NULL,
+ pp_value blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname);
+CREATE TABLE /*_*/updatelog (
+ ul_key varchar(255) NOT NULL PRIMARY KEY
+) /*$wgDBTableOptions*/;
+CREATE TABLE /*_*/change_tag (
+ ct_rc_id int NULL,
+ ct_log_id int NULL,
+ ct_rev_id int NULL,
+ ct_tag varchar(255) NOT NULL,
+ ct_params blob NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag);
+CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag);
+CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag);
+CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id);
+CREATE TABLE /*_*/tag_summary (
+ ts_rc_id int NULL,
+ ts_log_id int NULL,
+ ts_rev_id int NULL,
+ ts_tags blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id);
+CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id);
+CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id);
+CREATE TABLE /*_*/valid_tag (
+ vt_tag varchar(255) NOT NULL PRIMARY KEY
+) /*$wgDBTableOptions*/;
+CREATE TABLE /*_*/l10n_cache (
+ lc_lang varbinary(32) NOT NULL,
+ lc_key varchar(255) NOT NULL,
+ lc_value mediumblob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key);
diff --git a/tests/phpunit/includes/db/sqlite/tables-1.17.sql b/tests/phpunit/includes/db/sqlite/tables-1.17.sql
new file mode 100644
index 00000000..69ae3764
--- /dev/null
+++ b/tests/phpunit/includes/db/sqlite/tables-1.17.sql
@@ -0,0 +1,516 @@
+-- This is a copy of MediaWiki 1.17 schema shared by MySQL and SQLite.
+-- It is used for updater testing. Comments are stripped to decrease
+-- file size, as we don't need to maintain it.
+
+CREATE TABLE /*_*/user (
+ user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ user_name varchar(255) binary NOT NULL default '',
+ user_real_name varchar(255) binary NOT NULL default '',
+ user_password tinyblob NOT NULL,
+ user_newpassword tinyblob NOT NULL,
+ user_newpass_time binary(14),
+ user_email tinytext NOT NULL,
+ user_options blob NOT NULL,
+ user_touched binary(14) NOT NULL default '',
+ user_token binary(32) NOT NULL default '',
+ user_email_authenticated binary(14),
+ user_email_token binary(32),
+ user_email_token_expires binary(14),
+ user_registration binary(14),
+ user_editcount int
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name);
+CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token);
+CREATE TABLE /*_*/user_groups (
+ ug_user int unsigned NOT NULL default 0,
+ ug_group varbinary(16) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group);
+CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group);
+CREATE TABLE /*_*/user_newtalk (
+ user_id int NOT NULL default 0,
+ user_ip varbinary(40) NOT NULL default '',
+ user_last_timestamp binary(14) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id);
+CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip);
+CREATE TABLE /*_*/user_properties (
+ up_user int NOT NULL,
+ up_property varbinary(32) NOT NULL,
+ up_value blob
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property);
+CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property);
+CREATE TABLE /*_*/page (
+ page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ page_namespace int NOT NULL,
+ page_title varchar(255) binary NOT NULL,
+ page_restrictions tinyblob NOT NULL,
+ page_counter bigint unsigned NOT NULL default 0,
+ page_is_redirect tinyint unsigned NOT NULL default 0,
+ page_is_new tinyint unsigned NOT NULL default 0,
+ page_random real unsigned NOT NULL,
+ page_touched binary(14) NOT NULL default '',
+ page_latest int unsigned NOT NULL,
+ page_len int unsigned NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title);
+CREATE INDEX /*i*/page_random ON /*_*/page (page_random);
+CREATE INDEX /*i*/page_len ON /*_*/page (page_len);
+CREATE TABLE /*_*/revision (
+ rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ rev_page int unsigned NOT NULL,
+ rev_text_id int unsigned NOT NULL,
+ rev_comment tinyblob NOT NULL,
+ rev_user int unsigned NOT NULL default 0,
+ rev_user_text varchar(255) binary NOT NULL default '',
+ rev_timestamp binary(14) NOT NULL default '',
+ rev_minor_edit tinyint unsigned NOT NULL default 0,
+ rev_deleted tinyint unsigned NOT NULL default 0,
+ rev_len int unsigned,
+ rev_parent_id int unsigned default NULL
+) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024;
+CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id);
+CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp);
+CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp);
+CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp);
+CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp);
+CREATE TABLE /*_*/text (
+ old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ old_text mediumblob NOT NULL,
+ old_flags tinyblob NOT NULL
+) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240;
+CREATE TABLE /*_*/archive (
+ ar_namespace int NOT NULL default 0,
+ ar_title varchar(255) binary NOT NULL default '',
+ ar_text mediumblob NOT NULL,
+ ar_comment tinyblob NOT NULL,
+ ar_user int unsigned NOT NULL default 0,
+ ar_user_text varchar(255) binary NOT NULL,
+ ar_timestamp binary(14) NOT NULL default '',
+ ar_minor_edit tinyint NOT NULL default 0,
+ ar_flags tinyblob NOT NULL,
+ ar_rev_id int unsigned,
+ ar_text_id int unsigned,
+ ar_deleted tinyint unsigned NOT NULL default 0,
+ ar_len int unsigned,
+ ar_page_id int unsigned,
+ ar_parent_id int unsigned default NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
+CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp);
+CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id);
+CREATE TABLE /*_*/pagelinks (
+ pl_from int unsigned NOT NULL default 0,
+ pl_namespace int NOT NULL default 0,
+ pl_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title);
+CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from);
+CREATE TABLE /*_*/templatelinks (
+ tl_from int unsigned NOT NULL default 0,
+ tl_namespace int NOT NULL default 0,
+ tl_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title);
+CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from);
+CREATE TABLE /*_*/imagelinks (
+ il_from int unsigned NOT NULL default 0,
+ il_to varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to);
+CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from);
+CREATE TABLE /*_*/categorylinks (
+ cl_from int unsigned NOT NULL default 0,
+ cl_to varchar(255) binary NOT NULL default '',
+ cl_sortkey varbinary(230) NOT NULL default '',
+ cl_sortkey_prefix varchar(255) binary NOT NULL default '',
+ cl_timestamp timestamp NOT NULL,
+ cl_collation varbinary(32) NOT NULL default '',
+ cl_type ENUM('page', 'subcat', 'file') NOT NULL default 'page'
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to);
+CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_type,cl_sortkey,cl_from);
+CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp);
+CREATE INDEX /*i*/cl_collation ON /*_*/categorylinks (cl_collation);
+CREATE TABLE /*_*/category (
+ cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ cat_title varchar(255) binary NOT NULL,
+ cat_pages int signed NOT NULL default 0,
+ cat_subcats int signed NOT NULL default 0,
+ cat_files int signed NOT NULL default 0,
+ cat_hidden tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title);
+CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages);
+CREATE TABLE /*_*/externallinks (
+ el_from int unsigned NOT NULL default 0,
+ el_to blob NOT NULL,
+ el_index blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40));
+CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from);
+CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60));
+CREATE TABLE /*_*/external_user (
+ eu_local_id int unsigned NOT NULL PRIMARY KEY,
+ eu_external_id varchar(255) binary NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/eu_external_id ON /*_*/external_user (eu_external_id);
+CREATE TABLE /*_*/langlinks (
+ ll_from int unsigned NOT NULL default 0,
+ ll_lang varbinary(20) NOT NULL default '',
+ ll_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang);
+CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title);
+CREATE TABLE /*_*/iwlinks (
+ iwl_from int unsigned NOT NULL default 0,
+ iwl_prefix varbinary(20) NOT NULL default '',
+ iwl_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title);
+CREATE UNIQUE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from);
+CREATE TABLE /*_*/site_stats (
+ ss_row_id int unsigned NOT NULL,
+ ss_total_views bigint unsigned default 0,
+ ss_total_edits bigint unsigned default 0,
+ ss_good_articles bigint unsigned default 0,
+ ss_total_pages bigint default '-1',
+ ss_users bigint default '-1',
+ ss_active_users bigint default '-1',
+ ss_admins int default '-1',
+ ss_images int default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id);
+CREATE TABLE /*_*/hitcounter (
+ hc_id int unsigned NOT NULL
+) ENGINE=HEAP MAX_ROWS=25000;
+CREATE TABLE /*_*/ipblocks (
+ ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ ipb_address tinyblob NOT NULL,
+ ipb_user int unsigned NOT NULL default 0,
+ ipb_by int unsigned NOT NULL default 0,
+ ipb_by_text varchar(255) binary NOT NULL default '',
+ ipb_reason tinyblob NOT NULL,
+ ipb_timestamp binary(14) NOT NULL default '',
+ ipb_auto bool NOT NULL default 0,
+ ipb_anon_only bool NOT NULL default 0,
+ ipb_create_account bool NOT NULL default 1,
+ ipb_enable_autoblock bool NOT NULL default '1',
+ ipb_expiry varbinary(14) NOT NULL default '',
+ ipb_range_start tinyblob NOT NULL,
+ ipb_range_end tinyblob NOT NULL,
+ ipb_deleted bool NOT NULL default 0,
+ ipb_block_email bool NOT NULL default 0,
+ ipb_allow_usertalk bool NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only);
+CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user);
+CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8));
+CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp);
+CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry);
+CREATE TABLE /*_*/image (
+ img_name varchar(255) binary NOT NULL default '' PRIMARY KEY,
+ img_size int unsigned NOT NULL default 0,
+ img_width int NOT NULL default 0,
+ img_height int NOT NULL default 0,
+ img_metadata mediumblob NOT NULL,
+ img_bits int NOT NULL default 0,
+ img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown",
+ img_minor_mime varbinary(100) NOT NULL default "unknown",
+ img_description tinyblob NOT NULL,
+ img_user int unsigned NOT NULL default 0,
+ img_user_text varchar(255) binary NOT NULL,
+ img_timestamp varbinary(14) NOT NULL default '',
+ img_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
+CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
+CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1);
+CREATE TABLE /*_*/oldimage (
+ oi_name varchar(255) binary NOT NULL default '',
+ oi_archive_name varchar(255) binary NOT NULL default '',
+ oi_size int unsigned NOT NULL default 0,
+ oi_width int NOT NULL default 0,
+ oi_height int NOT NULL default 0,
+ oi_bits int NOT NULL default 0,
+ oi_description tinyblob NOT NULL,
+ oi_user int unsigned NOT NULL default 0,
+ oi_user_text varchar(255) binary NOT NULL,
+ oi_timestamp binary(14) NOT NULL default '',
+ oi_metadata mediumblob NOT NULL,
+ oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown",
+ oi_minor_mime varbinary(100) NOT NULL default "unknown",
+ oi_deleted tinyint unsigned NOT NULL default 0,
+ oi_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
+CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
+CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1);
+CREATE TABLE /*_*/filearchive (
+ fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ fa_name varchar(255) binary NOT NULL default '',
+ fa_archive_name varchar(255) binary default '',
+ fa_storage_group varbinary(16),
+ fa_storage_key varbinary(64) default '',
+ fa_deleted_user int,
+ fa_deleted_timestamp binary(14) default '',
+ fa_deleted_reason text,
+ fa_size int unsigned default 0,
+ fa_width int default 0,
+ fa_height int default 0,
+ fa_metadata mediumblob,
+ fa_bits int default 0,
+ fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown",
+ fa_minor_mime varbinary(100) default "unknown",
+ fa_description tinyblob,
+ fa_user int unsigned default 0,
+ fa_user_text varchar(255) binary,
+ fa_timestamp binary(14) default '',
+ fa_deleted tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp);
+CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key);
+CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
+CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+CREATE TABLE /*_*/recentchanges (
+ rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ rc_timestamp varbinary(14) NOT NULL default '',
+ rc_cur_time varbinary(14) NOT NULL default '',
+ rc_user int unsigned NOT NULL default 0,
+ rc_user_text varchar(255) binary NOT NULL,
+ rc_namespace int NOT NULL default 0,
+ rc_title varchar(255) binary NOT NULL default '',
+ rc_comment varchar(255) binary NOT NULL default '',
+ rc_minor tinyint unsigned NOT NULL default 0,
+ rc_bot tinyint unsigned NOT NULL default 0,
+ rc_new tinyint unsigned NOT NULL default 0,
+ rc_cur_id int unsigned NOT NULL default 0,
+ rc_this_oldid int unsigned NOT NULL default 0,
+ rc_last_oldid int unsigned NOT NULL default 0,
+ rc_type tinyint unsigned NOT NULL default 0,
+ rc_moved_to_ns tinyint unsigned NOT NULL default 0,
+ rc_moved_to_title varchar(255) binary NOT NULL default '',
+ rc_patrolled tinyint unsigned NOT NULL default 0,
+ rc_ip varbinary(40) NOT NULL default '',
+ rc_old_len int,
+ rc_new_len int,
+ rc_deleted tinyint unsigned NOT NULL default 0,
+ rc_logid int unsigned NOT NULL default 0,
+ rc_log_type varbinary(255) NULL default NULL,
+ rc_log_action varbinary(255) NULL default NULL,
+ rc_params blob NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp);
+CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title);
+CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id);
+CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp);
+CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip);
+CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text);
+CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp);
+CREATE TABLE /*_*/watchlist (
+ wl_user int unsigned NOT NULL,
+ wl_namespace int NOT NULL default 0,
+ wl_title varchar(255) binary NOT NULL default '',
+ wl_notificationtimestamp varbinary(14)
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title);
+CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title);
+CREATE TABLE /*_*/math (
+ math_inputhash varbinary(16) NOT NULL,
+ math_outputhash varbinary(16) NOT NULL,
+ math_html_conservativeness tinyint NOT NULL,
+ math_html text,
+ math_mathml text
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/math_inputhash ON /*_*/math (math_inputhash);
+CREATE TABLE /*_*/searchindex (
+ si_page int unsigned NOT NULL,
+ si_title varchar(255) NOT NULL default '',
+ si_text mediumtext NOT NULL
+) ENGINE=MyISAM;
+CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page);
+CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title);
+CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text);
+CREATE TABLE /*_*/interwiki (
+ iw_prefix varchar(32) NOT NULL,
+ iw_url blob NOT NULL,
+ iw_api blob NOT NULL,
+ iw_wikiid varchar(64) NOT NULL,
+ iw_local bool NOT NULL,
+ iw_trans tinyint NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix);
+CREATE TABLE /*_*/querycache (
+ qc_type varbinary(32) NOT NULL,
+ qc_value int unsigned NOT NULL default 0,
+ qc_namespace int NOT NULL default 0,
+ qc_title varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value);
+CREATE TABLE /*_*/objectcache (
+ keyname varbinary(255) NOT NULL default '' PRIMARY KEY,
+ value mediumblob,
+ exptime datetime
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime);
+CREATE TABLE /*_*/transcache (
+ tc_url varbinary(255) NOT NULL,
+ tc_contents text,
+ tc_time binary(14) NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url);
+CREATE TABLE /*_*/logging (
+ log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ log_type varbinary(32) NOT NULL default '',
+ log_action varbinary(32) NOT NULL default '',
+ log_timestamp binary(14) NOT NULL default '19700101000000',
+ log_user int unsigned NOT NULL default 0,
+ log_user_text varchar(255) binary NOT NULL default '',
+ log_namespace int NOT NULL default 0,
+ log_title varchar(255) binary NOT NULL default '',
+ log_page int unsigned NULL,
+ log_comment varchar(255) NOT NULL default '',
+ log_params blob NOT NULL,
+ log_deleted tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp);
+CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp);
+CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp);
+CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp);
+CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp);
+CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp);
+CREATE TABLE /*_*/log_search (
+ ls_field varbinary(32) NOT NULL,
+ ls_value varchar(255) NOT NULL,
+ ls_log_id int unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id);
+CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id);
+CREATE TABLE /*_*/trackbacks (
+ tb_id int PRIMARY KEY AUTO_INCREMENT,
+ tb_page int REFERENCES /*_*/page(page_id) ON DELETE CASCADE,
+ tb_title varchar(255) NOT NULL,
+ tb_url blob NOT NULL,
+ tb_ex text,
+ tb_name varchar(255)
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/tb_page ON /*_*/trackbacks (tb_page);
+CREATE TABLE /*_*/job (
+ job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ job_cmd varbinary(60) NOT NULL default '',
+ job_namespace int NOT NULL,
+ job_title varchar(255) binary NOT NULL,
+ job_params blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128));
+CREATE TABLE /*_*/querycache_info (
+ qci_type varbinary(32) NOT NULL default '',
+ qci_timestamp binary(14) NOT NULL default '19700101000000'
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type);
+CREATE TABLE /*_*/redirect (
+ rd_from int unsigned NOT NULL default 0 PRIMARY KEY,
+ rd_namespace int NOT NULL default 0,
+ rd_title varchar(255) binary NOT NULL default '',
+ rd_interwiki varchar(32) default NULL,
+ rd_fragment varchar(255) binary default NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from);
+CREATE TABLE /*_*/querycachetwo (
+ qcc_type varbinary(32) NOT NULL,
+ qcc_value int unsigned NOT NULL default 0,
+ qcc_namespace int NOT NULL default 0,
+ qcc_title varchar(255) binary NOT NULL default '',
+ qcc_namespacetwo int NOT NULL default 0,
+ qcc_titletwo varchar(255) binary NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value);
+CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title);
+CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo);
+CREATE TABLE /*_*/page_restrictions (
+ pr_page int NOT NULL,
+ pr_type varbinary(60) NOT NULL,
+ pr_level varbinary(60) NOT NULL,
+ pr_cascade tinyint NOT NULL,
+ pr_user int NULL,
+ pr_expiry varbinary(14) NULL,
+ pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type);
+CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level);
+CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level);
+CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade);
+CREATE TABLE /*_*/protected_titles (
+ pt_namespace int NOT NULL,
+ pt_title varchar(255) binary NOT NULL,
+ pt_user int unsigned NOT NULL,
+ pt_reason tinyblob,
+ pt_timestamp binary(14) NOT NULL,
+ pt_expiry varbinary(14) NOT NULL default '',
+ pt_create_perm varbinary(60) NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title);
+CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp);
+CREATE TABLE /*_*/page_props (
+ pp_page int NOT NULL,
+ pp_propname varbinary(60) NOT NULL,
+ pp_value blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname);
+CREATE TABLE /*_*/updatelog (
+ ul_key varchar(255) NOT NULL PRIMARY KEY,
+ ul_value blob
+) /*$wgDBTableOptions*/;
+CREATE TABLE /*_*/change_tag (
+ ct_rc_id int NULL,
+ ct_log_id int NULL,
+ ct_rev_id int NULL,
+ ct_tag varchar(255) NOT NULL,
+ ct_params blob NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag);
+CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag);
+CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag);
+CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id);
+CREATE TABLE /*_*/tag_summary (
+ ts_rc_id int NULL,
+ ts_log_id int NULL,
+ ts_rev_id int NULL,
+ ts_tags blob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id);
+CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id);
+CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id);
+CREATE TABLE /*_*/valid_tag (
+ vt_tag varchar(255) NOT NULL PRIMARY KEY
+) /*$wgDBTableOptions*/;
+CREATE TABLE /*_*/l10n_cache (
+ lc_lang varbinary(32) NOT NULL,
+ lc_key varchar(255) NOT NULL,
+ lc_value mediumblob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key);
+CREATE TABLE /*_*/msg_resource (
+ mr_resource varbinary(255) NOT NULL,
+ mr_lang varbinary(32) NOT NULL,
+ mr_blob mediumblob NOT NULL,
+ mr_timestamp binary(14) NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/mr_resource_lang ON /*_*/msg_resource (mr_resource, mr_lang);
+CREATE TABLE /*_*/msg_resource_links (
+ mrl_resource varbinary(255) NOT NULL,
+ mrl_message varbinary(255) NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/mrl_message_resource ON /*_*/msg_resource_links (mrl_message, mrl_resource);
+CREATE TABLE /*_*/module_deps (
+ md_module varbinary(255) NOT NULL,
+ md_skin varbinary(32) NOT NULL,
+ md_deps mediumblob NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/md_module_skin ON /*_*/module_deps (md_module, md_skin);
diff --git a/tests/phpunit/includes/installer/InstallDocFormatterTest.php b/tests/phpunit/includes/installer/InstallDocFormatterTest.php
new file mode 100644
index 00000000..56485d3e
--- /dev/null
+++ b/tests/phpunit/includes/installer/InstallDocFormatterTest.php
@@ -0,0 +1,64 @@
+<?php
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+class InstallDocFormatterTest extends MediaWikiTestCase {
+ /**
+ * @covers InstallDocFormatter::format
+ * @dataProvider provideDocFormattingTests
+ */
+ function testFormat( $expected, $unformattedText, $message = '' ) {
+ $this->assertEquals(
+ $expected,
+ InstallDocFormatter::format( $unformattedText ),
+ $message
+ );
+ }
+
+ /**
+ * Provider for testFormat()
+ */
+ function provideDocFormattingTests() {
+ # Format: (expected string, unformattedText string, optional message)
+ return array(
+ # Escape some wikitext
+ array( 'Install &lt;tag>' , 'Install <tag>', 'Escaping <' ),
+ array( 'Install &#123;&#123;template}}' , 'Install {{template}}', 'Escaping [[' ),
+ array( 'Install &#91;&#91;page]]' , 'Install [[page]]', 'Escaping {{' ),
+ array( 'Install ' , "Install \r", 'Removing \r' ),
+
+ # Transform \t{1,2} into :{1,2}
+ array( ':One indentation', "\tOne indentation", 'Replacing a single \t' ),
+ array( '::Two indentations', "\t\tTwo indentations", 'Replacing 2 x \t' ),
+
+ # Transform 'bug 123' links
+ array(
+ '<span class="config-plainlink">[https://bugzilla.wikimedia.org/123 bug 123]</span>',
+ 'bug 123', 'Testing bug 123 links' ),
+ array(
+ '(<span class="config-plainlink">[https://bugzilla.wikimedia.org/987654 bug 987654]</span>)',
+ '(bug 987654)', 'Testing (bug 987654) links' ),
+
+ # "bug abc" shouldn't work
+ array( 'bug foobar', 'bug foobar', "Don't match bug followed by non-digits" ),
+ array( 'bug !!fakefake!!', 'bug !!fakefake!!', "Don't match bug followed by non-digits" ),
+
+ # Transform '$wgFooBar' links
+ array(
+ '<span class="config-plainlink">[http://www.mediawiki.org/wiki/Manual:$wgFooBar $wgFooBar]</span>',
+ '$wgFooBar', 'Testing basic $wgFooBar' ),
+ array(
+ '<span class="config-plainlink">[http://www.mediawiki.org/wiki/Manual:$wgFooBar45 $wgFooBar45]</span>',
+ '$wgFooBar45', 'Testing $wgFooBar45 (with numbers)' ),
+ array(
+ '<span class="config-plainlink">[http://www.mediawiki.org/wiki/Manual:$wgFoo_Bar $wgFoo_Bar]</span>',
+ '$wgFoo_Bar', 'Testing $wgFoo_Bar (with underscore)' ),
+
+ # Icky variables that shouldn't link
+ array( '$myAwesomeVariable', '$myAwesomeVariable', 'Testing $myAwesomeVariable (not starting with $wg)' ),
+ array( '$()not!a&Var', '$()not!a&Var', 'Testing $()not!a&Var (obviously not a variable)' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/includes/libs/IEUrlExtensionTest.php
new file mode 100644
index 00000000..c6270e90
--- /dev/null
+++ b/tests/phpunit/includes/libs/IEUrlExtensionTest.php
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * Tests for IEUrlExtension::findIE6Extension
+ */
+class IEUrlExtensionTest extends MediaWikiTestCase {
+ function testSimple() {
+ $this->assertEquals(
+ 'y',
+ IEUrlExtension::findIE6Extension( 'x.y' ),
+ 'Simple extension'
+ );
+ }
+
+ function testSimpleNoExt() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( 'x' ),
+ 'No extension'
+ );
+ }
+
+ function testEmpty() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '' ),
+ 'Empty string'
+ );
+ }
+
+ function testQuestionMark() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '?' ),
+ 'Question mark only'
+ );
+ }
+
+ function testExtQuestionMark() {
+ $this->assertEquals(
+ 'x',
+ IEUrlExtension::findIE6Extension( '.x?' ),
+ 'Extension then question mark'
+ );
+ }
+
+ function testQuestionMarkExt() {
+ $this->assertEquals(
+ 'x',
+ IEUrlExtension::findIE6Extension( '?.x' ),
+ 'Question mark then extension'
+ );
+ }
+
+ function testInvalidChar() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '.x*' ),
+ 'Extension with invalid character'
+ );
+ }
+
+ function testInvalidCharThenExtension() {
+ $this->assertEquals(
+ 'x',
+ IEUrlExtension::findIE6Extension( '*.x' ),
+ 'Invalid character followed by an extension'
+ );
+ }
+
+ function testMultipleQuestionMarks() {
+ $this->assertEquals(
+ 'c',
+ IEUrlExtension::findIE6Extension( 'a?b?.c?.d?e?f' ),
+ 'Multiple question marks'
+ );
+ }
+
+ function testExeException() {
+ $this->assertEquals(
+ 'd',
+ IEUrlExtension::findIE6Extension( 'a?b?.exe?.d?.e' ),
+ '.exe exception'
+ );
+ }
+
+ function testExeException2() {
+ $this->assertEquals(
+ 'exe',
+ IEUrlExtension::findIE6Extension( 'a?b?.exe' ),
+ '.exe exception 2'
+ );
+ }
+
+ function testHash() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( 'a#b.c' ),
+ 'Hash character preceding extension'
+ );
+ }
+
+ function testHash2() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( 'a?#b.c' ),
+ 'Hash character preceding extension 2'
+ );
+ }
+
+ function testDotAtEnd() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '.' ),
+ 'Dot at end of string'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php
new file mode 100644
index 00000000..aa05500e
--- /dev/null
+++ b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php
@@ -0,0 +1,105 @@
+<?php
+
+class JavaScriptMinifierTest extends MediaWikiTestCase {
+
+ function provideCases() {
+ return array(
+ // Basic tokens
+ array( "\r\t\f \v\n\r", "" ),
+ array( "/* Foo *\n*bar\n*/", "" ),
+ /**
+ * ' Foo \' bar \
+ * baz \' quox ' .
+ */
+ array( "' Foo \\' bar \\\n baz \\' quox ' .length", "' Foo \\' bar \\\n baz \\' quox '.length" ),
+ array( "\" Foo \\\" bar \\\n baz \\\" quox \" .length", "\" Foo \\\" bar \\\n baz \\\" quox \".length" ),
+ array( "// Foo b/ar baz", "" ),
+ array( "/ Foo \\/ bar [ / \\] / ] baz / .length", "/ Foo \\/ bar [ / \\] / ] baz /.length" ),
+ // HTML comments
+ array( "<!-- Foo bar", "" ),
+ array( "<!-- Foo --> bar", "" ),
+ array( "--> Foo", "" ),
+ array( "x --> y", "x-->y" ),
+ // Semicolon insertion
+ array( "(function(){return\nx;})", "(function(){return\nx;})" ),
+ array( "throw\nx;", "throw\nx;" ),
+ array( "while(p){continue\nx;}", "while(p){continue\nx;}" ),
+ array( "while(p){break\nx;}", "while(p){break\nx;}" ),
+ array( "var\nx;", "var x;" ),
+ array( "x\ny;", "x\ny;" ),
+ array( "x\n++y;", "x\n++y;" ),
+ array( "x\n!y;", "x\n!y;" ),
+ array( "x\n{y}", "x\n{y}" ),
+ array( "x\n+y;", "x+y;" ),
+ array( "x\n(y);", "x(y);" ),
+ array( "5.\nx;", "5.\nx;" ),
+ array( "0xFF.\nx;", "0xFF.x;" ),
+ array( "5.3.\nx;", "5.3.x;" ),
+ // Token separation
+ array( "x in y", "x in y" ),
+ array( "/x/g in y", "/x/g in y" ),
+ array( "x in 30", "x in 30" ),
+ array( "x + ++ y", "x+ ++y" ),
+ array( "x / /y/.exec(z)", "x/ /y/.exec(z)" ),
+ // State machine
+ array( "/ x/g", "/ x/g" ),
+ array( "(function(){return/ x/g})", "(function(){return/ x/g})" ),
+ array( "+/ x/g", "+/ x/g" ),
+ array( "++/ x/g", "++/ x/g" ),
+ array( "x/ x/g", "x/x/g" ),
+ array( "(/ x/g)", "(/ x/g)" ),
+ array( "if(/ x/g);", "if(/ x/g);" ),
+ array( "(x/ x/g)", "(x/x/g)" ),
+ array( "([/ x/g])", "([/ x/g])" ),
+ array( "+x/ x/g", "+x/x/g" ),
+ array( "{}/ x/g", "{}/ x/g" ),
+ array( "+{}/ x/g", "+{}/x/g" ),
+ array( "(x)/ x/g", "(x)/x/g" ),
+ array( "if(x)/ x/g", "if(x)/ x/g" ),
+ array( "for(x;x;{}/ x/g);", "for(x;x;{}/x/g);" ),
+ array( "x;x;{}/ x/g", "x;x;{}/ x/g" ),
+ array( "x:{}/ x/g", "x:{}/ x/g" ),
+ array( "switch(x){case y?z:{}/ x/g:{}/ x/g;}", "switch(x){case y?z:{}/x/g:{}/ x/g;}" ),
+ array( "function x(){}/ x/g", "function x(){}/ x/g" ),
+ array( "+function x(){}/ x/g", "+function x(){}/x/g" ),
+
+ // Tests for things that broke in the past
+ // Multiline quoted string
+ array( "var foo=\"\\\nblah\\\n\";", "var foo=\"\\\nblah\\\n\";" ),
+ // Multiline quoted string followed by string with spaces
+ array( "var foo=\"\\\nblah\\\n\";\nvar baz = \" foo \";\n", "var foo=\"\\\nblah\\\n\";var baz=\" foo \";" ),
+ // URL in quoted string ( // is not a comment)
+ array( "aNode.setAttribute('href','http://foo.bar.org/baz');", "aNode.setAttribute('href','http://foo.bar.org/baz');" ),
+ // URL in quoted string after multiline quoted string
+ array( "var foo=\"\\\nblah\\\n\";\naNode.setAttribute('href','http://foo.bar.org/baz');", "var foo=\"\\\nblah\\\n\";aNode.setAttribute('href','http://foo.bar.org/baz');" ),
+ // Division vs. regex nastiness
+ array( "alert( (10+10) / '/'.charCodeAt( 0 ) + '//' );", "alert((10+10)/'/'.charCodeAt(0)+'//');" ),
+ array( "if(1)/a /g.exec('Pa ss');", "if(1)/a /g.exec('Pa ss');" ),
+
+ // newline insertion after 1000 chars: break after the "++", not before
+ array( str_repeat( ';', 996 ) . "if(x++);", str_repeat( ';', 996 ) . "if(x++\n);" ),
+
+ // Unicode letter characters should pass through ok in identifiers (bug 31187)
+ array( "var KaŝSkatolVal = {}", 'var KaŝSkatolVal={}'),
+ // And also per spec unicode char escape values should work in identifiers,
+ // as long as it's a valid char. In future it might get normalized.
+ array( "var Ka\\u015dSkatolVal = {}", 'var Ka\\u015dSkatolVal={}'),
+ );
+ }
+
+ /**
+ * @dataProvider provideCases
+ */
+ function testJavaScriptMinifierOutput( $code, $expectedOutput ) {
+ $minified = JavaScriptMinifier::minify( $code );
+
+ // JSMin+'s parser will throw an exception if output is not valid JS.
+ // suppression of warnings needed for stupid crap
+ wfSuppressWarnings();
+ $parser = new JSParser();
+ wfRestoreWarnings();
+ $parser->parse( $minified, 'minify-test.js', 1 );
+
+ $this->assertEquals( $expectedOutput, $minified, "Minified output should be in the form expected." );
+ }
+}
diff --git a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php
new file mode 100644
index 00000000..a0d5cd86
--- /dev/null
+++ b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php
@@ -0,0 +1,125 @@
+<?php
+class BitmapMetadataHandlerTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ $this->filePath = dirname( __FILE__ ) . '/../../data/media/';
+ }
+
+ /**
+ * Test if having conflicting metadata values from different
+ * types of metadata, that the right one takes precedence.
+ *
+ * Basically the file has IPTC and XMP metadata, the
+ * IPTC should override the XMP, except for the multilingual
+ * translation (to en) where XMP should win.
+ */
+ public function testMultilingualCascade() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ '/Xmp-exif-multilingual_test.jpg' );
+
+ $expected = array(
+ 'x-default' => 'right(iptc)',
+ 'en' => 'right translation',
+ '_type' => 'lang'
+ );
+
+ $this->assertArrayHasKey( 'ImageDescription', $meta,
+ 'Did not extract any ImageDescription info?!' );
+
+ $this->assertEquals( $expected, $meta['ImageDescription'] );
+ }
+
+ /**
+ * Test for jpeg comments are being handled by
+ * BitmapMetadataHandler correctly.
+ *
+ * There's more extensive tests of comment extraction in
+ * JpegMetadataExtractorTests.php
+ */
+ public function testJpegComment() {
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ 'jpeg-comment-utf.jpg' );
+
+ $this->assertEquals( 'UTF-8 JPEG Comment — ¼',
+ $meta['JPEGFileComment'][0] );
+ }
+
+ public function testIPTCDates() {
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ 'iptc-timetest.jpg' );
+
+ $this->assertEquals( '2020:07:14 01:36:05', $meta['DateTimeDigitized'] );
+ $this->assertEquals( '1997:03:02 00:01:02', $meta['DateTimeOriginal'] );
+ }
+ /* File has an invalid time (+ one valid but really weird time)
+ * that shouldn't be included
+ */
+ public function testIPTCDatesInvalid() {
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ 'iptc-timetest-invalid.jpg' );
+
+ $this->assertEquals( '1845:03:02 00:01:02', $meta['DateTimeOriginal'] );
+ $this->assertFalse( isset( $meta['DateTimeDigitized'] ) );
+ }
+
+ /**
+ * XMP data should take priority over iptc data
+ * when hash has been updated, but not when
+ * the hash is wrong.
+ */
+ public function testMerging() {
+ $merger = new BitmapMetadataHandler();
+ $merger->addMetadata( array( 'foo' => 'xmp' ), 'xmp-general' );
+ $merger->addMetadata( array( 'bar' => 'xmp' ), 'xmp-general' );
+ $merger->addMetadata( array( 'baz' => 'xmp' ), 'xmp-general' );
+ $merger->addMetadata( array( 'fred' => 'xmp' ), 'xmp-general' );
+ $merger->addMetadata( array( 'foo' => 'iptc (hash)' ), 'iptc-good-hash' );
+ $merger->addMetadata( array( 'bar' => 'iptc (bad hash)' ), 'iptc-bad-hash' );
+ $merger->addMetadata( array( 'baz' => 'iptc (bad hash)' ), 'iptc-bad-hash' );
+ $merger->addMetadata( array( 'fred' => 'iptc (no hash)' ), 'iptc-no-hash' );
+ $merger->addMetadata( array( 'baz' => 'exif' ), 'exif' );
+
+ $actual = $merger->getMetadataArray();
+ $expected = array(
+ 'foo' => 'xmp',
+ 'bar' => 'iptc (bad hash)',
+ 'baz' => 'exif',
+ 'fred' => 'xmp',
+ );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public function testPNGXMP() {
+ $handler = new BitmapMetadataHandler();
+ $result = $handler->png( $this->filePath . 'xmp.png' );
+ $expected = array (
+ 'frameCount' => 0,
+ 'loopCount' => 1,
+ 'duration' => 0,
+ 'bitDepth' => 1,
+ 'colorType' => 'index-coloured',
+ 'metadata' => array (
+ 'SerialNumber' => '123456789',
+ '_MW_PNG_VERSION' => 1,
+ ),
+ );
+ $this->assertEquals( $expected, $result );
+ }
+ public function testPNGNative() {
+ $handler = new BitmapMetadataHandler();
+ $result = $handler->png( $this->filePath . 'Png-native-test.png' );
+ $expected = 'http://example.com/url';
+ $this->assertEquals( $expected, $result['metadata']['Identifier']['x-default'] );
+ }
+ public function testTiffByteOrder() {
+ $handler = new BitmapMetadataHandler();
+ $res = $handler->getTiffByteOrder( $this->filePath . 'test.tiff' );
+ $this->assertEquals( 'LE', $res );
+ }
+
+}
diff --git a/tests/phpunit/includes/media/BitmapScalingTest.php b/tests/phpunit/includes/media/BitmapScalingTest.php
new file mode 100644
index 00000000..5bcd3232
--- /dev/null
+++ b/tests/phpunit/includes/media/BitmapScalingTest.php
@@ -0,0 +1,136 @@
+<?php
+
+class BitmapScalingTest extends MediaWikiTestCase {
+
+ function setUp() {
+ global $wgMaxImageArea;
+ $this->oldMaxImageArea = $wgMaxImageArea;
+ $wgMaxImageArea = 1.25e7; // 3500x3500
+ }
+ function tearDown() {
+ global $wgMaxImageArea;
+ $wgMaxImageArea = $this->oldMaxImageArea;
+ }
+ /**
+ * @dataProvider provideNormaliseParams
+ */
+ function testNormaliseParams( $fileDimensions, $expectedParams, $params, $msg ) {
+ $file = new FakeDimensionFile( $fileDimensions );
+ $handler = new BitmapHandler;
+ $valid = $handler->normaliseParams( $file, $params );
+ $this->assertTrue( $valid );
+ $this->assertEquals( $expectedParams, $params, $msg );
+ }
+
+ function provideNormaliseParams() {
+ return array(
+ /* Regular resize operations */
+ array(
+ array( 1024, 768 ),
+ array(
+ 'width' => 512, 'height' => 384,
+ 'physicalWidth' => 512, 'physicalHeight' => 384,
+ 'page' => 1,
+ ),
+ array( 'width' => 512 ),
+ 'Resizing with width set',
+ ),
+ array(
+ array( 1024, 768 ),
+ array(
+ 'width' => 512, 'height' => 384,
+ 'physicalWidth' => 512, 'physicalHeight' => 384,
+ 'page' => 1,
+ ),
+ array( 'width' => 512, 'height' => 768 ),
+ 'Resizing with height set too high',
+ ),
+ array(
+ array( 1024, 768 ),
+ array(
+ 'width' => 512, 'height' => 384,
+ 'physicalWidth' => 512, 'physicalHeight' => 384,
+ 'page' => 1,
+ ),
+ array( 'width' => 1024, 'height' => 384 ),
+ 'Resizing with height set',
+ ),
+
+ /* Very tall images */
+ array(
+ array( 1000, 100 ),
+ array(
+ 'width' => 5, 'height' => 1,
+ 'physicalWidth' => 5, 'physicalHeight' => 1,
+ 'page' => 1,
+ ),
+ array( 'width' => 5 ),
+ 'Very wide image',
+ ),
+
+ array(
+ array( 100, 1000 ),
+ array(
+ 'width' => 1, 'height' => 10,
+ 'physicalWidth' => 1, 'physicalHeight' => 10,
+ 'page' => 1,
+ ),
+ array( 'width' => 1 ),
+ 'Very high image',
+ ),
+ array(
+ array( 100, 1000 ),
+ array(
+ 'width' => 1, 'height' => 5,
+ 'physicalWidth' => 1, 'physicalHeight' => 10,
+ 'page' => 1,
+ ),
+ array( 'width' => 10, 'height' => 5 ),
+ 'Very high image with height set',
+ ),
+ /* Max image area */
+ array(
+ array( 4000, 4000 ),
+ array(
+ 'width' => 5000, 'height' => 5000,
+ 'physicalWidth' => 4000, 'physicalHeight' => 4000,
+ 'page' => 1,
+ ),
+ array( 'width' => 5000 ),
+ 'Bigger than max image size but doesn\'t need scaling',
+ ),
+ );
+ }
+ function testTooBigImage() {
+ $file = new FakeDimensionFile( array( 4000, 4000 ) );
+ $handler = new BitmapHandler;
+ $params = array( 'width' => '3700' ); // Still bigger than max size.
+ $this->assertFalse( $handler->normaliseParams( $file, $params ) );
+ }
+ function testTooBigMustRenderImage() {
+ $file = new FakeDimensionFile( array( 4000, 4000 ) );
+ $file->mustRender = true;
+ $handler = new BitmapHandler;
+ $params = array( 'width' => '5000' ); // Still bigger than max size.
+ $this->assertFalse( $handler->normaliseParams( $file, $params ) );
+ }
+}
+
+class FakeDimensionFile extends File {
+ public $mustRender = false;
+
+ public function __construct( $dimensions ) {
+ parent::__construct( Title::makeTitle( NS_FILE, 'Test' ), null );
+
+ $this->dimensions = $dimensions;
+ }
+ public function getWidth( $page = 1 ) {
+ return $this->dimensions[0];
+ }
+ public function getHeight( $page = 1 ) {
+ return $this->dimensions[1];
+ }
+ public function mustRender() {
+ return $this->mustRender;
+ }
+}
diff --git a/tests/phpunit/includes/media/ExifBitmapTest.php b/tests/phpunit/includes/media/ExifBitmapTest.php
new file mode 100644
index 00000000..4282d3c8
--- /dev/null
+++ b/tests/phpunit/includes/media/ExifBitmapTest.php
@@ -0,0 +1,122 @@
+<?php
+class ExifBitmapTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ global $wgShowEXIF;
+ $this->showExif = $wgShowEXIF;
+ $wgShowEXIF = true;
+ $this->handler = new ExifBitmapHandler;
+ if ( !wfDl( 'exif' ) ) {
+ $this->markTestSkipped( "This test needs the exif extension." );
+ }
+ }
+
+ public function tearDown() {
+ global $wgShowEXIF;
+ $wgShowEXIF = $this->showExif;
+ }
+
+ public function testIsOldBroken() {
+ if ( !wfDl( 'exif' ) ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::OLD_BROKEN_FILE );
+ $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res );
+ }
+ public function testIsBrokenFile() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::BROKEN_FILE );
+ $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res );
+ }
+ public function testIsInvalid() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $res = $this->handler->isMetadataValid( null, 'Something Invalid Here.' );
+ $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res );
+ }
+ public function testGoodMetadata() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}';
+ $res = $this->handler->isMetadataValid( null, $meta );
+ $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res );
+ }
+ public function testIsOldGood() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}';
+ $res = $this->handler->isMetadataValid( null, $meta );
+ $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res );
+ }
+ // Handle metadata from paged tiff handler (gotten via instant commons)
+ // gracefully.
+ public function testPagedTiffHandledGracefully() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $meta = 'a:6:{s:9:"page_data";a:1:{i:1;a:5:{s:5:"width";i:643;s:6:"height";i:448;s:5:"alpha";s:4:"true";s:4:"page";i:1;s:6:"pixels";i:288064;}}s:10:"page_count";i:1;s:10:"first_page";i:1;s:9:"last_page";i:1;s:4:"exif";a:9:{s:10:"ImageWidth";i:643;s:11:"ImageLength";i:448;s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:4;s:12:"RowsPerStrip";i:50;s:19:"PlanarConfiguration";i:1;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}s:21:"TIFF_METADATA_VERSION";s:3:"1.4";}';
+ $res = $this->handler->isMetadataValid( null, $meta );
+ $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res );
+ }
+
+ function testConvertMetadataLatest() {
+ $metadata = array(
+ 'foo' => array( 'First', 'Second', '_type' => 'ol' ),
+ 'MEDIAWIKI_EXIF_VERSION' => 2
+ );
+ $res = $this->handler->convertMetadataVersion( $metadata, 2 );
+ $this->assertEquals( $metadata, $res );
+ }
+ function testConvertMetadataToOld() {
+ $metadata = array(
+ 'foo' => array( 'First', 'Second', '_type' => 'ol' ),
+ 'bar' => array( 'First', 'Second', '_type' => 'ul' ),
+ 'baz' => array( 'First', 'Second' ),
+ 'fred' => 'Single',
+ 'MEDIAWIKI_EXIF_VERSION' => 2,
+ );
+ $expected = array(
+ 'foo' => "\n#First\n#Second",
+ 'bar' => "\n*First\n*Second",
+ 'baz' => "\n*First\n*Second",
+ 'fred' => 'Single',
+ 'MEDIAWIKI_EXIF_VERSION' => 1,
+ );
+ $res = $this->handler->convertMetadataVersion( $metadata, 1 );
+ $this->assertEquals( $expected, $res );
+ }
+ function testConvertMetadataSoftware() {
+ $metadata = array(
+ 'Software' => array( array('GIMP', '1.1' ) ),
+ 'MEDIAWIKI_EXIF_VERSION' => 2,
+ );
+ $expected = array(
+ 'Software' => 'GIMP (Version 1.1)',
+ 'MEDIAWIKI_EXIF_VERSION' => 1,
+ );
+ $res = $this->handler->convertMetadataVersion( $metadata, 1 );
+ $this->assertEquals( $expected, $res );
+ }
+ function testConvertMetadataSoftwareNormal() {
+ $metadata = array(
+ 'Software' => array( "GIMP 1.2", "vim" ),
+ 'MEDIAWIKI_EXIF_VERSION' => 2,
+ );
+ $expected = array(
+ 'Software' => "\n*GIMP 1.2\n*vim",
+ 'MEDIAWIKI_EXIF_VERSION' => 1,
+ );
+ $res = $this->handler->convertMetadataVersion( $metadata, 1 );
+ $this->assertEquals( $expected, $res );
+ }
+}
diff --git a/tests/phpunit/includes/media/ExifRotationTest.php b/tests/phpunit/includes/media/ExifRotationTest.php
new file mode 100644
index 00000000..639091d0
--- /dev/null
+++ b/tests/phpunit/includes/media/ExifRotationTest.php
@@ -0,0 +1,249 @@
+<?php
+
+/**
+ * Tests related to auto rotation
+ */
+class ExifRotationTest extends MediaWikiTestCase {
+
+ function setUp() {
+ parent::setUp();
+ $this->filePath = dirname( __FILE__ ) . '/../../data/media/';
+ $this->handler = new BitmapHandler();
+ $this->repo = new FSRepo(array(
+ 'name' => 'temp',
+ 'directory' => wfTempDir() . '/exif-test-' . time() . '-' . mt_rand(),
+ 'url' => 'http://localhost/thumbtest'
+ ));
+ if ( !wfDl( 'exif' ) ) {
+ $this->markTestSkipped( "This test needs the exif extension." );
+ }
+ global $wgShowEXIF;
+ $this->show = $wgShowEXIF;
+ $wgShowEXIF = true;
+
+ global $wgEnableAutoRotation;
+ $this->oldAuto = $wgEnableAutoRotation;
+ $wgEnableAutoRotation = true;
+ }
+ public function tearDown() {
+ global $wgShowEXIF, $wgEnableAutoRotation;
+ $wgShowEXIF = $this->show;
+ $wgEnableAutoRotation = $this->oldAuto;
+ }
+
+ /**
+ *
+ * @dataProvider providerFiles
+ */
+ function testMetadata( $name, $type, $info ) {
+ if ( !BitmapHandler::canRotate() ) {
+ $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." );
+ }
+ $file = UnregisteredLocalFile::newFromPath( $this->filePath . $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+ }
+
+ /**
+ *
+ * @dataProvider providerFiles
+ */
+ function testRotationRendering( $name, $type, $info, $thumbs ) {
+ if ( !BitmapHandler::canRotate() ) {
+ $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." );
+ }
+ foreach( $thumbs as $size => $out ) {
+ if( preg_match('/^(\d+)px$/', $size, $matches ) ) {
+ $params = array(
+ 'width' => $matches[1],
+ );
+ } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) {
+ $params = array(
+ 'width' => $matches[1],
+ 'height' => $matches[2]
+ );
+ } else {
+ throw new MWException('bogus test data format ' . $size);
+ }
+
+ $file = $this->localFile( $name, $type );
+ $thumb = $file->transform( $params, File::RENDER_NOW );
+
+ $this->assertEquals( $out[0], $thumb->getWidth(), "$name: thumb reported width check for $size" );
+ $this->assertEquals( $out[1], $thumb->getHeight(), "$name: thumb reported height check for $size" );
+
+ $gis = getimagesize( $thumb->getPath() );
+ if ($out[0] > $info['width']) {
+ // Physical image won't be scaled bigger than the original.
+ $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size");
+ $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size");
+ } else {
+ $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size");
+ $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size");
+ }
+ }
+ }
+
+ private function localFile( $name, $type ) {
+ return new UnregisteredLocalFile( false, $this->repo, $this->filePath . $name, $type );
+ }
+
+ function providerFiles() {
+ return array(
+ array(
+ 'landscape-plain.jpg',
+ 'image/jpeg',
+ array(
+ 'width' => 1024,
+ 'height' => 768,
+ ),
+ array(
+ '800x600px' => array( 800, 600 ),
+ '9999x800px' => array( 1067, 800 ),
+ '800px' => array( 800, 600 ),
+ '600px' => array( 600, 450 ),
+ )
+ ),
+ array(
+ 'portrait-rotated.jpg',
+ 'image/jpeg',
+ array(
+ 'width' => 768, // as rotated
+ 'height' => 1024, // as rotated
+ ),
+ array(
+ '800x600px' => array( 450, 600 ),
+ '9999x800px' => array( 600, 800 ),
+ '800px' => array( 800, 1067 ),
+ '600px' => array( 600, 800 ),
+ )
+ )
+ );
+ }
+
+ /**
+ * Same as before, but with auto-rotation disabled.
+ * @dataProvider providerFilesNoAutoRotate
+ */
+ function testMetadataNoAutoRotate( $name, $type, $info ) {
+ global $wgEnableAutoRotation;
+ $wgEnableAutoRotation = false;
+
+ $file = UnregisteredLocalFile::newFromPath( $this->filePath . $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+
+ $wgEnableAutoRotation = true;
+ }
+
+ /**
+ *
+ * @dataProvider providerFilesNoAutoRotate
+ */
+ function testRotationRenderingNoAutoRotate( $name, $type, $info, $thumbs ) {
+ global $wgEnableAutoRotation;
+ $wgEnableAutoRotation = false;
+
+ foreach( $thumbs as $size => $out ) {
+ if( preg_match('/^(\d+)px$/', $size, $matches ) ) {
+ $params = array(
+ 'width' => $matches[1],
+ );
+ } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) {
+ $params = array(
+ 'width' => $matches[1],
+ 'height' => $matches[2]
+ );
+ } else {
+ throw new MWException('bogus test data format ' . $size);
+ }
+
+ $file = $this->localFile( $name, $type );
+ $thumb = $file->transform( $params, File::RENDER_NOW );
+
+ $this->assertEquals( $out[0], $thumb->getWidth(), "$name: thumb reported width check for $size" );
+ $this->assertEquals( $out[1], $thumb->getHeight(), "$name: thumb reported height check for $size" );
+
+ $gis = getimagesize( $thumb->getPath() );
+ if ($out[0] > $info['width']) {
+ // Physical image won't be scaled bigger than the original.
+ $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size");
+ $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size");
+ } else {
+ $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size");
+ $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size");
+ }
+ }
+ $wgEnableAutoRotation = true;
+ }
+
+ function providerFilesNoAutoRotate() {
+ return array(
+ array(
+ 'landscape-plain.jpg',
+ 'image/jpeg',
+ array(
+ 'width' => 1024,
+ 'height' => 768,
+ ),
+ array(
+ '800x600px' => array( 800, 600 ),
+ '9999x800px' => array( 1067, 800 ),
+ '800px' => array( 800, 600 ),
+ '600px' => array( 600, 450 ),
+ )
+ ),
+ array(
+ 'portrait-rotated.jpg',
+ 'image/jpeg',
+ array(
+ 'width' => 1024, // since not rotated
+ 'height' => 768, // since not rotated
+ ),
+ array(
+ '800x600px' => array( 800, 600 ),
+ '9999x800px' => array( 1067, 800 ),
+ '800px' => array( 800, 600 ),
+ '600px' => array( 600, 450 ),
+ )
+ )
+ );
+ }
+
+
+ const TEST_WIDTH = 100;
+ const TEST_HEIGHT = 200;
+
+ /**
+ * @dataProvider provideBitmapExtractPreRotationDimensions
+ */
+ function testBitmapExtractPreRotationDimensions( $rotation, $expected ) {
+ $result = $this->handler->extractPreRotationDimensions( array(
+ 'physicalWidth' => self::TEST_WIDTH,
+ 'physicalHeight' => self::TEST_HEIGHT,
+ ), $rotation );
+ $this->assertEquals( $expected, $result );
+ }
+
+ function provideBitmapExtractPreRotationDimensions() {
+ return array(
+ array(
+ 0,
+ array( self::TEST_WIDTH, self::TEST_HEIGHT )
+ ),
+ array(
+ 90,
+ array( self::TEST_HEIGHT, self::TEST_WIDTH )
+ ),
+ array(
+ 180,
+ array( self::TEST_WIDTH, self::TEST_HEIGHT )
+ ),
+ array(
+ 270,
+ array( self::TEST_HEIGHT, self::TEST_WIDTH )
+ ),
+ );
+ }
+}
+
diff --git a/tests/phpunit/includes/media/ExifTest.php b/tests/phpunit/includes/media/ExifTest.php
new file mode 100644
index 00000000..9b490e92
--- /dev/null
+++ b/tests/phpunit/includes/media/ExifTest.php
@@ -0,0 +1,51 @@
+<?php
+class ExifTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ $this->mediaPath = dirname( __FILE__ ) . '/../../data/media/';
+
+ global $wgShowEXIF;
+ $this->showExif = $wgShowEXIF;
+ $wgShowEXIF = true;
+ }
+ public function tearDown() {
+ global $wgShowEXIF;
+ $wgShowEXIF = $this->showExif;
+ }
+
+ public function testGPSExtraction() {
+ if ( !wfDl( 'exif' ) ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+
+ $filename = $this->mediaPath . 'exif-gps.jpg';
+ $seg = JpegMetadataExtractor::segmentSplitter( $filename );
+ $exif = new Exif( $filename, $seg['byteOrder'] );
+ $data = $exif->getFilteredData();
+ $expected = array(
+ 'GPSLatitude' => 88.5180555556,
+ 'GPSLongitude' => -21.12357,
+ 'GPSAltitude' => -200,
+ 'GPSDOP' => '5/1',
+ 'GPSVersionID' => '2.2.0.0',
+ );
+ $this->assertEquals( $expected, $data, '', 0.0000000001 );
+ }
+ public function testUnicodeUserComment() {
+ if ( !wfDl( 'exif' ) ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+
+ $filename = $this->mediaPath . 'exif-user-comment.jpg';
+ $seg = JpegMetadataExtractor::segmentSplitter( $filename );
+ $exif = new Exif( $filename, $seg['byteOrder'] );
+ $data = $exif->getFilteredData();
+
+ $expected = array(
+ 'UserComment' => 'test⁔comment'
+ );
+ $this->assertEquals( $expected, $data );
+ }
+
+
+}
diff --git a/tests/phpunit/includes/media/FormatMetadataTest.php b/tests/phpunit/includes/media/FormatMetadataTest.php
new file mode 100644
index 00000000..db36dea3
--- /dev/null
+++ b/tests/phpunit/includes/media/FormatMetadataTest.php
@@ -0,0 +1,29 @@
+<?php
+class FormatMetadataTest extends MediaWikiTestCase {
+ public function testInvalidDate() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+
+ $file = UnregisteredLocalFile::newFromPath( dirname( __FILE__ ) .
+ '/../../data/media/broken_exif_date.jpg', 'image/jpeg' );
+
+ // Throws an error if bug hit
+ $meta = $file->formatMetadata();
+ $this->assertNotEquals( false, $meta, 'Valid metadata extracted' );
+
+ // Find date exif entry
+ $this->assertArrayHasKey( 'visible', $meta );
+ $dateIndex = null;
+ foreach ( $meta['visible'] as $i => $data ) {
+ if ( $data['id'] == 'exif-datetimeoriginal' ) {
+ $dateIndex = $i;
+ }
+ }
+ $this->assertNotNull( $dateIndex, 'Date entry exists in metadata' );
+ $this->assertEquals( '0000:01:00 00:02:27',
+ $meta['visible'][$dateIndex]['value'],
+ 'File with invalid date metadata (bug 29471)' );
+ }
+} \ No newline at end of file
diff --git a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php
new file mode 100644
index 00000000..59b30441
--- /dev/null
+++ b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php
@@ -0,0 +1,95 @@
+<?php
+class GIFMetadataExtractorTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ $this->mediaPath = dirname( __FILE__ ) . '/../../data/media/';
+ }
+ /**
+ * Put in a file, and see if the metadata coming out is as expected.
+ * @param $filename String
+ * @param $expected Array The extracted metadata.
+ * @dataProvider dataGetMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $actual = GIFMetadataExtractor::getMetadata( $this->mediaPath . $filename );
+ $this->assertEquals( $expected, $actual );
+ }
+ public function dataGetMetadata() {
+
+ $xmpNugget = <<<EOF
+<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
+<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'>
+<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+
+ <rdf:Description rdf:about=''
+ xmlns:Iptc4xmpCore='http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'>
+ <Iptc4xmpCore:Location>The interwebs</Iptc4xmpCore:Location>
+ </rdf:Description>
+
+ <rdf:Description rdf:about=''
+ xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
+ <tiff:Artist>Bawolff</tiff:Artist>
+ <tiff:ImageDescription>
+ <rdf:Alt>
+ <rdf:li xml:lang='x-default'>A file to test GIF</rdf:li>
+ </rdf:Alt>
+ </tiff:ImageDescription>
+ </rdf:Description>
+</rdf:RDF>
+</x:xmpmeta>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<?xpacket end='w'?>
+EOF;
+
+ return array(
+ array( 'nonanimated.gif', array(
+ 'comment' => array( 'GIF test file ⁕ Created with GIMP' ),
+ 'duration' => 0.1,
+ 'frameCount' => 1,
+ 'looped' => false,
+ 'xmp' => '',
+ )
+ ),
+ array( 'animated.gif', array(
+ 'comment' => array( 'GIF test file . Created with GIMP' ),
+ 'duration' => 2.4,
+ 'frameCount' => 4,
+ 'looped' => true,
+ 'xmp' => '',
+ )
+ ),
+
+ array( 'animated-xmp.gif', array(
+ 'xmp' => $xmpNugget,
+ 'duration' => 2.4,
+ 'frameCount' => 4,
+ 'looped' => true,
+ 'comment' => array( 'GIƒ·test·file' ),
+ )
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/GIFTest.php b/tests/phpunit/includes/media/GIFTest.php
new file mode 100644
index 00000000..42c25ca5
--- /dev/null
+++ b/tests/phpunit/includes/media/GIFTest.php
@@ -0,0 +1,85 @@
+<?php
+class GIFHandlerTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ $this->filePath = dirname( __FILE__ ) . '/../../data/media/';
+ $this->handler = new GIFHandler();
+ }
+
+ public function testInvalidFile() {
+ $res = $this->handler->getMetadata( null, $this->filePath . 'README' );
+ $this->assertEquals( GIFHandler::BROKEN_FILE, $res );
+ }
+ /**
+ * @param $filename String basename of the file to check
+ * @param $expected boolean Expected result.
+ * @dataProvider dataIsAnimated
+ */
+ public function testIsAnimanted( $filename, $expected ) {
+ $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename,
+ 'image/gif' );
+ $actual = $this->handler->isAnimatedImage( $file );
+ $this->assertEquals( $expected, $actual );
+ }
+ public function dataIsAnimated() {
+ return array(
+ array( 'animated.gif', true ),
+ array( 'nonanimated.gif', false ),
+ );
+ }
+
+ /**
+ * @param $filename String
+ * @param $expected Integer Total image area
+ * @dataProvider dataGetImageArea
+ */
+ public function testGetImageArea( $filename, $expected ) {
+ $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename,
+ 'image/gif' );
+ $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() );
+ $this->assertEquals( $expected, $actual );
+ }
+ public function dataGetImageArea() {
+ return array(
+ array( 'animated.gif', 5400 ),
+ array( 'nonanimated.gif', 1350 ),
+ );
+ }
+
+ /**
+ * @param $metadata String Serialized metadata
+ * @param $expected Integer One of the class constants of GIFHandler
+ * @dataProvider dataIsMetadataValid
+ */
+ public function testIsMetadataValid( $metadata, $expected ) {
+ $actual = $this->handler->isMetadataValid( null, $metadata );
+ $this->assertEquals( $expected, $actual );
+ }
+ public function dataIsMetadataValid() {
+ return array(
+ array( GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ),
+ array( '', GIFHandler::METADATA_BAD ),
+ array( null, GIFHandler::METADATA_BAD ),
+ array( 'Something invalid!', GIFHandler::METADATA_BAD ),
+ array( 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}', GIFHandler::METADATA_GOOD ),
+ );
+ }
+
+ /**
+ * @param $filename String
+ * @param $expected String Serialized array
+ * @dataProvider dataGetMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename,
+ 'image/gif' );
+ $actual = $this->handler->getMetadata( $file, $this->filePath . $filename );
+ $this->assertEquals( unserialize( $expected ), unserialize( $actual ) );
+ }
+ public function dataGetMetadata() {
+ return array(
+ array( 'nonanimated.gif', 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}' ),
+ array( 'animated-xmp.gif', 'a:4:{s:10:"frameCount";i:4;s:6:"looped";b:1;s:8:"duration";d:2.399999999999999911182158029987476766109466552734375;s:8:"metadata";a:5:{s:6:"Artist";s:7:"Bawolff";s:16:"ImageDescription";a:2:{s:9:"x-default";s:18:"A file to test GIF";s:5:"_type";s:4:"lang";}s:15:"SublocationDest";s:13:"The interwebs";s:14:"GIFFileComment";a:1:{i:0;s:16:"GIƒ·test·file";}s:15:"_MW_GIF_VERSION";i:1;}}' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/IPTCTest.php b/tests/phpunit/includes/media/IPTCTest.php
new file mode 100644
index 00000000..ec6deeb8
--- /dev/null
+++ b/tests/phpunit/includes/media/IPTCTest.php
@@ -0,0 +1,55 @@
+<?php
+class IPTCTest extends MediaWikiTestCase {
+ public function testRecognizeUtf8() {
+ // utf-8 is the only one used in practise.
+ $res = IPTC::getCharset( "\x1b%G" );
+ $this->assertEquals( 'UTF-8', $res );
+ }
+
+ public function testIPTCParseNoCharset88591() {
+ // basically IPTC for keyword with value of 0xBC which is 1/4 in iso-8859-1
+ // This data doesn't specify a charset. We're supposed to guess
+ // (which basically means utf-8 if valid, windows 1252 (iso 8859-1) if not)
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x06\x1c\x02\x19\x00\x01\xBC";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( '¼' ), $res['Keywords'] );
+ }
+ /* This one contains a sequence that's valid iso 8859-1 but not valid utf8 */
+ /* \xC3 = Ã, \xB8 = ¸ */
+ public function testIPTCParseNoCharset88591b() {
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x09\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( 'ÃÃø' ), $res['Keywords'] );
+ }
+ /* Same as testIPTCParseNoCharset88591b, but forcing the charset to utf-8.
+ * What should happen is the first "\xC3\xC3" should be dropped as invalid,
+ * leaving \xC3\xB8, which is ø
+ */
+ public function testIPTCParseForcedUTFButInvalid() {
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x11\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8"
+ . "\x1c\x01\x5A\x00\x03\x1B\x25\x47";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( 'ø' ), $res['Keywords'] );
+ }
+ public function testIPTCParseNoCharsetUTF8() {
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x07\x1c\x02\x19\x00\x02¼";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( '¼' ), $res['Keywords'] );
+ }
+ // Testing something that has 2 values for keyword
+ public function testIPTCParseMulti() {
+ $iptcData = /* identifier */ "Photoshop 3.0\08BIM\4\4"
+ /* length */ . "\0\0\0\0\0\x0D"
+ . "\x1c\x02\x19" . "\x00\x01" . "\xBC"
+ . "\x1c\x02\x19" . "\x00\x02" . "\xBC\xBD";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( '¼', '¼½' ), $res['Keywords'] );
+ }
+ public function testIPTCParseUTF8() {
+ // This has the magic "\x1c\x01\x5A\x00\x03\x1B\x25\x47" which marks content as UTF8.
+ $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x0F\x1c\x02\x19\x00\x02¼\x1c\x01\x5A\x00\x03\x1B\x25\x47";
+ $res = IPTC::Parse( $iptcData );
+ $this->assertEquals( array( '¼' ), $res['Keywords'] );
+ }
+
+}
diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php
new file mode 100644
index 00000000..61fc9c81
--- /dev/null
+++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php
@@ -0,0 +1,94 @@
+<?php
+/*
+ * @todo Could use a test of extended XMP segments. Hard to find programs that
+ * create example files, and creating my own in vim propbably wouldn't
+ * serve as a very good "test". (Adobe photoshop probably creates such files
+ * but it costs money). The implementation of it currently in MediaWiki is based
+ * solely on reading the standard, without any real world test files.
+ */
+class JpegMetadataExtractorTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ $this->filePath = dirname( __FILE__ ) . '/../../data/media/';
+ }
+
+ /**
+ * We also use this test to test padding bytes don't
+ * screw stuff up
+ *
+ * @param $file filename
+ *
+ * @dataProvider dataUtf8Comment
+ */
+ public function testUtf8Comment( $file ) {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
+ $this->assertEquals( array( 'UTF-8 JPEG Comment — ¼' ), $res['COM'] );
+ }
+ public function dataUtf8Comment() {
+ return array(
+ array( 'jpeg-comment-utf.jpg' ),
+ array( 'jpeg-padding-even.jpg' ),
+ array( 'jpeg-padding-odd.jpg' ),
+ );
+ }
+ /** The file is iso-8859-1, but it should get auto converted */
+ public function testIso88591Comment() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
+ $this->assertEquals( array( 'ISO-8859-1 JPEG Comment - ¼' ), $res['COM'] );
+ }
+ /** Comment values that are non-textual (random binary junk) should not be shown.
+ * The example test file has a comment with a 0x5 byte in it which is a control character
+ * and considered binary junk for our purposes.
+ */
+ public function testBinaryCommentStripped() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
+ $this->assertEmpty( $res['COM'] );
+ }
+ /* Very rarely a file can have multiple comments.
+ * Order of comments is based on order inside the file.
+ */
+ public function testMultipleComment() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
+ $this->assertEquals( array( 'foo', 'bar' ), $res['COM'] );
+ }
+ public function testXMPExtraction() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+ $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+ $this->assertEquals( $expected, $res['XMP'] );
+ }
+ public function testPSIRExtraction() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+ $expected = '50686f746f73686f7020332e30003842494d04040000000000181c02190004746573741c02190003666f6f1c020000020004';
+ $this->assertEquals( $expected, bin2hex( $res['PSIR'] ) );
+ }
+ public function testXMPExtractionAltAppId() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
+ $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+ $this->assertEquals( $expected, $res['XMP'] );
+ }
+
+
+ public function testIPTCHashComparisionNoHash() {
+ $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+ $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'] );
+
+ $this->assertEquals( 'iptc-no-hash', $res );
+ }
+ public function testIPTCHashComparisionBadHash() {
+ $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
+ $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'] );
+
+ $this->assertEquals( 'iptc-bad-hash', $res );
+ }
+ public function testIPTCHashComparisionGoodHash() {
+ $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
+ $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'] );
+
+ $this->assertEquals( 'iptc-good-hash', $res );
+ }
+ public function testExifByteOrder() {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
+ $expected = 'BE';
+ $this->assertEquals( $expected, $res['byteOrder'] );
+ }
+}
diff --git a/tests/phpunit/includes/media/JpegTest.php b/tests/phpunit/includes/media/JpegTest.php
new file mode 100644
index 00000000..713a3410
--- /dev/null
+++ b/tests/phpunit/includes/media/JpegTest.php
@@ -0,0 +1,29 @@
+<?php
+class JpegTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ $this->filePath = dirname( __FILE__ ) . '/../../data/media/';
+ }
+
+ public function testInvalidFile() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $jpeg = new JpegHandler;
+ $res = $jpeg->getMetadata( null, $this->filePath . 'README' );
+ $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res );
+ }
+ public function testJpegMetadataExtraction() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $h = new JpegHandler;
+ $res = $h->getMetadata( null, $this->filePath . 'test.jpg' );
+ $expected = 'a:7:{s:16:"ImageDescription";s:9:"Test file";s:11:"XResolution";s:4:"72/1";s:11:"YResolution";s:4:"72/1";s:14:"ResolutionUnit";i:2;s:16:"YCbCrPositioning";i:1;s:15:"JPEGFileComment";a:1:{i:0;s:17:"Created with GIMP";}s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}';
+
+ // Unserialize in case serialization format ever changes.
+ $this->assertEquals( unserialize( $expected ), unserialize( $res ) );
+ }
+}
diff --git a/tests/phpunit/includes/media/PNGMetadataExtractorTest.php b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php
new file mode 100644
index 00000000..9f702c50
--- /dev/null
+++ b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php
@@ -0,0 +1,141 @@
+<?php
+class PNGMetadataExtractorTest extends MediaWikiTestCase {
+
+ function setUp() {
+ $this->filePath = dirname( __FILE__ ) . '/../../data/media/';
+ }
+ /**
+ * Tests zTXt tag (compressed textual metadata)
+ */
+ function testPngNativetZtxt() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+ $expected = "foo bar baz foo foo foo foof foo foo foo foo";
+ $this->assertArrayHasKey( 'text', $meta );
+ $meta = $meta['text'];
+ $this->assertArrayHasKey( 'Make', $meta );
+ $this->assertArrayHasKey( 'x-default', $meta['Make'] );
+
+ $this->assertEquals( $expected, $meta['Make']['x-default'] );
+ }
+
+ /**
+ * Test tEXt tag (Uncompressed textual metadata)
+ */
+ function testPngNativeText() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+ $expected = "Some long image desc";
+ $this->assertArrayHasKey( 'text', $meta );
+ $meta = $meta['text'];
+ $this->assertArrayHasKey( 'ImageDescription', $meta );
+ $this->assertArrayHasKey( 'x-default', $meta['ImageDescription'] );
+ $this->assertArrayHasKey( '_type', $meta['ImageDescription'] );
+
+ $this->assertEquals( $expected, $meta['ImageDescription']['x-default'] );
+ }
+
+ /**
+ * tEXt tags must be encoded iso-8859-1 (vs iTXt which are utf-8)
+ * Make sure non-ascii characters get converted properly
+ */
+ function testPngNativeTextNonAscii() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ // Note the Copyright symbol here is a utf-8 one
+ // (aka \xC2\xA9) where in the file its iso-8859-1
+ // encoded as just \xA9.
+ $expected = "© 2010 Bawolff";
+
+
+ $this->assertArrayHasKey( 'text', $meta );
+ $meta = $meta['text'];
+ $this->assertArrayHasKey( 'Copyright', $meta );
+ $this->assertArrayHasKey( 'x-default', $meta['Copyright'] );
+
+ $this->assertEquals( $expected, $meta['Copyright']['x-default'] );
+ }
+
+ /**
+ * Test extraction of pHYs tags, which can tell what the
+ * actual resolution of the image is (aka in dots per meter).
+ function testPngPhysTag () {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertArrayHasKey( 'text', $meta );
+ $meta = $meta['text'];
+
+ $this->assertEquals( '2835/100', $meta['XResolution'] );
+ $this->assertEquals( '2835/100', $meta['YResolution'] );
+ $this->assertEquals( 3, $meta['ResolutionUnit'] ); // 3 = cm
+ }
+
+ /**
+ * Given a normal static PNG, check the animation metadata returned.
+ */
+ function testStaticPngAnimationMetadata() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertEquals( 0, $meta['frameCount'] );
+ $this->assertEquals( 1, $meta['loopCount'] );
+ $this->assertEquals( 0, $meta['duration'] );
+ }
+
+ /**
+ * Given an animated APNG image file
+ * check it gets animated metadata right.
+ */
+ function testApngAnimationMetadata() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Animated_PNG_example_bouncing_beach_ball.png' );
+
+ $this->assertEquals( 20, $meta['frameCount'] );
+ // Note loop count of 0 = infinity
+ $this->assertEquals( 0, $meta['loopCount'] );
+ $this->assertEquals( 1.5, $meta['duration'], '', 0.00001 );
+ }
+
+ function testPngBitDepth8() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertEquals( 8, $meta['bitDepth'] );
+ }
+ function testPngBitDepth1() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ '1bit-png.png' );
+ $this->assertEquals( 1, $meta['bitDepth'] );
+ }
+
+
+ function testPngIndexColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertEquals( 'index-coloured', $meta['colorType'] );
+ }
+ function testPngRgbColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'rgb-png.png' );
+ $this->assertEquals( 'truecolour-alpha', $meta['colorType'] );
+ }
+ function testPngRgbNoAlphaColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'rgb-na-png.png' );
+ $this->assertEquals( 'truecolour', $meta['colorType'] );
+ }
+ function testPngGreyscaleColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'greyscale-png.png' );
+ $this->assertEquals( 'greyscale-alpha', $meta['colorType'] );
+ }
+ function testPngGreyscaleNoAlphaColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'greyscale-na-png.png' );
+ $this->assertEquals( 'greyscale', $meta['colorType'] );
+ }
+
+}
diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php
new file mode 100644
index 00000000..b782918c
--- /dev/null
+++ b/tests/phpunit/includes/media/PNGTest.php
@@ -0,0 +1,88 @@
+<?php
+class PNGHandlerTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ $this->filePath = dirname( __FILE__ ) . '/../../data/media/';
+ $this->handler = new PNGHandler();
+ }
+
+ public function testInvalidFile() {
+ $res = $this->handler->getMetadata( null, $this->filePath . 'README' );
+ $this->assertEquals( PNGHandler::BROKEN_FILE, $res );
+ }
+ /**
+ * @param $filename String basename of the file to check
+ * @param $expected boolean Expected result.
+ * @dataProvider dataIsAnimated
+ */
+ public function testIsAnimanted( $filename, $expected ) {
+ $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename,
+ 'image/png' );
+ $actual = $this->handler->isAnimatedImage( $file );
+ $this->assertEquals( $expected, $actual );
+ }
+ public function dataIsAnimated() {
+ return array(
+ array( 'Animated_PNG_example_bouncing_beach_ball.png', true ),
+ array( '1bit-png.png', false ),
+ );
+ }
+
+ /**
+ * @param $filename String
+ * @param $expected Integer Total image area
+ * @dataProvider dataGetImageArea
+ */
+ public function testGetImageArea( $filename, $expected ) {
+ $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename,
+ 'image/png' );
+ $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() );
+ $this->assertEquals( $expected, $actual );
+ }
+ public function dataGetImageArea() {
+ return array(
+ array( '1bit-png.png', 2500 ),
+ array( 'greyscale-png.png', 2500 ),
+ array( 'Png-native-test.png', 126000 ),
+ array( 'Animated_PNG_example_bouncing_beach_ball.png', 10000 ),
+ );
+ }
+
+ /**
+ * @param $metadata String Serialized metadata
+ * @param $expected Integer One of the class constants of PNGHandler
+ * @dataProvider dataIsMetadataValid
+ */
+ public function testIsMetadataValid( $metadata, $expected ) {
+ $actual = $this->handler->isMetadataValid( null, $metadata );
+ $this->assertEquals( $expected, $actual );
+ }
+ public function dataIsMetadataValid() {
+ return array(
+ array( PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ),
+ array( '', PNGHandler::METADATA_BAD ),
+ array( null, PNGHandler::METADATA_BAD ),
+ array( 'Something invalid!', PNGHandler::METADATA_BAD ),
+ array( 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}', PNGHandler::METADATA_GOOD ),
+ );
+ }
+
+ /**
+ * @param $filename String
+ * @param $expected String Serialized array
+ * @dataProvider dataGetMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename,
+ 'image/png' );
+ $actual = $this->handler->getMetadata( $file, $this->filePath . $filename );
+// $this->assertEquals( unserialize( $expected ), unserialize( $actual ) );
+ $this->assertEquals( ( $expected ), ( $actual ) );
+ }
+ public function dataGetMetadata() {
+ return array(
+ array( 'rgb-na-png.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}' ),
+ array( 'xmp.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:1;s:9:"colorType";s:14:"index-coloured";s:8:"metadata";a:2:{s:12:"SerialNumber";s:9:"123456789";s:15:"_MW_PNG_VERSION";i:1;}}' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php
new file mode 100644
index 00000000..c2c81b98
--- /dev/null
+++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php
@@ -0,0 +1,88 @@
+<?php
+
+class SVGMetadataExtractorTest extends MediaWikiTestCase {
+
+ function setUp() {
+ AutoLoader::loadClass( 'SVGMetadataExtractorTest' );
+ }
+
+ /**
+ * @dataProvider providerSvgFiles
+ */
+ function testGetMetadata( $infile, $expected ) {
+ $this->assertMetadata( $infile, $expected );
+ }
+
+ /**
+ * @dataProvider providerSvgFilesWithXMLMetadata
+ */
+ function testGetXMLMetadata( $infile, $expected ) {
+ $r = new XMLReader();
+ if( !method_exists( $r, 'readInnerXML' ) ) {
+ $this->markTestSkipped( 'XMLReader::readInnerXML() does not exist (libxml >2.6.20 needed).' );
+ return;
+ }
+ $this->assertMetadata( $infile, $expected );
+ }
+
+ function assertMetadata( $infile, $expected ) {
+ try {
+ $data = SVGMetadataExtractor::getMetadata( $infile );
+ $this->assertEquals( $expected, $data, 'SVG metadata extraction test' );
+ } catch ( MWException $e ) {
+ if ( $expected === false ) {
+ $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' );
+ } else {
+ throw $e;
+ }
+ }
+ }
+
+ function providerSvgFiles() {
+ $base = dirname( __FILE__ ) . '/../../data/media';
+ return array(
+ array(
+ "$base/Wikimedia-logo.svg",
+ array(
+ 'width' => 1024,
+ 'height' => 1024
+ )
+ ),
+ array(
+ "$base/QA_icon.svg",
+ array(
+ 'width' => 60,
+ 'height' => 60
+ )
+ ),
+ array(
+ "$base/Gtk-media-play-ltr.svg",
+ array(
+ 'width' => 60,
+ 'height' => 60
+ )
+ ),
+ );
+ }
+
+ function providerSvgFilesWithXMLMetadata() {
+ $base = dirname( __FILE__ ) . '/../../data/media';
+ return array(
+ array(
+ "$base/US_states_by_total_state_tax_revenue.svg",
+ array(
+ 'height' => 593,
+ 'metadata' =>
+ '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about="">
+ <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format>
+ <ns5:type xmlns:ns5="http://purl.org/dc/elements/1.1/" rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ </ns4:Work>
+ </rdf:RDF>',
+ 'width' => 959
+ )
+ ),
+ );
+ }
+}
+
diff --git a/tests/phpunit/includes/media/TiffTest.php b/tests/phpunit/includes/media/TiffTest.php
new file mode 100644
index 00000000..0a7e8e8c
--- /dev/null
+++ b/tests/phpunit/includes/media/TiffTest.php
@@ -0,0 +1,36 @@
+<?php
+class TiffTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ global $wgShowEXIF;
+ $this->showExif = $wgShowEXIF;
+ $wgShowEXIF = true;
+ $this->filePath = dirname( __FILE__ ) . '/../../data/media/';
+ $this->handler = new TiffHandler;
+ }
+
+ public function tearDown() {
+ global $wgShowEXIF;
+ $wgShowEXIF = $this->showExif;
+ }
+
+ public function testInvalidFile() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $res = $this->handler->getMetadata( null, $this->filePath . 'README' );
+ $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res );
+ }
+ public function testTiffMetadataExtraction() {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ $this->markTestIncomplete( "This test needs the exif extension." );
+ }
+ $res = $this->handler->getMetadata( null, $this->filePath . 'test.tiff' );
+ $expected = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}';
+ // Re-unserialize in case there are subtle differences between how versions
+ // of php serialize stuff.
+ $this->assertEquals( unserialize( $expected ), unserialize( $res ) );
+ }
+}
diff --git a/tests/phpunit/includes/media/XMPTest.php b/tests/phpunit/includes/media/XMPTest.php
new file mode 100644
index 00000000..d1ec4767
--- /dev/null
+++ b/tests/phpunit/includes/media/XMPTest.php
@@ -0,0 +1,154 @@
+<?php
+class XMPTest extends MediaWikiTestCase {
+
+ /**
+ * Put XMP in, compare what comes out...
+ *
+ * @param $xmp String the actual xml data.
+ * @param $expected Array expected result of parsing the xmp.
+ * @param $info String Short sentence on what's being tested.
+ *
+ * @dataProvider dataXMPParse
+ */
+ public function testXMPParse( $xmp, $expected, $info ) {
+ if ( !function_exists( 'xml_parser_create_ns' ) ) {
+ $this->markIncomplete( 'Requires libxml to do XMP parsing' );
+ }
+ if ( !is_string( $xmp ) || !is_array( $expected ) ) {
+ throw new Exception( "Invalid data provided to " . __METHOD__ );
+ }
+ $reader = new XMPReader;
+ $reader->parse( $xmp );
+ $this->assertEquals( $expected, $reader->getResults(), $info );
+ }
+
+ public function dataXMPParse() {
+ $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/' ;
+ $data = array();
+
+ // $xmpFiles format: array of arrays with first arg file base name,
+ // with the actual file having .xmp on the end for the xmp
+ // and .result.php on the end for a php file containing the result
+ // array. Second argument is some info on what's being tested.
+ $xmpFiles = array(
+ array( '1', 'parseType=Resource test' ),
+ array( '2', 'Structure with mixed attribute and element props' ),
+ array( '3', 'Extra qualifiers (that should be ignored)' ),
+ array( '3-invalid', 'Test ignoring qualifiers that look like normal props' ),
+ array( '4', 'Flash as qualifier' ),
+ array( '5', 'Flash as qualifier 2' ),
+ array( '6', 'Multiple rdf:Description' ),
+ array( '7', 'Generic test of several property types' ),
+ array( 'flash', 'Test of Flash property' ),
+ array( 'invalid-child-not-struct', 'Test child props not in struct or ignored' ),
+ array( 'no-recognized-props', 'Test namespace and no recognized props' ),
+ array( 'no-namespace', 'Test non-namespaced attributes are ignored' ),
+ array( 'bag-for-seq', "Allow bag's instead of seq's. (bug 27105)" ),
+ array( 'utf16BE', 'UTF-16BE encoding' ),
+ array( 'utf16LE', 'UTF-16LE encoding' ),
+ array( 'utf32BE', 'UTF-32BE encoding' ),
+ array( 'utf32LE', 'UTF-32LE encoding' ),
+ array( 'xmpExt', 'Extended XMP missing second part' ),
+ );
+ foreach( $xmpFiles as $file ) {
+ $xmp = file_get_contents( $xmpPath . $file[0] . '.xmp' );
+ // I'm not sure if this is the best way to handle getting the
+ // result array, but it seems kind of big to put directly in the test
+ // file.
+ $result = null;
+ include( $xmpPath . $file[0] . '.result.php' );
+ $data[] = array( $xmp, $result, '[' . $file[0] . '.xmp] ' . $file[1] );
+ }
+ return $data;
+ }
+
+ /** Test ExtendedXMP block support. (Used when the XMP has to be split
+ * over multiple jpeg segments, due to 64k size limit on jpeg segments.
+ *
+ * @todo This is based on what the standard says. Need to find a real
+ * world example file to double check the support for this is right.
+ */
+ function testExtendedXMP() {
+ $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/';
+ $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
+ $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
+
+ $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp
+ $length = pack( 'N', strlen( $extendedXMP ) );
+ $offset = pack( 'N', 0 );
+ $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
+
+ $reader = new XMPReader();
+ $reader->parse( $standardXMP );
+ $reader->parseExtended( $extendedPacket );
+ $actual = $reader->getResults();
+
+ $expected = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => 9,
+ 'FNumber' => '2/10',
+ )
+ );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+ /**
+ * This test has an extended XMP block with a wrong guid (md5sum)
+ * and thus should only return the StandardXMP, not the ExtendedXMP.
+ */
+ function testExtendedXMPWithWrongGUID() {
+ $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/';
+ $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
+ $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
+
+ $md5sum = '28C74E0AC2D796886759006FBE2E57B9'; // Note last digit.
+ $length = pack( 'N', strlen( $extendedXMP ) );
+ $offset = pack( 'N', 0 );
+ $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
+
+ $reader = new XMPReader();
+ $reader->parse( $standardXMP );
+ $reader->parseExtended( $extendedPacket );
+ $actual = $reader->getResults();
+
+ $expected = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => 9,
+ )
+ );
+
+ $this->assertEquals( $expected, $actual );
+ }
+ /**
+ * Have a high offset to simulate a missing packet,
+ * which should cause it to ignore the ExtendedXMP packet.
+ */
+ function testExtendedXMPMissingPacket() {
+ $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/';
+ $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
+ $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
+
+ $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp
+ $length = pack( 'N', strlen( $extendedXMP ) );
+ $offset = pack( 'N', 2048 );
+ $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
+
+ $reader = new XMPReader();
+ $reader->parse( $standardXMP );
+ $reader->parseExtended( $extendedPacket );
+ $actual = $reader->getResults();
+
+ $expected = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => 9,
+ )
+ );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+}
diff --git a/tests/phpunit/includes/normal/CleanUpTest.php b/tests/phpunit/includes/normal/CleanUpTest.php
new file mode 100644
index 00000000..d5ad18d8
--- /dev/null
+++ b/tests/phpunit/includes/normal/CleanUpTest.php
@@ -0,0 +1,382 @@
+<?php
+/**
+ * Tests for UtfNormal::cleanUp() function.
+ *
+ * Copyright © 2004 Brion Vibber <brion@pobox.com>
+ * http://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Additional tests for UtfNormal::cleanUp() function, inclusion
+ * regression checks for known problems.
+ * Requires PHPUnit.
+ *
+ * @ingroup UtfNormal
+ */
+class CleanUpTest extends MediaWikiTestCase {
+ /** @todo document */
+ function testAscii() {
+ $text = 'This is plain ASCII text.';
+ $this->assertEquals( $text, UtfNormal::cleanUp( $text ) );
+ }
+
+ /** @todo document */
+ function testNull() {
+ $text = "a \x00 null";
+ $expect = "a \xef\xbf\xbd null";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testLatin() {
+ $text = "L'\xc3\xa9cole";
+ $this->assertEquals( $text, UtfNormal::cleanUp( $text ) );
+ }
+
+ /** @todo document */
+ function testLatinNormal() {
+ $text = "L'e\xcc\x81cole";
+ $expect = "L'\xc3\xa9cole";
+ $this->assertEquals( $expect, UtfNormal::cleanUp( $text ) );
+ }
+
+ /**
+ * This test is *very* expensive!
+ * @todo document
+ */
+ function XtestAllChars() {
+ $rep = UTF8_REPLACEMENT;
+ for( $i = 0x0; $i < UNICODE_MAX; $i++ ) {
+ $char = codepointToUtf8( $i );
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%04X", $i );
+ if( $i % 0x1000 == 0 ) echo "U+$x\n";
+ if( $i == 0x0009 ||
+ $i == 0x000a ||
+ $i == 0x000d ||
+ ($i > 0x001f && $i < UNICODE_SURROGATE_FIRST) ||
+ ($i > UNICODE_SURROGATE_LAST && $i < 0xfffe ) ||
+ ($i > 0xffff && $i <= UNICODE_MAX ) ) {
+ if( isset( UtfNormal::$utfCanonicalComp[$char] ) || isset( UtfNormal::$utfCanonicalDecomp[$char] ) ) {
+ $comp = UtfNormal::NFC( $char );
+ $this->assertEquals(
+ bin2hex( $comp ),
+ bin2hex( $clean ),
+ "U+$x should be decomposed" );
+ } else {
+ $this->assertEquals(
+ bin2hex( $char ),
+ bin2hex( $clean ),
+ "U+$x should be intact" );
+ }
+ } else {
+ $this->assertEquals( bin2hex( $rep ), bin2hex( $clean ), $x );
+ }
+ }
+ }
+
+ /** @todo document */
+ function testAllBytes() {
+ $this->doTestBytes( '', '' );
+ $this->doTestBytes( 'x', '' );
+ $this->doTestBytes( '', 'x' );
+ $this->doTestBytes( 'x', 'x' );
+ }
+
+ /** @todo document */
+ function doTestBytes( $head, $tail ) {
+ for( $i = 0x0; $i < 256; $i++ ) {
+ $char = $head . chr( $i ) . $tail;
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%02X", $i );
+ if( $i == 0x0009 ||
+ $i == 0x000a ||
+ $i == 0x000d ||
+ ($i > 0x001f && $i < 0x80) ) {
+ $this->assertEquals(
+ bin2hex( $char ),
+ bin2hex( $clean ),
+ "ASCII byte $x should be intact" );
+ if( $char != $clean ) return;
+ } else {
+ $norm = $head . UTF8_REPLACEMENT . $tail;
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Forbidden byte $x should be rejected" );
+ if( $norm != $clean ) return;
+ }
+ }
+ }
+
+ /** @todo document */
+ function testDoubleBytes() {
+ $this->doTestDoubleBytes( '', '' );
+ $this->doTestDoubleBytes( 'x', '' );
+ $this->doTestDoubleBytes( '', 'x' );
+ $this->doTestDoubleBytes( 'x', 'x' );
+ }
+
+ /**
+ * @todo document
+ */
+ function doTestDoubleBytes( $head, $tail ) {
+ for( $first = 0xc0; $first < 0x100; $first+=2 ) {
+ for( $second = 0x80; $second < 0x100; $second+=2 ) {
+ $char = $head . chr( $first ) . chr( $second ) . $tail;
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%02X,%02X", $first, $second );
+ if( $first > 0xc1 &&
+ $first < 0xe0 &&
+ $second < 0xc0 ) {
+ $norm = UtfNormal::NFC( $char );
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Pair $x should be intact" );
+ if( $norm != $clean ) return;
+ } elseif( $first > 0xfd || $second > 0xbf ) {
+ # fe and ff are not legal head bytes -- expect two replacement chars
+ $norm = $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail;
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Forbidden pair $x should be rejected" );
+ if( $norm != $clean ) return;
+ } else {
+ $norm = $head . UTF8_REPLACEMENT . $tail;
+ $this->assertEquals(
+ bin2hex( $norm ),
+ bin2hex( $clean ),
+ "Forbidden pair $x should be rejected" );
+ if( $norm != $clean ) return;
+ }
+ }
+ }
+ }
+
+ /** @todo document */
+ function testTripleBytes() {
+ $this->doTestTripleBytes( '', '' );
+ $this->doTestTripleBytes( 'x', '' );
+ $this->doTestTripleBytes( '', 'x' );
+ $this->doTestTripleBytes( 'x', 'x' );
+ }
+
+ /** @todo document */
+ function doTestTripleBytes( $head, $tail ) {
+ for( $first = 0xc0; $first < 0x100; $first+=2 ) {
+ for( $second = 0x80; $second < 0x100; $second+=2 ) {
+ #for( $third = 0x80; $third < 0x100; $third++ ) {
+ for( $third = 0x80; $third < 0x81; $third++ ) {
+ $char = $head . chr( $first ) . chr( $second ) . chr( $third ) . $tail;
+ $clean = UtfNormal::cleanUp( $char );
+ $x = sprintf( "%02X,%02X,%02X", $first, $second, $third );
+ if( $first >= 0xe0 &&
+ $first < 0xf0 &&
+ $second < 0xc0 &&
+ $third < 0xc0 ) {
+ if( $first == 0xe0 && $second < 0xa0 ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Overlong triplet $x should be rejected" );
+ } elseif( $first == 0xed &&
+ ( chr( $first ) . chr( $second ) . chr( $third )) >= UTF8_SURROGATE_FIRST ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Surrogate triplet $x should be rejected" );
+ } else {
+ $this->assertEquals(
+ bin2hex( UtfNormal::NFC( $char ) ),
+ bin2hex( $clean ),
+ "Triplet $x should be intact" );
+ }
+ } elseif( $first > 0xc1 && $first < 0xe0 && $second < 0xc0 ) {
+ $this->assertEquals(
+ bin2hex( UtfNormal::NFC( $head . chr( $first ) . chr( $second ) ) . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Valid 2-byte $x + broken tail" );
+ } elseif( $second > 0xc1 && $second < 0xe0 && $third < 0xc0 ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . UtfNormal::NFC( chr( $second ) . chr( $third ) . $tail ) ),
+ bin2hex( $clean ),
+ "Broken head + valid 2-byte $x" );
+ } elseif( ( $first > 0xfd || $second > 0xfd ) &&
+ ( ( $second > 0xbf && $third > 0xbf ) ||
+ ( $second < 0xc0 && $third < 0xc0 ) ||
+ ( $second > 0xfd ) ||
+ ( $third > 0xfd ) ) ) {
+ # fe and ff are not legal head bytes -- expect three replacement chars
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Forbidden triplet $x should be rejected" );
+ } elseif( $first > 0xc2 && $second < 0xc0 && $third < 0xc0 ) {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Forbidden triplet $x should be rejected" );
+ } else {
+ $this->assertEquals(
+ bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ),
+ bin2hex( $clean ),
+ "Forbidden triplet $x should be rejected" );
+ }
+ }
+ }
+ }
+ }
+
+ /** @todo document */
+ function testChunkRegression() {
+ # Check for regression against a chunking bug
+ $text = "\x46\x55\xb8" .
+ "\xdc\x96" .
+ "\xee" .
+ "\xe7" .
+ "\x44" .
+ "\xaa" .
+ "\x2f\x25";
+ $expect = "\x46\x55\xef\xbf\xbd" .
+ "\xdc\x96" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x44" .
+ "\xef\xbf\xbd" .
+ "\x2f\x25";
+
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testInterposeRegression() {
+ $text = "\x4e\x30" .
+ "\xb1" . # bad tail
+ "\x3a" .
+ "\x92" . # bad tail
+ "\x62\x3a" .
+ "\x84" . # bad tail
+ "\x43" .
+ "\xc6" . # bad head
+ "\x3f" .
+ "\x92" . # bad tail
+ "\xad" . # bad tail
+ "\x7d" .
+ "\xd9\x95";
+
+ $expect = "\x4e\x30" .
+ "\xef\xbf\xbd" .
+ "\x3a" .
+ "\xef\xbf\xbd" .
+ "\x62\x3a" .
+ "\xef\xbf\xbd" .
+ "\x43" .
+ "\xef\xbf\xbd" .
+ "\x3f" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x7d" .
+ "\xd9\x95";
+
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testOverlongRegression() {
+ $text = "\x67" .
+ "\x1a" . # forbidden ascii
+ "\xea" . # bad head
+ "\xc1\xa6" . # overlong sequence
+ "\xad" . # bad tail
+ "\x1c" . # forbidden ascii
+ "\xb0" . # bad tail
+ "\x3c" .
+ "\x9e"; # bad tail
+ $expect = "\x67" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x3c" .
+ "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testSurrogateRegression() {
+ $text = "\xed\xb4\x96" . # surrogate 0xDD16
+ "\x83" . # bad tail
+ "\xb4" . # bad tail
+ "\xac"; # bad head
+ $expect = "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testBomRegression() {
+ $text = "\xef\xbf\xbe" . # U+FFFE, illegal char
+ "\xb2" . # bad tail
+ "\xef" . # bad head
+ "\x59";
+ $expect = "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\xef\xbf\xbd" .
+ "\x59";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testForbiddenRegression() {
+ $text = "\xef\xbf\xbf"; # U+FFFF, illegal char
+ $expect = "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ function testHangulRegression() {
+ $text = "\xed\x9c\xaf" . # Hangul char
+ "\xe1\x87\x81"; # followed by another final jamo
+ $expect = $text; # Should *not* change.
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+}
diff --git a/tests/phpunit/includes/parser/MagicVariableTest.php b/tests/phpunit/includes/parser/MagicVariableTest.php
new file mode 100644
index 00000000..a47653e3
--- /dev/null
+++ b/tests/phpunit/includes/parser/MagicVariableTest.php
@@ -0,0 +1,195 @@
+<?php
+/**
+ * This file is intended to test magic variables in the parser
+ * It was inspired by Raymond & Matěj Grabovský commenting about r66200
+ *
+ * As of february 2011, it only tests some revisions and date related
+ * magic variables.
+ *
+ * @author Ashar Voultoiz
+ * @copyright Copyright © 2011, Ashar Voultoiz
+ * @file
+ */
+
+/** */
+class MagicVariableTest extends MediaWikiTestCase {
+ /** Will contains a parser object*/
+ private $testParser = null;
+
+ /**
+ * An array of magicword returned as type integer by the parser
+ * They are usually returned as a string for i18n since we support
+ * persan numbers for example, but some magic explicitly return
+ * them as integer.
+ * @see MagicVariableTest::assertMagic()
+ */
+ private $expectedAsInteger = array(
+ 'revisionday',
+ 'revisionmonth1',
+ );
+
+ /** setup a basic parser object */
+ function setUp() {
+ global $wgContLang;
+ $wgContLang = Language::factory( 'en' );
+
+ $this->testParser = new Parser();
+ $this->testParser->Options( new ParserOptions() );
+
+ # initialize parser output
+ $this->testParser->clearState();
+ }
+
+ /** destroy parser (TODO: is it really neded?)*/
+ function tearDown() {
+ unset( $this->testParser );
+ }
+
+ ############### TESTS #############################################
+ # @todo FIXME:
+ # - those got copy pasted, we can probably make them cleaner
+ # - tests are lacking useful messages
+
+ # day
+
+ /** @dataProvider MediaWikiProvide::Days */
+ function testCurrentdayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'currentday', $day );
+ }
+ /** @dataProvider MediaWikiProvide::Days */
+ function testCurrentdaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'currentday2', $day );
+ }
+ /** @dataProvider MediaWikiProvide::Days */
+ function testLocaldayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'localday', $day );
+ }
+ /** @dataProvider MediaWikiProvide::Days */
+ function testLocaldaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'localday2', $day );
+ }
+
+ # month
+
+ /** @dataProvider MediaWikiProvide::Months */
+ function testCurrentmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'currentmonth', $month );
+ }
+ /** @dataProvider MediaWikiProvide::Months */
+ function testCurrentmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'currentmonth1', $month );
+ }
+ /** @dataProvider MediaWikiProvide::Months */
+ function testLocalmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'localmonth', $month );
+ }
+ /** @dataProvider MediaWikiProvide::Months */
+ function testLocalmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'localmonth1', $month );
+ }
+
+
+ # revision day
+
+ /** @dataProvider MediaWikiProvide::Days */
+ function testRevisiondayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'revisionday', $day );
+ }
+ /** @dataProvider MediaWikiProvide::Days */
+ function testRevisiondaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'revisionday2', $day );
+ }
+
+ # revision month
+
+ /** @dataProvider MediaWikiProvide::Months */
+ function testRevisionmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'revisionmonth', $month );
+ }
+ /** @dataProvider MediaWikiProvide::Months */
+ function testRevisionmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'revisionmonth1', $month );
+ }
+
+ /**
+ * Rough tests for {{SERVERNAME}} magic word
+ * Bug 31176
+ */
+ function testServernameFromDifferentProtocols() {
+ global $wgServer;
+ $saved_wgServer= $wgServer;
+
+ $wgServer = 'http://localhost/';
+ $this->assertMagic( 'localhost', 'servername' );
+ $wgServer = 'https://localhost/';
+ $this->assertMagic( 'localhost', 'servername' );
+ $wgServer = '//localhost/'; # bug 31176
+ $this->assertMagic( 'localhost', 'servername' );
+
+ $wgServer = $saved_wgServer;
+ }
+
+ ############### HELPERS ############################################
+
+ /** assertion helper expecting a magic output which is zero padded */
+ PUBLIC function assertZeroPadded( $magic, $value ) {
+ $this->assertMagicPadding( $magic, $value, '%02d' );
+ }
+
+ /** assertion helper expecting a magic output which is unpadded */
+ PUBLIC function assertUnPadded( $magic, $value ) {
+ $this->assertMagicPadding( $magic, $value, '%d' );
+ }
+
+ /**
+ * Main assertion helper for magic variables padding
+ * @param $magic string Magic variable name
+ * @param $value mixed Month or day
+ * @param $format string sprintf format for $value
+ */
+ private function assertMagicPadding( $magic, $value, $format ) {
+ # Initialize parser timestamp as year 2010 at 12h34 56s.
+ # month and day are given by the caller ($value). Month < 12!
+ if( $value > 12 ) { $month = $value % 12; }
+ else { $month = $value; }
+
+ $this->setParserTS(
+ sprintf( '2010%02d%02d123456', $month, $value )
+ );
+
+ # please keep the following commented line of code. It helps debugging.
+ //print "\nDEBUG (value $value):" . sprintf( '2010%02d%02d123456', $value, $value ) . "\n";
+
+ # format expectation and test it
+ $expected = sprintf( $format, $value );
+ $this->assertMagic( $expected, $magic );
+ }
+
+ /** helper to set the parser timestamp and revision timestamp */
+ private function setParserTS( $ts ) {
+ $this->testParser->Options()->setTimestamp( $ts );
+ $this->testParser->mRevisionTimestamp = $ts;
+ }
+
+ /**
+ * Assertion helper to test a magic variable output
+ */
+ private function assertMagic( $expected, $magic ) {
+ if( in_array( $magic, $this->expectedAsInteger ) ) {
+ $expected = (int) $expected;
+ }
+
+ # Generate a message for the assertion
+ $msg = sprintf( "Magic %s should be <%s:%s>",
+ $magic,
+ $expected,
+ gettype( $expected )
+ );
+
+ $this->assertSame(
+ $expected,
+ $this->testParser->getVariableValue( $magic ),
+ $msg
+ );
+ }
+}
diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php
new file mode 100644
index 00000000..18510d9a
--- /dev/null
+++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php
@@ -0,0 +1,34 @@
+<?php
+
+require_once( dirname( __FILE__ ) . '/ParserHelpers.php' );
+require_once( dirname( __FILE__ ) . '/NewParserTest.php' );
+
+/**
+ * The UnitTest must be either a class that inherits from PHPUnit_Framework_TestCase
+ * or a class that provides a public static suite() method which returns
+ * an PHPUnit_Framework_Test object
+ *
+ * @group Parser
+ * @group Database
+ */
+class MediaWikiParserTest {
+
+ public static function suite() {
+ global $wgParserTestFiles;
+
+ $suite = new PHPUnit_Framework_TestSuite;
+
+ foreach ( $wgParserTestFiles as $filename ) {
+ $testsName = basename( $filename, '.txt' );
+ $className = /*ucfirst( basename( dirname( $filename ) ) ) .*/ ucfirst( basename( $filename, '.txt' ) );
+
+ eval( "/** @group Database\n@group Parser\n*/ class $className extends NewParserTest { protected \$file = \"" . addslashes( $filename ) . "\"; } " );
+
+ $parserTester = new $className( $testsName );
+ $suite->addTestSuite( new ReflectionClass ( $parserTester ) );
+ }
+
+
+ return $suite;
+ }
+}
diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php
new file mode 100644
index 00000000..f4d5f757
--- /dev/null
+++ b/tests/phpunit/includes/parser/NewParserTest.php
@@ -0,0 +1,850 @@
+<?php
+
+/**
+ * Although marked as a stub, can work independently.
+ *
+ * @group Database
+ * @group Parser
+ * @group Stub
+ */
+class NewParserTest extends MediaWikiTestCase {
+
+ static protected $articles = array(); // Array of test articles defined by the tests
+ /* The dataProvider is run on a different instance than the test, so it must be static
+ * When running tests from several files, all tests will see all articles.
+ */
+
+ public $uploadDir;
+ public $keepUploads = false;
+ public $runDisabled = false;
+ public $regex = '';
+ public $showProgress = true;
+ public $savedInitialGlobals = array();
+ public $savedWeirdGlobals = array();
+ public $savedGlobals = array();
+ public $hooks = array();
+ public $functionHooks = array();
+
+ //Fuzz test
+ public $maxFuzzTestLength = 300;
+ public $fuzzSeed = 0;
+ public $memoryLimit = 50;
+
+ protected $file = false;
+
+ /*function __construct($a = null,$b = array(),$c = null ) {
+ parent::__construct($a,$b,$c);
+ }*/
+
+ function setUp() {
+ global $wgContLang, $wgNamespaceProtection, $wgNamespaceAliases;
+ global $wgHooks, $IP;
+ $wgContLang = Language::factory( 'en' );
+
+ //Setup CLI arguments
+ if ( $this->getCliArg( 'regex=' ) ) {
+ $this->regex = $this->getCliArg( 'regex=' );
+ } else {
+ # Matches anything
+ $this->regex = '';
+ }
+
+ $this->keepUploads = $this->getCliArg( 'keep-uploads' );
+
+ $tmpGlobals = array();
+
+ $tmpGlobals['wgScript'] = '/index.php';
+ $tmpGlobals['wgScriptPath'] = '/';
+ $tmpGlobals['wgArticlePath'] = '/wiki/$1';
+ $tmpGlobals['wgStyleSheetPath'] = '/skins';
+ $tmpGlobals['wgStylePath'] = '/skins';
+ $tmpGlobals['wgThumbnailScriptPath'] = false;
+ $tmpGlobals['wgLocalFileRepo'] = array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'directory' => wfTempDir() . '/test-repo',
+ 'url' => 'http://example.com/images',
+ 'deletedDir' => wfTempDir() . '/test-repo/delete',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ );
+
+ $tmpGlobals['wgEnableParserCache'] = false;
+ $tmpGlobals['wgHooks'] = $wgHooks;
+ $tmpGlobals['wgDeferredUpdateList'] = array();
+ $tmpGlobals['wgMemc'] = wfGetMainCache();
+ $tmpGlobals['messageMemc'] = wfGetMessageCacheStorage();
+ $tmpGlobals['parserMemc'] = wfGetParserCacheStorage();
+
+ // $tmpGlobals['wgContLang'] = new StubContLang;
+ $tmpGlobals['wgUser'] = new User;
+ $context = new RequestContext();
+ $tmpGlobals['wgLang'] = $context->getLang();
+ $tmpGlobals['wgOut'] = $context->getOutput();
+ $tmpGlobals['wgParser'] = new StubObject( 'wgParser', $GLOBALS['wgParserConf']['class'], array( $GLOBALS['wgParserConf'] ) );
+ $tmpGlobals['wgRequest'] = new WebRequest;
+
+ if ( $GLOBALS['wgStyleDirectory'] === false ) {
+ $tmpGlobals['wgStyleDirectory'] = "$IP/skins";
+ }
+
+
+ foreach ( $tmpGlobals as $var => $val ) {
+ if ( array_key_exists( $var, $GLOBALS ) ) {
+ $this->savedInitialGlobals[$var] = $GLOBALS[$var];
+ }
+
+ $GLOBALS[$var] = $val;
+ }
+
+ $this->savedWeirdGlobals['mw_namespace_protection'] = $wgNamespaceProtection[NS_MEDIAWIKI];
+ $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image'];
+ $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk'];
+
+ $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
+ $wgNamespaceAliases['Image'] = NS_FILE;
+ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
+
+ }
+
+ public function tearDown() {
+
+ foreach ( $this->savedInitialGlobals as $var => $val ) {
+ $GLOBALS[$var] = $val;
+ }
+
+ global $wgNamespaceProtection, $wgNamespaceAliases;
+
+ $wgNamespaceProtection[NS_MEDIAWIKI] = $this->savedWeirdGlobals['mw_namespace_protection'];
+ $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias'];
+ $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias'];
+ }
+
+ function addDBData() {
+ # Hack: insert a few Wikipedia in-project interwiki prefixes,
+ # for testing inter-language links
+ $this->db->insert( 'interwiki', array(
+ array( 'iw_prefix' => 'wikipedia',
+ 'iw_url' => 'http://en.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ),
+ array( 'iw_prefix' => 'meatball',
+ 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ),
+ array( 'iw_prefix' => 'zh',
+ 'iw_url' => 'http://zh.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ array( 'iw_prefix' => 'es',
+ 'iw_url' => 'http://es.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ array( 'iw_prefix' => 'fr',
+ 'iw_url' => 'http://fr.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ array( 'iw_prefix' => 'ru',
+ 'iw_url' => 'http://ru.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ /**
+ * @todo Fixme! Why are we inserting duplicate data here? Shouldn't
+ * need this IGNORE or shouldn't need the insert at all.
+ */
+ ), __METHOD__, array( 'IGNORE' ) );
+
+
+ # Update certain things in site_stats
+ $this->db->insert( 'site_stats',
+ array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ),
+ __METHOD__,
+ /**
+ * @todo Fixme! Same as above!
+ */
+ array( 'IGNORE' )
+ );
+
+ # Reinitialise the LocalisationCache to match the database state
+ Language::getLocalisationCache()->unloadAll();
+
+ # Clear the message cache
+ MessageCache::singleton()->clear();
+
+ $this->uploadDir = $this->setupUploadDir();
+
+ $user = User::newFromId( 0 );
+ LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
+ $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', array(
+ 'size' => 12345,
+ 'width' => 1941,
+ 'height' => 220,
+ 'bits' => 24,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20010115123500' ), $user );
+
+ # This image will be blacklisted in [[MediaWiki:Bad image list]]
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
+ $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', array(
+ 'size' => 12345,
+ 'width' => 320,
+ 'height' => 240,
+ 'bits' => 24,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20010115123500' ), $user );
+
+ }
+
+
+
+
+ //ParserTest setup/teardown functions
+
+ /**
+ * Set up the global variables for a consistent environment for each test.
+ * Ideally this should replace the global configuration entirely.
+ */
+ protected function setupGlobals( $opts = '', $config = '' ) {
+ # Find out values for some special options.
+ $lang =
+ self::getOptionValue( 'language', $opts, 'en' );
+ $variant =
+ self::getOptionValue( 'variant', $opts, false );
+ $maxtoclevel =
+ self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
+ $linkHolderBatchSize =
+ self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
+
+ $settings = array(
+ 'wgServer' => 'http://Britney-Spears',
+ 'wgScript' => '/index.php',
+ 'wgScriptPath' => '/',
+ 'wgArticlePath' => '/wiki/$1',
+ 'wgActionPaths' => array(),
+ 'wgLocalFileRepo' => array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'directory' => $this->uploadDir,
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ ),
+ 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
+ 'wgStylePath' => '/skins',
+ 'wgStyleSheetPath' => '/skins',
+ 'wgSitename' => 'MediaWiki',
+ 'wgLanguageCode' => $lang,
+ 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_',
+ 'wgRawHtml' => isset( $opts['rawhtml'] ),
+ 'wgLang' => null,
+ 'wgContLang' => null,
+ 'wgNamespacesWithSubpages' => array( 0 => isset( $opts['subpage'] ) ),
+ 'wgMaxTocLevel' => $maxtoclevel,
+ 'wgCapitalLinks' => true,
+ 'wgNoFollowLinks' => true,
+ 'wgNoFollowDomainExceptions' => array(),
+ 'wgThumbnailScriptPath' => false,
+ 'wgUseImageResize' => false,
+ 'wgUseTeX' => isset( $opts['math'] ),
+ 'wgMathDirectory' => $this->uploadDir . '/math',
+ 'wgLocaltimezone' => 'UTC',
+ 'wgAllowExternalImages' => true,
+ 'wgUseTidy' => false,
+ 'wgDefaultLanguageVariant' => $variant,
+ 'wgVariantArticlePath' => false,
+ 'wgGroupPermissions' => array( '*' => array(
+ 'createaccount' => true,
+ 'read' => true,
+ 'edit' => true,
+ 'createpage' => true,
+ 'createtalk' => true,
+ ) ),
+ 'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ),
+ 'wgDefaultExternalStore' => array(),
+ 'wgForeignFileRepos' => array(),
+ 'wgLinkHolderBatchSize' => $linkHolderBatchSize,
+ 'wgExperimentalHtmlIds' => false,
+ 'wgExternalLinkTarget' => false,
+ 'wgAlwaysUseTidy' => false,
+ 'wgHtml5' => true,
+ 'wgWellFormedXml' => true,
+ 'wgAllowMicrodataAttributes' => true,
+ 'wgAdaptiveMessageCache' => true,
+ 'wgUseDatabaseMessages' => true,
+ );
+
+ if ( $config ) {
+ $configLines = explode( "\n", $config );
+
+ foreach ( $configLines as $line ) {
+ list( $var, $value ) = explode( '=', $line, 2 );
+
+ $settings[$var] = eval( "return $value;" ); //???
+ }
+ }
+
+ $this->savedGlobals = array();
+
+ foreach ( $settings as $var => $val ) {
+ if ( array_key_exists( $var, $GLOBALS ) ) {
+ $this->savedGlobals[$var] = $GLOBALS[$var];
+ }
+
+ $GLOBALS[$var] = $val;
+ }
+
+ $langObj = Language::factory( $lang );
+ $GLOBALS['wgContLang'] = $langObj;
+ $context = new RequestContext();
+ $GLOBALS['wgLang'] = $context->getLang();
+
+ $GLOBALS['wgMemc'] = new EmptyBagOStuff;
+ $GLOBALS['wgOut'] = $context->getOutput();
+
+ global $wgHooks;
+
+ $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
+ $wgHooks['ParserTestParser'][] = 'ParserTestStaticParserHook::setup';
+ $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
+
+ MagicWord::clearCache();
+
+ # Publish the articles after we have the final language set
+ $this->publishTestArticles();
+
+ # The entries saved into RepoGroup cache with previous globals will be wrong.
+ RepoGroup::destroySingleton();
+ MessageCache::singleton()->destroyInstance();
+
+ global $wgUser;
+ $wgUser = new User();
+ }
+
+ /**
+ * Create a dummy uploads directory which will contain a couple
+ * of files in order to pass existence tests.
+ *
+ * @return String: the directory
+ */
+ protected function setupUploadDir() {
+ global $IP;
+
+ if ( $this->keepUploads ) {
+ $dir = wfTempDir() . '/mwParser-images';
+
+ if ( is_dir( $dir ) ) {
+ return $dir;
+ }
+ } else {
+ $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
+ }
+
+ // wfDebug( "Creating upload directory $dir\n" );
+ if ( file_exists( $dir ) ) {
+ wfDebug( "Already exists!\n" );
+ return $dir;
+ }
+
+ wfMkdirParents( $dir . '/3/3a' );
+ copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" );
+ wfMkdirParents( $dir . '/0/09' );
+ copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" );
+
+ return $dir;
+ }
+
+ /**
+ * Restore default values and perform any necessary clean-up
+ * after each test runs.
+ */
+ protected function teardownGlobals() {
+ RepoGroup::destroySingleton();
+ LinkCache::singleton()->clear();
+
+ foreach ( $this->savedGlobals as $var => $val ) {
+ $GLOBALS[$var] = $val;
+ }
+
+ $this->teardownUploadDir( $this->uploadDir );
+ }
+
+ /**
+ * Remove the dummy uploads directory
+ */
+ private function teardownUploadDir( $dir ) {
+ if ( $this->keepUploads ) {
+ return;
+ }
+
+ // delete the files first, then the dirs.
+ self::deleteFiles(
+ array (
+ "$dir/3/3a/Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
+
+ "$dir/0/09/Bad.jpg",
+
+ "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
+ )
+ );
+
+ self::deleteDirs(
+ array (
+ "$dir/3/3a",
+ "$dir/3",
+ "$dir/thumb/6/65",
+ "$dir/thumb/6",
+ "$dir/thumb/3/3a/Foobar.jpg",
+ "$dir/thumb/3/3a",
+ "$dir/thumb/3",
+
+ "$dir/0/09/",
+ "$dir/0/",
+ "$dir/thumb",
+ "$dir/math/f/a/5",
+ "$dir/math/f/a",
+ "$dir/math/f",
+ "$dir/math",
+ "$dir",
+ )
+ );
+ }
+
+ /**
+ * Delete the specified files, if they exist.
+ * @param $files Array: full paths to files to delete.
+ */
+ private static function deleteFiles( $files ) {
+ foreach ( $files as $file ) {
+ if ( file_exists( $file ) ) {
+ unlink( $file );
+ }
+ }
+ }
+
+ /**
+ * Delete the specified directories, if they exist. Must be empty.
+ * @param $dirs Array: full paths to directories to delete.
+ */
+ private static function deleteDirs( $dirs ) {
+ foreach ( $dirs as $dir ) {
+ if ( is_dir( $dir ) ) {
+ rmdir( $dir );
+ }
+ }
+ }
+
+ public function parserTestProvider() {
+ if ( $this->file === false ) {
+ global $wgParserTestFiles;
+ $this->file = $wgParserTestFiles[0];
+ }
+ return new TestFileIterator( $this->file, $this );
+ }
+
+ /**
+ * Set the file from whose tests will be run by this instance
+ */
+ public function setParserTestFile( $filename ) {
+ $this->file = $filename;
+ }
+
+ /** @dataProvider parserTestProvider */
+ public function testParserTest( $desc, $input, $result, $opts, $config ) {
+ if ( !preg_match( '/' . $this->regex . '/', $desc ) ) return; //$this->markTestSkipped( 'Filtered out by the user' );
+
+ wfDebug( "Running parser test: $desc\n" );
+
+ $opts = $this->parseOptions( $opts );
+ $this->setupGlobals( $opts, $config );
+
+ $user = new User();
+ $options = ParserOptions::newFromUser( $user );
+
+ if ( isset( $opts['title'] ) ) {
+ $titleText = $opts['title'];
+ }
+ else {
+ $titleText = 'Parser test';
+ }
+
+ $local = isset( $opts['local'] );
+ $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
+ $parser = $this->getParser( $preprocessor );
+
+ $title = Title::newFromText( $titleText );
+
+ if ( isset( $opts['pst'] ) ) {
+ $out = $parser->preSaveTransform( $input, $title, $user, $options );
+ } elseif ( isset( $opts['msg'] ) ) {
+ $out = $parser->transformMsg( $input, $options, $title );
+ } elseif ( isset( $opts['section'] ) ) {
+ $section = $opts['section'];
+ $out = $parser->getSection( $input, $section );
+ } elseif ( isset( $opts['replace'] ) ) {
+ $section = $opts['replace'][0];
+ $replace = $opts['replace'][1];
+ $out = $parser->replaceSection( $input, $section, $replace );
+ } elseif ( isset( $opts['comment'] ) ) {
+ $linker = $user->getSkin();
+ $out = $linker->formatComment( $input, $title, $local );
+ } elseif ( isset( $opts['preload'] ) ) {
+ $out = $parser->getpreloadText( $input, $title, $options );
+ } else {
+ $output = $parser->parse( $input, $title, $options, true, true, 1337 );
+ $out = $output->getText();
+
+ if ( isset( $opts['showtitle'] ) ) {
+ if ( $output->getTitleText() ) {
+ $title = $output->getTitleText();
+ }
+
+ $out = "$title\n$out";
+ }
+
+ if ( isset( $opts['ill'] ) ) {
+ $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) );
+ } elseif ( isset( $opts['cat'] ) ) {
+ global $wgOut;
+
+ $wgOut->addCategoryLinks( $output->getCategories() );
+ $cats = $wgOut->getCategoryLinks();
+
+ if ( isset( $cats['normal'] ) ) {
+ $out = $this->tidy( implode( ' ', $cats['normal'] ) );
+ } else {
+ $out = '';
+ }
+ }
+ $parser->mPreprocessor = null;
+
+ $result = $this->tidy( $result );
+ }
+
+ $this->teardownGlobals();
+
+ $this->assertEquals( $result, $out, $desc );
+ }
+
+ /**
+ * Run a fuzz test series
+ * Draw input from a set of test files
+ */
+ function testFuzzTests() {
+
+ $this->markTestIncomplete( "Somebody is serializing PDO objects, that's a no-no" );
+
+ global $wgParserTestFiles;
+
+ $files = $wgParserTestFiles;
+
+ if( $this->getCliArg( 'file=' ) ) {
+ $files = array( $this->getCliArg( 'file=' ) );
+ }
+
+ $dict = $this->getFuzzInput( $files );
+ $dictSize = strlen( $dict );
+ $logMaxLength = log( $this->maxFuzzTestLength );
+
+ $user = new User;
+ $opts = ParserOptions::newFromUser( $user );
+ $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
+
+ $id = 1;
+
+ while ( true ) {
+
+ // Generate test input
+ mt_srand( ++$this->fuzzSeed );
+ $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
+ $input = '';
+
+ while ( strlen( $input ) < $totalLength ) {
+ $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
+ $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
+ $offset = mt_rand( 0, $dictSize - $hairLength );
+ $input .= substr( $dict, $offset, $hairLength );
+ }
+
+ $this->setupGlobals();
+ $parser = $this->getParser();
+
+ // Run the test
+ try {
+ $parser->parse( $input, $title, $opts );
+ $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" );
+ } catch ( Exception $exception ) {
+ $input_dump = sprintf( "string(%d) \"%s\"\n", strlen( $input ), $input );
+
+ $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\nInput: $input_dump\n\nError: {$exception->getMessage()}\n\nBacktrace: {$exception->getTraceAsString()}" );
+ }
+
+ $this->teardownGlobals();
+ $parser->__destruct();
+
+ if ( $id % 100 == 0 ) {
+ $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
+ //echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
+ if ( $usage > 90 ) {
+ $ret = "Out of memory:\n";
+ $memStats = $this->getMemoryBreakdown();
+
+ foreach ( $memStats as $name => $usage ) {
+ $ret .= "$name: $usage\n";
+ }
+
+ throw new MWException( $ret );
+ }
+ }
+
+ $id++;
+
+ }
+ }
+
+ //Various getter functions
+
+ /**
+ * Get an input dictionary from a set of parser test files
+ */
+ function getFuzzInput( $filenames ) {
+ $dict = '';
+
+ foreach ( $filenames as $filename ) {
+ $contents = file_get_contents( $filename );
+ preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
+
+ foreach ( $matches[1] as $match ) {
+ $dict .= $match . "\n";
+ }
+ }
+
+ return $dict;
+ }
+
+ /**
+ * Get a memory usage breakdown
+ */
+ function getMemoryBreakdown() {
+ $memStats = array();
+
+ foreach ( $GLOBALS as $name => $value ) {
+ $memStats['$' . $name] = strlen( serialize( $value ) );
+ }
+
+ $classes = get_declared_classes();
+
+ foreach ( $classes as $class ) {
+ $rc = new ReflectionClass( $class );
+ $props = $rc->getStaticProperties();
+ $memStats[$class] = strlen( serialize( $props ) );
+ $methods = $rc->getMethods();
+
+ foreach ( $methods as $method ) {
+ $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
+ }
+ }
+
+ $functions = get_defined_functions();
+
+ foreach ( $functions['user'] as $function ) {
+ $rf = new ReflectionFunction( $function );
+ $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
+ }
+
+ asort( $memStats );
+
+ return $memStats;
+ }
+
+ /**
+ * Get a Parser object
+ */
+ function getParser( $preprocessor = null ) {
+ global $wgParserConf;
+
+ $class = $wgParserConf['class'];
+ $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
+
+ wfRunHooks( 'ParserTestParser', array( &$parser ) );
+
+ return $parser;
+ }
+
+ //Various action functions
+
+ public function addArticle( $name, $text, $line ) {
+ self::$articles[$name] = $text;
+ }
+
+ public function publishTestArticles() {
+ if ( empty( self::$articles ) ) {
+ return;
+ }
+
+ foreach ( self::$articles as $name => $text ) {
+ $title = Title::newFromText( $name );
+
+ if ( $title->getArticleID( Title::GAID_FOR_UPDATE ) == 0 ) {
+ ParserTest::addArticle( $name, $text );
+ }
+ }
+ }
+
+ /**
+ * Steal a callback function from the primary parser, save it for
+ * application to our scary parser. If the hook is not installed,
+ * abort processing of this file.
+ *
+ * @param $name String
+ * @return Bool true if tag hook is present
+ */
+ public function requireHook( $name ) {
+ global $wgParser;
+ $wgParser->firstCallInit( ); // make sure hooks are loaded.
+ return isset( $wgParser->mTagHooks[$name] );
+ }
+
+ public function requireFunctionHook( $name ) {
+ global $wgParser;
+ $wgParser->firstCallInit( ); // make sure hooks are loaded.
+ return isset( $wgParser->mFunctionHooks[$name] );
+ }
+ //Various "cleanup" functions
+
+ /*
+ * Run the "tidy" command on text if the $wgUseTidy
+ * global is true
+ *
+ * @param $text String: the text to tidy
+ * @return String
+ */
+ protected function tidy( $text ) {
+ global $wgUseTidy;
+
+ if ( $wgUseTidy ) {
+ $text = MWTidy::tidy( $text );
+ }
+
+ return $text;
+ }
+
+ /**
+ * Remove last character if it is a newline
+ */
+ public function removeEndingNewline( $s ) {
+ if ( substr( $s, -1 ) === "\n" ) {
+ return substr( $s, 0, -1 );
+ }
+ else {
+ return $s;
+ }
+ }
+
+ public function showRunFile( $file ) {
+ /* NOP */
+ }
+
+ //Test options parser functions
+
+ protected function parseOptions( $instring ) {
+ $opts = array();
+ // foo
+ // foo=bar
+ // foo="bar baz"
+ // foo=[[bar baz]]
+ // foo=bar,"baz quux"
+ $regex = '/\b
+ ([\w-]+) # Key
+ \b
+ (?:\s*
+ = # First sub-value
+ \s*
+ (
+ "
+ [^"]* # Quoted val
+ "
+ |
+ \[\[
+ [^]]* # Link target
+ \]\]
+ |
+ [\w-]+ # Plain word
+ )
+ (?:\s*
+ , # Sub-vals 1..N
+ \s*
+ (
+ "[^"]*" # Quoted val
+ |
+ \[\[[^]]*\]\] # Link target
+ |
+ [\w-]+ # Plain word
+ )
+ )*
+ )?
+ /x';
+
+ if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
+ foreach ( $matches as $bits ) {
+ array_shift( $bits );
+ $key = strtolower( array_shift( $bits ) );
+ if ( count( $bits ) == 0 ) {
+ $opts[$key] = true;
+ } elseif ( count( $bits ) == 1 ) {
+ $opts[$key] = $this->cleanupOption( array_shift( $bits ) );
+ } else {
+ // Array!
+ $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
+ }
+ }
+ }
+ return $opts;
+ }
+
+ protected function cleanupOption( $opt ) {
+ if ( substr( $opt, 0, 1 ) == '"' ) {
+ return substr( $opt, 1, -1 );
+ }
+
+ if ( substr( $opt, 0, 2 ) == '[[' ) {
+ return substr( $opt, 2, -2 );
+ }
+ return $opt;
+ }
+
+ /**
+ * Use a regex to find out the value of an option
+ * @param $key String: name of option val to retrieve
+ * @param $opts Options array to look in
+ * @param $default Mixed: default value returned if not found
+ */
+ protected static function getOptionValue( $key, $opts, $default ) {
+ $key = strtolower( $key );
+
+ if ( isset( $opts[$key] ) ) {
+ return $opts[$key];
+ } else {
+ return $default;
+ }
+ }
+}
diff --git a/tests/phpunit/includes/parser/ParserHelpers.php b/tests/phpunit/includes/parser/ParserHelpers.php
new file mode 100644
index 00000000..4a6ce7c4
--- /dev/null
+++ b/tests/phpunit/includes/parser/ParserHelpers.php
@@ -0,0 +1,136 @@
+<?php
+
+class PHPUnitParserTest extends ParserTest {
+ function showTesting( $desc ) {
+ /* Do nothing since we don't want to show info during PHPUnit testing. */
+ }
+
+ public function showSuccess( $desc ) {
+ PHPUnit_Framework_Assert::assertTrue( true, $desc );
+ return true;
+ }
+
+ public function showFailure( $desc, $expected, $got ) {
+ PHPUnit_Framework_Assert::assertEquals( $expected, $got, $desc );
+ return false;
+ }
+
+ public function setupRecorder( $options ) {
+ $this->recorder = new PHPUnitTestRecorder( $this );
+ }
+}
+
+class ParserUnitTest extends MediaWikiTestCase {
+ private $test = "";
+
+ public function __construct( $suite, $test = null ) {
+ parent::__construct();
+ $this->test = $test;
+ $this->suite = $suite;
+ }
+
+ function count() { return 1; }
+
+ public function run( PHPUnit_Framework_TestResult $result = null ) {
+ PHPUnit_Framework_Assert::resetCount();
+ if ( $result === NULL ) {
+ $result = new PHPUnit_Framework_TestResult;
+ }
+
+ $this->suite->publishTestArticles(); // Add articles needed by the tests.
+ $backend = new ParserTestSuiteBackend;
+ $result->startTest( $this );
+
+ // Support the transition to PHPUnit 3.5 where PHPUnit_Util_Timer is replaced with PHP_Timer
+ if ( class_exists( 'PHP_Timer' ) ) {
+ PHP_Timer::start();
+ } else {
+ PHPUnit_Util_Timer::start();
+ }
+
+ $r = false;
+ try {
+ # Run the test.
+ # On failure, the subclassed backend will throw an exception with
+ # the details.
+ $pt = new PHPUnitParserTest;
+ $r = $pt->runTest( $this->test['test'], $this->test['input'],
+ $this->test['result'], $this->test['options'], $this->test['config']
+ );
+ }
+ catch ( PHPUnit_Framework_AssertionFailedError $e ) {
+
+ // PHPUnit_Util_Timer -> PHP_Timer support (see above)
+ if ( class_exists( 'PHP_Timer' ) ) {
+ $result->addFailure( $this, $e, PHP_Timer::stop() );
+ } else {
+ $result->addFailure( $this, $e, PHPUnit_Util_Timer::stop() );
+ }
+ }
+ catch ( Exception $e ) {
+ // PHPUnit_Util_Timer -> PHP_Timer support (see above)
+ if ( class_exists( 'PHP_Timer' ) ) {
+ $result->addFailure( $this, $e, PHP_Timer::stop() );
+ } else {
+ $result->addFailure( $this, $e, PHPUnit_Util_Timer::stop() );
+ }
+ }
+
+ // PHPUnit_Util_Timer -> PHP_Timer support (see above)
+ if ( class_exists( 'PHP_Timer' ) ) {
+ $result->endTest( $this, PHP_Timer::stop() );
+ } else {
+ $result->endTest( $this, PHPUnit_Util_Timer::stop() );
+ }
+
+ $backend->recorder->record( $this->test['test'], $r );
+ $this->addToAssertionCount( PHPUnit_Framework_Assert::getCount() );
+
+ return $result;
+ }
+
+ public function toString() {
+ return $this->test['test'];
+ }
+
+}
+
+class ParserTestSuiteBackend extends PHPUnit_FrameWork_TestSuite {
+ public $recorder;
+ public $term;
+ static $usePHPUnit = false;
+
+ function __construct() {
+ parent::__construct();
+ $this->setupRecorder(null);
+ self::$usePHPUnit = method_exists('PHPUnit_Framework_Assert', 'assertEquals');
+ }
+
+ function showTesting( $desc ) {
+ }
+
+ function showRunFile( $path ) {
+ }
+
+ function showTestResult( $desc, $result, $out ) {
+ if ( $result === $out ) {
+ return self::showSuccess( $desc, $result, $out );
+ } else {
+ return self::showFailure( $desc, $result, $out );
+ }
+ }
+
+ public function setupRecorder( $options ) {
+ $this->recorder = new PHPUnitTestRecorder( $this );
+ }
+}
+
+class PHPUnitTestRecorder extends TestRecorder {
+ function record( $test, $result ) {
+ $this->total++;
+ $this->success += $result;
+
+ }
+
+ function reportPercentage( $success, $total ) { }
+}
diff --git a/tests/phpunit/includes/parser/PreprocessorTest.php b/tests/phpunit/includes/parser/PreprocessorTest.php
new file mode 100644
index 00000000..7a5948d4
--- /dev/null
+++ b/tests/phpunit/includes/parser/PreprocessorTest.php
@@ -0,0 +1,195 @@
+<?php
+
+class PreprocessorTest extends MediaWikiTestCase {
+ var $mTitle = 'Page title';
+ var $mPPNodeCount = 0;
+ var $mOptions;
+
+ function setUp() {
+ global $wgParserConf;
+ $this->mOptions = new ParserOptions();
+ $name = isset( $wgParserConf['preprocessorClass'] ) ? $wgParserConf['preprocessorClass'] : 'Preprocessor_DOM';
+
+ $this->mPreprocessor = new $name( $this );
+ }
+
+ function getStripList() {
+ return array( 'gallery', 'display map' /* Used by Maps, see r80025 CR */, '/foo' );
+ }
+
+ function provideCases() {
+ return array(
+ array( "Foo", "<root>Foo</root>" ),
+ array( "<!-- Foo -->", "<root><comment>&lt;!-- Foo --&gt;</comment></root>" ),
+ array( "<!-- Foo --><!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment><comment>&lt;!-- Bar --&gt;</comment></root>" ),
+ array( "<!-- Foo --> <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment> <comment>&lt;!-- Bar --&gt;</comment></root>" ),
+ array( "<!-- Foo --> \n <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment> \n <comment>&lt;!-- Bar --&gt;</comment></root>" ),
+ array( "<!-- Foo --> \n <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment> \n<comment> &lt;!-- Bar --&gt;\n</comment></root>" ),
+ array( "<!-- Foo --> <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment> <comment>&lt;!-- Bar --&gt;</comment>\n</root>" ),
+ array( "<!-->Bar", "<root><comment>&lt;!--&gt;Bar</comment></root>" ),
+ array( "<!-- Comment -- comment", "<root><comment>&lt;!-- Comment -- comment</comment></root>" ),
+ array( "== Foo ==\n <!-- Bar -->\n== Baz ==\n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<comment> &lt;!-- Bar --&gt;\n</comment><h level=\"2\" i=\"2\">== Baz ==</h>\n</root>" ),
+ array( "<gallery/>", "<root><ext><name>gallery</name><attr></attr></ext></root>" ),
+ array( "Foo <gallery/> Bar", "<root>Foo <ext><name>gallery</name><attr></attr></ext> Bar</root>" ),
+ array( "<gallery></gallery>", "<root><ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "<foo> <gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "<foo> <gallery><gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner>&lt;gallery&gt;</inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "<noinclude> Foo bar </noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore> Foo bar <ignore>&lt;/noinclude&gt;</ignore></root>" ),
+ array( "<noinclude>\n{{Foo}}\n</noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore></root>" ),
+ array( "<noinclude>\n{{Foo}}\n</noinclude>\n", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore>\n</root>" ),
+ array( "<gallery>foo bar", "<root><ext><name>gallery</name><attr></attr><inner>foo bar</inner></ext></root>" ),
+ array( "<{{foo}}>", "<root>&lt;<template><title>foo</title></template>&gt;</root>" ),
+ array( "<{{{foo}}}>", "<root>&lt;<tplarg><title>foo</title></tplarg>&gt;</root>" ),
+ array( "<gallery></gallery</gallery>", "<root><ext><name>gallery</name><attr></attr><inner>&lt;/gallery</inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "=== Foo === ", "<root><h level=\"3\" i=\"1\">=== Foo === </h></root>" ),
+ array( "==<!-- -->= Foo === ", "<root><h level=\"2\" i=\"1\">==<comment>&lt;!-- --&gt;</comment>= Foo === </h></root>" ),
+ array( "=== Foo ==<!-- -->= ", "<root><h level=\"1\" i=\"1\">=== Foo ==<comment>&lt;!-- --&gt;</comment>= </h></root>" ),
+ array( "=== Foo ===<!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment></h>\n</root>" ),
+ array( "=== Foo ===<!-- --> <!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment> <comment>&lt;!-- --&gt;</comment></h>\n</root>" ),
+ array( "== Foo ==\n== Bar == \n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<h level=\"2\" i=\"2\">== Bar == </h>\n</root>" ),
+ array( "===========", "<root><h level=\"5\" i=\"1\">===========</h></root>" ),
+ array( "Foo\n=\n==\n=\n", "<root>Foo\n=\n==\n=\n</root>" ),
+ array( "{{Foo}}", "<root><template lineStart=\"1\"><title>Foo</title></template></root>" ),
+ array( "\n{{Foo}}", "<root>\n<template lineStart=\"1\"><title>Foo</title></template></root>" ),
+ array( "{{Foo|bar}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template></root>" ),
+ array( "{{Foo|bar}}a", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template>a</root>" ),
+ array( "{{Foo|bar|baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>1</name>=<value>bar</value></part></template></root>" ),
+ array( "{{Foo|=bar}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name></name>=<value>bar</value></part></template></root>" ),
+ array( "{{Foo|bar=baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>bar</name>=<value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar|baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name index=\"1\" /><value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar|2=baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name>2</name>=<value>baz</value></part></template></root>" ),
+ array( "{{Foo|bar|foo=baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name>foo</name>=<value>baz</value></part></template></root>" ),
+ array( "{{{1}}}", "<root><tplarg lineStart=\"1\"><title>1</title></tplarg></root>" ),
+ array( "{{{1|}}}", "<root><tplarg lineStart=\"1\"><title>1</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ),
+ array( "{{{Foo}}}", "<root><tplarg lineStart=\"1\"><title>Foo</title></tplarg></root>" ),
+ array( "{{{Foo|}}}", "<root><tplarg lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ),
+ array( "{{{Foo|bar|baz}}}", "<root><tplarg lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></tplarg></root>" ),
+ array( "{<!-- -->{Foo}}", "<root>{<comment>&lt;!-- --&gt;</comment>{Foo}}</root>" ),
+ array( "{{{{Foobar}}}}", "<root>{<tplarg><title>Foobar</title></tplarg>}</root>" ),
+ array( "{{{ {{Foo}} }}}", "<root><tplarg lineStart=\"1\"><title> <template><title>Foo</title></template> </title></tplarg></root>" ),
+ array( "{{ {{{Foo}}} }}", "<root><template lineStart=\"1\"><title> <tplarg><title>Foo</title></tplarg> </title></template></root>" ),
+ array( "{{{{{Foo}}}}}", "<root><template lineStart=\"1\"><title><tplarg><title>Foo</title></tplarg></title></template></root>" ),
+ array( "{{{{{Foo}} }}}", "<root><tplarg lineStart=\"1\"><title><template><title>Foo</title></template> </title></tplarg></root>" ),
+ array( "{{{{{{Foo}}}}}}", "<root><tplarg lineStart=\"1\"><title><tplarg><title>Foo</title></tplarg></title></tplarg></root>" ),
+ array( "{{{{{{Foo}}}}}", "<root>{<template><title><tplarg><title>Foo</title></tplarg></title></template></root>" ),
+ array( "[[[Foo]]", "<root>[[[Foo]]</root>" ),
+ array( "{{Foo|[[[[bar]]|baz]]}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>[[[[bar]]|baz]]</value></part></template></root>" ), // This test is important, since it means the difference between having the [[ rule stacked or not
+ array( "{{Foo|[[[[bar]|baz]]}}", "<root>{{Foo|[[[[bar]|baz]]}}</root>" ),
+ array( "{{Foo|Foo [[[[bar]|baz]]}}", "<root>{{Foo|Foo [[[[bar]|baz]]}}</root>" ),
+ array( "Foo <display map>Bar</display map >Baz", "<root>Foo <ext><name>display map</name><attr></attr><inner>Bar</inner><close>&lt;/display map &gt;</close></ext>Baz</root>" ),
+ array( "Foo <display map foo>Bar</display map >Baz", "<root>Foo <ext><name>display map</name><attr> foo</attr><inner>Bar</inner><close>&lt;/display map &gt;</close></ext>Baz</root>" ),
+ array( "Foo <gallery bar=\"baz\" />", "<root>Foo <ext><name>gallery</name><attr> bar=&quot;baz&quot; </attr></ext></root>" ),
+ array( "</foo>Foo<//foo>", "<root><ext><name>/foo</name><attr></attr><inner>Foo</inner><close>&lt;//foo&gt;</close></ext></root>" ), # Worth blacklisting IMHO
+ array( "{{#ifexpr: ({{{1|1}}} = 2) | Foo | Bar }}", "<root><template lineStart=\"1\"><title>#ifexpr: (<tplarg><title>1</title><part><name index=\"1\" /><value>1</value></part></tplarg> = 2) </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>"),
+ array( "{{#if: {{{1|}}} | Foo | {{Bar}} }}", "<root><template lineStart=\"1\"><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> <template><title>Bar</title></template> </value></part></template></root>"),
+ array( "{{#if: {{{1|}}} | Foo | [[Bar]] }}", "<root><template lineStart=\"1\"><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> [[Bar]] </value></part></template></root>"),
+ array( "{{#if: {{{1|}}} | [[Foo]] | Bar }}", "<root><template lineStart=\"1\"><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> [[Foo]] </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>"),
+ array( "{{#if: {{{1|}}} | 1 | {{#if: {{{1|}}} | 2 | 3 }} }}", "<root><template lineStart=\"1\"><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 1 </value></part><part><name index=\"2\" /><value> <template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 2 </value></part><part><name index=\"2\" /><value> 3 </value></part></template> </value></part></template></root>"),
+ array( "{{ {{Foo}}", "<root>{{ <template><title>Foo</title></template></root>"),
+ array( "{{Foobar {{Foo}} {{Bar}} {{Baz}} ", "<root>{{Foobar <template><title>Foo</title></template> <template><title>Bar</title></template> <template><title>Baz</title></template> </root>"),
+ array( "[[Foo]] |", "<root>[[Foo]] |</root>"),
+ array( "{{Foo|Bar|", "<root>{{Foo|Bar|</root>"),
+ array( "[[Foo]", "<root>[[Foo]</root>"),
+ array( "[[Foo|Bar]", "<root>[[Foo|Bar]</root>"),
+ array( "{{Foo| [[Bar] }}", "<root>{{Foo| [[Bar] }}</root>"),
+ array( "{{Foo| [[Bar|Baz] }}", "<root>{{Foo| [[Bar|Baz] }}</root>"),
+ array( "{{Foo|bar=[[baz]}}", "<root>{{Foo|bar=[[baz]}}</root>"),
+ array( "{{foo|", "<root>{{foo|</root>"),
+ array( "{{foo|}", "<root>{{foo|}</root>"),
+ array( "{{foo|} }}", "<root><template lineStart=\"1\"><title>foo</title><part><name index=\"1\" /><value>} </value></part></template></root>"),
+ array( "{{foo|bar=|}", "<root>{{foo|bar=|}</root>"),
+ array( "{{Foo|} Bar=", "<root>{{Foo|} Bar=</root>"),
+ array( "{{Foo|} Bar=}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>} Bar</name>=<value></value></part></template></root>"),
+ /* array( file_get_contents( dirname( __FILE__ ) . '/QuoteQuran.txt' ), file_get_contents( dirname( __FILE__ ) . '/QuoteQuranExpanded.txt' ) ), */
+ );
+ }
+
+ /**
+ * @dataProvider provideCases
+ */
+ function testPreprocessorOutput( $wikiText, $expectedXml ) {
+ $this->assertEquals( $expectedXml, $this->mPreprocessor->preprocessToXml( $wikiText ) );
+ }
+
+ /**
+ * These are more complex test cases taken out of wiki articles.
+ */
+ function provideFiles() {
+ return array(
+ array( "QuoteQuran" ), # http://en.wikipedia.org/w/index.php?title=Template:QuoteQuran/sandbox&oldid=237348988 GFDL + CC-BY-SA by Striver
+ array( "Factorial" ), # http://en.wikipedia.org/w/index.php?title=Template:Factorial&oldid=98548758 GFDL + CC-BY-SA by Polonium
+ array( "All_system_messages" ), # http://tl.wiktionary.org/w/index.php?title=Suleras:All_system_messages&oldid=2765 GPL text generated by MediaWiki
+ array( "Fundraising" ), # http://tl.wiktionary.org/w/index.php?title=MediaWiki:Sitenotice&oldid=5716 GFDL + CC-BY-SA, copied there by Sky Harbor.
+ );
+ }
+
+ /**
+ * @dataProvider provideFiles
+ */
+ function testPreprocessorOutputFiles( $filename ) {
+ $folder = dirname( __FILE__ ) . "/../../../parser/preprocess";
+ $wikiText = file_get_contents( "$folder/$filename.txt" );
+ $output = $this->mPreprocessor->preprocessToXml( $wikiText );
+
+ $expectedFilename = "$folder/$filename.expected";
+ if ( file_exists( $expectedFilename ) ) {
+ $this->assertStringEqualsFile( $expectedFilename, $output );
+ } else {
+ $tempFilename = tempnam( $folder, "$filename." );
+ file_put_contents( $tempFilename, $output );
+ $this->markTestIncomplete( "File $expectedFilename missing. Output stored as $tempFilename" );
+ }
+ }
+
+ /**
+ * Tests from Bug 28642 · https://bugzilla.wikimedia.org/28642
+ */
+ function provideHeadings() {
+ return array( /* These should become headings: */
+ array( "== h ==<!--c1-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--><!--c2--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--><!--c2--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--><!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--> <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--><!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--> <!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--><!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--><!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--><!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--><!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+
+ /* These are not working: */
+ array( "== h ==<!--c1--> <!--c2-->", "<root>== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment></root>" ),
+ array( "== h == <!--c1--> <!--c2-->", "<root>== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> </root>" ),
+ array( "== h == x <!--c1--><!--c2--><!--c3--> ", "<root>== h == x <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </root>" ),
+ array( "== h ==<!--c1--> x <!--c2--><!--c3--> ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment> x <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </root>" ),
+ array( "== h ==<!--c1--><!--c2--><!--c3--> x ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> x </root>" ),
+ );
+ }
+
+ /**
+ * @dataProvider provideHeadings
+ */
+ function testHeadings( $wikiText, $expectedXml ) {
+ $this->assertEquals( $expectedXml, $this->mPreprocessor->preprocessToXml( $wikiText ) );
+ }
+}
+
diff --git a/tests/phpunit/includes/parser/TagHooks.php b/tests/phpunit/includes/parser/TagHooks.php
new file mode 100644
index 00000000..713ce846
--- /dev/null
+++ b/tests/phpunit/includes/parser/TagHooks.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @group Parser
+ */
+class TagHookTest extends MediaWikiTestCase {
+
+ public static function provideValidNames() {
+ return array( array( 'foo' ), array( 'foo-bar' ), array( 'foo_bar' ), array( 'FOO-BAR' ), array( 'foo bar' ) );
+ }
+
+ public static function provideBadNames() {
+ return array( array( "foo<bar" ), array( "foo>bar" ), array( "foo\nbar" ), array( "foo\rbar" ) );
+ }
+
+ /**
+ * @dataProvider provideValidNames
+ */
+ function testTagHooks( $tag ) {
+ global $wgParserConf;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setHook( $tag, array( $this, 'tagCallback' ) );
+ $parserOutput = $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), new ParserOptions );
+ $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() );
+
+ $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle
+ }
+
+ /**
+ * @dataProvider provideBadNames
+ * @expectedException MWException
+ */
+ function testBadTagHooks( $tag ) {
+ global $wgParserConf;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setHook( $tag, array( $this, 'tagCallback' ) );
+ $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), new ParserOptions );
+ $this->fail('Exception not thrown.');
+ }
+
+ /**
+ * @dataProvider provideValidNames
+ */
+ function testFunctionTagHooks( $tag ) {
+ global $wgParserConf;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), 0 );
+ $parserOutput = $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), new ParserOptions );
+ $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() );
+
+ $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle
+ }
+
+ /**
+ * @dataProvider provideBadNames
+ * @expectedException MWException
+ */
+ function testBadFunctionTagHooks( $tag ) {
+ global $wgParserConf;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), SFH_OBJECT_ARGS );
+ $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), new ParserOptions );
+ $this->fail('Exception not thrown.');
+ }
+
+ function tagCallback( $text, $params, $parser ) {
+ return str_rot13( $text );
+ }
+
+ function functionTagCallback( &$parser, $frame, $code, $attribs ) {
+ return str_rot13( $code );
+ }
+}
diff --git a/tests/phpunit/includes/search/SearchEngineTest.php b/tests/phpunit/includes/search/SearchEngineTest.php
new file mode 100644
index 00000000..1a0fcd31
--- /dev/null
+++ b/tests/phpunit/includes/search/SearchEngineTest.php
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * This class is not directly tested. Instead it is extended by SearchDbTest.
+ * @group Search
+ * @group Database
+ */
+class SearchEngineTest extends MediaWikiTestCase {
+ protected $search, $pageList;
+
+ function tearDown() {
+ unset( $this->search );
+ }
+
+ /*
+ * Checks for database type & version.
+ * Will skip current test if DB does not support search.
+ */
+ function setUp() {
+ parent::setUp();
+ // Search tests require MySQL or SQLite with FTS
+ # Get database type and version
+ $dbType = $this->db->getType();
+ $dbSupported =
+ ($dbType === 'mysql')
+ || ( $dbType === 'sqlite' && $this->db->getFulltextSearchModule() == 'FTS3' );
+
+ if( !$dbSupported ) {
+ $this->markTestSkipped( "MySQL or SQLite with FTS3 only" );
+ }
+
+ $searchType = $this->db->getSearchEngine();
+ $this->search = new $searchType( $this->db );
+ }
+
+ function pageExists( $title ) {
+ return false;
+ }
+
+ function addDBData() {
+ if ( $this->pageExists( 'Not_Main_Page' ) ) {
+ return;
+ }
+ $this->insertPage( "Not_Main_Page", "This is not a main page", 0 );
+ $this->insertPage( 'Talk:Not_Main_Page', 'This is not a talk page to the main page, see [[smithee]]', 1 );
+ $this->insertPage( 'Smithee', 'A smithee is one who smiths. See also [[Alan Smithee]]', 0 );
+ $this->insertPage( 'Talk:Smithee', 'This article sucks.', 1 );
+ $this->insertPage( 'Unrelated_page', 'Nothing in this page is about the S word.', 0 );
+ $this->insertPage( 'Another_page', 'This page also is unrelated.', 0 );
+ $this->insertPage( 'Help:Help', 'Help me!', 4 );
+ $this->insertPage( 'Thppt', 'Blah blah', 0 );
+ $this->insertPage( 'Alan_Smithee', 'yum', 0 );
+ $this->insertPage( 'Pages', 'are\'food', 0 );
+ $this->insertPage( 'HalfOneUp', 'AZ', 0 );
+ $this->insertPage( 'FullOneUp', 'AZ', 0 );
+ $this->insertPage( 'HalfTwoLow', 'az', 0 );
+ $this->insertPage( 'FullTwoLow', 'az', 0 );
+ $this->insertPage( 'HalfNumbers', '1234567890', 0 );
+ $this->insertPage( 'FullNumbers', '1234567890', 0 );
+ $this->insertPage( 'DomainName', 'example.com', 0 );
+ }
+
+ function fetchIds( $results ) {
+ $this->assertTrue( is_object( $results ) );
+
+ $matches = array();
+ while ( $row = $results->next() ) {
+ $matches[] = $row->getTitle()->getPrefixedText();
+ }
+ $results->free();
+ # Search is not guaranteed to return results in a certain order;
+ # sort them numerically so we will compare simply that we received
+ # the expected matches.
+ sort( $matches );
+ return $matches;
+ }
+
+ /**
+ * Insert a new page
+ *
+ * @param $pageName String: page name
+ * @param $text String: page's content
+ * @param $n Integer: unused
+ */
+ function insertPage( $pageName, $text, $ns ) {
+ $dbw = $this->db;
+ $title = Title::newFromText( $pageName );
+
+ $user = User::newFromName( 'WikiSysop' );
+ $comment = 'Search Test';
+
+ // avoid memory leak...?
+ $linkCache = LinkCache::singleton();
+ $linkCache->clear();
+
+ $article = new Article( $title );
+ $article->doEdit( $text, $comment, 0, false, $user );
+
+ $this->pageList[] = array( $title, $article->getId() );
+
+ return true;
+ }
+
+ function testFullWidth() {
+ $this->assertEquals(
+ array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ),
+ $this->fetchIds( $this->search->searchText( 'AZ' ) ),
+ "Search for normalized from Half-width Upper" );
+ $this->assertEquals(
+ array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ),
+ $this->fetchIds( $this->search->searchText( 'az' ) ),
+ "Search for normalized from Half-width Lower" );
+ $this->assertEquals(
+ array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ),
+ $this->fetchIds( $this->search->searchText( 'AZ' ) ),
+ "Search for normalized from Full-width Upper" );
+ $this->assertEquals(
+ array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ),
+ $this->fetchIds( $this->search->searchText( 'az' ) ),
+ "Search for normalized from Full-width Lower" );
+ }
+
+ function testTextSearch() {
+ $this->assertEquals(
+ array( 'Smithee' ),
+ $this->fetchIds( $this->search->searchText( 'smithee' ) ),
+ "Plain search failed" );
+ }
+
+ function testTextPowerSearch() {
+ $this->search->setNamespaces( array( 0, 1, 4 ) );
+ $this->assertEquals(
+ array(
+ 'Smithee',
+ 'Talk:Not Main Page',
+ ),
+ $this->fetchIds( $this->search->searchText( 'smithee' ) ),
+ "Power search failed" );
+ }
+
+ function testTitleSearch() {
+ $this->assertEquals(
+ array(
+ 'Alan Smithee',
+ 'Smithee',
+ ),
+ $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
+ "Title search failed" );
+ }
+
+ function testTextTitlePowerSearch() {
+ $this->search->setNamespaces( array( 0, 1, 4 ) );
+ $this->assertEquals(
+ array(
+ 'Alan Smithee',
+ 'Smithee',
+ 'Talk:Smithee',
+ ),
+ $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
+ "Title power search failed" );
+ }
+
+}
diff --git a/tests/phpunit/includes/search/SearchUpdateTest.php b/tests/phpunit/includes/search/SearchUpdateTest.php
new file mode 100644
index 00000000..935425a6
--- /dev/null
+++ b/tests/phpunit/includes/search/SearchUpdateTest.php
@@ -0,0 +1,80 @@
+<?php
+
+class MockSearch extends SearchEngine {
+ public static $id;
+ public static $title;
+ public static $text;
+
+ public function __construct( $db ) {
+ }
+
+ public function update( $id, $title, $text ) {
+ self::$id = $id;
+ self::$title = $title;
+ self::$text = $text;
+ }
+}
+
+/**
+ * @group Search
+ */
+class SearchUpdateTest extends MediaWikiTestCase {
+ static $searchType;
+
+ function update( $text, $title = 'Test', $id = 1 ) {
+ $u = new SearchUpdate( $id, $title, $text );
+ $u->doUpdate();
+ return array( MockSearch::$title, MockSearch::$text );
+ }
+
+ function updateText( $text ) {
+ list( , $resultText ) = $this->update( $text );
+ $resultText = trim( $resultText ); // abstract from some implementation details
+ return $resultText;
+ }
+
+ function setUp() {
+ global $wgSearchType;
+
+ self::$searchType = $wgSearchType;
+ $wgSearchType = 'MockSearch';
+ }
+
+ function tearDown() {
+ global $wgSearchType;
+
+ $wgSearchType = self::$searchType;
+ }
+
+ function testUpdateText() {
+ $this->assertEquals(
+ 'test',
+ $this->updateText( '<div>TeSt</div>' ),
+ 'HTML stripped, text lowercased'
+ );
+
+ $this->assertEquals(
+ 'foo bar boz quux',
+ $this->updateText( <<<EOT
+<table style="color:red; font-size:100px">
+ <tr class="scary"><td><div>foo</div></td><tr>bar</td></tr>
+ <tr><td>boz</td><tr>quux</td></tr>
+</table>
+EOT
+ ), 'Stripping HTML tables' );
+
+ $this->assertEquals(
+ 'a b',
+ $this->updateText( 'a > b' ),
+ 'Handle unclosed tags'
+ );
+
+ $text = str_pad( "foo <barbarbar \n", 10000, 'x' );
+
+ $this->assertNotEquals(
+ '',
+ $this->updateText( $text ),
+ 'Bug 18609'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/specials/SpecialRecentchanges.php b/tests/phpunit/includes/specials/SpecialRecentchanges.php
new file mode 100644
index 00000000..a98e7c1a
--- /dev/null
+++ b/tests/phpunit/includes/specials/SpecialRecentchanges.php
@@ -0,0 +1,134 @@
+<?php
+/**
+ * Test class for SpecialRecentchanges class
+ *
+ * Copyright © 2011, Ashar Voultoiz
+ *
+ * @author Ashar Voultoiz
+ */
+class SpecialRecentchangesTest extends MediaWikiTestCase {
+
+ /**
+ * @var SpecialRecentChanges
+ */
+ protected $rc;
+
+ function setUp() {
+ }
+
+ /** helper to test SpecialRecentchanges::buildMainQueryConds() */
+ private function assertConditions( $expected, $requestOptions = null, $message = '' ) {
+ global $wgRequest;
+ $savedGlobal = $wgRequest;
+
+ # Initialize a WebRequest object ...
+ $wgRequest = new FauxRequest( $requestOptions );
+ # ... then setup the rc object (which use wgRequest internally)
+ $this->rc = new SpecialRecentChanges();
+ $formOptions = $this->rc->setup( null );
+
+ # Filter out rc_timestamp conditions which depends on the test runtime
+ # This condition is not needed as of march 2, 2011 -- hashar
+ # @todo FIXME: Find a way to generate the correct rc_timestamp
+ $queryConditions = array_filter(
+ $this->rc->buildMainQueryConds( $formOptions ),
+ 'SpecialRecentchangesTest::filterOutRcTimestampCondition'
+ );
+
+ $this->assertEquals(
+ $expected,
+ $queryConditions,
+ $message
+ );
+
+ $wgRequest = $savedGlobal;
+ }
+
+ /** return false if condition begin with 'rc_timestamp ' */
+ private static function filterOutRcTimestampCondition( $var ) {
+ return (false === strpos( $var, 'rc_timestamp ' ));
+
+ }
+
+ public function testRcNsFilter() {
+ $this->assertConditions(
+ array( # expected
+ 'rc_bot' => 0,
+ #0 => "rc_timestamp >= '20110223000000'",
+ 1 => "rc_namespace = '0'",
+ ),
+ array(
+ 'namespace' => NS_MAIN,
+ ),
+ "rc conditions with no options (aka default setting)"
+ );
+ }
+
+ public function testRcNsFilterInversion() {
+ $this->assertConditions(
+ array( # expected
+ #0 => "rc_timestamp >= '20110223000000'",
+ 'rc_bot' => 0,
+ 1 => sprintf( "rc_namespace != '%s'", NS_MAIN ),
+ ),
+ array(
+ 'namespace' => NS_MAIN,
+ 'invert' => 1,
+ ),
+ "rc conditions with namespace inverted"
+ );
+ }
+
+ /**
+ * @bug 2429
+ * @dataProvider provideNamespacesAssociations
+ */
+ public function testRcNsFilterAssociation( $ns1, $ns2 ) {
+ $this->assertConditions(
+ array( # expected
+ #0 => "rc_timestamp >= '20110223000000'",
+ 'rc_bot' => 0,
+ 1 => sprintf( "(rc_namespace = '%s' OR rc_namespace = '%s')", $ns1, $ns2 ),
+ ),
+ array(
+ 'namespace' => $ns1,
+ 'associated' => 1,
+ ),
+ "rc conditions with namespace inverted"
+ );
+ }
+
+ /**
+ * @bug 2429
+ * @dataProvider provideNamespacesAssociations
+ */
+ public function testRcNsFilterAssociationWithInversion( $ns1, $ns2 ) {
+ $this->assertConditions(
+ array( # expected
+ #0 => "rc_timestamp >= '20110223000000'",
+ 'rc_bot' => 0,
+ 1 => sprintf( "(rc_namespace != '%s' AND rc_namespace != '%s')", $ns1, $ns2 ),
+ ),
+ array(
+ 'namespace' => $ns1,
+ 'associated' => 1,
+ 'invert' => 1,
+ ),
+ "rc conditions with namespace inverted"
+ );
+ }
+
+ /**
+ * Provides associated namespaces to test recent changes
+ * namespaces association filtering.
+ */
+ public function provideNamespacesAssociations() {
+ return array( # (NS => Associated_NS)
+ array( NS_MAIN, NS_TALK),
+ array( NS_TALK, NS_MAIN),
+ );
+ }
+
+}
+
+
diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php
new file mode 100644
index 00000000..4722d408
--- /dev/null
+++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php
@@ -0,0 +1,348 @@
+<?php
+
+/**
+ * @group Broken
+ * @group Upload
+ */
+class UploadFromUrlTest extends ApiTestCase {
+
+ public function setUp() {
+ global $wgEnableUploads, $wgAllowCopyUploads, $wgAllowAsyncCopyUploads;
+ parent::setUp();
+
+ $wgEnableUploads = true;
+ $wgAllowCopyUploads = true;
+ $wgAllowAsyncCopyUploads = true;
+ wfSetupSession();
+
+ if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) {
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+ }
+ }
+
+ protected function doApiRequest( $params, $unused = null, $appendModule = false ) {
+ $sessionId = session_id();
+ session_write_close();
+
+ $req = new FauxRequest( $params, true, $_SESSION );
+ $module = new ApiMain( $req, true );
+ $module->execute();
+
+ wfSetupSession( $sessionId );
+ return array( $module->getResultData(), $req );
+ }
+
+ /**
+ * Ensure that the job queue is empty before continuing
+ */
+ public function testClearQueue() {
+ while ( $job = Job::pop() ) { }
+ $this->assertFalse( $job );
+ }
+
+ /**
+ * @todo Document why we test login, since the $wgUser hack used doesn't
+ * require login
+ */
+ public function testLogin() {
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => $this->user->userName,
+ 'lgpassword' => $this->user->passWord ) );
+ $this->assertArrayHasKey( "login", $data[0] );
+ $this->assertArrayHasKey( "result", $data[0]['login'] );
+ $this->assertEquals( "NeedToken", $data[0]['login']['result'] );
+ $token = $data[0]['login']['token'];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ "lgtoken" => $token,
+ 'lgname' => $this->user->userName,
+ 'lgpassword' => $this->user->passWord ) );
+
+ $this->assertArrayHasKey( "login", $data[0] );
+ $this->assertArrayHasKey( "result", $data[0]['login'] );
+ $this->assertEquals( "Success", $data[0]['login']['result'] );
+ $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] );
+
+ return $data;
+ }
+
+ /**
+ * @depends testLogin
+ * @depends testClearQueue
+ */
+ public function testSetupUrlDownload( $data ) {
+ $token = $this->user->editToken();
+ $exception = false;
+
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ ) );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "The token parameter must be set", $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+
+ $exception = false;
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'token' => $token,
+ ), $data );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required",
+ $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+
+ $exception = false;
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'url' => 'http://www.example.com/test.png',
+ 'token' => $token,
+ ), $data );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "The filename parameter must be set", $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+
+ $this->user->removeGroup( 'sysop' );
+ $exception = false;
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'url' => 'http://www.example.com/test.png',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'token' => $token,
+ ), $data );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "Permission denied", $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+
+ $this->user->addGroup( 'sysop' );
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'asyncdownload' => 1,
+ 'filename' => 'UploadFromUrlTest.png',
+ 'token' => $token,
+ ), $data );
+
+ $this->assertEquals( $data[0]['upload']['result'], 'Queued', 'Queued upload' );
+
+ $job = Job::pop();
+ $this->assertThat( $job, $this->isInstanceOf( 'UploadFromUrlJob' ), 'Queued upload inserted' );
+ }
+
+ /**
+ * @depends testLogin
+ * @depends testClearQueue
+ */
+ public function testAsyncUpload( $data ) {
+ $token = $this->user->editToken();
+
+ $this->user->addGroup( 'users' );
+
+ $data = $this->doAsyncUpload( $token, true );
+ $this->assertEquals( $data[0]['upload']['result'], 'Success' );
+ $this->assertEquals( $data[0]['upload']['filename'], 'UploadFromUrlTest.png' );
+ $this->assertTrue( wfLocalFile( $data[0]['upload']['filename'] )->exists() );
+
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ return $data;
+ }
+
+ /**
+ * @depends testLogin
+ * @depends testClearQueue
+ */
+ public function testAsyncUploadWarning( $data ) {
+ $token = $this->user->editToken();
+
+ $this->user->addGroup( 'users' );
+
+
+ $data = $this->doAsyncUpload( $token );
+
+ $this->assertEquals( $data[0]['upload']['result'], 'Warning' );
+ $this->assertTrue( isset( $data[0]['upload']['sessionkey'] ) );
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'sessionkey' => $data[0]['upload']['sessionkey'],
+ 'filename' => 'UploadFromUrlTest.png',
+ 'ignorewarnings' => 1,
+ 'token' => $token,
+ ) );
+ $this->assertEquals( $data[0]['upload']['result'], 'Success' );
+ $this->assertEquals( $data[0]['upload']['filename'], 'UploadFromUrlTest.png' );
+ $this->assertTrue( wfLocalFile( $data[0]['upload']['filename'] )->exists() );
+
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ return $data;
+ }
+
+ /**
+ * @depends testLogin
+ * @depends testClearQueue
+ */
+ public function testSyncDownload( $data ) {
+ $token = $this->user->editToken();
+
+ $job = Job::pop();
+ $this->assertFalse( $job, 'Starting with an empty jobqueue' );
+
+ $this->user->addGroup( 'users' );
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'ignorewarnings' => true,
+ 'token' => $token,
+ ), $data );
+
+ $job = Job::pop();
+ $this->assertFalse( $job );
+
+ $this->assertEquals( 'Success', $data[0]['upload']['result'] );
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ return $data;
+ }
+
+ public function testLeaveMessage() {
+ $token = $this->user->user->editToken();
+
+ $talk = $this->user->user->getTalkPage();
+ if ( $talk->exists() ) {
+ $a = new Article( $talk );
+ $a->doDeleteArticle( '' );
+ }
+
+ $this->assertFalse( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk does not exist' );
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'asyncdownload' => 1,
+ 'token' => $token,
+ 'leavemessage' => 1,
+ 'ignorewarnings' => 1,
+ ) );
+
+ $job = Job::pop();
+ $this->assertEquals( 'UploadFromUrlJob', get_class( $job ) );
+ $job->run();
+
+ $this->assertTrue( wfLocalFile( 'UploadFromUrlTest.png' )->exists() );
+ $this->assertTrue( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk exists' );
+
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ $talkRev = Revision::newFromTitle( $talk );
+ $talkSize = $talkRev->getSize();
+
+ $exception = false;
+ try {
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'asyncdownload' => 1,
+ 'token' => $token,
+ 'leavemessage' => 1,
+ ) );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( 'Using leavemessage without ignorewarnings is not supported', $e->getMessage() );
+ }
+ $this->assertTrue( $exception );
+
+ $job = Job::pop();
+ $this->assertFalse( $job );
+
+ return;
+
+ /**
+ // Broken until using leavemessage with ignorewarnings is supported
+ $job->run();
+
+ $this->assertFalse( wfLocalFile( 'UploadFromUrlTest.png' )->exists() );
+
+ $talkRev = Revision::newFromTitle( $talk );
+ $this->assertTrue( $talkRev->getSize() > $talkSize, 'New message left' );
+ */
+ }
+
+ /**
+ * Helper function to perform an async upload, execute the job and fetch
+ * the status
+ *
+ * @return array The result of action=upload&statuskey=key
+ */
+ private function doAsyncUpload( $token, $ignoreWarnings = false, $leaveMessage = false ) {
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'asyncdownload' => 1,
+ 'token' => $token,
+ );
+ if ( $ignoreWarnings ) {
+ $params['ignorewarnings'] = 1;
+ }
+ if ( $leaveMessage ) {
+ $params['leavemessage'] = 1;
+ }
+
+ $data = $this->doApiRequest( $params );
+ $this->assertEquals( $data[0]['upload']['result'], 'Queued' );
+ $this->assertTrue( isset( $data[0]['upload']['statuskey'] ) );
+ $statusKey = $data[0]['upload']['statuskey'];
+
+ $job = Job::pop();
+ $this->assertEquals( 'UploadFromUrlJob', get_class( $job ) );
+
+ $status = $job->run();
+ $this->assertTrue( $status );
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'statuskey' => $statusKey,
+ 'token' => $token,
+ ) );
+
+ return $data;
+ }
+
+
+ /**
+ *
+ */
+ protected function deleteFile( $name ) {
+ $t = Title::newFromText( $name, NS_FILE );
+ $this->assertTrue($t->exists(), "File '$name' exists");
+
+ if ( $t->exists() ) {
+ $file = wfFindFile( $name, array( 'ignoreRedirect' => true ) );
+ $empty = "";
+ FileDeleteForm::doDelete( $t, $file, $empty, "none", true );
+ $a = new Article ( $t );
+ $a->doDeleteArticle( "testing" );
+ }
+ $t = Title::newFromText( $name, NS_FILE );
+
+ $this->assertFalse($t->exists(), "File '$name' was deleted");
+ }
+ }
diff --git a/tests/phpunit/includes/upload/UploadStashTest.php b/tests/phpunit/includes/upload/UploadStashTest.php
new file mode 100644
index 00000000..9c39bc61
--- /dev/null
+++ b/tests/phpunit/includes/upload/UploadStashTest.php
@@ -0,0 +1,53 @@
+<?php
+
+class UploadStashTest extends MediaWikiTestCase {
+ /**
+ * @var Array of UploadStashTestUser
+ */
+ public static $users;
+
+ public function setUp() {
+ parent::setUp();
+
+ // Setup a file for bug 29408
+ $this->bug29408File = dirname( __FILE__ ) . '/bug29408';
+ file_put_contents( $this->bug29408File, "\x00" );
+
+ self::$users = array(
+ 'sysop' => new ApiTestUser(
+ 'Uploadstashtestsysop',
+ 'Upload Stash Test Sysop',
+ 'upload_stash_test_sysop@sample.com',
+ array( 'sysop' )
+ ),
+ 'uploader' => new ApiTestUser(
+ 'Uploadstashtestuser',
+ 'Upload Stash Test User',
+ 'upload_stash_test_user@sample.com',
+ array()
+ )
+ );
+ }
+
+ public function testBug29408() {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $stash = new UploadStash( $repo );
+
+ // Throws exception caught by PHPUnit on failure
+ $file = $stash->stashFile( $this->bug29408File );
+ // We'll never reach this point if we hit bug 29408
+ $this->assertTrue( true, 'Unrecognized file without extension' );
+
+ $stash->removeFile( $file->getFileKey() );
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+
+ unlink( $this->bug29408File . "." );
+
+ }
+}
diff --git a/tests/phpunit/includes/upload/UploadTest.php b/tests/phpunit/includes/upload/UploadTest.php
new file mode 100644
index 00000000..69c29032
--- /dev/null
+++ b/tests/phpunit/includes/upload/UploadTest.php
@@ -0,0 +1,142 @@
+<?php
+/**
+ * @group Upload
+ */
+class UploadTest extends MediaWikiTestCase {
+ protected $upload;
+
+
+ function setUp() {
+ global $wgHooks;
+ parent::setUp();
+
+ $this->upload = new UploadTestHandler;
+ $this->hooks = $wgHooks;
+ $wgHooks['InterwikiLoadPrefix'][] = 'MediaWikiTestCase::disableInterwikis';
+ }
+
+ function tearDown() {
+ global $wgHooks;
+ $wgHooks = $this->hooks;
+ }
+
+ /**
+ * Test various forms of valid and invalid titles that can be supplied.
+ */
+ public function testTitleValidation() {
+
+
+ /* Test a valid title */
+ $this->assertUploadTitleAndCode( 'ValidTitle.jpg',
+ 'ValidTitle.jpg', UploadBase::OK,
+ 'upload valid title' );
+
+ /* A title with a slash */
+ $this->assertUploadTitleAndCode( 'A/B.jpg',
+ 'B.jpg', UploadBase::OK,
+ 'upload title with slash' );
+
+ /* A title with illegal char */
+ $this->assertUploadTitleAndCode( 'A:B.jpg',
+ 'A-B.jpg', UploadBase::OK,
+ 'upload title with colon' );
+
+ /* Stripping leading File: prefix */
+ $this->assertUploadTitleAndCode( 'File:C.jpg',
+ 'C.jpg', UploadBase::OK,
+ 'upload title with File prefix' );
+
+ /* Test illegal suggested title (r94601) */
+ $this->assertUploadTitleAndCode( '%281%29.JPG',
+ null, UploadBase::ILLEGAL_FILENAME,
+ 'illegal title for upload' );
+
+ /* A title without extension */
+ $this->assertUploadTitleAndCode( 'A',
+ null, UploadBase::FILETYPE_MISSING,
+ 'upload title without extension' );
+
+ /* A title with no basename */
+ $this->assertUploadTitleAndCode( '.jpg',
+ null, UploadBase::MIN_LENGTH_PARTNAME,
+ 'upload title without basename' );
+
+ }
+ /**
+ * Helper function for testTitleValidation. First checks the return code
+ * of UploadBase::getTitle() and then the actual returned titl
+ */
+ private function assertUploadTitleAndCode( $srcFilename, $dstFilename, $code, $msg ) {
+ /* Check the result code */
+ $this->assertEquals( $code,
+ $this->upload->testTitleValidation( $srcFilename ),
+ "$msg code" );
+
+ /* If we expect a valid title, check the title itself. */
+ if ( $code == UploadBase::OK ) {
+ $this->assertEquals( $dstFilename,
+ $this->upload->getTitle()->getText(),
+ "$msg text" );
+ }
+ }
+
+ /**
+ * Test the upload verification functions
+ */
+ public function testVerifyUpload() {
+ /* Setup with zero file size */
+ $this->upload->initializePathInfo( '', '', 0 );
+ $result = $this->upload->verifyUpload();
+ $this->assertEquals( UploadBase::EMPTY_FILE,
+ $result['status'],
+ 'upload empty file' );
+ }
+
+ // Helper used to create an empty file of size $size.
+ private function createFileOfSize( $size ) {
+ $filename = tempnam( wfTempDir(), "mwuploadtest" );
+
+ $fh = fopen( $filename, 'w' );
+ ftruncate( $fh, $size );
+ fclose( $fh );
+
+ return $filename;
+ }
+
+ /**
+ * test uploading a 100 bytes file with wgMaxUploadSize = 100
+ *
+ * This method should be abstracted so we can test different settings.
+ */
+
+ public function testMaxUploadSize() {
+ global $wgMaxUploadSize;
+ $savedGlobal = $wgMaxUploadSize; // save global
+ global $wgFileExtensions;
+ $wgFileExtensions[] = 'txt';
+
+ $wgMaxUploadSize = 100;
+
+ $filename = $this->createFileOfSize( $wgMaxUploadSize );
+ $this->upload->initializePathInfo( basename($filename) . '.txt', $filename, 100 );
+ $result = $this->upload->verifyUpload();
+ unlink( $filename );
+
+ $this->assertEquals(
+ array( 'status' => UploadBase::OK ), $result );
+
+ $wgMaxUploadSize = $savedGlobal; // restore global
+ }
+}
+
+class UploadTestHandler extends UploadBase {
+ public function initializeFromRequest( &$request ) { }
+ public function testTitleValidation( $name ) {
+ $this->mTitle = false;
+ $this->mDesiredDestName = $name;
+ $this->getTitle();
+ return $this->mTitleError;
+ }
+
+
+}