summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gruntfile.js119
-rw-r--r--RELEASE-NOTES-1.2423
-rw-r--r--docs/kss/package.json13
-rw-r--r--extensions/ConfirmEdit/Asirra.class.php55
-rw-r--r--extensions/ConfirmEdit/Asirra.php43
-rw-r--r--extensions/ConfirmEdit/README4
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/ast.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/be-tarask.json19
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/br.json13
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/ca.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/cs.json8
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/de-formal.json13
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/de.json17
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/diq.json8
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/en.json14
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/es.json18
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/fa.json18
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/fi.json17
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/fr.json18
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/gl.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/he.json17
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/hsb.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/ia.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/it.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/ja.json17
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/ko.json17
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/ksh.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/lb.json13
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/mk.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/ms.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/mt.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/nb.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/nl-informal.json8
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/nl.json18
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/oc.json9
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/pl.json17
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/pms.json17
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/pt.json17
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/qqq.json19
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/roa-tara.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/ru.json18
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/si.json9
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/sv.json20
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/tl.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/uk.json17
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/wa.json16
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/zh-hans.json18
-rw-r--r--extensions/ConfirmEdit/i18n/asirra/zh-hant.json17
-rw-r--r--extensions/ConfirmEdit/resources/ext.confirmEdit.asirra.js54
-rw-r--r--extensions/Gadgets/tests/GadgetTest.php63
-rw-r--r--extensions/LocalisationUpdate/tests/phpunit/Makefile12
-rw-r--r--extensions/LocalisationUpdate/tests/phpunit/UpdaterTest.php80
-rw-r--r--extensions/LocalisationUpdate/tests/phpunit/finder/FinderTest.php70
-rw-r--r--extensions/LocalisationUpdate/tests/phpunit/reader/JSONReaderTest.php37
-rw-r--r--extensions/LocalisationUpdate/tests/phpunit/reader/ReaderFactoryTest.php38
-rw-r--r--extensions/ParserFunctions/tests/ExpressionTest.php76
-rw-r--r--extensions/PdfHandler/COPYING339
-rw-r--r--extensions/PdfHandler/CreatePdfThumbnailsJob.class.php126
-rw-r--r--extensions/PdfHandler/PdfHandler.i18n.php (renamed from extensions/ConfirmEdit/Asirra.i18n.php)8
-rw-r--r--extensions/PdfHandler/PdfHandler.image.php309
-rw-r--r--extensions/PdfHandler/PdfHandler.php66
-rw-r--r--extensions/PdfHandler/PdfHandler_body.php386
-rw-r--r--extensions/PdfHandler/i18n/af.json11
-rw-r--r--extensions/PdfHandler/i18n/aln.json10
-rw-r--r--extensions/PdfHandler/i18n/an.json10
-rw-r--r--extensions/PdfHandler/i18n/ar.json16
-rw-r--r--extensions/PdfHandler/i18n/arz.json10
-rw-r--r--extensions/PdfHandler/i18n/as.json13
-rw-r--r--extensions/PdfHandler/i18n/ast.json14
-rw-r--r--extensions/PdfHandler/i18n/azb.json8
-rw-r--r--extensions/PdfHandler/i18n/ba.json10
-rw-r--r--extensions/PdfHandler/i18n/bcl.json14
-rw-r--r--extensions/PdfHandler/i18n/be-tarask.json16
-rw-r--r--extensions/PdfHandler/i18n/bg.json13
-rw-r--r--extensions/PdfHandler/i18n/bn.json11
-rw-r--r--extensions/PdfHandler/i18n/br.json15
-rw-r--r--extensions/PdfHandler/i18n/bs.json10
-rw-r--r--extensions/PdfHandler/i18n/ca.json10
-rw-r--r--extensions/PdfHandler/i18n/ce.json12
-rw-r--r--extensions/PdfHandler/i18n/ckb.json8
-rw-r--r--extensions/PdfHandler/i18n/cs.json15
-rw-r--r--extensions/PdfHandler/i18n/cy.json14
-rw-r--r--extensions/PdfHandler/i18n/da.json14
-rw-r--r--extensions/PdfHandler/i18n/de-ch.json8
-rw-r--r--extensions/PdfHandler/i18n/de.json16
-rw-r--r--extensions/PdfHandler/i18n/diq.json16
-rw-r--r--extensions/PdfHandler/i18n/dsb.json14
-rw-r--r--extensions/PdfHandler/i18n/el.json10
-rw-r--r--extensions/PdfHandler/i18n/en-gb.json8
-rw-r--r--extensions/PdfHandler/i18n/en.json12
-rw-r--r--extensions/PdfHandler/i18n/eo.json13
-rw-r--r--extensions/PdfHandler/i18n/es.json15
-rw-r--r--extensions/PdfHandler/i18n/et.json15
-rw-r--r--extensions/PdfHandler/i18n/fa.json18
-rw-r--r--extensions/PdfHandler/i18n/fi.json18
-rw-r--r--extensions/PdfHandler/i18n/fr.json17
-rw-r--r--extensions/PdfHandler/i18n/frp.json10
-rw-r--r--extensions/PdfHandler/i18n/gl.json15
-rw-r--r--extensions/PdfHandler/i18n/grc.json9
-rw-r--r--extensions/PdfHandler/i18n/gsw.json14
-rw-r--r--extensions/PdfHandler/i18n/gu.json11
-rw-r--r--extensions/PdfHandler/i18n/he.json16
-rw-r--r--extensions/PdfHandler/i18n/hi.json10
-rw-r--r--extensions/PdfHandler/i18n/hr.json10
-rw-r--r--extensions/PdfHandler/i18n/hsb.json14
-rw-r--r--extensions/PdfHandler/i18n/hu.json15
-rw-r--r--extensions/PdfHandler/i18n/ia.json14
-rw-r--r--extensions/PdfHandler/i18n/id.json10
-rw-r--r--extensions/PdfHandler/i18n/ilo.json14
-rw-r--r--extensions/PdfHandler/i18n/it.json15
-rw-r--r--extensions/PdfHandler/i18n/ja.json15
-rw-r--r--extensions/PdfHandler/i18n/jv.json11
-rw-r--r--extensions/PdfHandler/i18n/ka.json15
-rw-r--r--extensions/PdfHandler/i18n/km.json13
-rw-r--r--extensions/PdfHandler/i18n/kn.json8
-rw-r--r--extensions/PdfHandler/i18n/ko.json15
-rw-r--r--extensions/PdfHandler/i18n/ksh.json14
-rw-r--r--extensions/PdfHandler/i18n/ky.json9
-rw-r--r--extensions/PdfHandler/i18n/lb.json14
-rw-r--r--extensions/PdfHandler/i18n/li.json10
-rw-r--r--extensions/PdfHandler/i18n/lrc.json8
-rw-r--r--extensions/PdfHandler/i18n/lt.json10
-rw-r--r--extensions/PdfHandler/i18n/mk.json15
-rw-r--r--extensions/PdfHandler/i18n/ml.json15
-rw-r--r--extensions/PdfHandler/i18n/mr.json12
-rw-r--r--extensions/PdfHandler/i18n/ms.json14
-rw-r--r--extensions/PdfHandler/i18n/mt.json8
-rw-r--r--extensions/PdfHandler/i18n/nb.json14
-rw-r--r--extensions/PdfHandler/i18n/nl.json15
-rw-r--r--extensions/PdfHandler/i18n/nn.json11
-rw-r--r--extensions/PdfHandler/i18n/oc.json14
-rw-r--r--extensions/PdfHandler/i18n/or.json15
-rw-r--r--extensions/PdfHandler/i18n/pdc.json8
-rw-r--r--extensions/PdfHandler/i18n/pl.json16
-rw-r--r--extensions/PdfHandler/i18n/pms.json15
-rw-r--r--extensions/PdfHandler/i18n/pnb.json10
-rw-r--r--extensions/PdfHandler/i18n/pt-br.json15
-rw-r--r--extensions/PdfHandler/i18n/pt.json16
-rw-r--r--extensions/PdfHandler/i18n/qqq.json16
-rw-r--r--extensions/PdfHandler/i18n/ro.json11
-rw-r--r--extensions/PdfHandler/i18n/roa-tara.json14
-rw-r--r--extensions/PdfHandler/i18n/ru.json15
-rw-r--r--extensions/PdfHandler/i18n/rue.json10
-rw-r--r--extensions/PdfHandler/i18n/sa.json10
-rw-r--r--extensions/PdfHandler/i18n/sah.json10
-rw-r--r--extensions/PdfHandler/i18n/si.json15
-rw-r--r--extensions/PdfHandler/i18n/sk.json10
-rw-r--r--extensions/PdfHandler/i18n/sl.json14
-rw-r--r--extensions/PdfHandler/i18n/sq.json10
-rw-r--r--extensions/PdfHandler/i18n/sr-ec.json11
-rw-r--r--extensions/PdfHandler/i18n/sr-el.json10
-rw-r--r--extensions/PdfHandler/i18n/stq.json10
-rw-r--r--extensions/PdfHandler/i18n/sv.json15
-rw-r--r--extensions/PdfHandler/i18n/ta.json14
-rw-r--r--extensions/PdfHandler/i18n/te.json8
-rw-r--r--extensions/PdfHandler/i18n/tk.json10
-rw-r--r--extensions/PdfHandler/i18n/tl.json10
-rw-r--r--extensions/PdfHandler/i18n/tr.json10
-rw-r--r--extensions/PdfHandler/i18n/ug-arab.json9
-rw-r--r--extensions/PdfHandler/i18n/uk.json15
-rw-r--r--extensions/PdfHandler/i18n/ur.json8
-rw-r--r--extensions/PdfHandler/i18n/vec.json15
-rw-r--r--extensions/PdfHandler/i18n/vi.json15
-rw-r--r--extensions/PdfHandler/i18n/yo.json8
-rw-r--r--extensions/PdfHandler/i18n/yue.json6
-rw-r--r--extensions/PdfHandler/i18n/zh-hans.json15
-rw-r--r--extensions/PdfHandler/i18n/zh-hant.json17
-rw-r--r--extensions/PdfHandler/tests/browser/Gemfile.lock62
-rw-r--r--extensions/PdfHandler/tests/browser/features/pdf.feature22
-rw-r--r--extensions/PdfHandler/tests/browser/features/step_definitions/pdf_steps.rb20
-rw-r--r--extensions/PdfHandler/tests/browser/features/support/env.rb12
-rw-r--r--extensions/PdfHandler/tests/browser/features/support/pages/random_page.rb17
-rw-r--r--extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php12
-rw-r--r--extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.php6
-rw-r--r--extensions/TitleBlacklist/tests/ApiQueryTitleBlacklistTest.php132
-rw-r--r--extensions/TitleBlacklist/tests/testSource5
-rw-r--r--extensions/WikiEditor/tests/selenium/WikiDialogs_Links.php67
-rw-r--r--extensions/WikiEditor/tests/selenium/WikiDialogs_Links_Setup.php295
-rw-r--r--extensions/WikiEditor/tests/selenium/WikiEditorConstants.php84
-rw-r--r--extensions/WikiEditor/tests/selenium/WikiEditorSeleniumConfig.php24
-rw-r--r--extensions/WikiEditor/tests/selenium/WikiEditorTestSuite.php32
-rw-r--r--includes/DefaultSettings.php14
-rw-r--r--includes/EditPage.php26
-rw-r--r--includes/Html.php7
-rw-r--r--includes/OutputPage.php8
-rw-r--r--includes/User.php40
-rw-r--r--includes/Xml.php4
-rw-r--r--includes/api/ApiFormatWddx.php48
-rw-r--r--includes/installer/PostgresUpdater.php24
-rw-r--r--includes/libs/XmlTypeCheck.php251
-rw-r--r--includes/media/BitmapMetadataHandler.php6
-rw-r--r--includes/media/JpegMetadataExtractor.php2
-rw-r--r--includes/media/XMP.php96
-rw-r--r--includes/specialpage/SpecialPageFactory.php4
-rw-r--r--includes/specials/SpecialActiveusers.php8
-rw-r--r--includes/specials/SpecialJavaScriptTest.php248
-rw-r--r--includes/specials/SpecialUserlogin.php13
-rw-r--r--includes/upload/UploadBase.php33
-rw-r--r--jsduck.json40
-rw-r--r--languages/i18n/en.json5
-rw-r--r--languages/i18n/qqq.json5
-rw-r--r--maintenance/jsduck/config.json40
-rw-r--r--maintenance/mwjsduck-gen25
-rw-r--r--maintenance/postgres/tables.sql2
-rw-r--r--resources/Resources.php4
-rw-r--r--resources/lib/jquery/jquery.js188
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js5
-rw-r--r--skins/ArchLinux/FF2Fixes.css4
-rw-r--r--skins/ArchLinux/arch.css199
-rw-r--r--skins/CologneBlue/SkinCologneBlue.php3
-rw-r--r--skins/MonoBook/MonoBookTemplate.php3
-rw-r--r--skins/Vector/SkinVector.php4
-rw-r--r--skins/Vector/Vector.php8
-rw-r--r--skins/Vector/VectorTemplate.php3
-rw-r--r--skins/Vector/skinStyles/jquery.ui/PATCHES25
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-anim_basic_16x16.gifbin1553 -> 0 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_100_000000_40x100.pngbin0 -> 205 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_15_cd0a0a_40x100.pngbin87 -> 206 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_70_000000_40x100.pngbin87 -> 205 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_100_f2f5f7_1x100.pngbin97 -> 332 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_80_d7ebf9_1x100.pngbin104 -> 331 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_e4f1fb_1x100.pngbin106 -> 362 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_ffffff_1x100.pngbin80 -> 203 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_25_ffef8f_1x100.pngbin152 -> 309 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-bg_inset-hard_100_f0f0f0_1x100.pngbin89 -> 253 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-icons_2694e8_256x240.pngbin3702 -> 4549 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-icons_3d80b3_256x240.pngbin3702 -> 4549 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-icons_666666_256x240.pngbin3702 -> 6988 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-icons_72a7cf_256x240.pngbin3702 -> 4549 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/images/ui-icons_ffffff_256x240.pngbin3702 -> 6299 bytes
-rw-r--r--skins/Vector/skinStyles/jquery.ui/jquery.ui.autocomplete.css53
-rw-r--r--skins/Vector/skinStyles/jquery.ui/jquery.ui.button.css78
-rw-r--r--skins/Vector/skinStyles/jquery.ui/jquery.ui.datepicker.css20
-rw-r--r--skins/Vector/skinStyles/jquery.ui/jquery.ui.dialog.css27
-rw-r--r--skins/Vector/skinStyles/jquery.ui/jquery.ui.menu.css30
-rw-r--r--skins/Vector/skinStyles/jquery.ui/jquery.ui.resizable.css14
-rw-r--r--skins/Vector/skinStyles/jquery.ui/jquery.ui.spinner.css23
-rw-r--r--skins/Vector/skinStyles/jquery.ui/jquery.ui.theme.css73
-rw-r--r--skins/Vector/skinStyles/jquery.ui/jquery.ui.tooltip.css21
-rw-r--r--tests/.htaccess1
-rw-r--r--tests/TestsAutoLoader.php115
-rw-r--r--tests/browser/Gemfile.lock82
-rw-r--r--tests/browser/README.mediawiki64
-rw-r--r--tests/browser/environment_variables5
-rw-r--r--tests/browser/features/create_account.feature12
-rw-r--r--tests/browser/features/create_and_follow_wiki_link.feature9
-rw-r--r--tests/browser/features/edit_page.feature11
-rw-r--r--tests/browser/features/file.feature23
-rw-r--r--tests/browser/features/login.feature42
-rw-r--r--tests/browser/features/main_page_links.feature19
-rw-r--r--tests/browser/features/preferences.feature60
-rw-r--r--tests/browser/features/step_definitions/create_account_steps.rb18
-rw-r--r--tests/browser/features/step_definitions/create_and_follow_wiki_link_steps.rb28
-rw-r--r--tests/browser/features/step_definitions/edit_page_steps.rb24
-rw-r--r--tests/browser/features/step_definitions/file_steps.rb18
-rw-r--r--tests/browser/features/step_definitions/login_steps.rb65
-rw-r--r--tests/browser/features/step_definitions/main_page_links_steps.rb47
-rw-r--r--tests/browser/features/step_definitions/preferences_appearance_steps.rb85
-rw-r--r--tests/browser/features/step_definitions/preferences_editing_steps.rb54
-rw-r--r--tests/browser/features/step_definitions/preferences_user_profile_steps.rb43
-rw-r--r--tests/browser/features/step_definitions/view_history_steps.rb8
-rw-r--r--tests/browser/features/support/env.rb2
-rw-r--r--tests/browser/features/support/hooks.rb2
-rw-r--r--tests/browser/features/support/modules/url_module.rb10
-rw-r--r--tests/browser/features/support/pages/create_account_page.rb19
-rw-r--r--tests/browser/features/support/pages/edit_page.rb8
-rw-r--r--tests/browser/features/support/pages/file_does_not_exist_page.rb19
-rw-r--r--tests/browser/features/support/pages/login_error_page.rb5
-rw-r--r--tests/browser/features/support/pages/main_page.rb19
-rw-r--r--tests/browser/features/support/pages/preferences_appearance_page.rb41
-rw-r--r--tests/browser/features/support/pages/preferences_editing_page.rb28
-rw-r--r--tests/browser/features/support/pages/preferences_page.rb22
-rw-r--r--tests/browser/features/support/pages/preferences_user_profile_page.rb28
-rw-r--r--tests/browser/features/support/pages/view_history_page.rb7
-rw-r--r--tests/browser/features/support/pages/ztargetpage.rb7
-rw-r--r--tests/browser/features/view_history.feature11
-rw-r--r--tests/parser/ParserTestResult.php45
-rw-r--r--tests/parser/README8
-rw-r--r--tests/parser/extraParserTests.txtbin0 -> 1261 bytes
-rw-r--r--tests/parser/parserTest.inc1655
-rw-r--r--tests/parser/parserTests.txt21904
-rw-r--r--tests/parser/parserTestsParserHook.php66
-rw-r--r--tests/parser/preprocess/All_system_messages.expected5625
-rw-r--r--tests/parser/preprocess/All_system_messages.txt5624
-rw-r--r--tests/parser/preprocess/Factorial.expected17
-rw-r--r--tests/parser/preprocess/Factorial.txt16
-rw-r--r--tests/parser/preprocess/Fundraising.expected18
-rw-r--r--tests/parser/preprocess/Fundraising.txt17
-rw-r--r--tests/parser/preprocess/NestedTemplates.expected90
-rw-r--r--tests/parser/preprocess/NestedTemplates.txt89
-rw-r--r--tests/parser/preprocess/QuoteQuran.expected140
-rw-r--r--tests/parser/preprocess/QuoteQuran.txt139
-rw-r--r--tests/parserTests.php95
-rw-r--r--tests/phpunit/LessFileCompilationTest.php60
-rw-r--r--tests/phpunit/Makefile91
-rw-r--r--tests/phpunit/MediaWikiLangTestCase.php32
-rw-r--r--tests/phpunit/MediaWikiPHPUnitTestListener.php129
-rw-r--r--tests/phpunit/MediaWikiTestCase.php1141
-rw-r--r--tests/phpunit/README53
-rw-r--r--tests/phpunit/ResourceLoaderTestCase.php95
-rw-r--r--tests/phpunit/TODO20
-rw-r--r--tests/phpunit/bootstrap.php36
-rw-r--r--tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php4
-rw-r--r--tests/phpunit/data/autoloader/TestAutoloadedClass.php4
-rw-r--r--tests/phpunit/data/autoloader/TestAutoloadedLocalClass.php4
-rw-r--r--tests/phpunit/data/autoloader/TestAutoloadedSerializedClass.php4
-rw-r--r--tests/phpunit/data/css/expected.css11
-rw-r--r--tests/phpunit/data/css/simple-ltr.gifbin0 -> 35 bytes
-rw-r--r--tests/phpunit/data/css/simple-rtl.gifbin0 -> 35 bytes
-rw-r--r--tests/phpunit/data/css/test.css11
-rw-r--r--tests/phpunit/data/cssmin/green.gifbin0 -> 35 bytes
-rw-r--r--tests/phpunit/data/cssmin/large.pngbin0 -> 36462 bytes
-rw-r--r--tests/phpunit/data/cssmin/red.gifbin0 -> 35 bytes
-rw-r--r--tests/phpunit/data/db/mysql/functions.sql12
-rw-r--r--tests/phpunit/data/db/postgres/functions.sql12
-rw-r--r--tests/phpunit/data/db/sqlite/tables-1.13.sql342
-rw-r--r--tests/phpunit/data/db/sqlite/tables-1.15.sql454
-rw-r--r--tests/phpunit/data/db/sqlite/tables-1.16.sql478
-rw-r--r--tests/phpunit/data/db/sqlite/tables-1.17.sql511
-rw-r--r--tests/phpunit/data/db/sqlite/tables-1.18.sql530
-rw-r--r--tests/phpunit/data/filerepo/video.pngbin0 -> 116 bytes
-rw-r--r--tests/phpunit/data/filerepo/wiki.pngbin0 -> 22589 bytes
-rw-r--r--tests/phpunit/data/gitinfo/info-testValidJsonData.json1
-rw-r--r--tests/phpunit/data/less/common/test.common.mixins.less5
-rw-r--r--tests/phpunit/data/less/module/dependency.less3
-rw-r--r--tests/phpunit/data/less/module/styles.css6
-rw-r--r--tests/phpunit/data/less/module/styles.less6
-rw-r--r--tests/phpunit/data/localisationcache/en.json5
-rw-r--r--tests/phpunit/data/localisationcache/ru.json4
-rw-r--r--tests/phpunit/data/localisationcache/uk.json3
-rw-r--r--tests/phpunit/data/media/1bit-png.pngbin0 -> 167 bytes
-rw-r--r--tests/phpunit/data/media/Animated_PNG_example_bouncing_beach_ball.pngbin0 -> 72209 bytes
-rw-r--r--tests/phpunit/data/media/Bishzilla_blink.gifbin0 -> 39057 bytes
-rw-r--r--tests/phpunit/data/media/Gtk-media-play-ltr.svg35
-rw-r--r--tests/phpunit/data/media/LoremIpsum.djvubin0 -> 3249 bytes
-rw-r--r--tests/phpunit/data/media/Png-native-test.pngbin0 -> 4665 bytes
-rw-r--r--tests/phpunit/data/media/QA_icon.svg77
-rw-r--r--tests/phpunit/data/media/README61
-rw-r--r--tests/phpunit/data/media/Soccer_ball_animated.svg55
-rw-r--r--tests/phpunit/data/media/Speech_bubbles.svg14
-rw-r--r--tests/phpunit/data/media/Toll_Texas_1.svg150
-rw-r--r--tests/phpunit/data/media/Tux.svg902
-rw-r--r--tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg248
-rw-r--r--tests/phpunit/data/media/Wikimedia-logo.svg14
-rw-r--r--tests/phpunit/data/media/Xmp-exif-multilingual_test.jpgbin0 -> 12544 bytes
-rw-r--r--tests/phpunit/data/media/animated-xmp.gifbin0 -> 3864 bytes
-rw-r--r--tests/phpunit/data/media/animated.gifbin0 -> 497 bytes
-rw-r--r--tests/phpunit/data/media/broken_exif_date.jpgbin0 -> 3233 bytes
-rw-r--r--tests/phpunit/data/media/exif-gps.jpgbin0 -> 665 bytes
-rw-r--r--tests/phpunit/data/media/exif-user-comment.jpgbin0 -> 484 bytes
-rw-r--r--tests/phpunit/data/media/greyscale-na-png.pngbin0 -> 365 bytes
-rw-r--r--tests/phpunit/data/media/greyscale-png.pngbin0 -> 415 bytes
-rw-r--r--tests/phpunit/data/media/iptc-invalid-psir.jpgbin0 -> 9574 bytes
-rw-r--r--tests/phpunit/data/media/iptc-timetest-invalid.jpgbin0 -> 9573 bytes
-rw-r--r--tests/phpunit/data/media/iptc-timetest.jpgbin0 -> 9573 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-comment-binary.jpgbin0 -> 448 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-comment-iso8859-1.jpgbin0 -> 447 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-comment-multiple.jpgbin0 -> 431 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-comment-utf.jpgbin0 -> 445 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-iptc-bad-hash.jpgbin0 -> 499 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-iptc-good-hash.jpgbin0 -> 499 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-padding-even.jpgbin0 -> 450 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-padding-odd.jpgbin0 -> 451 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-xmp-alt.jpgbin0 -> 3255 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-xmp-psir.jpgbin0 -> 3308 bytes
-rw-r--r--tests/phpunit/data/media/jpeg-xmp-psir.xmp35
-rw-r--r--tests/phpunit/data/media/landscape-plain.jpgbin0 -> 38771 bytes
-rw-r--r--tests/phpunit/data/media/nonanimated.gifbin0 -> 200 bytes
-rw-r--r--tests/phpunit/data/media/portrait-rotated.jpgbin0 -> 38577 bytes
-rw-r--r--tests/phpunit/data/media/rgb-na-png.pngbin0 -> 593 bytes
-rw-r--r--tests/phpunit/data/media/rgb-png.pngbin0 -> 663 bytes
-rw-r--r--tests/phpunit/data/media/say-test.oggbin0 -> 5132 bytes
-rw-r--r--tests/phpunit/data/media/test.jpgbin0 -> 437 bytes
-rw-r--r--tests/phpunit/data/media/test.tiffbin0 -> 566 bytes
-rw-r--r--tests/phpunit/data/media/xmp.pngbin0 -> 582 bytes
-rw-r--r--tests/phpunit/data/parser/LoremIpsum.djvubin0 -> 3249 bytes
-rw-r--r--tests/phpunit/data/parser/headbg.jpgbin0 -> 7881 bytes
-rw-r--r--tests/phpunit/data/parser/wiki.pngbin0 -> 22589 bytes
-rw-r--r--tests/phpunit/data/upload/headbg.jpgbin0 -> 7881 bytes
-rw-r--r--tests/phpunit/data/xmp/1.result.php8
-rw-r--r--tests/phpunit/data/xmp/1.xmp11
-rw-r--r--tests/phpunit/data/xmp/2.result.php8
-rw-r--r--tests/phpunit/data/xmp/2.xmp12
-rw-r--r--tests/phpunit/data/xmp/3-invalid.result.php7
-rw-r--r--tests/phpunit/data/xmp/3-invalid.xmp31
-rw-r--r--tests/phpunit/data/xmp/3.result.php8
-rw-r--r--tests/phpunit/data/xmp/3.xmp29
-rw-r--r--tests/phpunit/data/xmp/4.result.php7
-rw-r--r--tests/phpunit/data/xmp/4.xmp22
-rw-r--r--tests/phpunit/data/xmp/5.result.php7
-rw-r--r--tests/phpunit/data/xmp/5.xmp16
-rw-r--r--tests/phpunit/data/xmp/6.result.php8
-rw-r--r--tests/phpunit/data/xmp/6.xmp18
-rw-r--r--tests/phpunit/data/xmp/7.result.php52
-rw-r--r--tests/phpunit/data/xmp/7.xmp67
-rw-r--r--tests/phpunit/data/xmp/README3
-rw-r--r--tests/phpunit/data/xmp/bag-for-seq.result.php10
-rw-r--r--tests/phpunit/data/xmp/bag-for-seq.xmp1
-rw-r--r--tests/phpunit/data/xmp/doctype-included.result.php3
-rw-r--r--tests/phpunit/data/xmp/doctype-included.xmp12
-rw-r--r--tests/phpunit/data/xmp/doctype-not-included.xmp11
-rw-r--r--tests/phpunit/data/xmp/flash.result.php8
-rw-r--r--tests/phpunit/data/xmp/flash.xmp11
-rw-r--r--tests/phpunit/data/xmp/gps.result.php11
-rw-r--r--tests/phpunit/data/xmp/gps.xmp17
-rw-r--r--tests/phpunit/data/xmp/invalid-child-not-struct.result.php7
-rw-r--r--tests/phpunit/data/xmp/invalid-child-not-struct.xmp12
-rw-r--r--tests/phpunit/data/xmp/no-namespace.result.php7
-rw-r--r--tests/phpunit/data/xmp/no-namespace.xmp11
-rw-r--r--tests/phpunit/data/xmp/no-recognized-props.result.php2
-rw-r--r--tests/phpunit/data/xmp/no-recognized-props.xmp8
-rw-r--r--tests/phpunit/data/xmp/utf16BE.result.php12
-rw-r--r--tests/phpunit/data/xmp/utf16BE.xmpbin0 -> 930 bytes
-rw-r--r--tests/phpunit/data/xmp/utf16LE.result.php12
-rw-r--r--tests/phpunit/data/xmp/utf16LE.xmpbin0 -> 930 bytes
-rw-r--r--tests/phpunit/data/xmp/utf32BE.result.php12
-rw-r--r--tests/phpunit/data/xmp/utf32BE.xmpbin0 -> 1856 bytes
-rw-r--r--tests/phpunit/data/xmp/utf32LE.result.php12
-rw-r--r--tests/phpunit/data/xmp/utf32LE.xmpbin0 -> 1856 bytes
-rw-r--r--tests/phpunit/data/xmp/xmpExt.result.php8
-rw-r--r--tests/phpunit/data/xmp/xmpExt.xmp13
-rw-r--r--tests/phpunit/data/xmp/xmpExt2.xmp8
-rw-r--r--tests/phpunit/data/zip/cd-gap.zipbin0 -> 182 bytes
-rw-r--r--tests/phpunit/data/zip/cd-truncated.zipbin0 -> 171 bytes
-rw-r--r--tests/phpunit/data/zip/class-trailing-null.zipbin0 -> 173 bytes
-rw-r--r--tests/phpunit/data/zip/class-trailing-slash.zipbin0 -> 173 bytes
-rw-r--r--tests/phpunit/data/zip/class.zipbin0 -> 173 bytes
-rw-r--r--tests/phpunit/data/zip/empty.zipbin0 -> 22 bytes
-rw-r--r--tests/phpunit/data/zip/looks-like-zip64.zipbin0 -> 173 bytes
-rw-r--r--tests/phpunit/data/zip/nosig.zipbin0 -> 173 bytes
-rw-r--r--tests/phpunit/data/zip/split.zipbin0 -> 196 bytes
-rw-r--r--tests/phpunit/data/zip/trail.zipbin0 -> 181 bytes
-rw-r--r--tests/phpunit/data/zip/wrong-cd-start-disk.zipbin0 -> 173 bytes
-rw-r--r--tests/phpunit/data/zip/wrong-central-entry-sig.zipbin0 -> 173 bytes
-rw-r--r--tests/phpunit/docs/ExportDemoTest.php31
-rw-r--r--tests/phpunit/includes/ArrayUtilsTest.php311
-rw-r--r--tests/phpunit/includes/ArticleTablesTest.php53
-rw-r--r--tests/phpunit/includes/ArticleTest.php95
-rw-r--r--tests/phpunit/includes/BlockTest.php368
-rw-r--r--tests/phpunit/includes/CollationTest.php117
-rw-r--r--tests/phpunit/includes/DiffHistoryBlobTest.php40
-rw-r--r--tests/phpunit/includes/EditPageTest.php499
-rw-r--r--tests/phpunit/includes/ExternalStoreTest.php87
-rw-r--r--tests/phpunit/includes/ExtraParserTest.php218
-rw-r--r--tests/phpunit/includes/FallbackTest.php72
-rw-r--r--tests/phpunit/includes/FauxRequestTest.php18
-rw-r--r--tests/phpunit/includes/FauxResponseTest.php118
-rw-r--r--tests/phpunit/includes/FormOptionsInitializationTest.php89
-rw-r--r--tests/phpunit/includes/FormOptionsTest.php103
-rw-r--r--tests/phpunit/includes/GitInfoTest.php42
-rw-r--r--tests/phpunit/includes/GlobalFunctions/GlobalTest.php745
-rw-r--r--tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php32
-rw-r--r--tests/phpunit/includes/GlobalFunctions/README2
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php112
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php121
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php195
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php40
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php117
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php46
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php157
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php93
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfShellExecTest.php20
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php31
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php196
-rw-r--r--tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php124
-rw-r--r--tests/phpunit/includes/HooksTest.php202
-rw-r--r--tests/phpunit/includes/HtmlFormatterTest.php127
-rw-r--r--tests/phpunit/includes/HtmlTest.php773
-rw-r--r--tests/phpunit/includes/HttpTest.php216
-rw-r--r--tests/phpunit/includes/ImagePage404Test.php53
-rw-r--r--tests/phpunit/includes/ImagePageTest.php90
-rw-r--r--tests/phpunit/includes/ImportTest.php101
-rw-r--r--tests/phpunit/includes/LanguageConverterTest.php187
-rw-r--r--tests/phpunit/includes/LicensesTest.php25
-rw-r--r--tests/phpunit/includes/LinkFilterTest.php274
-rw-r--r--tests/phpunit/includes/LinkerTest.php192
-rw-r--r--tests/phpunit/includes/LinksUpdateTest.php266
-rw-r--r--tests/phpunit/includes/LocalFileTest.php184
-rw-r--r--tests/phpunit/includes/MWFunctionTest.php33
-rw-r--r--tests/phpunit/includes/MWNamespaceTest.php612
-rw-r--r--tests/phpunit/includes/MWTimestampTest.php342
-rw-r--r--tests/phpunit/includes/MediaWikiVersionFetcherTest.php21
-rw-r--r--tests/phpunit/includes/MessageTest.php368
-rw-r--r--tests/phpunit/includes/MimeMagicTest.php49
-rw-r--r--tests/phpunit/includes/OutputPageTest.php273
-rw-r--r--tests/phpunit/includes/PasswordTest.php33
-rw-r--r--tests/phpunit/includes/PathRouterTest.php264
-rw-r--r--tests/phpunit/includes/PreferencesTest.php91
-rw-r--r--tests/phpunit/includes/RequestContextTest.php96
-rw-r--r--tests/phpunit/includes/RevisionStorageTest.php574
-rw-r--r--tests/phpunit/includes/RevisionStorageTestContentHandlerUseDB.php89
-rw-r--r--tests/phpunit/includes/RevisionTest.php506
-rw-r--r--tests/phpunit/includes/SampleTest.php108
-rw-r--r--tests/phpunit/includes/SanitizerTest.php349
-rw-r--r--tests/phpunit/includes/SanitizerValidateEmailTest.php103
-rw-r--r--tests/phpunit/includes/SiteConfigurationTest.php363
-rw-r--r--tests/phpunit/includes/SpecialPageTest.php105
-rw-r--r--tests/phpunit/includes/StatusTest.php573
-rw-r--r--tests/phpunit/includes/TemplateCategoriesTest.php96
-rw-r--r--tests/phpunit/includes/TestUser.php62
-rw-r--r--tests/phpunit/includes/TimeAdjustTest.php39
-rw-r--r--tests/phpunit/includes/TitleArrayFromResultTest.php119
-rw-r--r--tests/phpunit/includes/TitleMethodsTest.php300
-rw-r--r--tests/phpunit/includes/TitlePermissionTest.php770
-rw-r--r--tests/phpunit/includes/TitleTest.php650
-rw-r--r--tests/phpunit/includes/UserArrayFromResultTest.php114
-rw-r--r--tests/phpunit/includes/UserTest.php369
-rw-r--r--tests/phpunit/includes/WebRequestTest.php358
-rw-r--r--tests/phpunit/includes/WikiPageTest.php1301
-rw-r--r--tests/phpunit/includes/WikiPageTestContentHandlerUseDB.php61
-rw-r--r--tests/phpunit/includes/XmlJsTest.php24
-rw-r--r--tests/phpunit/includes/XmlSelectTest.php185
-rw-r--r--tests/phpunit/includes/XmlTest.php411
-rw-r--r--tests/phpunit/includes/XmlTypeCheckTest.php49
-rw-r--r--tests/phpunit/includes/actions/ActionTest.php199
-rw-r--r--tests/phpunit/includes/api/ApiBaseTest.php46
-rw-r--r--tests/phpunit/includes/api/ApiBlockTest.php83
-rw-r--r--tests/phpunit/includes/api/ApiCreateAccountTest.php161
-rw-r--r--tests/phpunit/includes/api/ApiEditPageTest.php496
-rw-r--r--tests/phpunit/includes/api/ApiLoginTest.php181
-rw-r--r--tests/phpunit/includes/api/ApiMainTest.php72
-rw-r--r--tests/phpunit/includes/api/ApiModuleManagerTest.php318
-rw-r--r--tests/phpunit/includes/api/ApiOptionsTest.php459
-rw-r--r--tests/phpunit/includes/api/ApiParseTest.php35
-rw-r--r--tests/phpunit/includes/api/ApiPurgeTest.php45
-rw-r--r--tests/phpunit/includes/api/ApiQueryAllPagesTest.php34
-rw-r--r--tests/phpunit/includes/api/ApiRevisionDeleteTest.php114
-rw-r--r--tests/phpunit/includes/api/ApiTestCase.php196
-rw-r--r--tests/phpunit/includes/api/ApiTestCaseUpload.php171
-rw-r--r--tests/phpunit/includes/api/ApiTestContext.php21
-rw-r--r--tests/phpunit/includes/api/ApiTokensTest.php40
-rw-r--r--tests/phpunit/includes/api/ApiUnblockTest.php31
-rw-r--r--tests/phpunit/includes/api/ApiUploadTest.php572
-rw-r--r--tests/phpunit/includes/api/ApiWatchTest.php157
-rw-r--r--tests/phpunit/includes/api/MockApi.php20
-rw-r--r--tests/phpunit/includes/api/MockApiQueryBase.php11
-rw-r--r--tests/phpunit/includes/api/PrefixUniquenessTest.php30
-rw-r--r--tests/phpunit/includes/api/RandomImageGenerator.php496
-rw-r--r--tests/phpunit/includes/api/UserWrapper.php25
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatJsonTest.php22
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatNoneTest.php16
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatPhpTest.php17
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatTestBase.php32
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatWddxTest.php20
-rw-r--r--tests/phpunit/includes/api/generateRandomImages.php46
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryBasicTest.php353
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryContinue2Test.php71
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryContinueTest.php316
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php218
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php40
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryTest.php130
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryTestBase.php148
-rw-r--r--tests/phpunit/includes/api/words.txt1000
-rw-r--r--tests/phpunit/includes/cache/GenderCacheTest.php104
-rw-r--r--tests/phpunit/includes/cache/LocalisationCacheTest.php91
-rw-r--r--tests/phpunit/includes/cache/MessageCacheTest.php128
-rw-r--r--tests/phpunit/includes/cache/RedisBloomCacheTest.php71
-rw-r--r--tests/phpunit/includes/changes/EnhancedChangesListTest.php132
-rw-r--r--tests/phpunit/includes/changes/OldChangesListTest.php187
-rw-r--r--tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php331
-rw-r--r--tests/phpunit/includes/changes/RecentChangeTest.php286
-rw-r--r--tests/phpunit/includes/changes/TestRecentChangesHelper.php137
-rw-r--r--tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php161
-rw-r--r--tests/phpunit/includes/config/ConfigFactoryTest.php70
-rw-r--r--tests/phpunit/includes/config/GlobalVarConfigTest.php120
-rw-r--r--tests/phpunit/includes/config/HashConfigTest.php63
-rw-r--r--tests/phpunit/includes/config/MultiConfigTest.php38
-rw-r--r--tests/phpunit/includes/content/ContentHandlerTest.php525
-rw-r--r--tests/phpunit/includes/content/CssContentTest.php87
-rw-r--r--tests/phpunit/includes/content/JavaScriptContentTest.php293
-rw-r--r--tests/phpunit/includes/content/JsonContentTest.php114
-rw-r--r--tests/phpunit/includes/content/TextContentTest.php490
-rw-r--r--tests/phpunit/includes/content/WikitextContentHandlerTest.php241
-rw-r--r--tests/phpunit/includes/content/WikitextContentTest.php433
-rw-r--r--tests/phpunit/includes/db/DatabaseMysqlBaseTest.php247
-rw-r--r--tests/phpunit/includes/db/DatabaseSQLTest.php725
-rw-r--r--tests/phpunit/includes/db/DatabaseSqliteTest.php455
-rw-r--r--tests/phpunit/includes/db/DatabaseTest.php237
-rw-r--r--tests/phpunit/includes/db/DatabaseTestHelper.php170
-rw-r--r--tests/phpunit/includes/db/LBFactoryTest.php61
-rw-r--r--tests/phpunit/includes/db/ORMRowTest.php226
-rw-r--r--tests/phpunit/includes/db/ORMTableTest.php150
-rw-r--r--tests/phpunit/includes/db/TestORMRowTest.php218
-rw-r--r--tests/phpunit/includes/debug/MWDebugTest.php141
-rw-r--r--tests/phpunit/includes/deferred/DeferredUpdatesTest.php38
-rw-r--r--tests/phpunit/includes/diff/ArrayDiffFormatterTest.php135
-rw-r--r--tests/phpunit/includes/diff/DiffOpTest.php73
-rw-r--r--tests/phpunit/includes/diff/DiffTest.php20
-rw-r--r--tests/phpunit/includes/diff/DifferenceEngineTest.php121
-rw-r--r--tests/phpunit/includes/diff/FakeDiffOp.php11
-rw-r--r--tests/phpunit/includes/exception/BadTitleErrorTest.php43
-rw-r--r--tests/phpunit/includes/exception/ErrorPageErrorTest.php67
-rw-r--r--tests/phpunit/includes/exception/MWExceptionHandlerTest.php74
-rw-r--r--tests/phpunit/includes/exception/MWExceptionTest.php241
-rw-r--r--tests/phpunit/includes/exception/ReadOnlyErrorTest.php16
-rw-r--r--tests/phpunit/includes/exception/ThrottledErrorTest.php44
-rw-r--r--tests/phpunit/includes/exception/UserNotLoggedInTest.php16
-rw-r--r--tests/phpunit/includes/filebackend/FileBackendTest.php2472
-rw-r--r--tests/phpunit/includes/filerepo/FileRepoTest.php55
-rw-r--r--tests/phpunit/includes/filerepo/RepoGroupTest.php59
-rw-r--r--tests/phpunit/includes/filerepo/StoreBatchTest.php146
-rw-r--r--tests/phpunit/includes/filerepo/file/FileTest.php386
-rw-r--r--tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php68
-rw-r--r--tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php105
-rw-r--r--tests/phpunit/includes/installer/InstallDocFormatterTest.php72
-rw-r--r--tests/phpunit/includes/installer/OracleInstallerTest.php52
-rw-r--r--tests/phpunit/includes/jobqueue/JobQueueTest.php344
-rw-r--r--tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php112
-rw-r--r--tests/phpunit/includes/json/FormatJsonTest.php279
-rw-r--r--tests/phpunit/includes/libs/CSSMinTest.php401
-rw-r--r--tests/phpunit/includes/libs/GenericArrayObjectTest.php280
-rw-r--r--tests/phpunit/includes/libs/HashRingTest.php56
-rw-r--r--tests/phpunit/includes/libs/IEUrlExtensionTest.php173
-rw-r--r--tests/phpunit/includes/libs/IPSetTest.php252
-rw-r--r--tests/phpunit/includes/libs/JavaScriptMinifierTest.php204
-rw-r--r--tests/phpunit/includes/libs/MWMessagePackTest.php75
-rw-r--r--tests/phpunit/includes/libs/ProcessCacheLRUTest.php237
-rw-r--r--tests/phpunit/includes/libs/RunningStatTest.php79
-rw-r--r--tests/phpunit/includes/logging/LogFormatterTest.php242
-rw-r--r--tests/phpunit/includes/logging/LogTests.i18n.php15
-rw-r--r--tests/phpunit/includes/mail/MailAddressTest.php63
-rw-r--r--tests/phpunit/includes/mail/UserMailerTest.php14
-rw-r--r--tests/phpunit/includes/media/BitmapMetadataHandlerTest.php167
-rw-r--r--tests/phpunit/includes/media/BitmapScalingTest.php140
-rw-r--r--tests/phpunit/includes/media/DjVuTest.php69
-rw-r--r--tests/phpunit/includes/media/ExifBitmapTest.php146
-rw-r--r--tests/phpunit/includes/media/ExifRotationTest.php280
-rw-r--r--tests/phpunit/includes/media/ExifTest.php47
-rw-r--r--tests/phpunit/includes/media/FakeDimensionFile.php31
-rw-r--r--tests/phpunit/includes/media/FormatMetadataTest.php71
-rw-r--r--tests/phpunit/includes/media/GIFMetadataExtractorTest.php111
-rw-r--r--tests/phpunit/includes/media/GIFTest.php142
-rw-r--r--tests/phpunit/includes/media/IPTCTest.php85
-rw-r--r--tests/phpunit/includes/media/JpegMetadataExtractorTest.php111
-rw-r--r--tests/phpunit/includes/media/JpegTest.php54
-rw-r--r--tests/phpunit/includes/media/MediaHandlerTest.php56
-rw-r--r--tests/phpunit/includes/media/MediaWikiMediaTestCase.php86
-rw-r--r--tests/phpunit/includes/media/PNGMetadataExtractorTest.php155
-rw-r--r--tests/phpunit/includes/media/PNGTest.php131
-rw-r--r--tests/phpunit/includes/media/SVGMetadataExtractorTest.php160
-rw-r--r--tests/phpunit/includes/media/SVGTest.php41
-rw-r--r--tests/phpunit/includes/media/TiffTest.php45
-rw-r--r--tests/phpunit/includes/media/XCFTest.php78
-rw-r--r--tests/phpunit/includes/media/XMPTest.php223
-rw-r--r--tests/phpunit/includes/media/XMPValidateTest.php50
-rw-r--r--tests/phpunit/includes/normal/CleanUpTest.php409
-rw-r--r--tests/phpunit/includes/objectcache/BagOStuffTest.php147
-rw-r--r--tests/phpunit/includes/parser/MagicVariableTest.php229
-rw-r--r--tests/phpunit/includes/parser/MediaWikiParserTest.php134
-rw-r--r--tests/phpunit/includes/parser/NewParserTest.php1091
-rw-r--r--tests/phpunit/includes/parser/ParserMethodsTest.php187
-rw-r--r--tests/phpunit/includes/parser/ParserOutputTest.php87
-rw-r--r--tests/phpunit/includes/parser/ParserPreloadTest.php80
-rw-r--r--tests/phpunit/includes/parser/PreprocessorTest.php247
-rw-r--r--tests/phpunit/includes/parser/TagHooksTest.php108
-rw-r--r--tests/phpunit/includes/parser/TidyTest.php64
-rw-r--r--tests/phpunit/includes/password/BcryptPasswordTest.php40
-rw-r--r--tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php51
-rw-r--r--tests/phpunit/includes/password/PasswordTestCase.php88
-rw-r--r--tests/phpunit/includes/password/Pbkdf2PasswordTest.php24
-rw-r--r--tests/phpunit/includes/poolcounter/PoolCounterTest.php72
-rw-r--r--tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php132
-rw-r--r--tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php388
-rw-r--r--tests/phpunit/includes/resourceloader/ResourceLoaderTest.php249
-rw-r--r--tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php67
-rw-r--r--tests/phpunit/includes/search/SearchEngineTest.php187
-rw-r--r--tests/phpunit/includes/search/SearchUpdateTest.php81
-rw-r--r--tests/phpunit/includes/site/MediaWikiSiteTest.php109
-rw-r--r--tests/phpunit/includes/site/SiteListTest.php240
-rw-r--r--tests/phpunit/includes/site/SiteSQLStoreTest.php134
-rw-r--r--tests/phpunit/includes/site/SiteTest.php296
-rw-r--r--tests/phpunit/includes/site/TestSites.php115
-rw-r--r--tests/phpunit/includes/skins/SkinFactoryTest.php70
-rw-r--r--tests/phpunit/includes/skins/SkinTemplateTest.php43
-rw-r--r--tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php225
-rw-r--r--tests/phpunit/includes/specials/ImageListPagerTest.php22
-rw-r--r--tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php74
-rw-r--r--tests/phpunit/includes/specials/SpecialMIMESearchTest.php48
-rw-r--r--tests/phpunit/includes/specials/SpecialMyLanguageTest.php65
-rw-r--r--tests/phpunit/includes/specials/SpecialPreferencesTest.php57
-rw-r--r--tests/phpunit/includes/specials/SpecialRecentchangesTest.php123
-rw-r--r--tests/phpunit/includes/specials/SpecialSearchTest.php144
-rw-r--r--tests/phpunit/includes/title/MediaWikiPageLinkRendererTest.php169
-rw-r--r--tests/phpunit/includes/title/MediaWikiTitleCodecTest.php384
-rw-r--r--tests/phpunit/includes/title/TitleValueTest.php100
-rw-r--r--tests/phpunit/includes/upload/UploadBaseTest.php427
-rw-r--r--tests/phpunit/includes/upload/UploadFromUrlTest.php328
-rw-r--r--tests/phpunit/includes/upload/UploadStashTest.php107
-rw-r--r--tests/phpunit/includes/utils/CdbTest.php90
-rw-r--r--tests/phpunit/includes/utils/IPTest.php580
-rw-r--r--tests/phpunit/includes/utils/MWCryptHKDFTest.php89
-rw-r--r--tests/phpunit/includes/utils/StringUtilsTest.php149
-rw-r--r--tests/phpunit/includes/utils/UIDGeneratorTest.php129
-rw-r--r--tests/phpunit/includes/utils/ZipDirectoryReaderTest.php84
-rw-r--r--tests/phpunit/install-phpunit.sh38
-rw-r--r--tests/phpunit/languages/LanguageAmTest.php35
-rw-r--r--tests/phpunit/languages/LanguageArTest.php87
-rw-r--r--tests/phpunit/languages/LanguageArqTest.php26
-rw-r--r--tests/phpunit/languages/LanguageBeTest.php42
-rw-r--r--tests/phpunit/languages/LanguageBe_taraskTest.php97
-rw-r--r--tests/phpunit/languages/LanguageBhoTest.php35
-rw-r--r--tests/phpunit/languages/LanguageBsTest.php42
-rw-r--r--tests/phpunit/languages/LanguageClassesTestCase.php74
-rw-r--r--tests/phpunit/languages/LanguageCsTest.php41
-rw-r--r--tests/phpunit/languages/LanguageCuTest.php42
-rw-r--r--tests/phpunit/languages/LanguageCyTest.php43
-rw-r--r--tests/phpunit/languages/LanguageDsbTest.php41
-rw-r--r--tests/phpunit/languages/LanguageFrTest.php35
-rw-r--r--tests/phpunit/languages/LanguageGaTest.php35
-rw-r--r--tests/phpunit/languages/LanguageGdTest.php53
-rw-r--r--tests/phpunit/languages/LanguageGvTest.php44
-rw-r--r--tests/phpunit/languages/LanguageHeTest.php132
-rw-r--r--tests/phpunit/languages/LanguageHiTest.php35
-rw-r--r--tests/phpunit/languages/LanguageHrTest.php42
-rw-r--r--tests/phpunit/languages/LanguageHsbTest.php41
-rw-r--r--tests/phpunit/languages/LanguageHuTest.php35
-rw-r--r--tests/phpunit/languages/LanguageHyTest.php35
-rw-r--r--tests/phpunit/languages/LanguageKshTest.php35
-rw-r--r--tests/phpunit/languages/LanguageLnTest.php35
-rw-r--r--tests/phpunit/languages/LanguageLtTest.php63
-rw-r--r--tests/phpunit/languages/LanguageLvTest.php44
-rw-r--r--tests/phpunit/languages/LanguageMgTest.php36
-rw-r--r--tests/phpunit/languages/LanguageMkTest.php40
-rw-r--r--tests/phpunit/languages/LanguageMlTest.php38
-rw-r--r--tests/phpunit/languages/LanguageMoTest.php45
-rw-r--r--tests/phpunit/languages/LanguageMtTest.php77
-rw-r--r--tests/phpunit/languages/LanguageNlTest.php24
-rw-r--r--tests/phpunit/languages/LanguageNsoTest.php34
-rw-r--r--tests/phpunit/languages/LanguagePlTest.php77
-rw-r--r--tests/phpunit/languages/LanguageRoTest.php45
-rw-r--r--tests/phpunit/languages/LanguageRuTest.php115
-rw-r--r--tests/phpunit/languages/LanguageSeTest.php53
-rw-r--r--tests/phpunit/languages/LanguageSgsTest.php71
-rw-r--r--tests/phpunit/languages/LanguageShTest.php42
-rw-r--r--tests/phpunit/languages/LanguageSkTest.php42
-rw-r--r--tests/phpunit/languages/LanguageSlTest.php44
-rw-r--r--tests/phpunit/languages/LanguageSmaTest.php53
-rw-r--r--tests/phpunit/languages/LanguageSrTest.php249
-rw-r--r--tests/phpunit/languages/LanguageTest.php1635
-rw-r--r--tests/phpunit/languages/LanguageTiTest.php34
-rw-r--r--tests/phpunit/languages/LanguageTlTest.php34
-rw-r--r--tests/phpunit/languages/LanguageTrTest.php61
-rw-r--r--tests/phpunit/languages/LanguageUkTest.php72
-rw-r--r--tests/phpunit/languages/LanguageUzTest.php124
-rw-r--r--tests/phpunit/languages/LanguageWaTest.php34
-rw-r--r--tests/phpunit/languages/SpecialPageAliasTest.php63
-rw-r--r--tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php151
-rw-r--r--tests/phpunit/maintenance/DumpTestCase.php386
-rw-r--r--tests/phpunit/maintenance/MaintenanceTest.php830
-rw-r--r--tests/phpunit/maintenance/backupPrefetchTest.php277
-rw-r--r--tests/phpunit/maintenance/backupTextPassTest.php584
-rw-r--r--tests/phpunit/maintenance/backup_LogTest.php225
-rw-r--r--tests/phpunit/maintenance/backup_PageTest.php428
-rw-r--r--tests/phpunit/maintenance/fetchTextTest.php261
-rw-r--r--tests/phpunit/mocks/filebackend/MockFSFile.php69
-rw-r--r--tests/phpunit/mocks/filebackend/MockFileBackend.php39
-rw-r--r--tests/phpunit/mocks/media/MockBitmapHandler.php32
-rw-r--r--tests/phpunit/mocks/media/MockDjVuHandler.php49
-rw-r--r--tests/phpunit/mocks/media/MockImageHandler.php86
-rw-r--r--tests/phpunit/mocks/media/MockSvgHandler.php28
-rw-r--r--tests/phpunit/phpunit.php233
-rw-r--r--tests/phpunit/run-tests.bat1
-rw-r--r--tests/phpunit/skins/SideBarTest.php219
-rw-r--r--tests/phpunit/structure/AutoLoaderTest.php135
-rw-r--r--tests/phpunit/structure/ResourcesTest.php269
-rw-r--r--tests/phpunit/structure/StructureTest.php67
-rw-r--r--tests/phpunit/suite.xml64
-rw-r--r--tests/phpunit/suites/ExtensionsParserTestSuite.php8
-rw-r--r--tests/phpunit/suites/ExtensionsTestSuite.php47
-rw-r--r--tests/phpunit/suites/LessTestSuite.php34
-rw-r--r--tests/phpunit/suites/UploadFromUrlTestSuite.php207
-rw-r--r--tests/phpunit/tests/MediaWikiTestCaseTest.php77
-rw-r--r--tests/qunit/.htaccess1
-rw-r--r--tests/qunit/QUnitTestResources.php117
-rw-r--r--tests/qunit/data/callMwLoaderTestCallback.js1
-rw-r--r--tests/qunit/data/generateJqueryMsgData.php149
-rw-r--r--tests/qunit/data/load.mock.php74
-rw-r--r--tests/qunit/data/mediawiki.jqueryMsg.data.js491
-rw-r--r--tests/qunit/data/qunitOkCall.js2
-rw-r--r--tests/qunit/data/styleTest.css.php61
-rw-r--r--tests/qunit/data/testrunner.js547
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js108
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js53
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.byteLength.test.js37
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js252
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.client.test.js638
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.color.test.js18
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js63
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js13
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.hidpi.test.js22
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.highlightText.test.js235
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.localize.test.js135
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js339
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js55
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.placeholder.test.js145
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js35
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js1327
-rw-r--r--tests/qunit/suites/resources/jquery/jquery.textSelection.test.js273
-rw-r--r--tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js30
-rw-r--r--tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js25
-rw-r--r--tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js312
-rw-r--r--tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js46
-rw-r--r--tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js63
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js650
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js434
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js81
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js172
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js798
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js70
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js475
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.test.js985
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js40
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js54
-rw-r--r--tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js343
-rw-r--r--tests/qunit/suites/resources/startup.test.js143
-rw-r--r--tests/testHelpers.inc832
816 files changed, 115990 insertions, 1403 deletions
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 00000000..f8177807
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,119 @@
+/*jshint node:true */
+module.exports = function ( grunt ) {
+ grunt.loadNpmTasks( 'grunt-contrib-copy' );
+ grunt.loadNpmTasks( 'grunt-contrib-jshint' );
+ grunt.loadNpmTasks( 'grunt-contrib-watch' );
+ grunt.loadNpmTasks( 'grunt-banana-checker' );
+ grunt.loadNpmTasks( 'grunt-jscs' );
+ grunt.loadNpmTasks( 'grunt-jsonlint' );
+ grunt.loadNpmTasks( 'grunt-karma' );
+
+ var wgServer = process.env.MW_SERVER,
+ wgScriptPath = process.env.MW_SCRIPT_PATH,
+ karmaProxy = {};
+
+ karmaProxy[wgScriptPath] = wgServer + wgScriptPath;
+
+ grunt.initConfig( {
+ pkg: grunt.file.readJSON( 'package.json' ),
+ jshint: {
+ options: {
+ jshintrc: true
+ },
+ all: [
+ '*.js',
+ '{includes,languages,resources,tests}/**/*.js'
+ ]
+ },
+ jscs: {
+ all: [
+ '<%= jshint.all %>',
+ // Auto-generated file with JSON (double quotes)
+ '!tests/qunit/data/mediawiki.jqueryMsg.data.js',
+ // Skip functions are stored as script files but wrapped in a function when
+ // executed. node-jscs trips on the would-be "Illegal return statement".
+ '!resources/src/*-skip.js'
+
+ // Exclude all files ignored by jshint
+ ].concat( grunt.file.read( '.jshintignore' ).split( '\n' ).reduce( function ( patterns, pattern ) {
+ // Filter out empty lines
+ if ( pattern.length && pattern[0] !== '#' ) {
+ patterns.push( '!' + pattern );
+ }
+ return patterns;
+ }, [] ) )
+ },
+ jsonlint: {
+ all: [
+ '.jscsrc',
+ '{languages,maintenance,resources}/**/*.json',
+ 'package.json'
+ ]
+ },
+ banana: {
+ core: 'languages/i18n/',
+ installer: 'includes/installer/i18n/'
+ },
+ watch: {
+ files: [
+ '<%= jscs.all %>',
+ '<%= jsonlint.all %>',
+ '.jshintignore',
+ '.jshintrc'
+ ],
+ tasks: 'test'
+ },
+ karma: {
+ options: {
+ proxies: karmaProxy,
+ files: [ {
+ pattern: wgServer + wgScriptPath + '/index.php?title=Special:JavaScriptTest/qunit/export',
+ watched: false,
+ included: true,
+ served: false
+ } ],
+ frameworks: [ 'qunit' ],
+ reporters: [ 'dots' ],
+ singleRun: true,
+ autoWatch: false,
+ // Some tests in extensions don't yield for more than the default 10s (T89075)
+ browserNoActivityTimeout: 60 * 1000
+ },
+ main: {
+ browsers: [ 'Chrome' ]
+ },
+ more: {
+ browsers: [ 'Chrome', 'Firefox' ]
+ }
+ },
+ copy: {
+ jsduck: {
+ src: 'resources/**/*',
+ dest: 'docs/js/modules',
+ expand: true,
+ rename: function ( dest, src ) {
+ return require( 'path' ).join( dest, src.replace( 'resources/', '' ) );
+ }
+ }
+ }
+ } );
+
+ grunt.registerTask( 'assert-mw-env', function () {
+ if ( !process.env.MW_SERVER ) {
+ grunt.log.error( 'Environment variable MW_SERVER must be set.\n' +
+ 'Set this like $wgServer, e.g. "http://localhost"'
+ );
+ }
+ if ( !process.env.MW_SCRIPT_PATH ) {
+ grunt.log.error( 'Environment variable MW_SCRIPT_PATH must be set.\n' +
+ 'Set this like $wgScriptPath, e.g. "/w"');
+ }
+ return !!( process.env.MW_SERVER && process.env.MW_SCRIPT_PATH );
+ } );
+
+ grunt.registerTask( 'lint', ['jshint', 'jscs', 'jsonlint', 'banana'] );
+ grunt.registerTask( 'qunit', [ 'assert-mw-env', 'karma:main' ] );
+
+ grunt.registerTask( 'test', ['lint'] );
+ grunt.registerTask( 'default', 'test' );
+};
diff --git a/RELEASE-NOTES-1.24 b/RELEASE-NOTES-1.24
index 62e0c328..43ba2876 100644
--- a/RELEASE-NOTES-1.24
+++ b/RELEASE-NOTES-1.24
@@ -1,6 +1,29 @@
Security reminder: If you have PHP's register_globals option set, you must
turn it off. MediaWiki will no longer work with it enabled.
+== MediaWiki 1.24.2 ==
+
+This is a security and maintenance release of the MediaWiki 1.24 branch.
+
+== Changes since 1.24.1 ==
+
+* (T85848, T71210) SECURITY: Don't parse XMP blocks that contain XML entities,
+ to prevent various DoS attacks.
+* (T85848) SECURITY: Don't allow directly calling Xml::isWellFormed, to reduce
+ likelihood of DoS.
+* (T88310) SECURITY: Always expand xml entities when checking SVG's.
+* (T73394) SECURITY: Escape > in Html::expandAttributes to prevent XSS.
+* (T85855) SECURITY: Don't execute another user's CSS or JS on preview.
+* (T64685) SECURITY: Allow setting maximal password length to prevent DoS when
+ using PBKDF2.
+* (T85349, T85850, T86711) SECURITY: Multiple issues fixed in SVG filtering to
+ prevent XSS and protect viewer's privacy.
+* Fix case of SpecialAllPages/SpecialAllMessages in SpecialPageFactory to fix
+ loading these special pages when $wgAutoloadAttemptLowercase is false.
+* (bug T70087) Fix Special:ActiveUsers page for installations using
+ PostgreSQL.
+* (bug T76254) Fix deleting of pages with PostgreSQL. Requires a schema change
+ and running update.php to fix.
== MediaWiki 1.24.1 ==
diff --git a/docs/kss/package.json b/docs/kss/package.json
deleted file mode 100644
index 70cebd2c..00000000
--- a/docs/kss/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "mediawiki-ui-dependencies",
- "description": "Node.js dependencies used for KSS generation",
- "version": "0.0.1",
- "dependencies": {
- "kss": ">=0.3.7"
- },
- "repository" : {
- "type" : "git",
- "url" : "https://gerrit.wikimedia.org/r/p/mediawiki/core.git"
- }
-
-}
diff --git a/extensions/ConfirmEdit/Asirra.class.php b/extensions/ConfirmEdit/Asirra.class.php
deleted file mode 100644
index ae1178a1..00000000
--- a/extensions/ConfirmEdit/Asirra.class.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-/**
- * @author Bachsau
- * @author Niklas Laxström
- */
-
-class Asirra extends SimpleCaptcha {
- public $asirra_clientscript = '//challenge.asirra.com/js/AsirraClientSide.js';
-
- // As we don't have to store anything but some other things to do,
- // we're going to replace that constructor completely.
- function __construct() {
- global $wgExtensionAssetsPath;
- $this->asirra_localpath = "$wgExtensionAssetsPath/ConfirmEdit";
- }
-
- function getForm() {
- global $wgOut;
-
- $wgOut->addModules( 'ext.confirmEdit.asirra' );
- $js = Html::linkedScript( $this->asirra_clientscript );
-
- $message = Xml::encodeJsVar( wfMessage( 'asirra-createaccount-fail' )->plain() );
- $js .= Html::inlineScript( <<<JAVASCRIPT
-var asirra_js_failed = '$message';
-JAVASCRIPT
- );
- $js .= '<noscript>' . wfMessage( 'asirra-nojs' )->parse() . '</noscript>';
- return $js;
- }
-
- function getMessage( $action ) {
- $name = 'asirra-' . $action;
- $text = wfMessage( $name )->text();
- # Obtain a more tailored message, if possible, otherwise, fall
- # back to the default for edits
- return wfMessage( $name, $text )->isDisabled() ? wfMessage( 'asirra-edit' )->text() : $text;
- }
-
- function passCaptcha() {
- global $wgRequest;
-
- $ticket = $wgRequest->getVal( 'Asirra_Ticket' );
- $api = 'http://challenge.asirra.com/cgi/Asirra?';
- $params = array(
- 'action' => 'ValidateTicket',
- 'ticket' => $ticket,
- );
-
- $response = Http::get( $api . wfArrayToCgi( $params ) );
- $xml = simplexml_load_string( $response );
- $result = $xml->xpath( '/AsirraValidation/Result' );
- return strval( $result[0] ) === 'Pass';
- }
-}
diff --git a/extensions/ConfirmEdit/Asirra.php b/extensions/ConfirmEdit/Asirra.php
deleted file mode 100644
index a5d5012f..00000000
--- a/extensions/ConfirmEdit/Asirra.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-/**
- * Asirra CAPTCHA module for the ConfirmEdit MediaWiki extension.
- * @author Bachsau
- * @author Niklas Laxström
- *
- * Makes use of the Asirra (Animal Species Image Recognition for
- * Restricting Access) CAPTCHA service, developed by John Douceur, Jeremy
- * Elson and Jon Howell at Microsoft Research.
- *
- * Asirra uses a large set of images from http://petfinder.com.
- *
- * For more information about Asirra, see:
- * http://research.microsoft.com/en-us/um/redmond/projects/asirra/
- *
- * This MediaWiki code is released into the public domain, without any
- * warranty. YOU CAN DO WITH IT WHATEVER YOU LIKE!
- *
- * @file
- * @ingroup Extensions
- */
-
-if ( !defined( 'MEDIAWIKI' ) ) {
- exit;
-}
-
-$dir = __DIR__;
-require_once( $dir . '/ConfirmEdit.php' );
-
-$wgCaptchaClass = 'Asirra';
-$wgMessagesDirs['Asirra'] = __DIR__ . '/i18n/asirra';
-$wgExtensionMessagesFiles['Asirra'] = $dir . '/Asirra.i18n.php';
-$wgAutoloadClasses['Asirra'] = $dir . '/Asirra.class.php';
-
-$wgResourceModules['ext.confirmEdit.asirra'] = array(
- 'localBasePath' => $dir . '/resources',
- 'remoteExtPath' => 'ConfirmEdit/resources',
- 'scripts' => 'ext.confirmEdit.asirra.js',
- 'messages' => array(
- 'asirra-failed',
- ),
-);
-
diff --git a/extensions/ConfirmEdit/README b/extensions/ConfirmEdit/README
index acfe481a..7a331e6b 100644
--- a/extensions/ConfirmEdit/README
+++ b/extensions/ConfirmEdit/README
@@ -19,8 +19,6 @@ in a stylized way
questions defined by the administrator(s)
* ReCaptcha - users have to identify a series of characters, either
visually or audially, from a widget provided by the reCAPTCHA service
-* Asirra - users have to identify the cats in a set of photos of cats
-and dogs, from a widget provided by the Microsoft Asirra service
== License ==
@@ -37,8 +35,6 @@ The QuestyCaptcha module was written by Benjamin Lees.
The reCAPTCHA module was written by Mike Crawford and Ben Maurer.
-The Asirra module was written by Bachsau.
-
Additional maintenance work was done by Yaron Koren.
== Changelog ==
diff --git a/extensions/ConfirmEdit/i18n/asirra/ast.json b/extensions/ConfirmEdit/i18n/asirra/ast.json
deleted file mode 100644
index 626c4896..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/ast.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Xuacu"
- ]
- },
- "asirra-desc": "Módulu Asirra pa ConfirmEdit",
- "asirra-edit": "Pa protexer la wiki escontra'l spam d'edición automáticu, pidimos-y qu'esbille namái les fotos de gatos del cuadru d'abaxo:",
- "asirra-addurl": "La so edición incluye enllaces esternos nuevos. Pa protexer la wiki escontra'l spam automáticu, pidimos-y qu'esbille namái les fotos de gatos del cuadru d'abaxo:",
- "asirra-badlogin": "Pa protexer la wiki escontra'l frayamientu de claves automáticu, pidimos-y qu'esbille namái les fotos de gatos del cuadru d'abaxo:",
- "asirra-createaccount": "Pa protexer la wiki escontra la creación de cuentes automática, pidimos-y qu'esbille namái les fotos de gatos del cuadru d'abaxo:",
- "asirra-createaccount-fail": "Identifique correutamente los gatos.",
- "asirra-create": "Pa protexer la wiki escontra la creación de páxines automática, pidimos-y qu'esbille namái les fotos de gatos del cuadru d'abaxo:",
- "asirra-nojs": "'''Por favor active JavaScript y vuelva a unviar la páxina.'''",
- "asirra-failed": "Identifique toles imaxes de gatos"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/be-tarask.json b/extensions/ConfirmEdit/i18n/asirra/be-tarask.json
deleted file mode 100644
index c829700e..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/be-tarask.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "EugeneZelenko",
- "Jim-by",
- "Wizardist",
- "Red Winged Duck"
- ]
- },
- "asirra-desc": "Модуль Asirra для ConfirmEdit",
- "asirra-edit": "Для абароны вікі ад аўтаматычнага спаму ў праўках, просім вас выбраць толькі фотаздымкі з катом у полі ніжэй:",
- "asirra-addurl": "Вашае рэдагаваньне ўтрымлівае новыя вонкавыя спасылкі. Для абароны вікі ад аўтаматычнага спаму ў праўках, мы просім вас выбраць толькі фотаздымкі катоў у полі ніжэй:",
- "asirra-badlogin": "Для абароны вікі ад аўтаматычнага падбору паролю, просім вас выбраць толькі фотаздымкі катоў у полі ніжэй:",
- "asirra-createaccount": "Для абароны вікі ад аўтаматычнага стварэньня рахункаў, просім вас выбраць толькі фотаздымкі катоў у полі ніжэй:",
- "asirra-createaccount-fail": "Калі ласка, слушна выберыце катоў.",
- "asirra-create": "Для абароны вікі ад аўтаматычнага стварэньня старонак, просім вас выбраць толькі фотаздымкі катоў у полі ніжэй:",
- "asirra-nojs": "'''Калі ласка, дазвольце JavaScript і дашліце старонку зноў.'''",
- "asirra-failed": "Калі ласка, вызначце ўсе выявы з катамі"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/br.json b/extensions/ConfirmEdit/i18n/asirra/br.json
deleted file mode 100644
index 4b5d3c6d..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/br.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Fohanno"
- ]
- },
- "asirra-desc": "Modulenn Asirra evit ConfirmEdit",
- "asirra-edit": "Evit sikour da wareziñ ar wiki diouzh ar stroboù emgefre, diuzit ar skeudennoù kizhier er voest dindan :",
- "asirra-createaccount-fail": "Diuzit ar c'hizhier, mar plij.",
- "asirra-create": "Evit gwareziñ ar wiki diouzh ar c'hrouiñ pajennoù emgefre, diuzit ar skeudennoù kizhier er voest dindan :",
- "asirra-nojs": "'''Gweredekait JavaScript, mar plij, hag adkasit ar bajenn.'''",
- "asirra-failed": "Diuzit an holl skeudennoù kizhier, mar plij"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/ca.json b/extensions/ConfirmEdit/i18n/asirra/ca.json
deleted file mode 100644
index 9119f0fa..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/ca.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Toniher"
- ]
- },
- "asirra-desc": "Mòdul Asirra de ConfirmEdit",
- "asirra-edit": "Per tal de protegir el wiki contra les edicions brosses, us demanem que seleccioneu només les fotos de gats del requadre a continuació:",
- "asirra-addurl": "La modificació inclou nous enllaços externs. Per tal de protegir el wiki davant d'edicions brossa, us demanem que seleccioneu només les fotos de gots del requadre a continuació:",
- "asirra-badlogin": "Per tal de protegir el wiki contra els intents de trencament de contrasenyes, us demanem que seleccioneu només les fotos de gats del requadre a continuació:",
- "asirra-createaccount": "Per tal de protegir el wiki contra la creació automatitzada de comptes, us demanem que seleccioneu només les fotos de gats del requadre a continuació:",
- "asirra-createaccount-fail": "Identifiqueu correctament els gats.",
- "asirra-create": "Per tal de protegir el wiki contra la creació automàtica de pàgines, us demanem que seleccioneu només les fotos de gats del requadre a continuació:",
- "asirra-nojs": "'''Habilitieu el JavaScript i torneu a enviar la pàgina.'''",
- "asirra-failed": "Identifiqueu totes les imatges de gats"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/cs.json b/extensions/ConfirmEdit/i18n/asirra/cs.json
deleted file mode 100644
index df7cccbf..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/cs.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Vks"
- ]
- },
- "asirra-createaccount-fail": "Prosíme, správně identifikujte kočky."
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/de-formal.json b/extensions/ConfirmEdit/i18n/asirra/de-formal.json
deleted file mode 100644
index 87f8f119..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/de-formal.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Kghbln"
- ]
- },
- "asirra-addurl": "Ihre Bearbeitung enthält neue externe Links. Zum Schutz vor automatisiertem Spam bitten wir Sie, nur die Fotos mit Katzen im untenstehenden Feld auszuwählen:",
- "asirra-badlogin": "Zum Schutz gegen automatisiertes Knacken von Passwörtern bitten wir Sie, nur die Fotos mit Katzen im untenstehenden Feld auszuwählen:",
- "asirra-createaccount": "Zum Schutz gegen automatisiertes Erstellen von Benutzerkonten bitten wir Sie, nur die Fotos mit Katzen im untenstehenden Feld auszuwählen:",
- "asirra-createaccount-fail": "Bitte wählen Sie nur die Fotos mit Katzen aus.",
- "asirra-create": "Zum Schutz gegen automatisiertes Erstellen von Seiten bitten wir Sie, nur die Fotos mit Katzen im untenstehenden Feld auszuwählen:",
- "asirra-failed": "Bitte wählen Sie nur die Fotos mit Katzen aus."
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/de.json b/extensions/ConfirmEdit/i18n/asirra/de.json
deleted file mode 100644
index be745a09..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/de.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Kghbln",
- "Metalhead64"
- ]
- },
- "asirra-desc": "Ermöglicht die Nutzung des Anti-Spam-Moduls Asirra",
- "asirra-edit": "Zum Schutz des Wikis vor automatisiertem Spam bitten wir dich, nur die Fotos mit Katzen im untenstehenden Feld auszuwählen:",
- "asirra-addurl": "Deine Bearbeitung enthält neue externe Links. Zum Schutz des Wikis vor automatisiertem Spam bitten wir dich, nur die Fotos mit Katzen im untenstehenden Feld auszuwählen:",
- "asirra-badlogin": "Zum Schutz des Wikis gegen automatisiertes Knacken von Passwörtern bitten wir dich, nur die Fotos mit Katzen im untenstehenden Feld auszuwählen:",
- "asirra-createaccount": "Zum Schutz des Wikis gegen automatisiertes Erstellen von Benutzerkonten bitten wir dich, nur die Fotos mit Katzen im untenstehenden Feld auszuwählen:",
- "asirra-createaccount-fail": "Bitte wähle nur die Fotos mit Katzen aus.",
- "asirra-create": "Zum Schutz des Wikis gegen automatisiertes Erstellen von Seiten bitten wir dich, nur die Fotos mit Katzen im untenstehenden Feld auszuwählen:",
- "asirra-nojs": "'''Bitte JavaScript aktivieren und die Seiten nochmals Speichern.'''",
- "asirra-failed": "Bitte wähle nur die Fotos mit Katzen aus."
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/diq.json b/extensions/ConfirmEdit/i18n/asirra/diq.json
deleted file mode 100644
index 09196703..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/diq.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Erdemaslancan"
- ]
- },
- "asirra-desc": "Qandê Asirra modulê RaştkerdenVurnen"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/en.json b/extensions/ConfirmEdit/i18n/asirra/en.json
deleted file mode 100644
index 9c98683b..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/en.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "@metadata": {
- "authors": []
- },
- "asirra-desc": "Asirra module for ConfirmEdit",
- "asirra-edit": "To protect the wiki against automated edit spam, we kindly ask you to select just the cat photos in the box below:",
- "asirra-addurl": "Your edit includes new external links. To protect the wiki against automated edit spam, we kindly ask you to select just the cat photos in the box below:",
- "asirra-badlogin": "To protect the wiki against automated password cracking, we kindly ask you to select just the cat photos in the box below:",
- "asirra-createaccount": "To protect the wiki against automated account creation, we kindly ask you to select just the cat photos in the box below:",
- "asirra-createaccount-fail": "Please correctly identify the cats.",
- "asirra-create": "To protect the wiki against automated page creation, we kindly ask you to select just the cat photos in the box below:",
- "asirra-nojs": "'''Please enable JavaScript and resubmit the page.'''",
- "asirra-failed": "Please identify all cat images"
-} \ No newline at end of file
diff --git a/extensions/ConfirmEdit/i18n/asirra/es.json b/extensions/ConfirmEdit/i18n/asirra/es.json
deleted file mode 100644
index 875912d8..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/es.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Armando-Martin",
- "Carlosz22",
- "Ciencia Al Poder"
- ]
- },
- "asirra-desc": "Módulo de Asirra para ConfirmEdit",
- "asirra-edit": "Para ayudar a protegernos contra el spam de ediciones automáticas, seleccione sólo las fotos de gatos en el cuadro siguiente:",
- "asirra-addurl": "Tu edición incluye nuevos enlaces externos. Para proteger el wiki contra el spam automatizado, por favor, te pedimos que selecciones solo las fotos de gatos en el cuadro siguiente:",
- "asirra-badlogin": "Para proteger el wiki contra el robo automatizado de contraseñas, te pedimos por favor que selecciones únicamente las fotos de gatos en el cuadro siguiente:",
- "asirra-createaccount": "Para proteger el wiki contra la creación automatizada de cuentas de usuario, te pedimos por favor que selecciones únicamente las fotos de gatos en el cuadro siguiente:",
- "asirra-createaccount-fail": "Identifique correctamente los gatos.",
- "asirra-create": "Para proteger el wiki contra la creación automatizada de páginas, te pedimos por favor que selecciones únicamente las fotos de gatos en el cuadro siguiente:",
- "asirra-nojs": "'''Por favor active JavaScript y vuelva a la página.'''",
- "asirra-failed": "Identifique todas las imágenes de gatos"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/fa.json b/extensions/ConfirmEdit/i18n/asirra/fa.json
deleted file mode 100644
index b06627b9..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/fa.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Armin1392",
- "Ebraminio",
- "Alirezaaa"
- ]
- },
- "asirra-desc": "بخش آسیرا برای تأیید ویرایش",
- "asirra-edit": " برای محافظت ویکی دربرابر ویرایش خودکار اسپم، ما دوستانه از شما درخواست می‌کنیم که تنها عکس‌های گربه را در جعبهٔ زیر انتخاب کنید:",
- "asirra-addurl": "ویرایش شما شامل پیوندهای خارجی تازه است. برای محافظت ویکی دربرابر ویرایش خودکار هرزنگاری، ما دوستانه از شما درخواست می‌کنیم که تنها عکس‌های گربه را در جعبهٔ زیر انتخاب کنید:",
- "asirra-badlogin": "برای محافظت ویکی دربرابر رخنه به رمز‌ عبور به طور خودکار، ما دوستانه از شما درخواست می‌کنیم که تنها عکس‌های گربه را در جعبهٔ زیر انتخاب کنید:",
- "asirra-createaccount": "برای محافظت ویکی دربرابر ایجاد حساب به طور خودکار، ما دوستانه از شما درخواست می‌کنیم که تنها عکس‌های گربه را در جعبهٔ زیر انتخاب کنید:",
- "asirra-createaccount-fail": "لطفاً این گربه‌ها را به درستی شناسایی کنید.",
- "asirra-create": "برای محافظت ویکی دربرابر ایجاد صفحه به طور خودکار، ما دوستانه از شما درخواست می‌کنیم که تنها عکس‌های گربه را در جعبهٔ زیر انتخاب کنید:",
- "asirra-nojs": "'''لطفاً جاوااسکریپت را فعال کنید و صفحه را دوباره ارائه کنید.'''",
- "asirra-failed": "لطفاً همهٔ عکس‌های گربه را شناسایی کنید"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/fi.json b/extensions/ConfirmEdit/i18n/asirra/fi.json
deleted file mode 100644
index bf3bfe3d..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/fi.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "VezonThunder",
- "Nedergard"
- ]
- },
- "asirra-desc": "Asirra-moduuli muokkauksen varmennukseen",
- "asirra-edit": "Suojana automaattisia roskamuokkauksia vastaan sinun on valittava kissan kuvat laatikosta alla:",
- "asirra-addurl": "Muokkauksesi sisältää uusia ulkoisia linkkejä. Suojana automaattista roskapostia vastaan sinun on valittava kissan kuvat laatikosta alla:",
- "asirra-badlogin": "Suojana automaattisia salasanamurtoja vastaan sinun on valittava kissan kuvat laatikosta alla:",
- "asirra-createaccount": "Suojana automaattista tunnusten luontia vastaan sinun on valittava kissan kuvat laatikosta alla:",
- "asirra-createaccount-fail": "Tunnista kissat.",
- "asirra-create": "Suojana automaattista sivujen luontia vastaan sinun on valittava kissojen kuvat alta:",
- "asirra-nojs": "'''Salli JavaScript ja lähetä uudelleen.'''",
- "asirra-failed": "Tunnista kaikki kissan kuvat"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/fr.json b/extensions/ConfirmEdit/i18n/asirra/fr.json
deleted file mode 100644
index e9c0d73d..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/fr.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Gomoko",
- "Nicolas NALLET",
- "Seb35"
- ]
- },
- "asirra-desc": "Module Asirra pour ConfirmEdit",
- "asirra-edit": "Pour protéger le wiki contre le spam d’édition automatique, nous vous demandons de bien vouloir sélectionner uniquement les photos de chats dans la boîte ci-dessous :",
- "asirra-addurl": "Votre édition contient des liens externes. Pour protéger le wiki contre le spam de modification automatique, nous vous demandons de bien vouloir sélectionner uniquement les photos de chats dans la boîte ci-dessous :",
- "asirra-badlogin": "Pour protéger le wiki des essais automatiques de cassage de mot de passe, nous vous demandons de bien vouloir sélectionner uniquement les photos de chats dans la boîte ci-dessous :",
- "asirra-createaccount": "Pour protéger le wiki contre la création automatique de comptes, nous vous demandons de bien vouloir sélectionner uniquement les photos de chats dans la boîte ci-dessous :",
- "asirra-createaccount-fail": "Veuillez identifier correctement les chats.",
- "asirra-create": "Pour protéger le wiki contre la création automatique de pages, nous vous demandons de bien vouloir sélectionner uniquement les photos de chats dans la boîte ci-dessous :",
- "asirra-nojs": "'''Veuillez activer le JavaScript et re-soumettre la page.'''",
- "asirra-failed": "Veuillez identifier toutes les images de chat"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/gl.json b/extensions/ConfirmEdit/i18n/asirra/gl.json
deleted file mode 100644
index e5f33605..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/gl.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Toliño"
- ]
- },
- "asirra-desc": "Módulo Asirra para ConfirmEdit",
- "asirra-edit": "Para protexer o wiki contra o spam automático, seleccione só as fotos de gatos na caixa:",
- "asirra-addurl": "A súa edición inclúe novas ligazóns externas. Para protexer o wiki contra o spam automático, seleccione só as fotos de gatos na caixa:",
- "asirra-badlogin": "Para protexer o wiki contra o roubo de contrasinais, seleccione só as fotos de gatos na caixa:",
- "asirra-createaccount": "Para protexer o wiki contra a creación automática de contas, seleccione só as fotos de gatos na caixa:",
- "asirra-createaccount-fail": "Identifique correctamente os gatos.",
- "asirra-create": "Para protexer o wiki contra a creación automática de páxinas, seleccione só as fotos de gatos na caixa:",
- "asirra-nojs": "'''Active o JavaScript e volva enviar a páxina.'''",
- "asirra-failed": "Identifique todas as fotos de gatos"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/he.json b/extensions/ConfirmEdit/i18n/asirra/he.json
deleted file mode 100644
index 7288819a..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/he.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Yona b",
- "ערן"
- ]
- },
- "asirra-desc": "מודול Asirra לאישור עריכה (ConfirmEdit)",
- "asirra-edit": "כדי להגן על הוויקי מעריכות ספאם אוטומטיות, נבקשך לבחור רק את תמונות החתולים בתיבה שלהלן:",
- "asirra-addurl": "העריכה שלך כוללת קישורים חיצוניים חדשים. כדי להגן על הויקי מעריכות ספאם אוטומטיות, נבקשך לבחור רק את תמונות החתולים בתיבה שלהלן:",
- "asirra-badlogin": "כדי להגן על הוויקי מפיצוח אוטומטי של סיסמאות, נבקשך לבחור רק את תמונות החתולים בתיבה שלהלן:",
- "asirra-createaccount": "כדי להגן על הוויקי מפני יצירה אוטומטית של חשבונות, נבקשך לבחור רק את תמונות החתולים בתיבה שלהלן:",
- "asirra-createaccount-fail": "יש לזהות כראוי את החתולים.",
- "asirra-create": "כדי להגן על הוויקי מפני יצירה אוטומטית של דפים, נבקשך לבחור רק את תמונות החתולים בתיבה שלהלן:",
- "asirra-nojs": "'''יש לאפשר JavaScript ולשלוח מחדש את הדף.'''",
- "asirra-failed": "יש לזהות את כל תמונות החתולים"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/hsb.json b/extensions/ConfirmEdit/i18n/asirra/hsb.json
deleted file mode 100644
index 264d31e3..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/hsb.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Michawiki"
- ]
- },
- "asirra-desc": "Modul Asirra za ConfirmEdit",
- "asirra-edit": "Za škit přećiwo awtomatizowanemu spamej, prošu wubjer jenož fota kóčkow w slědowacym polu:",
- "asirra-addurl": "Twoja změna wobsahuje nowe eksterne wotkazy. Za škit přećiwo awtomatizowanemu spamej, prošu wubjer jenož fota kóčkow w slědowacym polu:",
- "asirra-badlogin": "Za škit přećiwo awtomatizowanemu złamanju hesłow, prošu wubjer jenož fota kóčkow w slědowacym polu:",
- "asirra-createaccount": "Za škit přećiwo awtomatiskemu wutworjenju konta, prošu wubjer jenož fota kóčkow w slědowacym polu:",
- "asirra-createaccount-fail": "Prošu identifikuj kóčki.",
- "asirra-create": "Za škit přećiwo awtomatiskemu wutworjenju strony, prošu wubjer jenož fota kóčkow w slědowacym polu:",
- "asirra-nojs": "'''Prošu zmóžń JavaScript a składuj stronu hišće raz.'''",
- "asirra-failed": "Prošu identifikuj wšě wobrazy z kóčkami"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/ia.json b/extensions/ConfirmEdit/i18n/asirra/ia.json
deleted file mode 100644
index ef2a783b..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/ia.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "McDutchie"
- ]
- },
- "asirra-desc": "Modulo de Asirra pro ConfirmEdit",
- "asirra-edit": "Pro adjutar a proteger le wiki contra le spam automatisate, per favor selige solmente le photos de cattos in le quadro sequente:",
- "asirra-addurl": "Iste modification include nove ligamines externe. Pro adjutar a proteger le wiki contra le spam automatisate, per favor selige solmente le photos de cattos in le quadro sequente:",
- "asirra-badlogin": "Pro adjutar a proteger le wiki contra le furto automatisate de contrasignos, per favor selige solmente le photos de cattos in le quadro sequente:",
- "asirra-createaccount": "Pro adjutar a proteger le wiki contra le creation automatisate de contos, per favor selige solmente le photos de cattos in le quadro sequente:",
- "asirra-createaccount-fail": "Per favor identifica correctemente le cattos.",
- "asirra-create": "Pro adjutar a proteger le wiki contra le creation automatisate de paginas, per favor selige solmente le photos de cattos in le quadro sequente:",
- "asirra-nojs": "'''Per favor activa JavaScript e resubmitte le pagina.'''",
- "asirra-failed": "Per favor identifica tote le imagines de cattos"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/it.json b/extensions/ConfirmEdit/i18n/asirra/it.json
deleted file mode 100644
index d70b715f..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/it.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Beta16"
- ]
- },
- "asirra-desc": "Modulo ASIRRA per ConfirmEdit",
- "asirra-edit": "Per proteggere il wiki dalle modifiche automatiche che aggiungono spam, ti chiediamo gentilmente di selezionare solo le foto di gatti nel riquadro sottostante:",
- "asirra-addurl": "La tua modifica aggiunge qualche nuovo collegamento esterno. Per proteggere il wiki dallo spam automatico, ti chiediamo gentilmente di selezionare solo le foto di gatti nel riquadro sottostante:",
- "asirra-badlogin": "Per proteggere il wiki dalla forzatura automatica delle password, ti chiediamo gentilmente di selezionare solo le foto di gatti nel riquadro sottostante:",
- "asirra-createaccount": "Per proteggere il wiki dalla creazione automatica di nuovi accessi, ti chiediamo gentilmente di selezionare solo le foto di gatti nel riquadro sottostante:",
- "asirra-createaccount-fail": "Identifica correttamente i gatti.",
- "asirra-create": "Per proteggere il wiki dalla creazione automatica di pagine, ti chiediamo gentilmente di selezionare solo le foto di gatti nel riquadro sottostante:",
- "asirra-nojs": "'''Attiva JavaScript ed invia di nuovo la pagina.'''",
- "asirra-failed": "Identifica tutte le immagini di gatti"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/ja.json b/extensions/ConfirmEdit/i18n/asirra/ja.json
deleted file mode 100644
index 7c920716..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/ja.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "2nd-player",
- "Shirayuki"
- ]
- },
- "asirra-desc": "ConfirmEdit 用 Asirra モジュール",
- "asirra-edit": "ウィキでの自動編集のスパム攻撃を防ぐため、お手数をおかけしますが猫が写っている画像を以下から選択してください:",
- "asirra-addurl": "あなたは新しい外部リンクを追加しようとしています。ウィキへの自動スパム攻撃を防ぐため、お手数をおかけしますが猫が写っている画像を以下から選択してください:",
- "asirra-badlogin": "ウィキへの自動パスワードクラック攻撃を防ぐため、お手数をおかけしますが猫が写っている画像を以下から選択してください:",
- "asirra-createaccount": "ウィキでのアカウント自動作成を防ぐため、お手数をおかけしますが猫が写っている画像を以下から選択してください:",
- "asirra-createaccount-fail": "猫を正しく選択してください。",
- "asirra-create": "ウィキでのページ自動作成を防ぐため、お手数をおかけしますが猫が写っている画像を以下から選択してください:",
- "asirra-nojs": "'''JavaScript を有効にしてページを再読込してください。'''",
- "asirra-failed": "猫が写っている画像をすべて選択してください"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/ko.json b/extensions/ConfirmEdit/i18n/asirra/ko.json
deleted file mode 100644
index 2c4057c9..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/ko.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Hym411",
- "아라"
- ]
- },
- "asirra-desc": "ConfirmEdit에 대한 Asirra 모듈",
- "asirra-edit": "자동화된 편집 스팸으로부터 보호하기 위해, 아래 상자에 있는 고양이 사진을 선택하세요:",
- "asirra-addurl": "편집에 새로운 바깥 링크가 포함되어 있습니다. 자동화된 스팸으로부터 보호하기 위해, 아래 상자에 있는 고양이 사진을 선택하세요:",
- "asirra-badlogin": "자동화된 비밀번호 크래킹으로부터 보호하기 위해, 아래 상자에 있는 고양이 사진을 선택하세요:",
- "asirra-createaccount": "자동화된 계정 만들기로부터 위키를 보호하기 위해, 아래 상자에 있는 고양이 사진을 선택하세요:",
- "asirra-createaccount-fail": "고양이를 올바르게 선택하세요.",
- "asirra-create": "자동화된 문서 만들기로부터 위키를 보호하기 위해, 아래 상자에 있는 고양이 사진을 선택하세요:",
- "asirra-nojs": "'''자바스크립트를 활성화하고 문서를 다시 제출하세요.'''",
- "asirra-failed": "고양이 그림을 모두 선택하세요"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/ksh.json b/extensions/ConfirmEdit/i18n/asirra/ksh.json
deleted file mode 100644
index be35837d..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/ksh.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Purodha"
- ]
- },
- "asirra-desc": "Dä Zohsaz <i lang=\"en\">Asirra</i> för et Zohsazprojramm <i lang=\"en\">ConfirmEdit</i>.",
- "asirra-edit": "Heh dat Wiki well sesch jääje <i lang=\"en\">SPAM</i> schöze. Dröm moß mer beim Ändere noch en Prööfong aflääje, dat mer ene Minsch un kei Projramm es. Söhk bloß de Katzebelder em Kaßte us:",
- "asirra-addurl": "Heh dat Wiki well sesch jääje <i lang=\"en\">SPAM</i> schöze. Dröm moß mer, wam_mer lengks noh ußerhallef enfööje well, noch en Prööfong aflääje, dat mer ene Minsch un kei Projramm es. Söhk bloß de Katzebelder em Kaßte us:",
- "asirra-badlogin": "Heh dat Wiki well sesch jääje et automattesche Paßwoot_Knacke schöze. Dröm moß mer heh nor_en Prööfong aflääje, dat mer ene Minsch un kei Projramm es. Söhk bloß de Katzebelder em Kaßte us:",
- "asirra-createaccount": "Heh dat Wiki well sesch jääje automattesch aanjelaate „Metmaacher“ schöze. Dröm moß mer heh nor_en Prööfong aflääje, dat mer ene Minsch un kei Projramm es. Söhk bloß de Katzebelder em Kaßte us:",
- "asirra-createaccount-fail": "Bes esu jood un don de Kazebelder ußwähle.",
- "asirra-create": "Heh dat Wiki well sesch jääje automattesch neu aanjelaate Sigge schöze. Dröm moß mer heh nor_en Prööfong aflääje, dat mer ene Minsch un kei Projramm es. Söhk bloß de Katzebelder em Kaßte us:",
- "asirra-nojs": "'''Bes esu jood un donn JavaSkrep en Dingem Brauser aanschallde un scheck heh di Sigg norr_ens af.'''",
- "asirra-failed": "Bes esu jood un don all de Kazebelder ußwähle."
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/lb.json b/extensions/ConfirmEdit/i18n/asirra/lb.json
deleted file mode 100644
index 42d266b8..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/lb.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Robby"
- ]
- },
- "asirra-desc": "Asirra-Modul fir ConfirmEdit",
- "asirra-edit": "Fir d'Wiki géint automatiséierte Spam ze schützen froe mir Iech just d'Fotoe mat Kazen, déi Dir an der Këscht ënnendrënner gesitt, erauszesichen:",
- "asirra-addurl": "An Ärer Ännerung sinn nei extern Linken. Fir d'Wiki géint automatiséierte Spam ze schützen, froe mir Iech d'Kategorie vun de Fotoen an der Këscht ënnendrënner erauszesichen:",
- "asirra-createaccount-fail": "Identifizéiert d'Kaze w.e.g. richteg.",
- "asirra-nojs": "'''Aktivéiert w.e.g. JavaScript a schéckt d'Säit nachemol.'''",
- "asirra-failed": "Identifizéiert w.e.g. all Biller wou Kazen drop sinn"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/mk.json b/extensions/ConfirmEdit/i18n/asirra/mk.json
deleted file mode 100644
index fe6c6a8e..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/mk.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Bjankuloski06"
- ]
- },
- "asirra-desc": "Asirra-модул за ПотврдиУредување",
- "asirra-edit": "Како заштитна мерка против автоматизиран спам, би ве замолиле да ги изберете само сликите со мачка прикажани во полето:",
- "asirra-addurl": "Во вашите измени има нови надворешни врски. Како заштитна мерка против автоматизиран спам, би ве замолиле да ги изберете само сликите со мачка прикажани во полето:",
- "asirra-badlogin": "Како заштитна мерка против автоматизирано провалување на лозинки, би ве замолиле да ги изберете само сликите со мачка прикажани во полето:",
- "asirra-createaccount": "Како заштитна мерка против автоматизирано создавање на сметки, би ве замолиле да ги изберете само сликите со мачка прикажани во полето:",
- "asirra-createaccount-fail": "Посочете кои од следниве се мачки.",
- "asirra-create": "Како заштитна мерка против автоматизирано создавање на страници, би ве замолиле да ги изберете само сликите со мачка прикажани во полето:",
- "asirra-nojs": "'''Овозможете JavaScript и поднесете ја страницата повторно.'''",
- "asirra-failed": "Изберете ги сликите што имаат мачка"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/ms.json b/extensions/ConfirmEdit/i18n/asirra/ms.json
deleted file mode 100644
index 71afc754..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/ms.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Anakmalaysia"
- ]
- },
- "asirra-desc": "Modul Asirra untuk ConfirmEdit",
- "asirra-edit": "Untuk mencegah suntingan spam automatik, sila pilih gambar-gambar kucing sahaja dalam petak di bawah:",
- "asirra-addurl": "Suntingan anda mengandungi pautan luar yang baru. Untuk mencegah spam janaan automatik, sila pilih gambar-gambar kucing sahaja dalam petak di bawah:",
- "asirra-badlogin": "Untuk mencegah pemecahan kata laluan automatik, sila pilih gambar-gambar kucing sahaja dalam petak di bawah:",
- "asirra-createaccount": "Untuk mencegah pembukaan akaun automatik, sila pilih gambar-gambar kucing sahaja dalam petak di bawah:",
- "asirra-createaccount-fail": "Sila kenal pasti kucing-kucing dengan betul.",
- "asirra-create": "Untuk mencegah pembukaan halaman automatik, sila pilih gambar-gambar kucing sahaja dalam petak di bawah:",
- "asirra-nojs": "'''Sila hidupkan JavaScript dan hantar semula halaman ini.'''",
- "asirra-failed": "Sila kenal pasti semua gambar kucing"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/mt.json b/extensions/ConfirmEdit/i18n/asirra/mt.json
deleted file mode 100644
index d57a1350..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/mt.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Chrisportelli"
- ]
- },
- "asirra-desc": "Modulu ASIRRA għal ConfirmEdit",
- "asirra-edit": "Sabiex tgħinna nipproteġu kontra l-modifiki li jżidu spam, jekk jogħġbok agħżel ir-ritratti tal-qtates fil-kaxxa t'hawn taħt:",
- "asirra-addurl": "Il-modifika tiegħek tinkludi ħoloq esterni ġodda. Sabiex tipproteġi kontra spam awtomatiku, jekk jogħġbok agħżel ir-ritratti tal-qtates fil-kaxxa t'hawn taħt:",
- "asirra-badlogin": "Sabiex tgħinna nipproteġu kontra l-infurzar awtomatiku tal-passwords, jekk jogħġbok agħżel ir-ritratti tal-qtates fil-kaxxa t'hawn taħt:",
- "asirra-createaccount": "Sabiex tgħinna nipproteġu kontra l-ħolqien awtomatiku ta' kontijiet ġodda, jekk jogħġbok agħżel ir-ritratti tal-qtates fil-kaxxa t'hawn taħt:",
- "asirra-createaccount-fail": "Sib il-qtates.",
- "asirra-create": "Sabiex tgħinna nipproteġu kontra l-ħolqien awtomatiku ta' paġni, jekk jogħġbok agħżel ir-ritratti tal-qtates fil-kaxxa t'hawn taħt:",
- "asirra-nojs": "'''Jekk jogħġbok attiva l-JavaScript u erġa' ibgħat din il-paġna.'''",
- "asirra-failed": "Sib l-istampi kollha tal-qtates"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/nb.json b/extensions/ConfirmEdit/i18n/asirra/nb.json
deleted file mode 100644
index ba30c700..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/nb.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Event"
- ]
- },
- "asirra-desc": "Assirra-modulen for ConfirmEdit",
- "asirra-edit": "Som beskyttelse mot automatisk redigert spam, vennligst velg kun kattebildene i boksen under:",
- "asirra-addurl": "Din redigering inneholder nye eksterne lenker. Som beskyttelse mot automatisk redigert spam, vennligst velg kun kattebildene i boksen under:",
- "asirra-badlogin": "Som beskyttelse mot automatisk passordknekking, vennligst velg kun kattebildene i boksen under:",
- "asirra-createaccount": "Som beskyttelse mot automatisk opprettelse av brukerkonto, vennligst velg kun kattebildene i boksen under:",
- "asirra-createaccount-fail": "Vennligst angi hva som er katter.",
- "asirra-create": "Som beskyttelse mot automatisk opprettelse av sider, vennligst velg kun kattebildene i boksen under:",
- "asirra-nojs": "'''Vennligst åpne for JavaScript og lagre siden en gang til.'''",
- "asirra-failed": "Vennligst merk alle kattebilder"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/nl-informal.json b/extensions/ConfirmEdit/i18n/asirra/nl-informal.json
deleted file mode 100644
index c3888b03..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/nl-informal.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Siebrand"
- ]
- },
- "asirra-addurl": "Je bewerking bevat nieuwe externe koppelingen. Selecteer de foto's van katten in het vak hieronder om te helpen beschermen tegen geautomatiseerde spam:"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/nl.json b/extensions/ConfirmEdit/i18n/asirra/nl.json
deleted file mode 100644
index ca9b5b04..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/nl.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "HanV",
- "SPQRobin",
- "Siebrand"
- ]
- },
- "asirra-desc": "Asirra-module voor ConfirmEdit",
- "asirra-edit": "Kies ter bescherming tegen geautomatiseerde spam de afbeeldingen met een poes in het onderstaande venster:",
- "asirra-addurl": "Uw bewerking bevat nieuwe externe koppelingen. Selecteer de foto's van katten in het vak hieronder om de wiki te beschermen tegen geautomatiseerde spam:",
- "asirra-badlogin": "Kies ter bescherming tegen het automatisch kraken van wachtwoorden de afbeeldingen met een poes in het onderstaande venster:",
- "asirra-createaccount": "Kies om het automatisch aanmaken van gebruikers tegen te gaan de afbeeldingen met een poes in het onderstaande venster:",
- "asirra-createaccount-fail": "Identificeer de katten juist.",
- "asirra-create": "Kies om het automatisch aanmaken van een pagina tegen te gaan de afbeeldingen met een poes in het onderstaande venster:",
- "asirra-nojs": "'''Schakel JavaScript in en probeer de pagina opnieuw op te slaan.'''",
- "asirra-failed": "Identificeer alle afbeeldingen van katten."
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/oc.json b/extensions/ConfirmEdit/i18n/asirra/oc.json
deleted file mode 100644
index 807ef432..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/oc.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Cedric31"
- ]
- },
- "asirra-desc": "Modul Asirra per ConfirmEdit",
- "asirra-createaccount-fail": "Identificatz corrèctament los gats."
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/pl.json b/extensions/ConfirmEdit/i18n/asirra/pl.json
deleted file mode 100644
index 9590fd84..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/pl.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "BeginaFelicysym",
- "WTM"
- ]
- },
- "asirra-desc": "Moduł Asirra dla ConfirmEdit",
- "asirra-edit": "W celu ochrony przed zautomatyzowanym spamem, proszę wybrać tylko zdjęcia kotów w poniższym polu:",
- "asirra-addurl": "Wprowadzony przez ciebie tekst zawiera nowe linki zewnętrzne. W celu ochrony przed zautomatyzowanym spamem, proszę wskazać tylko zdjęcia kotów w poniższym polu:",
- "asirra-badlogin": "W celu ochrony przed zautomatyzowanym łamaniem haseł, proszę wybrać tylko zdjęcia kotów w poniższym polu:",
- "asirra-createaccount": "W celu ochrony przed zautomatyzowanym tworzeniem kont, proszę wybrać tylko zdjęcia kotów w poniższym polu:",
- "asirra-createaccount-fail": "Prosimy prawidłowo zidentyfikować koty.",
- "asirra-create": "W celu ochrony przed przed automatycznym tworzeniem stron, proszę wybrać tylko zdjęcia kotów w poniższym polu:",
- "asirra-nojs": "'''Prosimy włączyć obsługę języka JavaScript i ponowne przesłanie strony.'''",
- "asirra-failed": "Prosimy wskazać wszystkie obrazy kotów"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/pms.json b/extensions/ConfirmEdit/i18n/asirra/pms.json
deleted file mode 100644
index 6440c6ec..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/pms.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Borichèt",
- "Dragonòt"
- ]
- },
- "asirra-desc": "Mòdul Asirra për ConfirmEdit",
- "asirra-edit": "Për giuté a protege contra la rumenta dle modìfiche automàtiche, për piasì ch'a selession-a mach le fòto ëd gat ant ël quàder sì-sota:",
- "asirra-addurl": "Soa modìfica a conten dle liure esterne neuve. Për giuté a protege contra la rumenta dle modìfiche automàtiche, për piasì ch'a selession-a mach le fòto ëd gat ant ël quàder sì-sota:",
- "asirra-badlogin": "Për giuté a protege contra la forsadura automatisà ëd le ciav, për piasì ch'a selession-a mach la fòto dël gat ant ël quàder sì-sota:",
- "asirra-createaccount": "Për giuté a protege contra la creassion automatisà ëd cont, për piasì ch'a selession-a mach la fòto dël gat ant ël quàder sì-sota:",
- "asirra-createaccount-fail": "Për piasì identifica coretament ij gat.",
- "asirra-create": "Për giuté a protege contra la creassion automatisà ëd pàgine, për piasì ch'a selession-a mach le fòto ëd gat ant ël quàder sì-sota:",
- "asirra-nojs": "'''Për piasì, ch'a abìlita JavaScript e ch'a spedissa torna la pàgina.'''",
- "asirra-failed": "Për piasì identìfica tute le figure ëd gat"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/pt.json b/extensions/ConfirmEdit/i18n/asirra/pt.json
deleted file mode 100644
index 75c660d4..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/pt.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Hamilton Abreu",
- "Luckas"
- ]
- },
- "asirra-desc": "Módulo Asirra para o ConfirmEdit",
- "asirra-edit": "Para proteger a wiki contra sistemas automatizados de inserção de ''spam'', pedimos que selecione só as fotografias de gatos na caixa abaixo:",
- "asirra-addurl": "A sua edição contém links externos novos. Para proteger a wiki contra sistemas automatizados de inserção de ''spam'', pedimos que selecione só as fotografias de gatos na caixa abaixo:",
- "asirra-badlogin": "Para proteger a wiki contra sistemas automatizados de descoberta de palavras-chave, pedimos que selecione só as fotografias de gatos na caixa abaixo:",
- "asirra-createaccount": "Para proteger a wiki contra sistemas automatizados de criação de contas, pedimos que selecione só as fotografias de gatos na caixa abaixo:",
- "asirra-createaccount-fail": "Identifique corretamente os gatos, por favor.",
- "asirra-create": "Para proteger a wiki contra sistemas automatizados de criação de páginas, pedimos que selecione só as fotografias de gatos na caixa abaixo:",
- "asirra-nojs": "'''Possibilite o uso de JavaScript e reenvie a página, por favor.'''",
- "asirra-failed": "Identifique todas as imagens de gatos, por favor"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/qqq.json b/extensions/ConfirmEdit/i18n/asirra/qqq.json
deleted file mode 100644
index 4cb78990..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/qqq.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "2nd-player",
- "Beta16",
- "Raymond",
- "Shirayuki"
- ]
- },
- "asirra-desc": "{{desc|name=Asirra|url=http://www.mediawiki.org/wiki/Extension:Asirra}}",
- "asirra-edit": "{{Related|ConfirmEdit-edit}}",
- "asirra-addurl": "{{Related|ConfirmEdit-addurl}}",
- "asirra-badlogin": "{{Related|ConfirmEdit-badlogin}}",
- "asirra-createaccount": "{{Related|ConfirmEdit-createaccount}}",
- "asirra-createaccount-fail": "Used as failure message in JavaScript code.\n{{Related|ConfirmEdit-createaccount-fail}}",
- "asirra-create": "{{Related|ConfirmEdit-create}}",
- "asirra-nojs": "Used in HTML <code><nowiki><noscript></nowiki></code> tag.",
- "asirra-failed": "Used as alert message in JavaScript code."
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/roa-tara.json b/extensions/ConfirmEdit/i18n/asirra/roa-tara.json
deleted file mode 100644
index e55f55ec..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/roa-tara.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Joetaras"
- ]
- },
- "asirra-desc": "Module Asirra pe confermà le cangiaminde",
- "asirra-edit": "Pe proteggere condre le cangiaminde automatece de le rummate, pe piacere scacchie 'a categorije de le fote jndr'à buatte aqquà sotte:",
- "asirra-addurl": "Le cangiaminde tune 'ngludone collegaminde de fore nuève. Pe proteggere condre le cangiaminde automatece de le rummate, pe piacere scacchie 'a categorije d'a fote 'ndruche jndr'à buatte aqquà sotte:",
- "asirra-badlogin": "Pe proteggere condre le futteminde automatece de le passuord, pe piacere scacchie 'a categorije de le fote jndr'à buatte aqquà sotte:",
- "asirra-createaccount": "Pe proteggere condre le ccrejaziune automatece de le cunde, pe piacere scacchie 'a categorije de le fote jndr'à buatte aqquà sotte:",
- "asirra-createaccount-fail": "Pe piacere idendifiche correttamende le categorije.",
- "asirra-create": "Pe proteggere condre le ccrejaziune automatece de le pàggene, pe piacere scacchie 'a categorije de le fote jndr'à buatte aqquà sotte:",
- "asirra-nojs": "'''Pe piacere abbilite JavaScript e conferme arrete 'a pàgene.'''",
- "asirra-failed": "Pe piacere idendifiche tutte le categorije de le immaggine"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/ru.json b/extensions/ConfirmEdit/i18n/asirra/ru.json
deleted file mode 100644
index d628aec8..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/ru.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "DCamer",
- "Lockal",
- "Okras"
- ]
- },
- "asirra-desc": "Модуль Asirra для ConfirmEdit",
- "asirra-edit": "В целях защиты проекта от автоматического спама в правках, просим вас выбрать среди нижеприведённых изображений только фотографии кошек:",
- "asirra-addurl": "Ваша правка содержит новые внешние ссылки. В целях защиты проекта от автоматического спама в правках просим вас выбрать среди нижеприведённых изображений только фотографии кошек:",
- "asirra-badlogin": "В целях защиты от автоматического подбора пароля просим вас выбрать среди нижеприведённых изображений только фотографии кошек:",
- "asirra-createaccount": "В целях защиты от автоматического создания учётных записей просим вас выбрать среди нижеприведённых изображений только фотографии кошек:",
- "asirra-createaccount-fail": "Пожалуйста правильно идентифицируйте котов.",
- "asirra-create": "В целях защиты от автоматического создания страниц просим вас выбрать среди нижеприведённых изображений только фотографии кошек:",
- "asirra-nojs": "'''Пожалуйста, включите JavaScript и обновите страницу.'''",
- "asirra-failed": "Пожалуйста, идентифицируйте все фотографии кота"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/si.json b/extensions/ConfirmEdit/i18n/asirra/si.json
deleted file mode 100644
index bd853b93..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/si.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "පසිඳු කාවින්ද"
- ]
- },
- "asirra-desc": "ConfirmEdit සඳහා Asirra මොඩියුලය",
- "asirra-failed": "කරුණාකර සියලුම cat පින්තූරයන් හඳුනාගන්න"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/sv.json b/extensions/ConfirmEdit/i18n/asirra/sv.json
deleted file mode 100644
index a99278b2..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/sv.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Jopparn",
- "Rotsee",
- "Tobulos1",
- "WikiPhoenix",
- "Lokal Profil"
- ]
- },
- "asirra-desc": "Asirra-modul för ConfirmEdit",
- "asirra-edit": "För att skydda wikin mot spam ber vi dig att markera de fotografier som föreställer katter i rutan nedan:",
- "asirra-addurl": "Din redigering innehåller nya externa länkar. För att skydda wikin mot automatiserat redigerings-spam ber vi dig att endast markera fotografierna på katter i rutan nedan:",
- "asirra-badlogin": "För att skydda wikin mot automatiserade försök att knäcka lösenord ber vi dig att endast markera fotografierna på katter i rutan nedan:",
- "asirra-createaccount": "För att skydda wikin mot automatiserat kontoskapande ber vi dig att endast markera fotografierna på katter i rutan nedan:",
- "asirra-createaccount-fail": "Vänligen identifiera katterna korrekt.",
- "asirra-create": "För att skydda wikin mot automatiserat sidskapande ber vi dig att endast markera fotografierna på katter i rutan nedan:",
- "asirra-nojs": "'''Var god aktivera JavaScript och hämta sidan igen.'''",
- "asirra-failed": "Var god identifiera alla kattbilder"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/tl.json b/extensions/ConfirmEdit/i18n/asirra/tl.json
deleted file mode 100644
index 9f0bb4bd..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/tl.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "AnakngAraw"
- ]
- },
- "asirra-desc": "Modyul ng Asirra para sa ConfirmEdit",
- "asirra-edit": "Upang makatulong sa pagprutekta laban sa kusang basurang pamamatnugot, paki piliin iyong mga litrato lamang ng pusa na nasa loob ng kahong nasa ibaba:",
- "asirra-addurl": "Ang pagbabago mo ay nagsasama ng bagong panlabas na mga kawing. Upang makatulong sa pagprutekta laban sa kusang paglusob ng basurang-liham, paki piliin iyong mga litrato lamang ng pusa na nasa loob ng kahong nasa ibaba:",
- "asirra-badlogin": "Upang makatulong sa pagprutekta laban sa kusang pag-alam ng hudyat, paki piliin lamang iyong mga litrato ng pusa na nasa loob ng kahong nasa ibaba:",
- "asirra-createaccount": "Upang makatulong sa pagprutekta laban sa kusang paglikha ng akawnt, paki piliin lamang iyong mga litrato ng pusa na nasa loob ng kahong nasa ibaba:",
- "asirra-createaccount-fail": "Paki kilalanin ng tama ang mga pusa.",
- "asirra-create": "Upang makatulong sa pagprutekta laban sa kusang paglikha ng pahina, paki piliin lamang iyong mga litrato ng pusa na nasa loob ng kahong nasa ibaba:",
- "asirra-nojs": "'''Paki paganahin ang JavaScript at muling ipasa ang pahina.'''",
- "asirra-failed": "Paki kilalanin ang lahat ng mga imahe ng pusa"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/uk.json b/extensions/ConfirmEdit/i18n/asirra/uk.json
deleted file mode 100644
index 6438a504..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/uk.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Andriykopanytsia",
- "Base"
- ]
- },
- "asirra-desc": "Модуль Asirra для ConfirmEdit",
- "asirra-edit": "З метою захисту вікі від автоматичного спаму у статтях, просимо вас обрати фотографії кота, у блоці нижче:",
- "asirra-addurl": "Ваше повідомлення включає нові зовнішні посилання. З метою захисту вікі проти автоматичного спаму у статтях, просимо вас обрати фотографії кота, у блоці нижче:",
- "asirra-badlogin": "З метою захисту вікі проти автоматичного підбору паролю, просимо вас обрати фотографії кота у блоці нижче:",
- "asirra-createaccount": "З метою захисту вікі проти автоматичного створення облікових записів просимо вас обрати фотографії кота у блоці нижче:",
- "asirra-createaccount-fail": "Будь ласка, правильно ідентифікуйте котів.",
- "asirra-create": "З метою захисту вікі проти автоматичного створення статей просимо вас обрати фотографії кота у блоці нижче:",
- "asirra-nojs": "'''Будь ласка увімкніть JavaScript і оновіть сторінку.'''",
- "asirra-failed": "Будь ласка, ідентифікуйте усі фотографії кота"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/wa.json b/extensions/ConfirmEdit/i18n/asirra/wa.json
deleted file mode 100644
index 6dc5a815..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/wa.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Srtxg"
- ]
- },
- "asirra-desc": "Module Asirra pol passete d' acertinaedje des candjmints (ConfirmEdit)",
- "asirra-edit": "Po s' mete a houte des des robots di spam, nos vs dimandans d' acertiner ki vos estoz bén ene djin, po çoula tchoezixhoz seulmint les imådjes avou des tchets e l' boesse chal pa dzo:",
- "asirra-addurl": "Dins vos candjmints i gn a des dfoûtrinnès hårdêyes (URL).\nPo s' mete a houte des des robots di spam, nos vs dimandans d' acertiner ki vos estoz bén ene djin, po çoula tchoezixhoz seulmint les imådjes avou des tchets e l' boesse chal pa dzo:",
- "asirra-badlogin": "Po s' mete a houte des des robots ki sayèt d' adviner les screts, nos vs dimandans d' acertiner ki vos estoz bén ene djin, po çoula tchoezixhoz seulmint les imådjes avou des tchets e l' boesse chal pa dzo:",
- "asirra-createaccount": "Po s' mete a houte des des robots k' ahivèt des contes otomaticmint, nos vs dimandans d' acertiner ki vos estoz bén ene djin, po çoula tchoezixhoz seulmint les imådjes avou des tchets e l' boesse chal pa dzo:",
- "asirra-createaccount-fail": "Tchoezixhoz comifåt les tchets (les biesses ki gnawèt).",
- "asirra-create": "Po s' mete a houte des des robots k' ahivèt des pådjes otomaticmint, nos vs dimandans d' acertiner ki vos estoz bén ene djin, po çoula tchoezixhoz seulmint les imådjes avou des tchets e l' boesse chal pa dzo:",
- "asirra-nojs": "'''Metoz s' i vs plait en alaedje li JavaScrit et s' revoyî l' pådje.'''",
- "asirra-failed": "Idintifyî totes les imådjes avou des tchets"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/zh-hans.json b/extensions/ConfirmEdit/i18n/asirra/zh-hans.json
deleted file mode 100644
index 1675e6ae..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/zh-hans.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Fantasticfears",
- "Hzy980512",
- "Mywood"
- ]
- },
- "asirra-desc": "用于确认编辑的Asirra模块",
- "asirra-edit": "为保护本wiki免受自动垃圾程序的破坏,我们恳请你在下面的方框中选出猫的图片:",
- "asirra-addurl": "你的编辑包含新的外部链接。为保护本wiki免受自动垃圾程序的破坏,我们恳请你在下面的方框中选出猫的图片:",
- "asirra-badlogin": "为保护本wiki免受自动密码破解的破坏,我们恳请你在下面的方框中选出猫的图片:",
- "asirra-createaccount": "为保护本wiki免受自动账户创建的破坏,我们恳请你在下面的方框中选出猫的图片:",
- "asirra-createaccount-fail": "请选出正确的猫的图片。",
- "asirra-create": "为保护本wiki免受自动页面创建的破坏,我们恳请你在下面的方框中选出猫的图片:",
- "asirra-nojs": "'''请启用JavaScript并重新提交页面。'''",
- "asirra-failed": "请选出所有猫的图片"
-}
diff --git a/extensions/ConfirmEdit/i18n/asirra/zh-hant.json b/extensions/ConfirmEdit/i18n/asirra/zh-hant.json
deleted file mode 100644
index 39bbdf63..00000000
--- a/extensions/ConfirmEdit/i18n/asirra/zh-hant.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Justincheng12345",
- "Cwlin0416"
- ]
- },
- "asirra-desc": "ConfirmEdit 的 Asirra 模組",
- "asirra-edit": "為了防止垃圾編輯程式,我們要麻煩您在下面的方框中選出貓的圖片:",
- "asirra-addurl": "您的編輯使用了新的外部連結。為了防止垃圾編輯程式,我們要麻煩您在下面的方框中選出貓的圖片:",
- "asirra-badlogin": "為了防止密碼破解程式,我們要麻煩您在下面的方框中選出貓的圖片:",
- "asirra-createaccount": "為了防止自動註冊程式,我們要麻煩您在下面的方框中選出貓的圖片:",
- "asirra-createaccount-fail": "請再正確選擇貓的圖片。",
- "asirra-create": "為了防止自動建立頁面程式,我們要麻煩您在下面的方框中選出貓的圖片:",
- "asirra-nojs": "'''請開啟 JavaScript 並重新送出頁面。'''",
- "asirra-failed": "請選出所有貓的圖片"
-}
diff --git a/extensions/ConfirmEdit/resources/ext.confirmEdit.asirra.js b/extensions/ConfirmEdit/resources/ext.confirmEdit.asirra.js
deleted file mode 100644
index 34296d03..00000000
--- a/extensions/ConfirmEdit/resources/ext.confirmEdit.asirra.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/*======================================================================*\
-|| #################################################################### ||
-|| # Asirra module for ConfirmEdit by Bachsau # ||
-|| # ---------------------------------------------------------------- # ||
-|| # This code is released into public domain, in the hope that it # ||
-|| # will be useful, but without any warranty. # ||
-|| # ------------ YOU CAN DO WITH IT WHATEVER YOU LIKE! ------------- # ||
-|| #################################################################### ||
-\*======================================================================*/
-
-jQuery( function( $ ) {
- // Selectors for create account, login, and page edit forms.
- var asirraform = $( 'form#userlogin2, #userloginForm form, form#editform' );
- var submitButtonClicked = document.createElement("input");
- var passThroughFormSubmit = false;
-
- function PrepareSubmit() {
- submitButtonClicked.type = "hidden";
- var inputFields = asirraform.find( "input" );
- for (var i=0; i<inputFields.length; i++) {
- if (inputFields[i].type === "submit") {
- inputFields[i].onclick = function(event) {
- submitButtonClicked.name = this.name;
- submitButtonClicked.value = this.value;
- }
- }
- }
-
- asirraform.submit( function() {
- return MySubmitForm();
- } );
- }
-
- function MySubmitForm() {
- if (passThroughFormSubmit) {
- return true;
- }
- Asirra_CheckIfHuman(HumanCheckComplete);
- return false;
- }
-
- function HumanCheckComplete(isHuman) {
- if (!isHuman) {
- window.alert( mediaWiki.msg( 'asirra-failed' ) );
- } else {
- asirraform.append(submitButtonClicked);
- passThroughFormSubmit = true;
- asirraform.submit();
- }
- }
-
- PrepareSubmit();
-
-} );
diff --git a/extensions/Gadgets/tests/GadgetTest.php b/extensions/Gadgets/tests/GadgetTest.php
new file mode 100644
index 00000000..8fd09295
--- /dev/null
+++ b/extensions/Gadgets/tests/GadgetTest.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * @group Gadgets
+ */
+
+class GadgetsTest extends MediaWikiTestCase {
+ private function create( $line ) {
+ $g = Gadget::newFromDefinition( $line );
+ $this->assertInstanceOf( 'Gadget', $g );
+
+ return $g;
+ }
+
+ function testInvalidLines() {
+ $this->assertFalse( Gadget::newFromDefinition( '' ) );
+ $this->assertFalse( Gadget::newFromDefinition( '<foo|bar>' ) );
+ }
+
+ function testSimpleCases() {
+ $g = $this->create( '* foo bar| foo.css|foo.js|foo.bar' );
+ $this->assertEquals( 'foo_bar', $g->getName() );
+ $this->assertEquals( 'ext.gadget.foo_bar', $g->getModuleName() );
+ $this->assertEquals( array( 'Gadget-foo.js' ), $g->getScripts() );
+ $this->assertEquals( array( 'Gadget-foo.css' ), $g->getStyles() );
+ $this->assertEquals( array( 'Gadget-foo.js', 'Gadget-foo.css' ),
+ $g->getScriptsAndStyles() );
+ $this->assertEquals( array( 'Gadget-foo.js' ), $g->getLegacyScripts() );
+ $this->assertFalse( $g->supportsResourceLoader() );
+ $this->assertTrue( $g->hasModule() );
+ }
+
+ function testRLtag() {
+ $g = $this->create( '*foo [ResourceLoader]|foo.js|foo.css' );
+ $this->assertEquals( 'foo', $g->getName() );
+ $this->assertTrue( $g->supportsResourceLoader() );
+ $this->assertEquals( 0, count( $g->getLegacyScripts() ) );
+ }
+
+ function testDependencies() {
+ $g = $this->create( '* foo[ResourceLoader|dependencies=jquery.ui]|bar.js' );
+ $this->assertEquals( array( 'Gadget-bar.js' ), $g->getScripts() );
+ $this->assertTrue( $g->supportsResourceLoader() );
+ $this->assertEquals( array( 'jquery.ui' ), $g->getDependencies() );
+ }
+
+ function testPreferences() {
+ $prefs = array();
+
+ Gadget::loadStructuredList( '* foo | foo.js
+==keep-section1==
+* bar| bar.js
+==remove-section==
+* baz [rights=embezzle] |baz.js
+==keep-section2==
+* quux [rights=read] | quux.js' );
+ $this->assertTrue( GadgetHooks::getPreferences( new User, $prefs ), 'GetPrefences hook should return true' );
+
+ $options = $prefs['gadgets']['options'];
+ $this->assertFalse( isset( $options['&lt;gadget-section-remove-section&gt;'] ), 'Must not show empty sections' );
+ $this->assertTrue( isset( $options['&lt;gadget-section-keep-section1&gt;'] ) );
+ $this->assertTrue( isset( $options['&lt;gadget-section-keep-section2&gt;'] ) );
+ }
+}
diff --git a/extensions/LocalisationUpdate/tests/phpunit/Makefile b/extensions/LocalisationUpdate/tests/phpunit/Makefile
new file mode 100644
index 00000000..e98c12ca
--- /dev/null
+++ b/extensions/LocalisationUpdate/tests/phpunit/Makefile
@@ -0,0 +1,12 @@
+ifndef MW_INSTALL_PATH
+ MW_INSTALL_PATH=../../../..
+endif
+
+DIRS=reader
+
+default:
+ php ${MW_INSTALL_PATH}/tests/phpunit/phpunit.php .
+
+.PHONY: *Test.php $(DIRS)
+*Test.php $(DIRS):
+ php ${MW_INSTALL_PATH}/tests/phpunit/phpunit.php $@
diff --git a/extensions/LocalisationUpdate/tests/phpunit/UpdaterTest.php b/extensions/LocalisationUpdate/tests/phpunit/UpdaterTest.php
new file mode 100644
index 00000000..ce742cba
--- /dev/null
+++ b/extensions/LocalisationUpdate/tests/phpunit/UpdaterTest.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+class LU_UpdaterTest extends MediaWikiTestCase {
+ public function testIsDirectory() {
+ $updater = new LU_Updater();
+
+ $this->assertTrue(
+ $updater->isDirectory( '/IP/extensions/Translate/i18n/*.json' ),
+ 'Extension json files are a file pattern'
+ );
+
+ $this->assertFalse(
+ $updater->isDirectory( '/IP/extensions/Translate/Translate.i18n.php' ),
+ 'Extension php file is not a pattern'
+ );
+ }
+
+ public function testExpandRemotePath() {
+ $updater = new LU_Updater();
+ $repos = array( 'main' => 'file:///repos/%NAME%/%SOME-VAR%' );
+
+ $info = array(
+ 'repo' => 'main',
+ 'name' => 'product',
+ 'some-var' => 'file',
+ );
+ $this->assertEquals(
+ 'file:///repos/product/file',
+ $updater->expandRemotePath( $info, $repos ),
+ 'Variables are expanded correctly'
+ );
+ }
+
+ public function testReadMessages() {
+ $updater = $updater = new LU_Updater();
+
+ $input = array( 'file' => 'Hello World!' );
+ $output = array( 'en' => array( 'key' => $input['file'] ) );
+
+ $reader = $this->getMock( 'LU_Reader' );
+ $reader
+ ->expects( $this->once() )
+ ->method( 'parse' )
+ ->will( $this->returnValue( $output ) );
+
+ $factory = $this->getMock( 'LU_ReaderFactory' );
+ $factory
+ ->expects( $this->once() )
+ ->method( 'getReader' )
+ ->will( $this->returnValue( $reader ) );
+
+ $observed = $updater->readMessages( $factory, $input );
+ $this->assertEquals( $output, $observed, 'Tries to parse given file' );
+ }
+
+ public function testFindChangedTranslations() {
+ $updater = $updater = new LU_Updater();
+
+ $origin = array(
+ 'A' => '1',
+ 'C' => '3',
+ 'D' => '4',
+ );
+ $remote = array(
+ 'A' => '1', // No change key
+ 'B' => '2', // New key
+ 'C' => '33', // Changed key
+ 'D' => '44', // Blacklisted key
+ );
+ $blacklist = array( 'D' => 0 );
+ $expected = array( 'B' => '2', 'C' => '33' );
+ $observed = $updater->findChangedTranslations( $origin, $remote, $blacklist );
+ $this->assertEquals( $expected, $observed, 'Changed and new keys returned' );
+ }
+}
diff --git a/extensions/LocalisationUpdate/tests/phpunit/finder/FinderTest.php b/extensions/LocalisationUpdate/tests/phpunit/finder/FinderTest.php
new file mode 100644
index 00000000..8cc0f7d7
--- /dev/null
+++ b/extensions/LocalisationUpdate/tests/phpunit/finder/FinderTest.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+class LU_FinderTest extends MediaWikiTestCase {
+ public function testGetComponents() {
+ $finder = new LU_Finder(
+ array(
+ 'TranslateSearch' => '/IP/extensions/Translate/TranslateSearch.i18n.php',
+ 'Babel' => '/IP/extensions/Babel/Babel.i18n.php',
+ ),
+ array(
+ 'Babel' => '/IP/extensions/Babel/i18n',
+ 'Door' => array(
+ 'core' => '/IP/extensions/Door/i18n/core',
+ 'extra' => '/IP/extensions/Door/i18n/extra',
+ ),
+ ),
+ '/IP'
+ );
+ $observed = $finder->getComponents();
+
+ $expected = array(
+ 'repo' => 'mediawiki',
+ 'orig' => "file:///IP/languages/messages/Messages*.php",
+ 'path' => 'languages/messages/Messages*.php',
+ );
+ $this->assertArrayHasKey( 'core', $observed );
+ $this->assertSame( $expected, $observed['core'], 'Core php file' );
+
+ $expected = array(
+ 'repo' => 'extension',
+ 'name' => 'Translate',
+ 'orig' => 'file:///IP/extensions/Translate/TranslateSearch.i18n.php',
+ 'path' => 'TranslateSearch.i18n.php'
+ );
+ $this->assertArrayHasKey( 'TranslateSearch', $observed );
+ $this->assertSame( $expected, $observed['TranslateSearch'], 'PHP only extension' );
+
+ $expected = array(
+ 'repo' => 'extension',
+ 'name' => 'Babel',
+ 'orig' => 'file:///IP/extensions/Babel/i18n/*.json',
+ 'path' => 'i18n/*.json'
+ );
+ $this->assertArrayHasKey( 'Babel-0', $observed );
+ $this->assertSame( $expected, $observed['Babel-0'], 'PHP&JSON extension' );
+
+ $expected = array(
+ 'repo' => 'extension',
+ 'name' => 'Door',
+ 'orig' => 'file:///IP/extensions/Door/i18n/core/*.json',
+ 'path' => 'i18n/core/*.json'
+ );
+ $this->assertArrayHasKey( 'Door-core', $observed );
+ $this->assertSame( $expected, $observed['Door-core'], 'Multidir json extension' );
+
+ $expected = array(
+ 'repo' => 'extension',
+ 'name' => 'Door',
+ 'orig' => 'file:///IP/extensions/Door/i18n/extra/*.json',
+ 'path' => 'i18n/extra/*.json'
+ );
+ $this->assertArrayHasKey( 'Door-extra', $observed );
+ $this->assertSame( $expected, $observed['Door-extra'], 'Multidir json extension' );
+ }
+}
diff --git a/extensions/LocalisationUpdate/tests/phpunit/reader/JSONReaderTest.php b/extensions/LocalisationUpdate/tests/phpunit/reader/JSONReaderTest.php
new file mode 100644
index 00000000..4bb53af9
--- /dev/null
+++ b/extensions/LocalisationUpdate/tests/phpunit/reader/JSONReaderTest.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+class LU_JSONReaderTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider parseProvider
+ */
+ public function testParse( $input, $expected, $comment ) {
+ $reader = new LU_JSONReader( 'xx' );
+ $observed = $reader->parse( $input );
+ $this->assertEquals( $expected, $observed['xx'], $comment );
+ }
+
+ public function parseProvider() {
+ return array(
+ array(
+ '{}',
+ array(),
+ 'empty file',
+ ),
+ array(
+ '{"key":"value"}',
+ array( 'key' => 'value' ),
+ 'file with one string',
+ ),
+ array(
+ '{"@metadata":{"authors":["Nike"]},"key":"value2"}',
+ array( 'key' => 'value2' ),
+ '@metadata is ignored',
+ )
+ );
+ }
+}
diff --git a/extensions/LocalisationUpdate/tests/phpunit/reader/ReaderFactoryTest.php b/extensions/LocalisationUpdate/tests/phpunit/reader/ReaderFactoryTest.php
new file mode 100644
index 00000000..ee155b3a
--- /dev/null
+++ b/extensions/LocalisationUpdate/tests/phpunit/reader/ReaderFactoryTest.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+class LU_ReaderFactoryTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider getReaderProvider
+ */
+ public function testGetReader( $input, $expected, $comment ) {
+ $factory = new LU_ReaderFactory();
+ $reader = $factory->getReader( $input );
+ $observed = get_class( $reader );
+ $this->assertEquals( $expected, $observed, $comment );
+ }
+
+ public function getReaderProvider() {
+ return array(
+ array(
+ 'languages/messages/MessagesFi.php',
+ 'LU_PHPReader',
+ 'core php file',
+ ),
+ array(
+ 'extensions/Translate/Translate.i18n.php',
+ 'LU_PHPReader',
+ 'extension php file',
+ ),
+ array(
+ 'extension/Translate/i18n/core/de.json',
+ 'LU_JSONReader',
+ 'extension json file',
+ ),
+ );
+ }
+}
diff --git a/extensions/ParserFunctions/tests/ExpressionTest.php b/extensions/ParserFunctions/tests/ExpressionTest.php
new file mode 100644
index 00000000..169a9cb4
--- /dev/null
+++ b/extensions/ParserFunctions/tests/ExpressionTest.php
@@ -0,0 +1,76 @@
+<?php
+class ExpressionTest extends MediaWikiTestCase {
+
+ /**
+ * @var ExprParser
+ */
+ protected $parser;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->parser = new ExprParser();
+ }
+
+ /**
+ * @dataProvider provideExpressions
+ */
+ function testExpression( $input, $expected ) {
+ $this->assertEquals(
+ $expected,
+ $this->parser->doExpression( $input )
+ );
+ }
+
+ function provideExpressions() {
+ return array(
+ array( '1 or 0', '1' ),
+ array( 'not (1 and 0)', '1' ),
+ array( 'not 0', '1' ),
+ array( '4 < 5', '1' ),
+ array( '-5 < 2', '1' ),
+ array( '-2 <= -2', '1' ),
+ array( '4 > 3', '1' ),
+ array( '4 > -3', '1' ),
+ array( '5 >= 2', '1' ),
+ array( '2 >= 2', '1' ),
+ array( '1 != 2', '1' ),
+ array( '-4 * -4 = 4 * 4', '1' ),
+ array( 'not (1 != 1)', '1' ),
+ array( '1 + 1', '2' ),
+ array( '-1 + 1', '0' ),
+ array( '+1 + 1', '2' ),
+ array( '4 * 4', '16' ),
+ array( '(1/3) * 3', '1' ),
+ array( '3 / 1.5', '2' ),
+ array( '3 / 0.2', '15' ),
+ array( '3 / ( 2.0 * 0.1 )', '15' ),
+ array( '3 / ( 2.0 / 10 )', '15' ),
+ array( '3 / (- 0.2 )', '-15' ),
+ array( '3 / abs( 0.2 )', '15' ),
+ array( '3 mod 2', '1' ),
+ array( '1e4', '10000' ),
+ array( '1e-2', '0.01' ),
+ array( '4.0 round 0', '4' ),
+ array( 'ceil 4', '4' ),
+ array( 'floor 4', '4' ),
+ array( '4.5 round 0', '5' ),
+ array( '4.2 round 0', '4' ),
+ array( '-4.2 round 0', '-4' ),
+ array( '-4.5 round 0', '-5' ),
+ array( '-2.0 round 0', '-2' ),
+ array( 'ceil -3', '-3' ),
+ array( 'floor -6.0', '-6' ),
+ array( 'ceil 4.2', '5' ),
+ array( 'ceil -4.5', '-4' ),
+ array( 'floor -4.5', '-5' ),
+ array( 'abs(-2)', '2' ),
+ array( 'ln(exp(1))', '1' ),
+ array( 'trunc(4.5)', '4' ),
+ array( 'trunc(-4.5)', '-4' ),
+ array( '123 fmod (2^64-1)', '123' ),
+ array( '5.7 mod 1.3', '0' ),
+ array( '5.7 fmod 1.3', '0.5' ),
+ );
+ }
+}
+
diff --git a/extensions/PdfHandler/COPYING b/extensions/PdfHandler/COPYING
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/extensions/PdfHandler/COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/extensions/PdfHandler/CreatePdfThumbnailsJob.class.php b/extensions/PdfHandler/CreatePdfThumbnailsJob.class.php
new file mode 100644
index 00000000..aba204f2
--- /dev/null
+++ b/extensions/PdfHandler/CreatePdfThumbnailsJob.class.php
@@ -0,0 +1,126 @@
+<?php
+
+class CreatePdfThumbnailsJob extends Job {
+ /**
+ * Flags for thumbnail jobs
+ */
+ const BIG_THUMB = 1;
+ const SMALL_THUMB = 2;
+
+ /**
+ * Construct a thumbnail job
+ *
+ * @param $title Title Title object
+ * @param $params array Associative array of options:
+ * page: page number for which the thumbnail will be created
+ * jobtype: CreatePDFThumbnailsJob::BIG_THUMB or CreatePDFThumbnailsJob::SMALL_THUMB
+ * BIG_THUMB will create a thumbnail visible for full thumbnail view,
+ * SMALL_THUMB will create a thumbnail shown in "previous page"/"next page" boxes
+ *
+ */
+ public function __construct( $title, $params ) {
+ parent::__construct( 'createPdfThumbnailsJob', $title, $params );
+ }
+
+ /**
+ * Run a thumbnail job on a given PDF file.
+ * @return bool true
+ */
+ public function run() {
+ if ( !isset( $this->params['page'] ) ) {
+ wfDebugLog('thumbnails', 'A page for thumbnails job of ' . $this->title->getText() . ' was not specified! That should never happen!');
+ return true; // no page set? that should never happen
+ }
+
+ $file = wfLocalFile( $this->title ); // we just want a local file
+ if ( !$file ) {
+ return true; // Just silently fail, perhaps the file was already deleted, don't bother
+ }
+
+ switch ($this->params['jobtype']) {
+ case self::BIG_THUMB:
+ global $wgImageLimits;
+ // Ignore user preferences, do default thumbnails
+ // everything here shamelessy copied and reused from includes/ImagePage.php
+ $sizeSel = User::getDefaultOption( 'imagesize' );
+
+ // The user offset might still be incorrect, specially if
+ // $wgImageLimits got changed (see bug #8858).
+ if ( !isset( $wgImageLimits[$sizeSel] ) ) {
+ // Default to the first offset in $wgImageLimits
+ $sizeSel = 0;
+ }
+ $max = $wgImageLimits[$sizeSel];
+ $maxWidth = $max[0];
+ $maxHeight = $max[1];
+
+ $width_orig = $file->getWidth( $this->params['page'] );
+ $width = $width_orig;
+ $height_orig = $file->getHeight( $this->params['page'] );
+ $height = $height_orig;
+ if ( $width > $maxWidth || $height > $maxHeight ) {
+ # Calculate the thumbnail size.
+ # First case, the limiting factor is the width, not the height.
+ if ( $width / $height >= $maxWidth / $maxHeight ) {
+ //$height = round( $height * $maxWidth / $width );
+ $width = $maxWidth;
+ # Note that $height <= $maxHeight now.
+ } else {
+ $newwidth = floor( $width * $maxHeight / $height );
+ //$height = round( $height * $newwidth / $width );
+ $width = $newwidth;
+ # Note that $height <= $maxHeight now, but might not be identical
+ # because of rounding.
+ }
+ $transformParams = array( 'page' => $this->params['page'], 'width' => $width );
+ $file->transform( $transformParams );
+ }
+ break;
+
+ case self::SMALL_THUMB:
+ Linker::makeThumbLinkObj( $this->title, $file, '', '', 'none', array( 'page' => $this->params['page'] ) );
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param $upload UploadBase
+ * @param $mime
+ * @param $error
+ * @return bool
+ */
+ public static function insertJobs( $upload, $mime, &$error ) {
+ global $wgPdfCreateThumbnailsInJobQueue;
+ if ( !$wgPdfCreateThumbnailsInJobQueue ) {
+ return true;
+ }
+ if (!MimeMagic::singleton()->isMatchingExtension('pdf', $mime)) {
+ return true; // not a PDF, abort
+ }
+
+ $title = $upload->getTitle();
+ $uploadFile = $upload->getLocalFile();
+ if ( is_null( $uploadFile ) ) {
+ wfDebugLog('thumbnails', '$uploadFile seems to be null, should never happen...');
+ return true; // should never happen, but it's better to be secure
+ }
+
+ $metadata = $uploadFile->getMetadata();
+ $unserialized = unserialize( $metadata );
+ $pages = intval( $unserialized['Pages'] );
+
+ $jobs = array();
+ for ( $i = 1; $i <= $pages; $i++ ) {
+ $jobs[] = new CreatePdfThumbnailsJob( $title,
+ array( 'page' => $i, 'jobtype' => self::BIG_THUMB )
+ );
+ $jobs[] = new CreatePdfThumbnailsJob( $title,
+ array( 'page' => $i, 'jobtype' => self::SMALL_THUMB )
+ );
+ }
+ Job::batchInsert( $jobs );
+ return true;
+ }
+}
diff --git a/extensions/ConfirmEdit/Asirra.i18n.php b/extensions/PdfHandler/PdfHandler.i18n.php
index eb2d8fe3..46a34a6c 100644
--- a/extensions/ConfirmEdit/Asirra.i18n.php
+++ b/extensions/PdfHandler/PdfHandler.i18n.php
@@ -11,11 +11,11 @@
* This shim maintains compatibility back to MediaWiki 1.17.
*/
$messages = array();
-if ( !function_exists( 'wfJsonI18nShimc0e16a38c3b0633f' ) ) {
- function wfJsonI18nShimc0e16a38c3b0633f( $cache, $code, &$cachedData ) {
+if ( !function_exists( 'wfJsonI18nShim88f78f66a49810c2' ) ) {
+ function wfJsonI18nShim88f78f66a49810c2( $cache, $code, &$cachedData ) {
$codeSequence = array_merge( array( $code ), $cachedData['fallbackSequence'] );
foreach ( $codeSequence as $csCode ) {
- $fileName = dirname( __FILE__ ) . "/i18n/asirra/$csCode.json";
+ $fileName = dirname( __FILE__ ) . "/i18n/$csCode.json";
if ( is_readable( $fileName ) ) {
$data = FormatJson::decode( file_get_contents( $fileName ), true );
foreach ( array_keys( $data ) as $key ) {
@@ -31,5 +31,5 @@ if ( !function_exists( 'wfJsonI18nShimc0e16a38c3b0633f' ) ) {
return true;
}
- $GLOBALS['wgHooks']['LocalisationCacheRecache'][] = 'wfJsonI18nShimc0e16a38c3b0633f';
+ $GLOBALS['wgHooks']['LocalisationCacheRecache'][] = 'wfJsonI18nShim88f78f66a49810c2';
}
diff --git a/extensions/PdfHandler/PdfHandler.image.php b/extensions/PdfHandler/PdfHandler.image.php
new file mode 100644
index 00000000..49da7f4e
--- /dev/null
+++ b/extensions/PdfHandler/PdfHandler.image.php
@@ -0,0 +1,309 @@
+<?php
+/**
+ *
+ * Copyright © 2007 Xarax <jodeldi@gmx.de>
+ *
+ * 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
+ */
+
+/**
+ * inspired by djvuimage from Brion Vibber
+ * modified and written by xarax
+ */
+
+class PdfImage {
+
+ /**
+ * @param $filename
+ */
+ function __construct( $filename ) {
+ $this->mFilename = $filename;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isValid() {
+ return true;
+ }
+
+ /**
+ * @return array|bool
+ */
+ public function getImageSize() {
+ $data = $this->retrieveMetadata();
+ $size = self::getPageSize( $data, 1 );
+
+ if( $size ) {
+ $width = $size['width'];
+ $height = $size['height'];
+ return array( $width, $height, 'Pdf',
+ "width=\"$width\" height=\"$height\"" );
+ }
+ return false;
+ }
+
+ /**
+ * @param $data array
+ * @param $page
+ * @return array|bool
+ */
+ public static function getPageSize( $data, $page ) {
+ global $wgPdfHandlerDpi;
+
+ if( isset( $data['pages'][$page]['Page size'] ) ) {
+ $o = $data['pages'][$page]['Page size'];
+ } elseif( isset( $data['Page size'] ) ) {
+ $o = $data['Page size'];
+ } else {
+ $o = false;
+ }
+
+ if ( $o ) {
+ if( isset( $data['pages'][$page]['Page rot'] ) ) {
+ $r = $data['pages'][$page]['Page rot'];
+ } elseif( isset( $data['Page rot'] ) ) {
+ $r = $data['Page rot'];
+ } else {
+ $r = 0;
+ }
+ $size = explode( 'x', $o, 2 );
+
+ if ( $size ) {
+ $width = intval( trim( $size[0] ) / 72 * $wgPdfHandlerDpi );
+ $height = explode( ' ', trim( $size[1] ), 2 );
+ $height = intval( trim( $height[0] ) / 72 * $wgPdfHandlerDpi );
+ if ( ( $r/90 ) & 1 ) {
+ // Swap width and height for landscape pages
+ $t = $width;
+ $width = $height;
+ $height = $t;
+ }
+
+ return array(
+ 'width' => $width,
+ 'height' => $height
+ );
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return array|bool|null
+ */
+ public function retrieveMetaData() {
+ global $wgPdfInfo, $wgPdftoText;
+
+ if ( $wgPdfInfo ) {
+ wfProfileIn( 'pdfinfo' );
+ $cmd = wfEscapeShellArg( $wgPdfInfo ) .
+ " -enc UTF-8 " . # Report metadata as UTF-8 text...
+ " -l 9999999 " . # Report page sizes for all pages
+ " -meta " . # Report XMP metadata
+ wfEscapeShellArg( $this->mFilename );
+ $retval = '';
+ $dump = wfShellExec( $cmd, $retval );
+ $data = $this->convertDumpToArray( $dump );
+ wfProfileOut( 'pdfinfo' );
+ } else {
+ $data = null;
+ }
+
+ # Read text layer
+ if ( isset( $wgPdftoText ) ) {
+ wfProfileIn( 'pdftotext' );
+ $cmd = wfEscapeShellArg( $wgPdftoText ) . ' '. wfEscapeShellArg( $this->mFilename ) . ' - ';
+ wfDebug( __METHOD__.": $cmd\n" );
+ $retval = '';
+ $txt = wfShellExec( $cmd, $retval );
+ wfProfileOut( 'pdftotext' );
+ if( $retval == 0 ) {
+ $txt = str_replace( "\r\n", "\n", $txt );
+ $pages = explode( "\f", $txt );
+ foreach( $pages as $page => $pageText ) {
+ # Get rid of invalid UTF-8, strip control characters
+ # Note we need to do this per page, as \f page feed would be stripped.
+ $pages[$page] = UtfNormal::cleanUp( $pageText );
+ }
+ $data['text'] = $pages;
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * @param $dump string
+ * @return array|bool
+ */
+ protected function convertDumpToArray( $dump ) {
+ if ( strval( $dump ) == '' ) {
+ return false;
+ }
+
+ $lines = explode( "\n", $dump );
+ $data = array();
+
+ // Metadata is always the last item, and spans multiple lines.
+ $inMetadata = false;
+
+ // Basically this loop will go through each line, splitting key value
+ // pairs on the colon, until it gets to a "Metadata:\n" at which point
+ // it will gather all remaining lines into the xmp key.
+ foreach( $lines as $line ) {
+ if ( $inMetadata ) {
+ # Handle XMP differently due to diffence in line break
+ $data['xmp'] .= "\n$line";
+ continue;
+ }
+ $bits = explode( ':', $line, 2 );
+ if( count( $bits ) > 1 ) {
+ $key = trim( $bits[0] );
+ if ( $key === 'Metadata' ) {
+ $inMetadata = true;
+ $data['xmp'] = '';
+ continue;
+ }
+ $value = trim( $bits[1] );
+ $matches = array();
+ // "Page xx rot" will be in poppler 0.20's pdfinfo output
+ // See https://bugs.freedesktop.org/show_bug.cgi?id=41867
+ if( preg_match( '/^Page +(\d+) (size|rot)$/', $key, $matches ) ) {
+ $data['pages'][$matches[1]][$matches[2] == 'size' ? 'Page size' : 'Page rot'] = $value;
+ } else {
+ $data[$key] = $value;
+ }
+ }
+ }
+ $data = $this->postProcessDump( $data );
+ return $data;
+ }
+
+ /**
+ * Postprocess the metadata (convert xmp into useful form, etc)
+ *
+ * This is used to generate the metadata table at the bottom
+ * of the image description page.
+ *
+ * @param $data Array metadata
+ * @return Array post-processed metadata
+ */
+ protected function postProcessDump( array $data ) {
+
+ $meta = new BitmapMetadataHandler();
+ $items = array();
+ foreach( $data as $key => $val ) {
+ switch ( $key ) {
+ case 'Title':
+ $items['ObjectName'] = $val;
+ break;
+ case 'Subject':
+ $items['ImageDescription'] = $val;
+ break;
+ case 'Keywords':
+ // Sometimes we have empty keywords. This seems
+ // to be a product of how pdfinfo deals with keywords
+ // with spaces in them. Filter such empty keywords
+ $keyList = array_filter( explode( ' ', $val ) );
+ if ( count( $keyList ) > 0 ) {
+ $items['Keywords'] = $keyList;
+ }
+ break;
+ case 'Author':
+ $items['Artist'] = $val;
+ break;
+ case 'Creator':
+ // Program used to create file.
+ // Different from program used to convert to pdf.
+ $items['Software'] = $val;
+ break;
+ case 'Producer':
+ // Conversion program
+ $items['pdf-Producer'] = $val;
+ break;
+ case 'ModTime':
+ $timestamp = wfTimestamp( TS_EXIF, $val );
+ if ( $timestamp ) {
+ // 'if' is just paranoia
+ $items['DateTime'] = $timestamp;
+ }
+ break;
+ case 'CreationTime':
+ $timestamp = wfTimestamp( TS_EXIF, $val );
+ if ( $timestamp ) {
+ $items['DateTimeDigitized'] = $timestamp;
+ }
+ break;
+ // These last two (version and encryption) I was unsure
+ // if we should include in the table, since they aren't
+ // all that useful to editors. I leaned on the side
+ // of including. However not including if file
+ // is optimized/linearized since that is really useless
+ // to an editor.
+ case 'PDF version':
+ $items['pdf-Version'] = $val;
+ break;
+ case 'Encrypted':
+ // @todo: The value isn't i18n-ised. The appropriate
+ // place to do that is in FormatMetadata.php
+ // should add a hook a there.
+ // For reference, if encrypted this fields value looks like:
+ // "yes (print:yes copy:no change:no addNotes:no)"
+ $items['pdf-Encrypted'] = $val;
+ break;
+ // Note 'pages' and 'Pages' are different keys (!)
+ case 'pages':
+ // A pdf document can have multiple sized pages in it.
+ // (However 95% of the time, all pages are the same size)
+ // get a list of all the unique page sizes in document.
+ // This doesn't do anything with rotation as of yet,
+ // mostly because I am unsure of what a good way to
+ // present that information to the user would be.
+ $pageSizes = array();
+ foreach( $val as $page ) {
+ if( isset( $page['Page size'] ) ) {
+ $pageSizes[ $page['Page size'] ] = true;
+ }
+ }
+
+ $pageSizeArray = array_keys( $pageSizes );
+ if ( count( $pageSizeArray ) > 0 ) {
+ $items['pdf-PageSize'] = $pageSizeArray;
+ }
+ break;
+ }
+
+ }
+ $meta->addMetadata( $items, 'native' );
+
+ if ( isset( $data['xmp'] ) && function_exists( 'xml_parser_create_ns' ) ) {
+ // func exists verifies that the xml extension required for XMPReader
+ // is present (Almost always is present)
+ // @todo: This only handles generic xmp properties. Would be improved
+ // by handling pdf xmp properties (pdf and pdfx) via XMPInfo hook.
+ $xmp = new XMPReader();
+ $xmp->parse( $data['xmp'] );
+ $xmpRes = $xmp->getResults();
+ foreach ( $xmpRes as $type => $xmpSection ) {
+ $meta->addMetadata( $xmpSection, $type );
+ }
+ }
+ unset( $data['xmp'] );
+ $data['mergedMetadata'] = $meta->getMetadataArray();
+ return $data;
+ }
+}
diff --git a/extensions/PdfHandler/PdfHandler.php b/extensions/PdfHandler/PdfHandler.php
new file mode 100644
index 00000000..f4e15657
--- /dev/null
+++ b/extensions/PdfHandler/PdfHandler.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * PDF Handler extension -- handler for viewing PDF files in image mode.
+ *
+ * @file
+ * @ingroup Extensions
+ * @author Martin Seidel (Xarax) <jodeldi@gmx.de>
+ * @copyright Copyright © 2007 Martin Seidel (Xarax) <jodeldi@gmx.de>
+ *
+ * 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
+ */
+
+# Not a valid entry point, skip unless MEDIAWIKI is defined
+if ( !defined( 'MEDIAWIKI' ) ) {
+ echo 'PdfHandler extension';
+ exit( 1 );
+}
+
+$wgExtensionCredits['media'][] = array(
+ 'path' => __FILE__,
+ 'name' => 'PDF Handler',
+ 'author' => array( 'Martin Seidel', 'Mike Połtyn' ),
+ 'descriptionmsg' => 'pdf-desc',
+ 'url' => 'https://www.mediawiki.org/wiki/Extension:PdfHandler',
+);
+
+// External program requirements...
+$wgPdfProcessor = 'gs';
+$wgPdfPostProcessor = 'convert';
+$wgPdfInfo = 'pdfinfo';
+$wgPdftoText = 'pdftotext';
+
+$wgPdfOutputExtension = 'jpg';
+$wgPdfHandlerDpi = 150;
+$wgPdfHandlerJpegQuality = 95;
+
+// This setting, if enabled, will put creating thumbnails into a job queue,
+// so they do not have to be created on-the-fly,
+// but rather inconspicuously during normal wiki browsing
+$wgPdfCreateThumbnailsInJobQueue = false;
+
+// To upload new PDF files you'll need to do this too:
+// $wgFileExtensions[] = 'pdf';
+
+$dir = __DIR__ . '/';
+$wgMessagesDirs['PdfHandler'] = __DIR__ . '/i18n';
+$wgExtensionMessagesFiles['PdfHandler'] = $dir . 'PdfHandler.i18n.php';
+$wgAutoloadClasses['PdfImage'] = $dir . 'PdfHandler.image.php';
+$wgAutoloadClasses['PdfHandler'] = $dir . 'PdfHandler_body.php';
+$wgAutoloadClasses['CreatePdfThumbnailsJob'] = $dir . 'CreatePdfThumbnailsJob.class.php';
+$wgMediaHandlers['application/pdf'] = 'PdfHandler';
+$wgJobClasses['createPdfThumbnailsJob'] = 'CreatePdfThumbnailsJob';
+$wgHooks['UploadVerifyFile'][] = 'CreatePdfThumbnailsJob::insertJobs';
diff --git a/extensions/PdfHandler/PdfHandler_body.php b/extensions/PdfHandler/PdfHandler_body.php
new file mode 100644
index 00000000..2a08a95b
--- /dev/null
+++ b/extensions/PdfHandler/PdfHandler_body.php
@@ -0,0 +1,386 @@
+<?php
+/**
+ * Copyright © 2007 Martin Seidel (Xarax) <jodeldi@gmx.de>
+ *
+ * Inspired by djvuhandler from Tim Starling
+ * Modified and written by Xarax
+ *
+ * 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
+ */
+
+class PdfHandler extends ImageHandler {
+
+ /**
+ * @return bool
+ */
+ function isEnabled() {
+ global $wgPdfProcessor, $wgPdfPostProcessor, $wgPdfInfo;
+
+ if ( !isset( $wgPdfProcessor ) || !isset( $wgPdfPostProcessor ) || !isset( $wgPdfInfo ) ) {
+ wfDebug( "PdfHandler is disabled, please set the following\n" );
+ wfDebug( "variables in LocalSettings.php:\n" );
+ wfDebug( "\$wgPdfProcessor, \$wgPdfPostProcessor, \$wgPdfInfo\n" );
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @param $file
+ * @return bool
+ */
+ function mustRender( $file ) {
+ return true;
+ }
+
+ /**
+ * @param $file
+ * @return bool
+ */
+ function isMultiPage( $file ) {
+ return true;
+ }
+
+ /**
+ * @param $name
+ * @param $value
+ * @return bool
+ */
+ function validateParam( $name, $value ) {
+ if ( $name === 'page' && trim( $value ) !== (string) intval( $value ) ) {
+ // Extra junk on the end of page, probably actually a caption
+ // e.g. [[File:Foo.pdf|thumb|Page 3 of the document shows foo]]
+ return false;
+ }
+ if ( in_array( $name, array( 'width', 'height', 'page' ) ) ) {
+ return ( $value > 0 );
+ }
+ return false;
+ }
+
+ /**
+ * @param $params array
+ * @return bool|string
+ */
+ function makeParamString( $params ) {
+ $page = isset( $params['page'] ) ? $params['page'] : 1;
+ if ( !isset( $params['width'] ) ) {
+ return false;
+ }
+ return "page{$page}-{$params['width']}px";
+ }
+
+ /**
+ * @param $str string
+ * @return array|bool
+ */
+ function parseParamString( $str ) {
+ $m = false;
+
+ if ( preg_match( '/^page(\d+)-(\d+)px$/', $str, $m ) ) {
+ return array( 'width' => $m[2], 'page' => $m[1] );
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $params array
+ * @return array
+ */
+ function getScriptParams( $params ) {
+ return array(
+ 'width' => $params['width'],
+ 'page' => $params['page'],
+ );
+ }
+
+ /**
+ * @return array
+ */
+ function getParamMap() {
+ return array(
+ 'img_width' => 'width',
+ 'img_page' => 'page',
+ );
+ }
+
+ /**
+ * @param $width
+ * @param $height
+ * @param $msg
+ * @return MediaTransformError
+ */
+ protected function doThumbError( $width, $height, $msg ) {
+ return new MediaTransformError( 'thumbnail_error',
+ $width, $height, wfMessage( $msg )->inContentLanguage()->text() );
+ }
+
+ /**
+ * @param $image File
+ * @param $dstPath string
+ * @param $dstUrl string
+ * @param $params array
+ * @param $flags int
+ * @return MediaTransformError|MediaTransformOutput|ThumbnailImage|TransformParameterError
+ */
+ function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ global $wgPdfProcessor, $wgPdfPostProcessor, $wgPdfHandlerDpi, $wgPdfHandlerJpegQuality;
+
+ $metadata = $image->getMetadata();
+
+ if ( !$metadata ) {
+ return $this->doThumbError(
+ isset( $params['width'] ) ? $params['width'] : null,
+ isset( $params['height'] ) ? $params['height'] : null,
+ 'pdf_no_metadata'
+ );
+ }
+
+ if ( !$this->normaliseParams( $image, $params ) ) {
+ return new TransformParameterError( $params );
+ }
+
+ $width = $params['width'];
+ $height = $params['height'];
+ $page = $params['page'];
+
+ if ( $page > $this->pageCount( $image ) ) {
+ return $this->doThumbError( $width, $height, 'pdf_page_error' );
+ }
+
+ if ( $flags & self::TRANSFORM_LATER ) {
+ return new ThumbnailImage( $image, $dstUrl, $width, $height, false, $page );
+ }
+
+ if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
+ return $this->doThumbError( $width, $height, 'thumbnail_dest_directory' );
+ }
+
+ // Thumbnail extraction is very inefficient for large files.
+ // Provide a way to pool count limit the number of downloaders.
+ if ( $image->getSize() >= 1e7 ) { // 10MB
+ $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $image->getName() ),
+ array(
+ 'doWork' => function() use ( $image ) {
+ return $image->getLocalRefPath();
+ }
+ )
+ );
+ $srcPath = $work->execute();
+ } else {
+ $srcPath = $image->getLocalRefPath();
+ }
+
+ if ( $srcPath === false ) { // could not download original
+ return $this->doThumbError( $width, $height, 'filemissing' );
+ }
+
+ $cmd = '(' . wfEscapeShellArg(
+ $wgPdfProcessor,
+ "-sDEVICE=jpeg",
+ "-sOutputFile=-",
+ "-dFirstPage={$page}",
+ "-dLastPage={$page}",
+ "-r{$wgPdfHandlerDpi}",
+ "-dBATCH",
+ "-dNOPAUSE",
+ "-q",
+ $srcPath
+ );
+ $cmd .= " | " . wfEscapeShellArg(
+ $wgPdfPostProcessor,
+ "-depth",
+ "8",
+ "-quality",
+ $wgPdfHandlerJpegQuality,
+ "-resize",
+ $width,
+ "-",
+ $dstPath
+ );
+ $cmd .= ")";
+
+ wfProfileIn( 'PdfHandler' );
+ wfDebug( __METHOD__ . ": $cmd\n" );
+ $retval = '';
+ $err = wfShellExecWithStderr( $cmd, $retval );
+ wfProfileOut( 'PdfHandler' );
+
+ $removed = $this->removeBadFile( $dstPath, $retval );
+
+ if ( $retval != 0 || $removed ) {
+ wfDebugLog( 'thumbnail',
+ sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
+ wfHostname(), $retval, trim( $err ), $cmd ) );
+ return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
+ } else {
+ return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page );
+ }
+ }
+
+ /**
+ * @param $image File
+ * @param $path string
+ * @return PdfImage
+ */
+ function getPdfImage( $image, $path ) {
+ if ( !$image ) {
+ $pdfimg = new PdfImage( $path );
+ } elseif ( !isset( $image->pdfImage ) ) {
+ $pdfimg = $image->pdfImage = new PdfImage( $path );
+ } else {
+ $pdfimg = $image->pdfImage;
+ }
+
+ return $pdfimg;
+ }
+
+ /**
+ * @param $image File
+ * @return bool
+ */
+ function getMetaArray( $image ) {
+ if ( isset( $image->pdfMetaArray ) ) {
+ return $image->pdfMetaArray;
+ }
+
+ $metadata = $image->getMetadata();
+
+ if ( !$this->isMetadataValid( $image, $metadata ) ) {
+ wfDebug( "Pdf metadata is invalid or missing, should have been fixed in upgradeRow\n" );
+ return false;
+ }
+
+ wfProfileIn( __METHOD__ );
+ wfSuppressWarnings();
+ $image->pdfMetaArray = unserialize( $metadata );
+ wfRestoreWarnings();
+ wfProfileOut( __METHOD__ );
+
+ return $image->pdfMetaArray;
+ }
+
+ /**
+ * @param $image File
+ * @param $path string
+ * @return array|bool
+ */
+ function getImageSize( $image, $path ) {
+ return $this->getPdfImage( $image, $path )->getImageSize();
+ }
+
+ /**
+ * @param $ext
+ * @param $mime string
+ * @param $params null
+ * @return array
+ */
+ function getThumbType( $ext, $mime, $params = null ) {
+ global $wgPdfOutputExtension;
+ static $mime;
+
+ if ( !isset( $mime ) ) {
+ $magic = MimeMagic::singleton();
+ $mime = $magic->guessTypesForExtension( $wgPdfOutputExtension );
+ }
+ return array( $wgPdfOutputExtension, $mime );
+ }
+
+ /**
+ * @param $image File
+ * @param $path string
+ * @return string
+ */
+ function getMetadata( $image, $path ) {
+ return serialize( $this->getPdfImage( $image, $path )->retrieveMetaData() );
+ }
+
+ /**
+ * @param $image File
+ * @param $metadata string
+ * @return bool
+ */
+ function isMetadataValid( $image, $metadata ) {
+ if ( !$metadata || $metadata === serialize(array()) ) {
+ return self::METADATA_BAD;
+ } elseif ( strpos( $metadata, 'mergedMetadata' ) === false ) {
+ return self::METADATA_COMPATIBLE;
+ }
+ return self::METADATA_GOOD;
+ }
+
+ /**
+ * @param $image File
+ * @return bool|int
+ */
+ function formatMetadata( $image ) {
+ $meta = $image->getMetadata();
+
+ if ( !$meta ) {
+ return false;
+ }
+ wfSuppressWarnings();
+ $meta = unserialize( $meta );
+ wfRestoreWarnings();
+
+ if ( !isset( $meta['mergedMetadata'] )
+ || !is_array( $meta['mergedMetadata'] )
+ || count( $meta['mergedMetadata'] ) < 1
+ ) {
+ return false;
+ }
+
+ // Inherited from MediaHandler.
+ return $this->formatMetadataHelper( $meta['mergedMetadata'] );
+ }
+
+ /**
+ * @param $image
+ * @return bool|int
+ */
+ function pageCount( $image ) {
+ $data = $this->getMetaArray( $image );
+ if ( !$data || !isset( $data['Pages'] ) ) {
+ return false;
+ }
+ return intval( $data['Pages'] );
+ }
+
+ /**
+ * @param $image File
+ * @param $page int
+ * @return array|bool
+ */
+ function getPageDimensions( $image, $page ) {
+ $data = $this->getMetaArray( $image );
+ return PdfImage::getPageSize( $data, $page );
+ }
+
+ /**
+ * @param $image File
+ * @param $page int
+ * @return bool
+ */
+ function getPageText( $image, $page ) {
+ $data = $this->getMetaArray( $image, true );
+ if ( !$data || !isset( $data['text'] ) || !isset( $data['text'][$page - 1] ) ) {
+ return false;
+ }
+ return $data['text'][$page - 1];
+ }
+
+}
diff --git a/extensions/PdfHandler/i18n/af.json b/extensions/PdfHandler/i18n/af.json
new file mode 100644
index 00000000..0bb386a2
--- /dev/null
+++ b/extensions/PdfHandler/i18n/af.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Naudefj",
+ "පසිඳු කාවින්ද"
+ ]
+ },
+ "pdf-desc": "Handler vir die lees van PDF-lêers in beeld af",
+ "pdf_no_metadata": "Kan nie metadata uit PDF kry nie",
+ "pdf_page_error": "Bladsynommer kom nie in dokument voor nie"
+}
diff --git a/extensions/PdfHandler/i18n/aln.json b/extensions/PdfHandler/i18n/aln.json
new file mode 100644
index 00000000..38372cdb
--- /dev/null
+++ b/extensions/PdfHandler/i18n/aln.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mdupont"
+ ]
+ },
+ "pdf-desc": "Mbajtës për shikimin PDF files në imazh mode",
+ "pdf_no_metadata": "Nuk mund të merrni nga metadata PDF",
+ "pdf_page_error": "numrin e faqes nuk është në varg"
+}
diff --git a/extensions/PdfHandler/i18n/an.json b/extensions/PdfHandler/i18n/an.json
new file mode 100644
index 00000000..ed321d67
--- /dev/null
+++ b/extensions/PdfHandler/i18n/an.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Juanpabl"
+ ]
+ },
+ "pdf-desc": "Maneyador ta veyer fichers PDF en modo imachen",
+ "pdf_no_metadata": "No s'obtenioron metadatos d'o PDF",
+ "pdf_page_error": "Numero de pachina difuera de rango"
+}
diff --git a/extensions/PdfHandler/i18n/ar.json b/extensions/PdfHandler/i18n/ar.json
new file mode 100644
index 00000000..6bd54a16
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ar.json
@@ -0,0 +1,16 @@
+{
+ "@metadata": {
+ "authors": [
+ "Meno25",
+ "Mido",
+ "أحمد"
+ ]
+ },
+ "pdf-desc": "معالج عرض ملفات PDF في طور الصور",
+ "pdf_no_metadata": "تعذّر استخراج البيانات الفوقية من ملف PDF",
+ "pdf_page_error": "رقم الصفحة خارج عن النطاق",
+ "exif-pdf-producer": "برمجية التحويل",
+ "exif-pdf-version": "إصدارة صيغة PDF",
+ "exif-pdf-encrypted": "مُعمّى",
+ "exif-pdf-pagesize": "حجم الصفحة"
+}
diff --git a/extensions/PdfHandler/i18n/arz.json b/extensions/PdfHandler/i18n/arz.json
new file mode 100644
index 00000000..16b3a730
--- /dev/null
+++ b/extensions/PdfHandler/i18n/arz.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Meno25"
+ ]
+ },
+ "pdf-desc": "متحكم لرؤية ملفات PDF فى نمط صورة",
+ "pdf_no_metadata": "لم يمكن أخذ معلومات ميتا من PDF",
+ "pdf_page_error": "رقم الصفحة ليس فى النطاق"
+}
diff --git a/extensions/PdfHandler/i18n/as.json b/extensions/PdfHandler/i18n/as.json
new file mode 100644
index 00000000..bcd58e33
--- /dev/null
+++ b/extensions/PdfHandler/i18n/as.json
@@ -0,0 +1,13 @@
+{
+ "@metadata": {
+ "authors": [
+ "Bishnu Saikia"
+ ]
+ },
+ "pdf-desc": "পিডিএফ ফাইল ছবি হিচাপে ব্যৱহাৰৰ পদ্ধতি",
+ "pdf_no_metadata": "পি.ডি.এফ.ৰ পৰা মেটাডাটা উপলদ্ধ নহয়",
+ "pdf_page_error": "পৃষ্ঠাৰ নম্বৰ সীমাৰ ভিতৰত নাই",
+ "exif-pdf-producer": "ৰূপান্তৰক প্ৰগ্ৰাম",
+ "exif-pdf-version": "পি.ডি.এফ. ৰূপত সংস্কৰণ",
+ "exif-pdf-pagesize": "পৃষ্ঠাৰ আকাৰ"
+}
diff --git a/extensions/PdfHandler/i18n/ast.json b/extensions/PdfHandler/i18n/ast.json
new file mode 100644
index 00000000..b2ad9d48
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ast.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Xuacu"
+ ]
+ },
+ "pdf-desc": "Xestor pa ver los ficheros PDF en mou d'imaxe",
+ "pdf_no_metadata": "Nun se pudieron sacar los metadatos del PDF",
+ "pdf_page_error": "El númberu de la páxina nun ta nel rangu",
+ "exif-pdf-producer": "Programa de conversión",
+ "exif-pdf-version": "Versión del formatu PDF",
+ "exif-pdf-encrypted": "Cifráu",
+ "exif-pdf-pagesize": "Tamañu de la páxina"
+}
diff --git a/extensions/PdfHandler/i18n/azb.json b/extensions/PdfHandler/i18n/azb.json
new file mode 100644
index 00000000..d947868b
--- /dev/null
+++ b/extensions/PdfHandler/i18n/azb.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Amir a57"
+ ]
+ },
+ "exif-pdf-pagesize": "صحیفه اولچوسو"
+}
diff --git a/extensions/PdfHandler/i18n/ba.json b/extensions/PdfHandler/i18n/ba.json
new file mode 100644
index 00000000..451d0637
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ba.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Assele"
+ ]
+ },
+ "pdf-desc": "PDF файлдарҙы рәсемдәр рәүешендә ҡарау өсөн эшкәртеүсе ҡорал",
+ "pdf_no_metadata": "PDF-тан мета-мәғлүмәтте алыу мөмкин түгел",
+ "pdf_page_error": "Бит һаны биттәр һанынан ашҡан"
+}
diff --git a/extensions/PdfHandler/i18n/bcl.json b/extensions/PdfHandler/i18n/bcl.json
new file mode 100644
index 00000000..3614c142
--- /dev/null
+++ b/extensions/PdfHandler/i18n/bcl.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Geopoet"
+ ]
+ },
+ "pdf-desc": "An tagapagkapot para sa pagtatanaw kan PDF na mga sagunson na yaon sa moda nin imahe.",
+ "pdf_no_metadata": "Dae makakakua nin datos na meta gikan sa PDF.",
+ "pdf_page_error": "An numero kan pahina dae tabi abot.",
+ "exif-pdf-producer": "Programa nin kombersyon",
+ "exif-pdf-version": "Bersyon kan PDF pormat",
+ "exif-pdf-encrypted": "Enkriptado",
+ "exif-pdf-pagesize": "Sukol kan pahina"
+}
diff --git a/extensions/PdfHandler/i18n/be-tarask.json b/extensions/PdfHandler/i18n/be-tarask.json
new file mode 100644
index 00000000..95401de0
--- /dev/null
+++ b/extensions/PdfHandler/i18n/be-tarask.json
@@ -0,0 +1,16 @@
+{
+ "@metadata": {
+ "authors": [
+ "EugeneZelenko",
+ "Jim-by",
+ "Wizardist"
+ ]
+ },
+ "pdf-desc": "Апрацоўшчык для прагляду PDF-файлаў у выглядзе выяваў",
+ "pdf_no_metadata": "Немагчыма атрымаць мэта-зьвесткі з PDF-файла",
+ "pdf_page_error": "Нумар старонкі паза дыяпазонам",
+ "exif-pdf-producer": "Праграма канвэртацыі",
+ "exif-pdf-version": "Вэрсія фармату PDF",
+ "exif-pdf-encrypted": "Зашыфравана",
+ "exif-pdf-pagesize": "Памер старонкі"
+}
diff --git a/extensions/PdfHandler/i18n/bg.json b/extensions/PdfHandler/i18n/bg.json
new file mode 100644
index 00000000..e04fd36b
--- /dev/null
+++ b/extensions/PdfHandler/i18n/bg.json
@@ -0,0 +1,13 @@
+{
+ "@metadata": {
+ "authors": [
+ "DCLXVI",
+ "Stanqo",
+ "Turin"
+ ]
+ },
+ "pdf_no_metadata": "невъзможно е да бъдат извлечени метаданни от PDF",
+ "pdf_page_error": "Номерът на страница е извън обхвата",
+ "exif-pdf-encrypted": "Криптиране",
+ "exif-pdf-pagesize": "Размер на страницата"
+}
diff --git a/extensions/PdfHandler/i18n/bn.json b/extensions/PdfHandler/i18n/bn.json
new file mode 100644
index 00000000..661fffe7
--- /dev/null
+++ b/extensions/PdfHandler/i18n/bn.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Nasir8891",
+ "Wikitanvir"
+ ]
+ },
+ "pdf-desc": "পিডিএফ ফাইল ছবি হিসাবে ব্যবহারের পদ্ধতি",
+ "pdf_no_metadata": "পিডিএফ থেকে মেটাডেটা পাওয়া যায়নি",
+ "pdf_page_error": "পাতার নম্বর সীমার মধ্যে নেই"
+}
diff --git a/extensions/PdfHandler/i18n/br.json b/extensions/PdfHandler/i18n/br.json
new file mode 100644
index 00000000..5b9f8a82
--- /dev/null
+++ b/extensions/PdfHandler/i18n/br.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Fohanno",
+ "Fulup"
+ ]
+ },
+ "pdf-desc": "Maveg evit gwelet ar restroù PDF e mod skeudenn",
+ "pdf_no_metadata": "Dibosupl tapout meta-roadennoù digant ar restr PDF",
+ "pdf_page_error": "N'emañ ket niverenn ar bajenn er skeuliad",
+ "exif-pdf-producer": "Program amdreiñ",
+ "exif-pdf-version": "Stumm ar furmad PDF",
+ "exif-pdf-encrypted": "Sifret",
+ "exif-pdf-pagesize": "Ment ar bajenn"
+}
diff --git a/extensions/PdfHandler/i18n/bs.json b/extensions/PdfHandler/i18n/bs.json
new file mode 100644
index 00000000..7085dad0
--- /dev/null
+++ b/extensions/PdfHandler/i18n/bs.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "CERminator"
+ ]
+ },
+ "pdf-desc": "Uređivač za pregled PDF datoteka u modu za slike",
+ "pdf_no_metadata": "Ne mogu se naći metapodaci u PDFu",
+ "pdf_page_error": "Broj stranice nije u rasponu"
+}
diff --git a/extensions/PdfHandler/i18n/ca.json b/extensions/PdfHandler/i18n/ca.json
new file mode 100644
index 00000000..6bf49e64
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ca.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Aleator"
+ ]
+ },
+ "pdf-desc": "Gestor per a visualitzar arxius PDF en mode imatge",
+ "pdf_no_metadata": "No s'han pogut obtenir metadades del PDF",
+ "pdf_page_error": "Número de pàgina fora d'abast"
+}
diff --git a/extensions/PdfHandler/i18n/ce.json b/extensions/PdfHandler/i18n/ce.json
new file mode 100644
index 00000000..906c8510
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ce.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": [
+ "Sasan700",
+ "Умар"
+ ]
+ },
+ "pdf-desc": "Хьажа аттон кечйо PDF-файлаш суьрта куьцехь",
+ "pdf_no_metadata": "схьацаэцало чура бух оцу PDF",
+ "pdf_page_error": "Агlон терахь дозан чулацамца дац",
+ "exif-pdf-pagesize": "АгӀона барам"
+}
diff --git a/extensions/PdfHandler/i18n/ckb.json b/extensions/PdfHandler/i18n/ckb.json
new file mode 100644
index 00000000..756f3c59
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ckb.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Calak"
+ ]
+ },
+ "exif-pdf-pagesize": "قەبارەی پەڕە"
+}
diff --git a/extensions/PdfHandler/i18n/cs.json b/extensions/PdfHandler/i18n/cs.json
new file mode 100644
index 00000000..204a5374
--- /dev/null
+++ b/extensions/PdfHandler/i18n/cs.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Matěj Grabovský",
+ "Mormegil"
+ ]
+ },
+ "pdf-desc": "Ovladač pro prohlížení PDF souborů jako obrázků",
+ "pdf_no_metadata": "Z PDF se nepodařilo získat metadata",
+ "pdf_page_error": "Číslo stránky mimo rozsah",
+ "exif-pdf-producer": "Konverzní program",
+ "exif-pdf-version": "Verze formátu PDF",
+ "exif-pdf-encrypted": "Šifrovaný",
+ "exif-pdf-pagesize": "Velikost stránky"
+}
diff --git a/extensions/PdfHandler/i18n/cy.json b/extensions/PdfHandler/i18n/cy.json
new file mode 100644
index 00000000..bdaa8abc
--- /dev/null
+++ b/extensions/PdfHandler/i18n/cy.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Lloffiwr"
+ ]
+ },
+ "pdf-desc": "Teclyn i weld ffeiliau PDF ar lun delwedd",
+ "pdf_no_metadata": "Yn methu cael y metadata o'r PDF",
+ "pdf_page_error": "Nid yw'r rhif hwn oddi mewn i ystod rhifau'r tudalennau",
+ "exif-pdf-producer": "Rhaglen trosi",
+ "exif-pdf-version": "Fersiwn y fformat PDF",
+ "exif-pdf-encrypted": "Amgriptiwyd",
+ "exif-pdf-pagesize": "Maint y dudalen"
+}
diff --git a/extensions/PdfHandler/i18n/da.json b/extensions/PdfHandler/i18n/da.json
new file mode 100644
index 00000000..06a9b63e
--- /dev/null
+++ b/extensions/PdfHandler/i18n/da.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Peter Alberti"
+ ]
+ },
+ "pdf-desc": "Håndtering af PDF-visning i billedtilstand",
+ "pdf_no_metadata": "Kan ikke hente metadata fra PDF",
+ "pdf_page_error": "Sidetallet er større end antallet af sider i dokumentet",
+ "exif-pdf-producer": "Konverteringsprogram",
+ "exif-pdf-version": "Version af PDF-format",
+ "exif-pdf-encrypted": "Krypteret",
+ "exif-pdf-pagesize": "Sidestørrelse"
+}
diff --git a/extensions/PdfHandler/i18n/de-ch.json b/extensions/PdfHandler/i18n/de-ch.json
new file mode 100644
index 00000000..1e9af268
--- /dev/null
+++ b/extensions/PdfHandler/i18n/de-ch.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Geitost"
+ ]
+ },
+ "pdf_page_error": "Seitenzahl ausserhalb des Dokumentes."
+}
diff --git a/extensions/PdfHandler/i18n/de.json b/extensions/PdfHandler/i18n/de.json
new file mode 100644
index 00000000..ea9c169d
--- /dev/null
+++ b/extensions/PdfHandler/i18n/de.json
@@ -0,0 +1,16 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kghbln",
+ "Metalhead64",
+ "Raimond Spekking"
+ ]
+ },
+ "pdf-desc": "Stellt eine Schnittstelle zur Ansicht von PDF-Dateien im Bildermodus bereit",
+ "pdf_no_metadata": "Keine Metadaten im PDF vorhanden.",
+ "pdf_page_error": "Seitenzahl außerhalb des Dokumentes.",
+ "exif-pdf-producer": "Umwandlungsprogramm",
+ "exif-pdf-version": "Version des PDF-Formats",
+ "exif-pdf-encrypted": "Verschlüsselt",
+ "exif-pdf-pagesize": "Seitengröße"
+}
diff --git a/extensions/PdfHandler/i18n/diq.json b/extensions/PdfHandler/i18n/diq.json
new file mode 100644
index 00000000..5978d419
--- /dev/null
+++ b/extensions/PdfHandler/i18n/diq.json
@@ -0,0 +1,16 @@
+{
+ "@metadata": {
+ "authors": [
+ "Aspar",
+ "Erdemaslancan",
+ "Mirzali"
+ ]
+ },
+ "pdf-desc": "şuxulnayoxo ke dosyayê PDFyan modê mocnayiş de mocneno",
+ "pdf_no_metadata": "PDF ra metadata nêgeriyeno",
+ "pdf_page_error": "numreyê peli benate de niyo",
+ "exif-pdf-producer": "Programa çerxiney",
+ "exif-pdf-version": "Versiyona babet da PDF",
+ "exif-pdf-encrypted": "Kodıno",
+ "exif-pdf-pagesize": "Ebadê pele"
+}
diff --git a/extensions/PdfHandler/i18n/dsb.json b/extensions/PdfHandler/i18n/dsb.json
new file mode 100644
index 00000000..ec2ec39d
--- /dev/null
+++ b/extensions/PdfHandler/i18n/dsb.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Michawiki"
+ ]
+ },
+ "pdf-desc": "Źěłowy rěd za woglědowanje PDF-datajow we wobrazowem modusu",
+ "pdf_no_metadata": "Metadaty njedaju se z PDF dobyś",
+ "pdf_page_error": "Bokowe cysło zwenka wobcerka",
+ "exif-pdf-producer": "Konwertěrowański program",
+ "exif-pdf-version": "Wersija PDF-formata",
+ "exif-pdf-encrypted": "Skoděrowany",
+ "exif-pdf-pagesize": "Wjelikosć boka"
+}
diff --git a/extensions/PdfHandler/i18n/el.json b/extensions/PdfHandler/i18n/el.json
new file mode 100644
index 00000000..f835d016
--- /dev/null
+++ b/extensions/PdfHandler/i18n/el.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Omnipaedista"
+ ]
+ },
+ "pdf-desc": "Διαχειριστής για την εμφάνιση αρχείων PDF σε μορφή εικόνας",
+ "pdf_no_metadata": "Αδύνατη η απόκτηση μεταδεδομένων από PDF",
+ "pdf_page_error": "Αριθμός σελίδας εκτός ορίου"
+}
diff --git a/extensions/PdfHandler/i18n/en-gb.json b/extensions/PdfHandler/i18n/en-gb.json
new file mode 100644
index 00000000..e7f8ae14
--- /dev/null
+++ b/extensions/PdfHandler/i18n/en-gb.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Shirayuki"
+ ]
+ },
+ "exif-pdf-producer": "Conversion programme"
+}
diff --git a/extensions/PdfHandler/i18n/en.json b/extensions/PdfHandler/i18n/en.json
new file mode 100644
index 00000000..18bdff89
--- /dev/null
+++ b/extensions/PdfHandler/i18n/en.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": []
+ },
+ "pdf-desc": "Handler for viewing PDF files in image mode.",
+ "pdf_no_metadata": "Cannot get metadata from PDF.",
+ "pdf_page_error": "Page number not in range.",
+ "exif-pdf-producer": "Conversion program",
+ "exif-pdf-version": "Version of PDF format",
+ "exif-pdf-encrypted": "Encrypted",
+ "exif-pdf-pagesize": "Page size"
+} \ No newline at end of file
diff --git a/extensions/PdfHandler/i18n/eo.json b/extensions/PdfHandler/i18n/eo.json
new file mode 100644
index 00000000..0f74f7c4
--- /dev/null
+++ b/extensions/PdfHandler/i18n/eo.json
@@ -0,0 +1,13 @@
+{
+ "@metadata": {
+ "authors": [
+ "Yekrats"
+ ]
+ },
+ "pdf-desc": "Ilo por vidi PDF-dosierojn en bilda reĝimo",
+ "pdf_no_metadata": "Ne povas preni metadatenon el PDF",
+ "pdf_page_error": "Paĝnombro ekster valida intervalo",
+ "exif-pdf-version": "Versio de PDF-formato",
+ "exif-pdf-encrypted": "Ĉifrita",
+ "exif-pdf-pagesize": "Grandeco de paĝo"
+}
diff --git a/extensions/PdfHandler/i18n/es.json b/extensions/PdfHandler/i18n/es.json
new file mode 100644
index 00000000..c658bf30
--- /dev/null
+++ b/extensions/PdfHandler/i18n/es.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Armando-Martin",
+ "Sanbec"
+ ]
+ },
+ "pdf-desc": "Manejador para ver archivos PDF en modo imagen",
+ "pdf_no_metadata": "No se obtuvieron metadatos del PDF",
+ "pdf_page_error": "Número de página fuera de rango",
+ "exif-pdf-producer": "Programa de conversión",
+ "exif-pdf-version": "Versión del formato PDF",
+ "exif-pdf-encrypted": "Cifrado",
+ "exif-pdf-pagesize": "Tamaño de página"
+}
diff --git a/extensions/PdfHandler/i18n/et.json b/extensions/PdfHandler/i18n/et.json
new file mode 100644
index 00000000..7cebfda7
--- /dev/null
+++ b/extensions/PdfHandler/i18n/et.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Avjoska",
+ "Pikne"
+ ]
+ },
+ "pdf-desc": "Töötleja PDF-failide piltidena kuvamiseks",
+ "pdf_no_metadata": "Ei õnnestu PDF-faili meta-andmeid saada",
+ "pdf_page_error": "Leheküljenumber pole vahemikus.",
+ "exif-pdf-producer": "Teisendusprogramm",
+ "exif-pdf-version": "PDF-vormingu versioon",
+ "exif-pdf-encrypted": "Krüptitud",
+ "exif-pdf-pagesize": "Lehe suurus"
+}
diff --git a/extensions/PdfHandler/i18n/fa.json b/extensions/PdfHandler/i18n/fa.json
new file mode 100644
index 00000000..2ceeac1c
--- /dev/null
+++ b/extensions/PdfHandler/i18n/fa.json
@@ -0,0 +1,18 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ebraminio",
+ "Huji",
+ "Reza1615",
+ "Sahim",
+ "Wayiran"
+ ]
+ },
+ "pdf-desc": "گرداننده‌ای برای مشاهدهٔ پرونده‌های پی‌دی‌اف در حالت تصویر",
+ "pdf_no_metadata": "نمی‌توان فراداده‌ها را از پی‌دی‌اف گرفت",
+ "pdf_page_error": "شماره صفحه در محدوده نیست",
+ "exif-pdf-producer": "برنامهٔ مبدل",
+ "exif-pdf-version": "نسخهٔ قالب پی‌دی‌اف",
+ "exif-pdf-encrypted": "رمز شده",
+ "exif-pdf-pagesize": "حجم صفحه"
+}
diff --git a/extensions/PdfHandler/i18n/fi.json b/extensions/PdfHandler/i18n/fi.json
new file mode 100644
index 00000000..7dd03d03
--- /dev/null
+++ b/extensions/PdfHandler/i18n/fi.json
@@ -0,0 +1,18 @@
+{
+ "@metadata": {
+ "authors": [
+ "Crt",
+ "Kulmalukko",
+ "Nike",
+ "VezonThunder",
+ "Vililikku"
+ ]
+ },
+ "pdf-desc": "Käsittelijä PDF-tiedostojen katsomiseen kuvatilassa.",
+ "pdf_no_metadata": "Metatietojen hakeminen PDF-tiedostosta epäonnistui",
+ "pdf_page_error": "Sivunumero ei ole alueella.",
+ "exif-pdf-producer": "Muunto-ohjelma",
+ "exif-pdf-version": "PDF-muodon versio",
+ "exif-pdf-encrypted": "Salattu",
+ "exif-pdf-pagesize": "Sivun koko"
+}
diff --git a/extensions/PdfHandler/i18n/fr.json b/extensions/PdfHandler/i18n/fr.json
new file mode 100644
index 00000000..9970cb21
--- /dev/null
+++ b/extensions/PdfHandler/i18n/fr.json
@@ -0,0 +1,17 @@
+{
+ "@metadata": {
+ "authors": [
+ "Crochet.david",
+ "Gomoko",
+ "Grondin",
+ "Verdy p"
+ ]
+ },
+ "pdf-desc": "Gestionnaire permettant de visualiser les fichiers PDF en mode image",
+ "pdf_no_metadata": "Impossible d’obtenir les métadonnées du fichier PDF",
+ "pdf_page_error": "Le numéro de page est hors de l’étendue.",
+ "exif-pdf-producer": "Programme de conversion",
+ "exif-pdf-version": "Version du format PDF",
+ "exif-pdf-encrypted": "Crypté",
+ "exif-pdf-pagesize": "Taille de la page"
+}
diff --git a/extensions/PdfHandler/i18n/frp.json b/extensions/PdfHandler/i18n/frp.json
new file mode 100644
index 00000000..256d38a1
--- /dev/null
+++ b/extensions/PdfHandler/i18n/frp.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "ChrisPtDe"
+ ]
+ },
+ "pdf-desc": "Utilitèro por vêre los fichiérs PDF en fôrma émâge.",
+ "pdf_no_metadata": "Pôt pas avêr les mètabalyês du fichiér PDF.",
+ "pdf_page_error": "Lo numerô de pâge est en defôr de la portâ."
+}
diff --git a/extensions/PdfHandler/i18n/gl.json b/extensions/PdfHandler/i18n/gl.json
new file mode 100644
index 00000000..06c708a4
--- /dev/null
+++ b/extensions/PdfHandler/i18n/gl.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Alma",
+ "Toliño"
+ ]
+ },
+ "pdf-desc": "Manipulador para ver ficheiros PDF no modo de imaxe",
+ "pdf_no_metadata": "Non se puideron obter os metadatos do PDF.",
+ "pdf_page_error": "O número da páxina non está no rango.",
+ "exif-pdf-producer": "Programa de conversión",
+ "exif-pdf-version": "Versión en formato PDF",
+ "exif-pdf-encrypted": "Cifrado",
+ "exif-pdf-pagesize": "Tamaño da páxina"
+}
diff --git a/extensions/PdfHandler/i18n/grc.json b/extensions/PdfHandler/i18n/grc.json
new file mode 100644
index 00000000..fd2249ab
--- /dev/null
+++ b/extensions/PdfHandler/i18n/grc.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Omnipaedista"
+ ]
+ },
+ "pdf_no_metadata": "Ἀδύνατον τὸ ἀποκομίζειν μεταδεδομένα ἐκ PDF",
+ "pdf_page_error": "Ἀριθμὸς δέλτου ἐκτὸς ἐμβελείας"
+}
diff --git a/extensions/PdfHandler/i18n/gsw.json b/extensions/PdfHandler/i18n/gsw.json
new file mode 100644
index 00000000..7dc1324e
--- /dev/null
+++ b/extensions/PdfHandler/i18n/gsw.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Als-Holder"
+ ]
+ },
+ "pdf-desc": "Schnittstell fir d Aasicht vu PDF-Dateien im Bilder-Modus",
+ "pdf_no_metadata": "Kei Metadate im PDF vorhande.",
+ "pdf_page_error": "Sytezahl usserhalb vum Dokumänt.",
+ "exif-pdf-producer": "Umwandligsprogramm",
+ "exif-pdf-version": "Version vum PDF-Format",
+ "exif-pdf-encrypted": "Verschlisslet",
+ "exif-pdf-pagesize": "Sytegreßi"
+}
diff --git a/extensions/PdfHandler/i18n/gu.json b/extensions/PdfHandler/i18n/gu.json
new file mode 100644
index 00000000..1605e38a
--- /dev/null
+++ b/extensions/PdfHandler/i18n/gu.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "KartikMistry",
+ "Sushant savla"
+ ]
+ },
+ "pdf-desc": "PDF ફાઈલોને ચિત્ર સ્વરૂપે જોવાનું સાધન",
+ "pdf_no_metadata": "PDFમાંથી મેટા ડાટા ન મેળવી શકાયો",
+ "pdf_page_error": "પાનાં ક્રમાંક અવધિમાં નથી"
+}
diff --git a/extensions/PdfHandler/i18n/he.json b/extensions/PdfHandler/i18n/he.json
new file mode 100644
index 00000000..1569df31
--- /dev/null
+++ b/extensions/PdfHandler/i18n/he.json
@@ -0,0 +1,16 @@
+{
+ "@metadata": {
+ "authors": [
+ "Amire80",
+ "Rotemliss",
+ "YaronSh"
+ ]
+ },
+ "pdf-desc": "טיפול בצפייה בקובצי PDF במצב תמונה",
+ "pdf_no_metadata": "לא ניתן לאחזר את נתוני המסמך מה־PDF",
+ "pdf_page_error": "מספר הדף אינו בטווח",
+ "exif-pdf-producer": "תוכנת המרה",
+ "exif-pdf-version": "הגרסה של תסדיר PDF",
+ "exif-pdf-encrypted": "מוצפן",
+ "exif-pdf-pagesize": "גודל דף"
+}
diff --git a/extensions/PdfHandler/i18n/hi.json b/extensions/PdfHandler/i18n/hi.json
new file mode 100644
index 00000000..e904654e
--- /dev/null
+++ b/extensions/PdfHandler/i18n/hi.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kaustubh"
+ ]
+ },
+ "pdf-desc": "चित्र मोड में पीडीएफ फ़ाईल देखनेके लिये आवश्यक प्रणाली",
+ "pdf_no_metadata": "पीडीएफ से मेटाडाटा ले नहीं पायें",
+ "pdf_page_error": "पन्ने का क्रमांक सीमामें नहीं हैं"
+}
diff --git a/extensions/PdfHandler/i18n/hr.json b/extensions/PdfHandler/i18n/hr.json
new file mode 100644
index 00000000..af9859c4
--- /dev/null
+++ b/extensions/PdfHandler/i18n/hr.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ex13"
+ ]
+ },
+ "pdf-desc": "Program za gledanje PDF datoteka u slikovnom modu",
+ "pdf_no_metadata": "Nije moguće dobiti metapodatke iz PDF",
+ "pdf_page_error": "Broj stranice nije u opsegu"
+}
diff --git a/extensions/PdfHandler/i18n/hsb.json b/extensions/PdfHandler/i18n/hsb.json
new file mode 100644
index 00000000..917cf858
--- /dev/null
+++ b/extensions/PdfHandler/i18n/hsb.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Michawiki"
+ ]
+ },
+ "pdf-desc": "Program za wobhladowanje datajow PDF we wobrazowym modusu",
+ "pdf_no_metadata": "W PDF žane metadaty njejsu.",
+ "pdf_page_error": "Ličba strony zwonka dokumenta.",
+ "exif-pdf-producer": "Konwertowanski program",
+ "exif-pdf-version": "Wersija PDF-formata",
+ "exif-pdf-encrypted": "Zaklučowany",
+ "exif-pdf-pagesize": "Wulkosć strony"
+}
diff --git a/extensions/PdfHandler/i18n/hu.json b/extensions/PdfHandler/i18n/hu.json
new file mode 100644
index 00000000..425a4474
--- /dev/null
+++ b/extensions/PdfHandler/i18n/hu.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Dani",
+ "Dj"
+ ]
+ },
+ "pdf-desc": "PDF fájlok megjelenítse képként",
+ "pdf_no_metadata": "nem sikerült lekérni a PDF metaadatait",
+ "pdf_page_error": "Az oldalszám a tartományon kívül esik",
+ "exif-pdf-producer": "Konvertáló program",
+ "exif-pdf-version": "PDF formátum verziója",
+ "exif-pdf-encrypted": "Titkosított",
+ "exif-pdf-pagesize": "Lapméret"
+}
diff --git a/extensions/PdfHandler/i18n/ia.json b/extensions/PdfHandler/i18n/ia.json
new file mode 100644
index 00000000..66550a72
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ia.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "McDutchie"
+ ]
+ },
+ "pdf-desc": "Gestor pro visualisar files PDF in modo de imagine",
+ "pdf_no_metadata": "Non pote obtener metadatos ab PDF",
+ "pdf_page_error": "Numero de pagina foras del intervallo",
+ "exif-pdf-producer": "Programma de conversion",
+ "exif-pdf-version": "Version del formato PDF",
+ "exif-pdf-encrypted": "Cryptate",
+ "exif-pdf-pagesize": "Dimension del pagina"
+}
diff --git a/extensions/PdfHandler/i18n/id.json b/extensions/PdfHandler/i18n/id.json
new file mode 100644
index 00000000..fddfc094
--- /dev/null
+++ b/extensions/PdfHandler/i18n/id.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Bennylin"
+ ]
+ },
+ "pdf-desc": "Yang menangani tampilan berkas PDF dalam mode gambar",
+ "pdf_no_metadata": "Tidak dapat membaca metadata dari PDF",
+ "pdf_page_error": "Nomor halaman di luar batas"
+}
diff --git a/extensions/PdfHandler/i18n/ilo.json b/extensions/PdfHandler/i18n/ilo.json
new file mode 100644
index 00000000..2939be6d
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ilo.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Lam-ang"
+ ]
+ },
+ "pdf-desc": "Panagtengngel para iti panagkita kadagiti PDF a papeles iti moda a ladawan",
+ "pdf_no_metadata": "Saan a makaala ti metadata manipud idiay PDF.",
+ "pdf_page_error": "Saan a masakupan ti numero ti panid.",
+ "exif-pdf-producer": "Konbersion a programa",
+ "exif-pdf-version": "Bersion iti PDF a pormat",
+ "exif-pdf-encrypted": "Naenkripto",
+ "exif-pdf-pagesize": "Kadakkel ti panid"
+}
diff --git a/extensions/PdfHandler/i18n/it.json b/extensions/PdfHandler/i18n/it.json
new file mode 100644
index 00000000..5d4ae85a
--- /dev/null
+++ b/extensions/PdfHandler/i18n/it.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Beta16",
+ "Darth Kule"
+ ]
+ },
+ "pdf-desc": "Gestore per la visualizzazione di file PDF in modalità immagine",
+ "pdf_no_metadata": "Impossibile ottenere i metadati da PDF",
+ "pdf_page_error": "Numero di pagina non compreso nell'intervallo",
+ "exif-pdf-producer": "Programma di conversione",
+ "exif-pdf-version": "Versione del formato PDF",
+ "exif-pdf-encrypted": "Crittografato",
+ "exif-pdf-pagesize": "Dimensioni pagina"
+}
diff --git a/extensions/PdfHandler/i18n/ja.json b/extensions/PdfHandler/i18n/ja.json
new file mode 100644
index 00000000..0bbffde3
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ja.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Fryed-peach",
+ "Shirayuki"
+ ]
+ },
+ "pdf-desc": "画像モードで PDF ファイルを表示するためのハンドラー",
+ "pdf_no_metadata": "PDF ファイルからメタデータを取得できません",
+ "pdf_page_error": "ページ番号が正しい範囲内にありません。",
+ "exif-pdf-producer": "変換プログラム",
+ "exif-pdf-version": "PDF 形式のバージョン",
+ "exif-pdf-encrypted": "暗号化済み",
+ "exif-pdf-pagesize": "ページのサイズ"
+}
diff --git a/extensions/PdfHandler/i18n/jv.json b/extensions/PdfHandler/i18n/jv.json
new file mode 100644
index 00000000..a9a25b2e
--- /dev/null
+++ b/extensions/PdfHandler/i18n/jv.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Meursault2004",
+ "NoiX180"
+ ]
+ },
+ "pdf-desc": "Sing nadhangi kanggo ndelok berkas PDF mawa modé gambar",
+ "pdf_no_metadata": "Ora bisa olèh metadata saka PDF",
+ "pdf_page_error": "Nomèr kaca nèng njaba wates"
+}
diff --git a/extensions/PdfHandler/i18n/ka.json b/extensions/PdfHandler/i18n/ka.json
new file mode 100644
index 00000000..34a327ad
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ka.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "BRUTE",
+ "David1010"
+ ]
+ },
+ "pdf-desc": "დამამუშავებელი PDF-ფაილების სურათების სახით დასათვალიერებლად",
+ "pdf_no_metadata": "შეუძლებელია PDF-დან მეტამონაცემების მიღება",
+ "pdf_page_error": "გვერდის ნომერი არ არის დიაპაზონში",
+ "exif-pdf-producer": "პროგრამის გარდაქმნა",
+ "exif-pdf-version": "ვერსია PDF ფორმატში",
+ "exif-pdf-encrypted": "დაშიფრული",
+ "exif-pdf-pagesize": "გვერდის ზომა"
+}
diff --git a/extensions/PdfHandler/i18n/km.json b/extensions/PdfHandler/i18n/km.json
new file mode 100644
index 00000000..db8ec3e2
--- /dev/null
+++ b/extensions/PdfHandler/i18n/km.json
@@ -0,0 +1,13 @@
+{
+ "@metadata": {
+ "authors": [
+ "Chhorran",
+ "Lovekhmer",
+ "Thearith",
+ "គីមស៊្រុន"
+ ]
+ },
+ "pdf-desc": "កម្មវិធីសំរាប់បើកមើលឯកសារ PDF ជាទំរង់រូបភាព",
+ "pdf_no_metadata": "មិនអាចទទួលយកទិន្នន័យមេតាពី PDF បានទេ",
+ "pdf_page_error": "គ្មានលេខទំព័រ ក្នុងដែនកំណត់"
+}
diff --git a/extensions/PdfHandler/i18n/kn.json b/extensions/PdfHandler/i18n/kn.json
new file mode 100644
index 00000000..a0587589
--- /dev/null
+++ b/extensions/PdfHandler/i18n/kn.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "VASANTH S.N."
+ ]
+ },
+ "exif-pdf-pagesize": "ಪುಟದ ಗಾತ್ರ"
+}
diff --git a/extensions/PdfHandler/i18n/ko.json b/extensions/PdfHandler/i18n/ko.json
new file mode 100644
index 00000000..d8bc29c9
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ko.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kwj2772",
+ "아라"
+ ]
+ },
+ "pdf-desc": "PDF 파일을 이미지 방식으로 볼 수 있게 하는 핸들러",
+ "pdf_no_metadata": "PDF 파일에서 메타데이터를 추출할 수 없습니다.",
+ "pdf_page_error": "쪽수가 범위 안에 있지 않습니다.",
+ "exif-pdf-producer": "변환 프로그램",
+ "exif-pdf-version": "PDF 형식 버전",
+ "exif-pdf-encrypted": "암호화함",
+ "exif-pdf-pagesize": "페이지 크기"
+}
diff --git a/extensions/PdfHandler/i18n/ksh.json b/extensions/PdfHandler/i18n/ksh.json
new file mode 100644
index 00000000..75347d70
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ksh.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Purodha"
+ ]
+ },
+ "pdf-desc": "Määd et möjjelesch, PDF-Dateie wie Bellder ze beloore.",
+ "pdf_no_metadata": "Kann de Metta-Date nit fun dä PDF-Datei holle.",
+ "pdf_page_error": "En Sigge-Nommer es ußerhallef",
+ "exif-pdf-producer": "Ömwandelongsprojramm",
+ "exif-pdf-version": "PDF-Fommaat-Version",
+ "exif-pdf-encrypted": "Verschlößelt",
+ "exif-pdf-pagesize": "Dä Sigg(e) ier Jrüüße"
+}
diff --git a/extensions/PdfHandler/i18n/ky.json b/extensions/PdfHandler/i18n/ky.json
new file mode 100644
index 00000000..7df1c9c4
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ky.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Chorobek"
+ ]
+ },
+ "pdf-desc": "PDF файлдарды сүрөт түрүндө көрсөткүч",
+ "pdf_no_metadata": "PDF-тин метамаалыматтарын алуу мүмкүн эмес"
+}
diff --git a/extensions/PdfHandler/i18n/lb.json b/extensions/PdfHandler/i18n/lb.json
new file mode 100644
index 00000000..36a9a88f
--- /dev/null
+++ b/extensions/PdfHandler/i18n/lb.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Robby"
+ ]
+ },
+ "pdf-desc": "\"Programm\" den et erméiglecht PDF-Fichieren als Bild ze kucken",
+ "pdf_no_metadata": "Meta-Informatiounen aus dem PDF Dokument kënnen net gelies ginn",
+ "pdf_page_error": "D'Säitenzuel ass net an dem Beräich.",
+ "exif-pdf-producer": "Ëmwandlungsprogramm",
+ "exif-pdf-version": "Versioun vum PDF-Format",
+ "exif-pdf-encrypted": "Verschlësselt",
+ "exif-pdf-pagesize": "Gréisst vun der Säit"
+}
diff --git a/extensions/PdfHandler/i18n/li.json b/extensions/PdfHandler/i18n/li.json
new file mode 100644
index 00000000..6cb890bf
--- /dev/null
+++ b/extensions/PdfHandler/i18n/li.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ooswesthoesbes"
+ ]
+ },
+ "pdf-desc": "Hanjeltj PDF-bestenj aaf en maak 't meugelik die es aafbeildjing te zeen",
+ "pdf_no_metadata": "Kèn gein metadata vanne PDF kriege",
+ "pdf_page_error": "paginanómmer besteit neet"
+}
diff --git a/extensions/PdfHandler/i18n/lrc.json b/extensions/PdfHandler/i18n/lrc.json
new file mode 100644
index 00000000..b0f7f5ef
--- /dev/null
+++ b/extensions/PdfHandler/i18n/lrc.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mogoeilor"
+ ]
+ },
+ "exif-pdf-pagesize": "انازه بلگه"
+}
diff --git a/extensions/PdfHandler/i18n/lt.json b/extensions/PdfHandler/i18n/lt.json
new file mode 100644
index 00000000..6d1315c6
--- /dev/null
+++ b/extensions/PdfHandler/i18n/lt.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Matasg"
+ ]
+ },
+ "pdf-desc": "Įrankis PDF failų peržiūrai vaizdo režime",
+ "pdf_no_metadata": "Nepavyko gauti metaduomenų iš PDF",
+ "pdf_page_error": "Puslapis numeris nėra diapazone"
+}
diff --git a/extensions/PdfHandler/i18n/mk.json b/extensions/PdfHandler/i18n/mk.json
new file mode 100644
index 00000000..30232e1f
--- /dev/null
+++ b/extensions/PdfHandler/i18n/mk.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Bjankuloski06",
+ "Brest"
+ ]
+ },
+ "pdf-desc": "Ракувач за прегледување PDF податотеки во сликовен режим",
+ "pdf_no_metadata": "Не може да се земат метаподатоци од PDF",
+ "pdf_page_error": "Бројот на страница е надвор од опсег",
+ "exif-pdf-producer": "Програм за претворање",
+ "exif-pdf-version": "Верзија на PDF-форматот",
+ "exif-pdf-encrypted": "Шифрирано",
+ "exif-pdf-pagesize": "Големина на страницата"
+}
diff --git a/extensions/PdfHandler/i18n/ml.json b/extensions/PdfHandler/i18n/ml.json
new file mode 100644
index 00000000..7c75a12b
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ml.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Praveenp",
+ "Shijualex"
+ ]
+ },
+ "pdf-desc": "പി.ഡി.എഫ്. പ്രമാണങ്ങൾ ചിത്രരൂപത്തിൽ കാണുന്നതിനുള്ള കൈകാര്യോപകരണം",
+ "pdf_no_metadata": "PDF-ൽ നിന്നു മെറ്റാഡാറ്റ ലഭിച്ചില്ല",
+ "pdf_page_error": "താളിന്റെ ക്രമസംഖ്യ പരിധിയിലധികമാണ്",
+ "exif-pdf-producer": "പരിവർത്തന പ്രോഗ്രാം",
+ "exif-pdf-version": "പി.ഡി.എഫ്. തരത്തിന്റെ പതിപ്പ്",
+ "exif-pdf-encrypted": "നിഗൂഢീകരിക്കപ്പെട്ടത്",
+ "exif-pdf-pagesize": "താളിന്റെ വലിപ്പം"
+}
diff --git a/extensions/PdfHandler/i18n/mr.json b/extensions/PdfHandler/i18n/mr.json
new file mode 100644
index 00000000..dfc9894c
--- /dev/null
+++ b/extensions/PdfHandler/i18n/mr.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kaustubh",
+ "Sankalpdravid",
+ "V.narsikar"
+ ]
+ },
+ "pdf-desc": "चित्र मोडमध्ये पीडीएफ संचिका पाहण्यासाठी आवश्यक प्रणाली",
+ "pdf_no_metadata": "पीडीएफमधून मेटाडाटा घेऊ शकत नाही",
+ "pdf_page_error": "पान क्रमांक आवाक्यात नाही"
+}
diff --git a/extensions/PdfHandler/i18n/ms.json b/extensions/PdfHandler/i18n/ms.json
new file mode 100644
index 00000000..843b160d
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ms.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Anakmalaysia"
+ ]
+ },
+ "pdf-desc": "Pengendali untuk melihat fail PDF dalam mod imej",
+ "pdf_no_metadata": "Metadata tidak boleh diperoleh dari PDF",
+ "pdf_page_error": "Nombor halaman tiada dalam julat",
+ "exif-pdf-producer": "Program penukaran",
+ "exif-pdf-version": "Versi format PDF",
+ "exif-pdf-encrypted": "Disulitkan",
+ "exif-pdf-pagesize": "Saiz halaman"
+}
diff --git a/extensions/PdfHandler/i18n/mt.json b/extensions/PdfHandler/i18n/mt.json
new file mode 100644
index 00000000..1f707e8a
--- /dev/null
+++ b/extensions/PdfHandler/i18n/mt.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Chrisportelli"
+ ]
+ },
+ "pdf_page_error": "In-numru tal-paġna ma jinsabx fl-intervall"
+}
diff --git a/extensions/PdfHandler/i18n/nb.json b/extensions/PdfHandler/i18n/nb.json
new file mode 100644
index 00000000..8f18d2da
--- /dev/null
+++ b/extensions/PdfHandler/i18n/nb.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Jsoby"
+ ]
+ },
+ "pdf-desc": "Håndtering av PDF-visning i bildemodus",
+ "pdf_no_metadata": "kan ikke hente metadata fra PDF",
+ "pdf_page_error": "Sidenummer overstiger antall sider i dokumentet",
+ "exif-pdf-producer": "Koverteringsprogram",
+ "exif-pdf-version": "Versjon av PDF-format",
+ "exif-pdf-encrypted": "Kryptert",
+ "exif-pdf-pagesize": "Sidestørrelse"
+}
diff --git a/extensions/PdfHandler/i18n/nl.json b/extensions/PdfHandler/i18n/nl.json
new file mode 100644
index 00000000..f019d3e9
--- /dev/null
+++ b/extensions/PdfHandler/i18n/nl.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Siebrand",
+ "Wiki13"
+ ]
+ },
+ "pdf-desc": "Handelt pdfbestanden af en maakt het mogelijk ze als afbeeldingen te bekijken",
+ "pdf_no_metadata": "De metadata van het pdfbestand kan niet uitgelezen worden",
+ "pdf_page_error": "Het paginanummer ligt niet binnen het bereik",
+ "exif-pdf-producer": "Conversieprogramma",
+ "exif-pdf-version": "Versie van pdfopmaak",
+ "exif-pdf-encrypted": "Versleuteld",
+ "exif-pdf-pagesize": "Papierformaat"
+}
diff --git a/extensions/PdfHandler/i18n/nn.json b/extensions/PdfHandler/i18n/nn.json
new file mode 100644
index 00000000..19f20f46
--- /dev/null
+++ b/extensions/PdfHandler/i18n/nn.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Harald Khan",
+ "Njardarlogar"
+ ]
+ },
+ "pdf-desc": "Handering av PDF-vising i biletmodus",
+ "pdf_no_metadata": "Kan ikkje henta metadata frå PDF",
+ "pdf_page_error": "Sidenummer overstig talet på sider i dokumentet"
+}
diff --git a/extensions/PdfHandler/i18n/oc.json b/extensions/PdfHandler/i18n/oc.json
new file mode 100644
index 00000000..618cfebe
--- /dev/null
+++ b/extensions/PdfHandler/i18n/oc.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Cedric31"
+ ]
+ },
+ "pdf-desc": "Utilitari per visualizar los fichièrs PDF en mòde imatge",
+ "pdf_no_metadata": "Pòt pas obténer las metadonadas del fichièr PDF",
+ "pdf_page_error": "Lo numèro de pagina es pas dins la gama.",
+ "exif-pdf-producer": "Programa de conversion",
+ "exif-pdf-version": "Version del format PDF",
+ "exif-pdf-encrypted": "Chifrat",
+ "exif-pdf-pagesize": "Talha de la pagina"
+}
diff --git a/extensions/PdfHandler/i18n/or.json b/extensions/PdfHandler/i18n/or.json
new file mode 100644
index 00000000..c11db2ef
--- /dev/null
+++ b/extensions/PdfHandler/i18n/or.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Jnanaranjan Sahu",
+ "Psubhashish"
+ ]
+ },
+ "pdf-desc": "PDF ଫାଇଲକୁ ଛବି ମୋଡ଼ରେ ଦେଖିବାର ପରିଚାଳକ",
+ "pdf_no_metadata": "ପି.ଡ଼ି.ଏଫ.ରୁ ମେଟାଡାଟା ବାହାର କରିପାରିଲୁଁ ନାହିଁ",
+ "pdf_page_error": "ପୃଷ୍ଠା ସଂଖ୍ୟା ସୀମା ଭିତରେ ନାହିଁ",
+ "exif-pdf-producer": "ରୂପାନ୍ତର କାମ",
+ "exif-pdf-version": "PDF ପ୍ରକାରର ସଂସ୍କରଣ",
+ "exif-pdf-encrypted": "ଏନକ୍ରିପ୍ଟ ହୋଇଥିବା",
+ "exif-pdf-pagesize": "ପୃଷ୍ଠା ଆକାର"
+}
diff --git a/extensions/PdfHandler/i18n/pdc.json b/extensions/PdfHandler/i18n/pdc.json
new file mode 100644
index 00000000..1d7798c2
--- /dev/null
+++ b/extensions/PdfHandler/i18n/pdc.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Xqt"
+ ]
+ },
+ "pdf_no_metadata": "Keene Meta-Daade im PDF"
+}
diff --git a/extensions/PdfHandler/i18n/pl.json b/extensions/PdfHandler/i18n/pl.json
new file mode 100644
index 00000000..1eed58a3
--- /dev/null
+++ b/extensions/PdfHandler/i18n/pl.json
@@ -0,0 +1,16 @@
+{
+ "@metadata": {
+ "authors": [
+ "Holek",
+ "Matma Rex",
+ "Sp5uhe"
+ ]
+ },
+ "pdf-desc": "Konwerter graficznego podglądu plików PDF",
+ "pdf_no_metadata": "nie można pobrać metadanych z pliku PDF",
+ "pdf_page_error": "Numer strony poza zakresem",
+ "exif-pdf-producer": "Program użyty do konwersji",
+ "exif-pdf-version": "Wersja formatu PDF",
+ "exif-pdf-encrypted": "Zaszyfrowany",
+ "exif-pdf-pagesize": "Wymiary strony"
+}
diff --git a/extensions/PdfHandler/i18n/pms.json b/extensions/PdfHandler/i18n/pms.json
new file mode 100644
index 00000000..4ec09417
--- /dev/null
+++ b/extensions/PdfHandler/i18n/pms.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Borichèt",
+ "Dragonòt"
+ ]
+ },
+ "pdf-desc": "Ël gestor për vëdde ij file PDF an manera image",
+ "pdf_no_metadata": "as peulo nen pijesse ij metadat dal PDF",
+ "pdf_page_error": "Ël nùmer ëd pàgina a l'é pa ant ël range",
+ "exif-pdf-producer": "Programa ëd conversion",
+ "exif-pdf-version": "Version dël formà PDF",
+ "exif-pdf-encrypted": "Criptà",
+ "exif-pdf-pagesize": "Dimension dla pàgina"
+}
diff --git a/extensions/PdfHandler/i18n/pnb.json b/extensions/PdfHandler/i18n/pnb.json
new file mode 100644
index 00000000..c239b8b3
--- /dev/null
+++ b/extensions/PdfHandler/i18n/pnb.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Khalid Mahmood"
+ ]
+ },
+ "pdf-desc": "پی ڈی ایف فائلاں امیج موڈ چ ویکھن لئی ہینڈلر",
+ "pdf_no_metadata": "پی ڈی ایف توں میٹاڈیٹا نئیں مل سکیا۔",
+ "pdf_page_error": "صفہ نمبر ولگن چ نئیں۔"
+}
diff --git a/extensions/PdfHandler/i18n/pt-br.json b/extensions/PdfHandler/i18n/pt-br.json
new file mode 100644
index 00000000..38947a35
--- /dev/null
+++ b/extensions/PdfHandler/i18n/pt-br.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Eduardo.mps",
+ "555"
+ ]
+ },
+ "pdf-desc": "Ferramenta de visualização de arquivos PDF em modo de imagem",
+ "pdf_no_metadata": "Não foi possível obter os metadados do PDF",
+ "pdf_page_error": "Número de página fora do intervalo",
+ "exif-pdf-producer": "Programa de conversão",
+ "exif-pdf-version": "Versão do formato PDF",
+ "exif-pdf-encrypted": "Criptografado",
+ "exif-pdf-pagesize": "Tamanho da página"
+}
diff --git a/extensions/PdfHandler/i18n/pt.json b/extensions/PdfHandler/i18n/pt.json
new file mode 100644
index 00000000..35d892f8
--- /dev/null
+++ b/extensions/PdfHandler/i18n/pt.json
@@ -0,0 +1,16 @@
+{
+ "@metadata": {
+ "authors": [
+ "Hamilton Abreu",
+ "Malafaya",
+ "Vitorvicentevalente"
+ ]
+ },
+ "pdf-desc": "Manuseador de visionamento de ficheiros PDF em modo de imagem",
+ "pdf_no_metadata": "não foi possível obter os metadados do PDF",
+ "pdf_page_error": "Número de página fora do intervalo",
+ "exif-pdf-producer": "Programa de conversão",
+ "exif-pdf-version": "Versão do formato PDF",
+ "exif-pdf-encrypted": "Criptografado",
+ "exif-pdf-pagesize": "Tamanho da página"
+}
diff --git a/extensions/PdfHandler/i18n/qqq.json b/extensions/PdfHandler/i18n/qqq.json
new file mode 100644
index 00000000..0d657592
--- /dev/null
+++ b/extensions/PdfHandler/i18n/qqq.json
@@ -0,0 +1,16 @@
+{
+ "@metadata": {
+ "authors": [
+ "Purodha",
+ "Shirayuki",
+ "The Evil IP address"
+ ]
+ },
+ "pdf-desc": "{{desc|name=Pdf Handler|url=http://www.mediawiki.org/wiki/Extension:PdfHandler}}",
+ "pdf_no_metadata": "Error message given when metadata cannot be retrieved from a PDF file",
+ "pdf_page_error": "Error message given when a PDF does not have the requested page number",
+ "exif-pdf-producer": "The label used in the metadata table at the bottom of the file description page for the program used to convert this PDF file into a PDF.\n\nThis is separate from the program used to create the original file (Which is labeled by {{msg-mw|Exif-software}}).",
+ "exif-pdf-version": "Label for the version of the pdf file format in the metadata table at the bottom of an image description page. Usually a number between 1.2 and 1.6",
+ "exif-pdf-encrypted": "Label for field in metadata table at bottom of an image description page to denote if the PDF file is encrypted. The value of the field this references is either \"no\" (most common) or something like \"yes (print:yes copy:no change:no addNotes:no)\"",
+ "exif-pdf-pagesize": "Label for the field in the metadata table at the bottom of an image description page to denote the size of the pages in the pdf. If there is more than one size of page used in this document, each size is listed once.\n{{Identical|Page size}}"
+}
diff --git a/extensions/PdfHandler/i18n/ro.json b/extensions/PdfHandler/i18n/ro.json
new file mode 100644
index 00000000..8d576489
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ro.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Stelistcristi"
+ ]
+ },
+ "pdf-desc": "Operator pentru vizualizarea fișierelor PDF în modul de imagine",
+ "pdf_no_metadata": "Nu se poate obține metadate din PDF",
+ "pdf_page_error": "Numărul paginii nu e în șir",
+ "exif-pdf-pagesize": "Dimensiunea paginii"
+}
diff --git a/extensions/PdfHandler/i18n/roa-tara.json b/extensions/PdfHandler/i18n/roa-tara.json
new file mode 100644
index 00000000..e7a5d949
--- /dev/null
+++ b/extensions/PdfHandler/i18n/roa-tara.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Joetaras"
+ ]
+ },
+ "pdf-desc": "Gestore pe vedè le file PDF in mode immaggine",
+ "pdf_no_metadata": "Non ge pozze pigghià le metadata da 'u PDF",
+ "pdf_page_error": "Numere de pàgene fore da l'indervalle",
+ "exif-pdf-producer": "Programme de conversione",
+ "exif-pdf-version": "Versione d'u formate PDF",
+ "exif-pdf-encrypted": "Criptate",
+ "exif-pdf-pagesize": "Dimenzione d'a pàgene"
+}
diff --git a/extensions/PdfHandler/i18n/ru.json b/extensions/PdfHandler/i18n/ru.json
new file mode 100644
index 00000000..e97ec2ad
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ru.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "DCamer",
+ "Александр Сигачёв"
+ ]
+ },
+ "pdf-desc": "Обработчик для просмотра PDF-файлов в виде изображений",
+ "pdf_no_metadata": "невозможно получить метаданные из PDF",
+ "pdf_page_error": "Номер страницы вне диапазона",
+ "exif-pdf-producer": "Программа преобразования",
+ "exif-pdf-version": "Версия в формате PDF",
+ "exif-pdf-encrypted": "Шифрование",
+ "exif-pdf-pagesize": "Размер страницы"
+}
diff --git a/extensions/PdfHandler/i18n/rue.json b/extensions/PdfHandler/i18n/rue.json
new file mode 100644
index 00000000..44ad4d71
--- /dev/null
+++ b/extensions/PdfHandler/i18n/rue.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Gazeb"
+ ]
+ },
+ "pdf-desc": "Овладач про перегляд PDF файлів як образків",
+ "pdf_no_metadata": "Не годен обтримати метадата з PDF",
+ "pdf_page_error": "Чісло сторінкы не є в россягу"
+}
diff --git a/extensions/PdfHandler/i18n/sa.json b/extensions/PdfHandler/i18n/sa.json
new file mode 100644
index 00000000..50a61c1e
--- /dev/null
+++ b/extensions/PdfHandler/i18n/sa.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Shubha"
+ ]
+ },
+ "pdf-desc": "सुलेख(PDF)सञ्चिकाः चित्रदशायां दर्शनाय अपेक्षिता प्रणाली",
+ "pdf_no_metadata": "सुलेखात् मेटादत्तांशः प्राप्तुम् अशक्यः",
+ "pdf_page_error": "पृष्ठक्रमाङ्कः सीमायां न विद्यते"
+}
diff --git a/extensions/PdfHandler/i18n/sah.json b/extensions/PdfHandler/i18n/sah.json
new file mode 100644
index 00000000..e893873b
--- /dev/null
+++ b/extensions/PdfHandler/i18n/sah.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "HalanTul"
+ ]
+ },
+ "pdf-desc": "PDF билэлэри ойуу курдук көрдөрөөччү",
+ "pdf_no_metadata": "PDF-тан мета дааннайдарын ылар кыах суох",
+ "pdf_page_error": "Сирэй нүөмэрэ диапазоҥҥа киирбэт"
+}
diff --git a/extensions/PdfHandler/i18n/si.json b/extensions/PdfHandler/i18n/si.json
new file mode 100644
index 00000000..aae61f8c
--- /dev/null
+++ b/extensions/PdfHandler/i18n/si.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Budhajeewa",
+ "පසිඳු කාවින්ද"
+ ]
+ },
+ "pdf-desc": "PDF ගොනු රූප මාදිලියෙන් හසුරුවනය",
+ "pdf_no_metadata": "PDF ගොනුවෙන් මෙටාදත්ත ගත නොහැක",
+ "pdf_page_error": "පිටු අංකය නිවැරදි පරාසයේ නොමැත",
+ "exif-pdf-producer": "හැරවුම් වැඩසටහන",
+ "exif-pdf-version": "PDF ආකෘතියේ අනුවාදය",
+ "exif-pdf-encrypted": "ගුප්තකේතීකරණය වූ",
+ "exif-pdf-pagesize": "පිටු ප්‍රමාණය"
+}
diff --git a/extensions/PdfHandler/i18n/sk.json b/extensions/PdfHandler/i18n/sk.json
new file mode 100644
index 00000000..b0e0a59c
--- /dev/null
+++ b/extensions/PdfHandler/i18n/sk.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Helix84"
+ ]
+ },
+ "pdf-desc": "Obsluha zobrazovania PDF súborov v režime obrázkov",
+ "pdf_no_metadata": "nie je možné získať metadáta z PDF",
+ "pdf_page_error": "Číslo stránky nie je v intervale"
+}
diff --git a/extensions/PdfHandler/i18n/sl.json b/extensions/PdfHandler/i18n/sl.json
new file mode 100644
index 00000000..bb355a3c
--- /dev/null
+++ b/extensions/PdfHandler/i18n/sl.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Dbc334"
+ ]
+ },
+ "pdf-desc": "Upravljavec ogledovanja datotek PDF v slikovnem načinu",
+ "pdf_no_metadata": "Ne morem pridobiti metapodatkov iz PDF",
+ "pdf_page_error": "Številka strani ni v dosegu",
+ "exif-pdf-producer": "Pretvorbeni program",
+ "exif-pdf-version": "Različica oblike PDF",
+ "exif-pdf-encrypted": "Šifrirano",
+ "exif-pdf-pagesize": "Velikost strani"
+}
diff --git a/extensions/PdfHandler/i18n/sq.json b/extensions/PdfHandler/i18n/sq.json
new file mode 100644
index 00000000..3ade0ae6
--- /dev/null
+++ b/extensions/PdfHandler/i18n/sq.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Olsi"
+ ]
+ },
+ "pdf-desc": "Mbajtës për pamjen e skedave PDF në mënyrën e figurave",
+ "pdf_no_metadata": "Nuk mund të merren metadata nga PDF",
+ "pdf_page_error": "Numri i faqes nuk është në varg"
+}
diff --git a/extensions/PdfHandler/i18n/sr-ec.json b/extensions/PdfHandler/i18n/sr-ec.json
new file mode 100644
index 00000000..81e69443
--- /dev/null
+++ b/extensions/PdfHandler/i18n/sr-ec.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Rancher",
+ "Михајло Анђелковић"
+ ]
+ },
+ "pdf-desc": "Програм за прегледање PDF докумената у сликовном режиму",
+ "pdf_no_metadata": "Не могу да преузмем метаподатке из PDF-а",
+ "pdf_page_error": "Број страница ван опсега"
+}
diff --git a/extensions/PdfHandler/i18n/sr-el.json b/extensions/PdfHandler/i18n/sr-el.json
new file mode 100644
index 00000000..68f497ef
--- /dev/null
+++ b/extensions/PdfHandler/i18n/sr-el.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Michaello"
+ ]
+ },
+ "pdf-desc": "Handler za pregled PDF fajlova kao slika",
+ "pdf_no_metadata": "Ne mogu se dobiti meta-podaci iz PDF-a",
+ "pdf_page_error": "Broj strane izlazi van opsega"
+}
diff --git a/extensions/PdfHandler/i18n/stq.json b/extensions/PdfHandler/i18n/stq.json
new file mode 100644
index 00000000..d4cfc773
--- /dev/null
+++ b/extensions/PdfHandler/i18n/stq.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Pyt"
+ ]
+ },
+ "pdf-desc": "Snitsteede foar dät Bekiekjen fon PDF-Doatäie in dän Bielde-Modus",
+ "pdf_no_metadata": "Neen Metadoaten in dät PDF deer.",
+ "pdf_page_error": "Siedentaal buute Riege."
+}
diff --git a/extensions/PdfHandler/i18n/sv.json b/extensions/PdfHandler/i18n/sv.json
new file mode 100644
index 00000000..2ba54aea
--- /dev/null
+++ b/extensions/PdfHandler/i18n/sv.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ainali",
+ "M.M.S."
+ ]
+ },
+ "pdf-desc": "Hantering av PDF-visning i bildläge",
+ "pdf_no_metadata": "Kan inte hämta metadata från PDF",
+ "pdf_page_error": "Sidnummer överstiger antal sidor i dokumentet",
+ "exif-pdf-producer": "Konverteringsprogram",
+ "exif-pdf-version": "Version av PDF-format",
+ "exif-pdf-encrypted": "Krypterad",
+ "exif-pdf-pagesize": "Sidstorlek"
+}
diff --git a/extensions/PdfHandler/i18n/ta.json b/extensions/PdfHandler/i18n/ta.json
new file mode 100644
index 00000000..9bd5a2f8
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ta.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Shanmugamp7",
+ "TRYPPN",
+ "மதனாஹரன்"
+ ]
+ },
+ "pdf-desc": "PDF கோப்புகளை உருவ முறையில் பார்க்க கையாளுனர்",
+ "pdf_no_metadata": "PDF இருந்து மேல்தரவை பெற இயலவில்லை",
+ "pdf_page_error": "பக்கத்தின் எண் குறிப்பிட்ட வரையறையில் இல்லை",
+ "exif-pdf-producer": "மாற்றனிரல்",
+ "exif-pdf-pagesize": "பக்க அளவு"
+}
diff --git a/extensions/PdfHandler/i18n/te.json b/extensions/PdfHandler/i18n/te.json
new file mode 100644
index 00000000..e40216f3
--- /dev/null
+++ b/extensions/PdfHandler/i18n/te.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Veeven"
+ ]
+ },
+ "pdf_page_error": "పుట సంఖ్య అవధిలో లేదు"
+}
diff --git a/extensions/PdfHandler/i18n/tk.json b/extensions/PdfHandler/i18n/tk.json
new file mode 100644
index 00000000..ada6e7f5
--- /dev/null
+++ b/extensions/PdfHandler/i18n/tk.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Hanberke"
+ ]
+ },
+ "pdf-desc": "PDF faýllaryny görkeziş režiminde görkezmek üçin işleýji",
+ "pdf_no_metadata": "PDF-den meta-maglumat alyp bolanok",
+ "pdf_page_error": "Sahypa belgisi diapazonda däl"
+}
diff --git a/extensions/PdfHandler/i18n/tl.json b/extensions/PdfHandler/i18n/tl.json
new file mode 100644
index 00000000..d736043b
--- /dev/null
+++ b/extensions/PdfHandler/i18n/tl.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "AnakngAraw"
+ ]
+ },
+ "pdf-desc": "Tagapaghawak para sa pagtanaw ng mga talaksang PDF na nasa modalidad na panglarawan",
+ "pdf_no_metadata": "Hindi makuha ang dato ng meta mula sa PDF",
+ "pdf_page_error": "Wala sa sakop ang bilang ng pahina"
+}
diff --git a/extensions/PdfHandler/i18n/tr.json b/extensions/PdfHandler/i18n/tr.json
new file mode 100644
index 00000000..75a6e1a2
--- /dev/null
+++ b/extensions/PdfHandler/i18n/tr.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Joseph"
+ ]
+ },
+ "pdf-desc": "PDF dosyalarını görüntü modunda görüntülemek için işleyici",
+ "pdf_no_metadata": "PDF'den metadata alınamıyor",
+ "pdf_page_error": "Sayfa numarası aralıkta değil"
+}
diff --git a/extensions/PdfHandler/i18n/ug-arab.json b/extensions/PdfHandler/i18n/ug-arab.json
new file mode 100644
index 00000000..4010aca1
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ug-arab.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Sahran"
+ ]
+ },
+ "exif-pdf-encrypted": "شىفىرلانغان",
+ "exif-pdf-pagesize": "بەت چوڭلۇقى"
+}
diff --git a/extensions/PdfHandler/i18n/uk.json b/extensions/PdfHandler/i18n/uk.json
new file mode 100644
index 00000000..d5af168c
--- /dev/null
+++ b/extensions/PdfHandler/i18n/uk.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Base",
+ "Prima klasy4na"
+ ]
+ },
+ "pdf-desc": "Оброблювач для перегляду PDF-файлів в режимі зображень",
+ "pdf_no_metadata": "Не виходить отримати метадані з PDF",
+ "pdf_page_error": "Номер сторінки не в діапазоні",
+ "exif-pdf-producer": "програма конвертації",
+ "exif-pdf-version": "Версія формату PDF",
+ "exif-pdf-encrypted": "Зашифровано",
+ "exif-pdf-pagesize": "Розмір сторінки"
+}
diff --git a/extensions/PdfHandler/i18n/ur.json b/extensions/PdfHandler/i18n/ur.json
new file mode 100644
index 00000000..7c3c6392
--- /dev/null
+++ b/extensions/PdfHandler/i18n/ur.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "පසිඳු කාවින්ද"
+ ]
+ },
+ "pdf_page_error": "صفحہ نمبر رینج میں نہیں"
+}
diff --git a/extensions/PdfHandler/i18n/vec.json b/extensions/PdfHandler/i18n/vec.json
new file mode 100644
index 00000000..7345331d
--- /dev/null
+++ b/extensions/PdfHandler/i18n/vec.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Candalua",
+ "GatoSelvadego"
+ ]
+ },
+ "pdf-desc": "Handler par vardar i file PDF in modalità imagine",
+ "pdf_no_metadata": "No se riesse a recuperar i metadati dal PDF",
+ "pdf_page_error": "Nùmaro de pagina mia conpreso in te l'intervalo",
+ "exif-pdf-producer": "Programa de conversion",
+ "exif-pdf-version": "Version del formato PDF",
+ "exif-pdf-encrypted": "Critigrafà",
+ "exif-pdf-pagesize": "Dimension pàjina"
+}
diff --git a/extensions/PdfHandler/i18n/vi.json b/extensions/PdfHandler/i18n/vi.json
new file mode 100644
index 00000000..e4e50ac6
--- /dev/null
+++ b/extensions/PdfHandler/i18n/vi.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Minh Nguyen",
+ "Vinhtantran"
+ ]
+ },
+ "pdf-desc": "Bộ xử lý để xem tập tin PDF ở dạng hình ảnh",
+ "pdf_no_metadata": "Không thấy truy xuất siêu dữ liệu từ PDF",
+ "pdf_page_error": "Số trang không nằm trong giới hạn",
+ "exif-pdf-producer": "Chương trình chuyển đổi",
+ "exif-pdf-version": "Phiên bản định dạng PDF",
+ "exif-pdf-encrypted": "Mã hóa",
+ "exif-pdf-pagesize": "Kích thước trang"
+}
diff --git a/extensions/PdfHandler/i18n/yo.json b/extensions/PdfHandler/i18n/yo.json
new file mode 100644
index 00000000..52995a6a
--- /dev/null
+++ b/extensions/PdfHandler/i18n/yo.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Demmy"
+ ]
+ },
+ "pdf_no_metadata": "Dátà-àtẹ̀yìnwá kó ṣe é mú láti inú PDF"
+}
diff --git a/extensions/PdfHandler/i18n/yue.json b/extensions/PdfHandler/i18n/yue.json
new file mode 100644
index 00000000..a5aa1050
--- /dev/null
+++ b/extensions/PdfHandler/i18n/yue.json
@@ -0,0 +1,6 @@
+{
+ "@metadata": [],
+ "pdf-desc": "響圖像模式睇PDF檔嘅處理器",
+ "pdf_no_metadata": "唔能夠響PDF度拎metadata",
+ "pdf_page_error": "頁數唔響範圍度"
+}
diff --git a/extensions/PdfHandler/i18n/zh-hans.json b/extensions/PdfHandler/i18n/zh-hans.json
new file mode 100644
index 00000000..3b789624
--- /dev/null
+++ b/extensions/PdfHandler/i18n/zh-hans.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Shirayuki",
+ "Yfdyh000"
+ ]
+ },
+ "pdf-desc": "在图像模式中查看PDF文件的处理器。",
+ "pdf_no_metadata": "无法在PDF中获取元数据。",
+ "pdf_page_error": "页数不在范围内。",
+ "exif-pdf-producer": "转换程序",
+ "exif-pdf-version": "PDF格式的版本",
+ "exif-pdf-encrypted": "加密",
+ "exif-pdf-pagesize": "页面大小"
+}
diff --git a/extensions/PdfHandler/i18n/zh-hant.json b/extensions/PdfHandler/i18n/zh-hant.json
new file mode 100644
index 00000000..d18de97c
--- /dev/null
+++ b/extensions/PdfHandler/i18n/zh-hant.json
@@ -0,0 +1,17 @@
+{
+ "@metadata": {
+ "authors": [
+ "Justincheng12345",
+ "Mark85296341",
+ "Simon Shek",
+ "Cwlin0416"
+ ]
+ },
+ "pdf-desc": "使用圖片模式檢視 PDF 檔案的處理程式。",
+ "pdf_no_metadata": "無法在 PDF 中取得資料定義。",
+ "pdf_page_error": "頁數超出範圍。",
+ "exif-pdf-producer": "轉換程式",
+ "exif-pdf-version": "PDF 格式版本",
+ "exif-pdf-encrypted": "已加密",
+ "exif-pdf-pagesize": "頁面大小"
+}
diff --git a/extensions/PdfHandler/tests/browser/Gemfile.lock b/extensions/PdfHandler/tests/browser/Gemfile.lock
new file mode 100644
index 00000000..c48276e7
--- /dev/null
+++ b/extensions/PdfHandler/tests/browser/Gemfile.lock
@@ -0,0 +1,62 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ builder (3.2.2)
+ childprocess (0.5.3)
+ ffi (~> 1.0, >= 1.0.11)
+ cucumber (1.3.15)
+ builder (>= 2.1.2)
+ diff-lcs (>= 1.1.3)
+ gherkin (~> 2.12)
+ multi_json (>= 1.7.5, < 2.0)
+ multi_test (>= 0.1.1)
+ data_magic (0.19)
+ faker (>= 1.1.2)
+ yml_reader (>= 0.3)
+ diff-lcs (1.2.5)
+ faker (1.3.0)
+ i18n (~> 0.5)
+ ffi (1.9.3)
+ gherkin (2.12.2)
+ multi_json (~> 1.3)
+ headless (1.0.2)
+ i18n (0.6.9)
+ json (1.8.1)
+ mediawiki_selenium (0.2.25)
+ cucumber (~> 1.3, >= 1.3.10)
+ headless (~> 1.0, >= 1.0.1)
+ json (~> 1.8, >= 1.8.1)
+ page-object (~> 1.0)
+ rest-client (~> 1.6, >= 1.6.7)
+ rspec-expectations (~> 2.14, >= 2.14.4)
+ syntax (~> 1.2, >= 1.2.0)
+ mime-types (2.3)
+ multi_json (1.10.1)
+ multi_test (0.1.1)
+ page-object (1.0)
+ page_navigation (>= 0.9)
+ selenium-webdriver (>= 2.42.0)
+ watir-webdriver (>= 0.6.9)
+ page_navigation (0.9)
+ data_magic (>= 0.14)
+ rest-client (1.6.7)
+ mime-types (>= 1.16)
+ rspec-expectations (2.99.1)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rubyzip (1.1.4)
+ selenium-webdriver (2.42.0)
+ childprocess (>= 0.5.0)
+ multi_json (~> 1.0)
+ rubyzip (~> 1.0)
+ websocket (~> 1.0.4)
+ syntax (1.2.0)
+ watir-webdriver (0.6.10)
+ selenium-webdriver (>= 2.18.0)
+ websocket (1.0.7)
+ yml_reader (0.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ mediawiki_selenium
diff --git a/extensions/PdfHandler/tests/browser/features/pdf.feature b/extensions/PdfHandler/tests/browser/features/pdf.feature
new file mode 100644
index 00000000..f78dd1ca
--- /dev/null
+++ b/extensions/PdfHandler/tests/browser/features/pdf.feature
@@ -0,0 +1,22 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+@chrome @firefox @internet_explorer_8 @internet_explorer_9 @internet_explorer_10 @phantomjs @test2.wikipedia.org
+Feature: PDF
+
+ Scenario: Check for Download as PDF link
+ Given I am at a random page
+ Then Download as PDF should be present
+
+ Scenario: Click on Download as PDF link
+ Given I am at a random page
+ When I click on Download as PDF
+ Then Download the file link should be present
diff --git a/extensions/PdfHandler/tests/browser/features/step_definitions/pdf_steps.rb b/extensions/PdfHandler/tests/browser/features/step_definitions/pdf_steps.rb
new file mode 100644
index 00000000..25cf8ef4
--- /dev/null
+++ b/extensions/PdfHandler/tests/browser/features/step_definitions/pdf_steps.rb
@@ -0,0 +1,20 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+Then(/^Download as PDF should be present$/) do
+ on(PdfPage).download_as_pdf_element.should exist
+end
+When(/^I click on Download as PDF$/) do
+ on(PdfPage).download_as_pdf_element.when_present.click
+end
+Then(/^Download the file link should be present$/) do
+ on(PdfPage).download_the_file_element.when_present(30).should exist
+end
diff --git a/extensions/PdfHandler/tests/browser/features/support/env.rb b/extensions/PdfHandler/tests/browser/features/support/env.rb
new file mode 100644
index 00000000..515ff78a
--- /dev/null
+++ b/extensions/PdfHandler/tests/browser/features/support/env.rb
@@ -0,0 +1,12 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+require "mediawiki_selenium"
diff --git a/extensions/PdfHandler/tests/browser/features/support/pages/random_page.rb b/extensions/PdfHandler/tests/browser/features/support/pages/random_page.rb
new file mode 100644
index 00000000..8d77e976
--- /dev/null
+++ b/extensions/PdfHandler/tests/browser/features/support/pages/random_page.rb
@@ -0,0 +1,17 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+class PdfPage
+ include PageObject
+
+ a(:download_as_pdf, text: "Download as PDF")
+ a(:download_the_file, text: "Download the file")
+end
diff --git a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php
index 6d1f73c7..3580d013 100644
--- a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php
+++ b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php
@@ -470,18 +470,6 @@ class SyntaxHighlight_GeSHi {
}
/**
- * Get the GeSHI's version information while Special:Version is read.
- * @param $extensionTypes
- * @return bool
- */
- public static function extensionTypes( &$extensionTypes ) {
- global $wgExtensionCredits;
- self::initialise();
- $wgExtensionCredits['parserhook']['SyntaxHighlight_GeSHi']['version'] = GESHI_VERSION;
- return true;
- }
-
- /**
* Register a ResourceLoader module providing styles for each supported language.
*
* @param ResourceLoader $resourceLoader
diff --git a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.php b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.php
index da33ebee..6820ae1e 100644
--- a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.php
+++ b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.php
@@ -36,12 +36,15 @@ if( !defined( 'MEDIAWIKI' ) ) {
die();
}
-$wgExtensionCredits['parserhook']['SyntaxHighlight_GeSHi'] = array(
+require_once __DIR__ . '/geshi/geshi.php';
+
+$wgExtensionCredits['parserhook'][] = array(
'path' => __FILE__,
'name' => 'SyntaxHighlight',
'author' => array( 'Brion Vibber', 'Tim Starling', 'Rob Church', 'Niklas Laxström' ),
'descriptionmsg' => 'syntaxhighlight-desc',
'url' => 'https://www.mediawiki.org/wiki/Extension:SyntaxHighlight_GeSHi',
+ 'version' => GESHI_VERSION,
);
// Change these in LocalSettings.php
@@ -56,7 +59,6 @@ $wgAutoloadClasses['SyntaxHighlight_GeSHi'] = $dir . 'SyntaxHighlight_GeSHi.clas
$wgAutoloadClasses['ResourceLoaderGeSHiModule'] = $dir . 'ResourceLoaderGeSHiModule.php';
$wgAutoloadClasses['ResourceLoaderGeSHiLocalModule'] = $dir . 'ResourceLoaderGeSHiLocalModule.php';
-$wgHooks['ExtensionTypes'][] = 'SyntaxHighlight_GeSHi::extensionTypes';
$wgHooks['ResourceLoaderRegisterModules'][] = 'SyntaxHighlight_GeSHi::resourceLoaderRegisterModules';
$wgHooks['ContentGetParserOutput'][] = 'SyntaxHighlight_GeSHi::renderHook';
diff --git a/extensions/TitleBlacklist/tests/ApiQueryTitleBlacklistTest.php b/extensions/TitleBlacklist/tests/ApiQueryTitleBlacklistTest.php
new file mode 100644
index 00000000..344e9996
--- /dev/null
+++ b/extensions/TitleBlacklist/tests/ApiQueryTitleBlacklistTest.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Test the TitleBlacklist API.
+ *
+ * This wants to run with phpunit.php, like so:
+ * cd $IP/tests/phpunit
+ * php phpunit.php ../../extensions/TitleBlacklist/tests/ApiQueryTitleBlacklistTest.php
+ *
+ * The blacklist file is `testSource` and shared by all tests.
+ *
+ * Ian Baker <ian@wikimedia.org>
+ */
+
+ini_set( 'include_path', ini_get( 'include_path' ) . ':' . __DIR__ . '/../../../tests/phpunit/includes/api' );
+
+/**
+ * @group medium
+ **/
+class ApiQueryTitleBlacklistTest extends ApiTestCase {
+
+ function setUp() {
+ global $wgTitleBlacklistSources;
+ parent::setUp();
+ $this->doLogin();
+
+ $wgTitleBlacklistSources = array(
+ array(
+ 'type' => TBLSRC_FILE,
+ 'src' => __DIR__ . '/testSource',
+ ),
+ );
+ }
+
+ /**
+ * Verify we allow a title which is not blacklisted
+ */
+ function testCheckingUnlistedTitle() {
+ $unlisted = $this->doApiRequest( array(
+ 'action' => 'titleblacklist',
+ // evil_acc is blacklisted as <newaccountonly>
+ 'tbtitle' => 'evil_acc',
+ 'tbaction' => 'create',
+ 'tbnooverride' => true,
+ ) );
+
+ $this->assertEquals(
+ 'ok',
+ $unlisted[0]['titleblacklist']['result'],
+ 'Not blacklisted title returns ok'
+ );
+ }
+
+ /**
+ * Verify tboverride works
+ */
+ function testTboverride() {
+ global $wgGroupPermissions;
+
+ // Allow all users to override the titleblacklist
+ $wgGroupPermissions['*']['tboverride'] = true;
+
+ $unlisted = $this->doApiRequest( array(
+ 'action' => 'titleblacklist',
+ 'tbtitle' => 'bar',
+ 'tbaction' => 'create',
+ ) );
+
+ $this->assertEquals(
+ 'ok',
+ $unlisted[0]['titleblacklist']['result'],
+ 'Blacklisted title returns ok if the user is allowd to tboverride'
+ );
+ }
+
+ /**
+ * Verify a blacklisted title gives out an error.
+ */
+ function testCheckingBlackListedTitle() {
+ $listed = $this->doApiRequest( array(
+ 'action' => 'titleblacklist',
+ 'tbtitle' => 'bar',
+ 'tbaction' => 'create',
+ 'tbnooverride' => true,
+ ) );
+
+ $this->assertEquals(
+ 'blacklisted',
+ $listed[0]['titleblacklist']['result'],
+ 'Listed title returns error'
+ );
+ $this->assertEquals(
+ "The title \"bar\" has been banned from creation.\nIt matches the following blacklist entry: <code>[Bb]ar #example blacklist entry</code>",
+ $listed[0]['titleblacklist']['reason'],
+ 'Listed title error text is as expected'
+ );
+
+ $this->assertEquals(
+ "titleblacklist-forbidden-edit",
+ $listed[0]['titleblacklist']['message'],
+ 'Correct blacklist message name is returned'
+ );
+
+ $this->assertEquals(
+ "[Bb]ar #example blacklist entry",
+ $listed[0]['titleblacklist']['line'],
+ 'Correct blacklist line is returned'
+ );
+ }
+
+ /**
+ * Tests integration with the AntiSpoof extension
+ */
+ function testAntiSpoofIntegration() {
+ if ( !class_exists( 'AntiSpoof') ) {
+ $this->markTestSkipped( "This test requires the AntiSpoof extension" );
+ }
+
+ $listed = $this->doApiRequest( array(
+ 'action' => 'titleblacklist',
+ 'tbtitle' => 'AVVVV',
+ 'tbaction' => 'create',
+ 'tbnooverride' => true,
+ ) );
+
+ $this->assertEquals(
+ 'blacklisted',
+ $listed[0]['titleblacklist']['result'],
+ 'Spoofed title is blacklisted'
+ );
+
+ }
+}
diff --git a/extensions/TitleBlacklist/tests/testSource b/extensions/TitleBlacklist/tests/testSource
new file mode 100644
index 00000000..235cc671
--- /dev/null
+++ b/extensions/TitleBlacklist/tests/testSource
@@ -0,0 +1,5 @@
+[Bb]ar #example blacklist entry
+.*[Ff]ail.*
+.*[Nn]yancat.* <errmsg=blacklisted-nyancat>
+.*evil_acc.* <newaccountonly>
+AW{1,10} <antispoof>
diff --git a/extensions/WikiEditor/tests/selenium/WikiDialogs_Links.php b/extensions/WikiEditor/tests/selenium/WikiDialogs_Links.php
new file mode 100644
index 00000000..7153f49f
--- /dev/null
+++ b/extensions/WikiEditor/tests/selenium/WikiDialogs_Links.php
@@ -0,0 +1,67 @@
+<?php
+require_once 'WikiDialogs_Links_Setup.php';
+/**
+ * Description of WikiNewPageDialogs
+ *
+ * @author bhagyag, pdhanda
+ *
+ * This test case is part of the WikiEditorTestSuite.
+ * Configuration for these tests are dosumented as part of extensions/WikiEditor/tests/selenium/WikiEditorTestSuite.php
+ *
+ */
+class WikiDialogs_Links extends WikiDialogs_Links_Setup {
+ // Set up the testing environment
+ function setup() {
+ parent::setUp();
+ parent::doCreateInternalTestPageIfMissing();
+ }
+
+ function tearDown() {
+ parent::doLogout();
+ parent::tearDown();
+ }
+
+ // Create a new page temporary
+ function createNewPage() {
+ parent::doOpenLink();
+ parent::login();
+ parent::doCreateNewPageTemporary();
+ }
+
+ // Add a internal link and verify
+ function testInternalLink() {
+ $this->createNewPage();
+ parent::verifyInternalLink();
+ }
+
+ // Add a internal link with different display text and verify
+ function testInternalLinkWithDisplayText() {
+ $this->createNewPage();
+ parent::verifyInternalLinkWithDisplayText();
+ }
+
+ // Add a internal link with blank display text and verify
+ function testInternalLinkWithBlankDisplayText() {
+ $this->createNewPage();
+ parent::verifyInternalLinkWithBlankDisplayText();
+ }
+
+ // Add external link and verify
+ function testExternalLink() {
+ $this->createNewPage();
+ parent::verifyExternalLink();
+ }
+
+ // Add external link with different display text and verify
+ function testExternalLinkWithDisplayText( ) {
+ $this->createNewPage();
+ parent::verifyExternalLinkWithDisplayText();
+ }
+
+ // Add external link with Blank display text and verify
+ function testExternalLinkWithBlankDisplayText() {
+ $this->createNewPage();
+ parent::verifyExternalLinkWithBlankDisplayText();
+ }
+
+}
diff --git a/extensions/WikiEditor/tests/selenium/WikiDialogs_Links_Setup.php b/extensions/WikiEditor/tests/selenium/WikiDialogs_Links_Setup.php
new file mode 100644
index 00000000..352ebec0
--- /dev/null
+++ b/extensions/WikiEditor/tests/selenium/WikiDialogs_Links_Setup.php
@@ -0,0 +1,295 @@
+<?php
+include( "WikiEditorConstants.php" );
+/**
+ * This test case will be handling the Wiki Tool bar Dialog functions
+ * Date : Apr - 2010
+ * @author : BhagyaG - Calcey
+ */
+class WikiDialogs_Links_Setup extends SeleniumTestCase {
+
+ // Open the page.
+ function doOpenLink() {
+ $this->open( $this->getUrl() . '/index.php' );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ }
+
+ // Expand advance tool bar section if its not
+ function doExpandAdvanceSection() {
+ if ( !$this->isTextPresent( TEXT_HEADING ) ) {
+ $this->click( LINK_ADVANCED );
+ }
+ }
+
+ // Log out from the application
+ function doLogout() {
+ $this->open( $this->getUrl() . '/index.php' );
+ if ( $this->isTextPresent( TEXT_LOGOUT ) ) {
+ $this->click( LINK_LOGOUT );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( TEXT_LOGOUT_CONFIRM, $this->getText( LINK_LOGIN ) );
+ $this->open( $this->getUrl() . '/index.php' );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ }
+ }
+
+ // Create a temporary fixture page
+ function doCreateInternalTestPageIfMissing() {
+ $this->type( INPUT_SEARCH_BOX, WIKI_INTERNAL_LINK );
+ $this->click( BUTTON_SEARCH );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->click( LINK_START . WIKI_INTERNAL_LINK );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $location = $this->getLocation() . "\n";
+ if ( strpos( $location, '&redlink=1' ) !== false ) {
+ $this->type( TEXT_EDITOR, "Test fixture page. No real content here" );
+ $this->click( BUTTON_SAVE_WATCH );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertTrue( $this->isTextPresent( WIKI_INTERNAL_LINK ),
+ $this->getText( TEXT_PAGE_HEADING ) );
+ }
+ }
+
+ // Create a temporary new page
+ function doCreateNewPageTemporary() {
+ $this->type( INPUT_SEARCH_BOX, WIKI_TEMP_NEWPAGE );
+ $this->click( BUTTON_SEARCH );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->click( LINK_START . WIKI_TEMP_NEWPAGE );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ }
+
+ // Add a internal link and verify
+ function verifyInternalLink() {
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDLINK );
+ $this->waitForPopup( 'addLink', WIKI_TEST_WAIT_TIME );
+ $this->type( TEXT_LINKNAME, ( WIKI_INTERNAL_LINK ) );
+ $this->assertTrue( $this->isElementPresent( ICON_PAGEEXISTS ), 'Element ' . ICON_PAGEEXISTS . 'Not found' );
+ $this->assertEquals( "on", $this->getValue( OPT_INTERNAL ) );
+ $this->click( BUTTON_INSERTLINK );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( ( WIKI_INTERNAL_LINK ), $this->getText( LINK_START . WIKI_INTERNAL_LINK ) );
+ $this->click( LINK_START . WIKI_INTERNAL_LINK );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertTrue( $this->isTextPresent( WIKI_INTERNAL_LINK ), $this->getText( TEXT_PAGE_HEADING ) );
+ }
+
+ // Add a internal link with different display text and verify
+ function verifyInternalLinkWithDisplayText() {
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDLINK );
+ $this->waitForPopup( 'addLink', WIKI_TEST_WAIT_TIME );
+ $this->type( TEXT_LINKNAME, WIKI_INTERNAL_LINK );
+ $this->type ( TEXT_LINKDISPLAYNAME, WIKI_INTERNAL_LINK . TEXT_LINKDISPLAYNAME_APPENDTEXT );
+ $this->assertTrue( $this->isElementPresent( ICON_PAGEEXISTS ) );
+ $this->assertEquals( "on", $this->getValue( OPT_INTERNAL ) );
+ $this->click( BUTTON_INSERTLINK );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( WIKI_INTERNAL_LINK . TEXT_LINKDISPLAYNAME_APPENDTEXT,
+ $this->getText( LINK_START . WIKI_INTERNAL_LINK . TEXT_LINKDISPLAYNAME_APPENDTEXT ) );
+ $this->click( LINK_START . WIKI_INTERNAL_LINK . TEXT_LINKDISPLAYNAME_APPENDTEXT );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertTrue( $this->isTextPresent( WIKI_INTERNAL_LINK ), $this->getText( TEXT_PAGE_HEADING ) );
+
+ }
+
+ // Add a internal link with blank display text and verify
+ function verifyInternalLinkWithBlankDisplayText() {
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDLINK );
+ $this->waitForPopup( 'addLink', WIKI_TEST_WAIT_TIME );
+ $this->type( TEXT_LINKNAME, WIKI_INTERNAL_LINK );
+ $this->type( TEXT_LINKDISPLAYNAME, "" );
+ $this->assertTrue( $this->isElementPresent( ICON_PAGEEXISTS ) );
+ $this->assertEquals( "on", $this->getValue( OPT_INTERNAL ) );
+ $this->click( BUTTON_INSERTLINK );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( WIKI_INTERNAL_LINK, $this->getText( LINK_START . WIKI_INTERNAL_LINK ) );
+ $this->click( LINK_START . WIKI_INTERNAL_LINK );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( WIKI_INTERNAL_LINK, $this->getText( TEXT_PAGE_HEADING ) );
+
+ }
+
+ // Add external link and verify
+ function verifyExternalLink() {
+ $this->type( LINK_PREVIEW, "" );
+ $this->click( LINK_ADDLINK );
+ $this->type( TEXT_LINKNAME, WIKI_EXTERNAL_LINK );
+ $this->assertTrue( $this->isElementPresent( ICON_PAGEEXTERNAL ) );
+ $this->assertEquals( "on", $this->getValue( OPT_EXTERNAL ) );
+ $this->click( BUTTON_INSERTLINK );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( WIKI_EXTERNAL_LINK, $this->getText( LINK_START . WIKI_EXTERNAL_LINK ) );
+
+ $this->click( LINK_START . WIKI_EXTERNAL_LINK );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( WIKI_EXTERNAL_LINK_TITLE, $this->getTitle() );
+ }
+
+ // Add external link with different display text and verify
+ function verifyExternalLinkWithDisplayText() {
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDLINK );
+ $this->type( TEXT_LINKNAME, WIKI_EXTERNAL_LINK );
+ $this->type( TEXT_LINKDISPLAYNAME, WIKI_EXTERNAL_LINK_TITLE );
+ $this->assertTrue( $this->isElementPresent( ICON_PAGEEXTERNAL ) );
+ $this->assertEquals( "on", $this->getValue( OPT_EXTERNAL ) );
+ $this->click( BUTTON_INSERTLINK );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( WIKI_EXTERNAL_LINK_TITLE, $this->getText( LINK_START . WIKI_EXTERNAL_LINK_TITLE ) );
+ $this->click( LINK_START . ( WIKI_EXTERNAL_LINK_TITLE ) );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( WIKI_EXTERNAL_LINK_TITLE , $this->getTitle() );
+ }
+
+ // Add external link with Blank display text and verify
+ function verifyExternalLinkWithBlankDisplayText() {
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDLINK );
+ $this->type( TEXT_LINKNAME, WIKI_EXTERNAL_LINK );
+ $this->type( TEXT_LINKDISPLAYNAME, "" );
+ $this->assertTrue( $this->isElementPresent( ICON_PAGEEXTERNAL ) );
+ $this->assertEquals( "on", $this->getValue( OPT_EXTERNAL ) );
+ $this->click( BUTTON_INSERTLINK );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( "[1]", $this->getText( LINK_START . "[1]" ) );
+ $this->click( LINK_START . "[1]" );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertEquals( WIKI_EXTERNAL_LINK_TITLE, $this->getTitle() );
+ }
+
+ // Add a table and verify
+ function verifyCreateTable() {
+ $WIKI_TABLE_ROW = 2;
+ $WIKI_TABLE_COL = "5";
+ $this->doExpandAdvanceSection();
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDTABLE );
+ $this->click( CHK_SORT );
+ $this->type( TEXT_ROW, $WIKI_TABLE_ROW );
+ $this->type( TEXT_COL, $WIKI_TABLE_COL );
+ $this->click( BUTTON_INSERTABLE );
+ $this->click( CHK_SORT );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $WIKI_TABLE_ROW = $WIKI_TABLE_ROW + 1;
+ $this->assertTrue( $this->isElementPresent( TEXT_TABLEID_OTHER .
+ TEXT_VALIDATE_TABLE_PART1 . $WIKI_TABLE_ROW .
+ TEXT_VALIDATE_TABLE_PART2 . $WIKI_TABLE_COL .
+ TEXT_VALIDATE_TABLE_PART3 ) );
+ }
+
+ // Add a table and verify only with head row
+ function verifyCreateTableWithHeadRow() {
+ $WIKI_TABLE_ROW = 3;
+ $WIKI_TABLE_COL = "4";
+ $this->doExpandAdvanceSection();
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDTABLE );
+ $this->click( CHK_BOARDER );
+ $this->type( TEXT_ROW, $WIKI_TABLE_ROW );
+ $this->type( TEXT_COL, $WIKI_TABLE_COL );
+ $this->click( BUTTON_INSERTABLE );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $WIKI_TABLE_ROW = $WIKI_TABLE_ROW + 1;
+ $this->assertTrue( $this->isElementPresent( TEXT_TABLEID_OTHER .
+ TEXT_VALIDATE_TABLE_PART1 . $WIKI_TABLE_ROW .
+ TEXT_VALIDATE_TABLE_PART2 . $WIKI_TABLE_COL .
+ TEXT_VALIDATE_TABLE_PART3 ) );
+ }
+
+ // Add a table and verify only with borders
+ function verifyCreateTableWithBorders() {
+ $WIKI_TABLE_ROW = "4";
+ $WIKI_TABLE_COL = "6";
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDTABLE );
+ $this->click( CHK_HEADER );
+ $this->type( TEXT_ROW, $WIKI_TABLE_ROW );
+ $this->type( TEXT_COL, $WIKI_TABLE_COL );
+ $this->click( BUTTON_INSERTABLE );
+ $this->click( CHK_HEADER );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertTrue( $this->isElementPresent( TEXT_TABLEID_OTHER .
+ TEXT_VALIDATE_TABLE_PART1 . $WIKI_TABLE_ROW .
+ TEXT_VALIDATE_TABLE_PART2 . $WIKI_TABLE_COL .
+ TEXT_VALIDATE_TABLE_PART3 ) );
+ }
+
+ // Add a table and verify only with sort row
+ function verifyCreateTableWithSortRow() {
+ $WIKI_TABLE_ROW = "2";
+ $WIKI_TABLE_COL = "5";
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDTABLE );
+ $this->click( CHK_HEADER );
+ $this->click( CHK_BOARDER );
+ $this->click( CHK_SORT );
+ $this->type( TEXT_ROW, $WIKI_TABLE_ROW );
+ $this->type( TEXT_COL, $WIKI_TABLE_COL );
+ $this->click( BUTTON_INSERTABLE );
+ $this->click( CHK_HEADER );
+ $this->click( CHK_BOARDER );
+ $this->click( CHK_SORT );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertTrue( $this->isElementPresent( TEXT_TABLEID_WITHALLFEATURES .
+ TEXT_VALIDATE_TABLE_PART1 . $WIKI_TABLE_ROW .
+ TEXT_VALIDATE_TABLE_PART2 . $WIKI_TABLE_COL .
+ TEXT_VALIDATE_TABLE_PART3 ) );
+ }
+
+ // Add a table without headers,borders and sort rows
+ function verifyCreateTableWithNoSpecialEffects() {
+ $WIKI_TABLE_ROW = "6";
+ $WIKI_TABLE_COL = "2";
+ $this->
+ $this->doExpandAdvanceSection();
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDTABLE );
+ $this->click( CHK_BOARDER );
+ $this->click( CHK_HEADER );
+ $this->type( TEXT_ROW, $WIKI_TABLE_ROW );
+ $this->type( TEXT_COL, $WIKI_TABLE_COL );
+ $this->click( BUTTON_INSERTABLE );
+ $this->click( CHK_BOARDER );
+ $this->click( CHK_HEADER );
+ $this->click( INK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $this->assertTrue( $this->isElementPresent( TEXT_TABLEID_OTHER .
+ TEXT_VALIDATE_TABLE_PART1 . $WIKI_TABLE_ROW .
+ TEXT_VALIDATE_TABLE_PART2 . $WIKI_TABLE_COL .
+ TEXT_VALIDATE_TABLE_PART3 ) );
+ }
+
+ // Add a table with headers,borders and sort rows
+ function verifyCreateTableWithAllSpecialEffects() {
+ $WIKI_TABLE_ROW = 6;
+ $WIKI_TABLE_COL = "2";
+ $this->doExpandAdvanceSection();
+ $this->type( TEXT_EDITOR, "" );
+ $this->click( LINK_ADDTABLE );
+ $this->click( CHK_SORT );
+ $this->type( TEXT_ROW, $WIKI_TABLE_ROW );
+ $this->type( TEXT_COL, $WIKI_TABLE_COL );
+ $this->click( BUTTON_INSERTABLE );
+ $this->click( CHK_SORT );
+ $this->click( LINK_PREVIEW );
+ $this->waitForPageToLoad( WIKI_TEST_WAIT_TIME );
+ $WIKI_TABLE_ROW = $WIKI_TABLE_ROW + 1;
+ $this->assertTrue( $this->isElementPresent( TEXT_TABLEID_WITHALLFEATURES .
+ TEXT_VALIDATE_TABLE_PART1 . $WIKI_TABLE_ROW .
+ TEXT_VALIDATE_TABLE_PART2 . $WIKI_TABLE_COL .
+ TEXT_VALIDATE_TABLE_PART3 ) );
+ }
+
+}
diff --git a/extensions/WikiEditor/tests/selenium/WikiEditorConstants.php b/extensions/WikiEditor/tests/selenium/WikiEditorConstants.php
new file mode 100644
index 00000000..090f96bf
--- /dev/null
+++ b/extensions/WikiEditor/tests/selenium/WikiEditorConstants.php
@@ -0,0 +1,84 @@
+<?php
+define ( 'WIKI_TEST_WAIT_TIME', "3000" ); // Waiting time
+
+// tool bar, buttons , links
+// commonly using links
+define ( 'LINK_MAIN_PAGE', "link=Main page" );
+define ( 'LINK_RANDOM_PAGE', "link=Random article" );
+define ( 'TEXT_PAGE_HEADING', "firstHeading" );
+define ( 'LINK_START', "link=" );
+define ( 'LINK_EDITPAGE', "//li[@id='ca-edit']/a/span" );
+define ( 'TEXT_EDITOR', "wpTextbox1" );
+define ( 'LINK_PREVIEW', "wpPreview" );
+
+define ( 'WIKI_SEARCH_PAGE', "Hair (musical)" ); // Page name to search
+define ( 'WIKI_TEXT_SEARCH', "TV" ); // Text to search
+define ( 'WIKI_INTERNAL_LINK', "Wikieditor-Fixture-Page" ); // Exisiting page name to add as an internal tag
+define ( 'WIKI_EXTERNAL_LINK', "www.google.com" ); // External web site name
+define ( 'WIKI_EXTERNAL_LINK_TITLE', "Google" ); // Page title of the external web site name
+define ( 'WIKI_CODE_PATH', getcwd() ); // get the current path of the program
+define ( 'WIKI_SCREENSHOTS_PATH', "screenshots" ); // the folder the error screen shots will be saved
+define ( 'WIKI_SCREENSHOTS_TYPE', "png" ); // screen print type
+define ( 'WIKI_TEMP_NEWPAGE', "TestWikiPage" ); // temporary creating new page name
+// for WikiCommonFunction_TC
+
+// for WikiSearch_TC
+define ( 'INPUT_SEARCH_BOX', "searchInput" );
+define ( 'BUTTON_SEARCH', "mw-searchButton" );
+define ( 'TEXT_SEARCH_RESULT_HEADING', " - Search results - Wikipedia, the free encyclopedia" );
+
+// for WikiWatchUnWatch_TC
+define ( 'LINK_WATCH_PAGE', "link=Watch" );
+define ( 'LINK_WATCH_LIST', "link=My watchlist" );
+define ( 'LINK_WATCH_EDIT', "link=View and edit watchlist" );
+define ( 'LINK_UNWATCH', "link=Unwatch" );
+define ( 'BUTTON_WATCH', "wpWatchthis" );
+define ( 'BUTTON_SAVE_WATCH', "wpSave" );
+define ( 'TEXT_WATCH', "Watch" );
+define ( 'TEXT_UNWATCH', "Unwatch" );
+
+// for WikiCommonFunction_TC
+define ( 'TEXT_LOGOUT', "Log out" );
+define ( 'LINK_LOGOUT', "link=Log out" );
+define ( 'LINK_LOGIN', "link=Log in / create account" );
+define ( 'TEXT_LOGOUT_CONFIRM', "Log in / create account" );
+define ( 'INPUT_USER_NAME', "wpName1" );
+define ( 'INPUT_PASSWD', "wpPassword1" );
+define ( 'BUTTON_LOGIN', "wpLoginAttempt" );
+define ( 'TEXT_HEADING', "Heading" );
+define ( 'LINK_ADVANCED', "link=Advanced" );
+
+// for WikiDialogs_TC
+define ( 'LINK_ADDLINK', "//div[@id='wikiEditor-ui-toolbar']/div[1]/div[2]/span[2 ]" );
+define ( 'TEXT_LINKNAME', "wikieditor-toolbar-link-int-target" );
+define ( 'TEXT_LINKDISPLAYNAME', "wikieditor-toolbar-link-int-text" );
+define ( 'TEXT_LINKDISPLAYNAME_APPENDTEXT', " Test" );
+define ( 'ICON_PAGEEXISTS', "wikieditor-toolbar-link-int-target-status-exists" );
+define ( 'ICON_PAGEEXTERNAL', "wikieditor-toolbar-link-int-target-status-external" );
+define ( 'OPT_INTERNAL', "wikieditor-toolbar-link-type-int" );
+define ( 'OPT_EXTERNAL', "wikieditor-toolbar-link-type-ext" );
+define ( 'BUTTON_INSERTLINK', "//div[10]/div[11]/button[1]" );
+define ( 'LINK_ADDTABLE', "//div[@id='wikiEditor-ui-toolbar']/div[3]/div[1]/div[4]/span[2]" );
+define ( 'CHK_HEADER', "wikieditor-toolbar-table-dimensions-header" );
+define ( 'CHK_BOARDER', "wikieditor-toolbar-table-wikitable" );
+define ( 'CHK_SORT', "wikieditor-toolbar-table-sortable" );
+define ( 'TEXT_ROW', "wikieditor-toolbar-table-dimensions-rows" );
+define ( 'TEXT_COL', "wikieditor-toolbar-table-dimensions-columns" );
+define ( 'BUTTON_INSERTABLE', "//div[3]/button[1]" );
+define ( 'TEXT_HEADTABLE_TEXT', "Header text" );
+define ( 'TEXT_TABLEID_WITHALLFEATURES', "//table[@id='sortable_table_id_0']/tbody/" );
+define ( 'TEXT_TABLEID_OTHER', "//div[@id='wikiPreview']/table/tbody/" );
+define ( 'TEXT_VALIDATE_TABLE_PART1', "tr[" );
+define ( 'TEXT_VALIDATE_TABLE_PART2', "]/td[" );
+define ( 'TEXT_VALIDATE_TABLE_PART3', "]" );
+define ( 'LINK_SEARCH', "//div[@id='wikiEditor-ui-toolbar']/div[3]/div[1]/div[5]/span" );
+define ( 'INPUT_SEARCH', "wikieditor-toolbar-replace-search" );
+define ( 'INPUT_REPLACE', "wikieditor-toolbar-replace-replace" );
+define ( 'BUTTON_REPLACEALL', "//button[3]" );
+define ( 'BUTTON_REPLACENEXT', "//button[2]" );
+define ( 'BUTTON_CANCEL', "//button[4]" );
+define ( 'TEXT_PREVIEW_TEXT1', "//div[@id='wikiPreview']/p[1]" );
+define ( 'TEXT_PREVIEW_TEXT2', "//div[@id='wikiPreview']/p[2]" );
+define ( 'TEXT_PREVIEW_TEXT3', "//div[@id='wikiPreview']/p[3]" );
+
+
diff --git a/extensions/WikiEditor/tests/selenium/WikiEditorSeleniumConfig.php b/extensions/WikiEditor/tests/selenium/WikiEditorSeleniumConfig.php
new file mode 100644
index 00000000..137e67b0
--- /dev/null
+++ b/extensions/WikiEditor/tests/selenium/WikiEditorSeleniumConfig.php
@@ -0,0 +1,24 @@
+<?php
+
+class WikiEditorSeleniumConfig {
+
+ public static function getSettings( &$includeFiles, &$globalConfigs ) {
+ $includes = array(
+ 'extensions/Vector/Vector.php',
+ 'extensions/WikiEditor/WikiEditor.php'
+ );
+ $configs = array(
+ 'wgDefaultSkin' => 'vector',
+ 'wgWikiEditorFeatures' => array(
+ 'toolbar' => array( 'global' => true, 'user' => true ),
+ 'dialogs' => array( 'global' => true, 'user' => true )
+ ),
+ 'wgVectorFeatures' => array(
+ 'editwarning' => array( 'global' => false, 'user' => false )
+ )
+ );
+ $includeFiles = array_merge( $includeFiles, $includes );
+ $globalConfigs = array_merge( $globalConfigs, $configs );
+ return true;
+ }
+}
diff --git a/extensions/WikiEditor/tests/selenium/WikiEditorTestSuite.php b/extensions/WikiEditor/tests/selenium/WikiEditorTestSuite.php
new file mode 100644
index 00000000..14a8bf20
--- /dev/null
+++ b/extensions/WikiEditor/tests/selenium/WikiEditorTestSuite.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * To configure MW for these tests
+ * 1) If you are running multiple test suites, add the following in LocalSettings.php
+ * require_once("extensions/WikiEditor/tests/selenium/WikiEditorSeleniumConfig.php");
+ * $wgSeleniumTestConfigs['WikiEditorTestSuite'] = 'WikiEditorSeleniumConfig::getSettings';
+ * OR
+ * 2) Add the following to your Localsettings.php
+ * require_once( "$IP/extensions/Vector/Vector.php" );
+ * require_once( "$IP/extensions/WikiEditor/WikiEditor.php" );
+ * $wgDefaultSkin = 'vector';
+ * $wgVectorFeatures['editwarning'] = array( 'global' => false, 'user' => false );
+ * $wgWikiEditorFeatures['toolbar'] = array( 'global' => true, 'user' => true );
+ * $wgWikiEditorFeatures['dialogs'] = array( 'global' => true, 'user' => true );
+ *
+ */
+class WikiEditorTestSuite extends SeleniumTestSuite
+{
+ public function setUp() {
+ $this->setLoginBeforeTests( false );
+ parent::setUp();
+ }
+ public function addTests() {
+ $testFiles = array(
+ 'extensions/WikiEditor/tests/selenium/WikiDialogs_Links.php'
+ );
+ parent::addTestFiles( $testFiles );
+ }
+
+
+}
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 71268932..aad42aac 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -75,7 +75,7 @@ $wgConfigRegistry = array(
* Using single quotes is, therefore, important here.
* @since 1.2
*/
-$wgVersion = '1.24.1';
+$wgVersion = '1.24.2';
/**
* Name of the site. It must be changed in LocalSettings.php
@@ -4146,6 +4146,18 @@ $wgPasswordSalt = true;
$wgMinimalPasswordLength = 1;
/**
+ * Specifies the maximal length of a user password (T64685).
+ *
+ * It is not recommended to make this greater than the default, as it can
+ * allow DoS attacks by users setting really long passwords. In addition,
+ * this should not be lowered too much, as it enforces weak passwords.
+ *
+ * @warning Unlike other password settings, user with passwords greater than
+ * the maximum will not be able to log in.
+ */
+$wgMaximalPasswordLength = 4096;
+
+/**
* Specifies if users should be sent to a password-reset form on login, if their
* password doesn't meet the requirements of User::isValidPassword().
* @since 1.23
diff --git a/includes/EditPage.php b/includes/EditPage.php
index 128244a8..38c80ba8 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -2654,19 +2654,21 @@ class EditPage {
array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() )
);
}
- if ( $this->formtype !== 'preview' ) {
- if ( $this->isCssSubpage && $wgAllowUserCss ) {
- $wgOut->wrapWikiMsg(
- "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
- array( 'usercssyoucanpreview' )
- );
- }
+ if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
+ if ( $this->formtype !== 'preview' ) {
+ if ( $this->isCssSubpage && $wgAllowUserCss ) {
+ $wgOut->wrapWikiMsg(
+ "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
+ array( 'usercssyoucanpreview' )
+ );
+ }
- if ( $this->isJsSubpage && $wgAllowUserJs ) {
- $wgOut->wrapWikiMsg(
- "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
- array( 'userjsyoucanpreview' )
- );
+ if ( $this->isJsSubpage && $wgAllowUserJs ) {
+ $wgOut->wrapWikiMsg(
+ "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
+ array( 'userjsyoucanpreview' )
+ );
+ }
}
}
}
diff --git a/includes/Html.php b/includes/Html.php
index 1e16e394..2e148140 100644
--- a/includes/Html.php
+++ b/includes/Html.php
@@ -546,17 +546,20 @@ class Html {
} else {
// Apparently we need to entity-encode \n, \r, \t, although the
// spec doesn't mention that. Since we're doing strtr() anyway,
- // and we don't need <> escaped here, we may as well not call
- // htmlspecialchars().
+ // we may as well not call htmlspecialchars().
// @todo FIXME: Verify that we actually need to
// escape \n\r\t here, and explain why, exactly.
#
// We could call Sanitizer::encodeAttribute() for this, but we
// don't because we're stubborn and like our marginal savings on
// byte size from not having to encode unnecessary quotes.
+ // The only difference between this transform and the one by
+ // Sanitizer::encodeAttribute() is '<' is only encoded here if
+ // $wgWellFormedXml is set, and ' is not encoded.
$map = array(
'&' => '&amp;',
'"' => '&quot;',
+ '>' => '&gt;',
"\n" => '&#10;',
"\r" => '&#13;',
"\t" => '&#9;'
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index 2f8094ab..55b1da00 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -2743,7 +2743,7 @@ $templates
* call rather than a "<script src='...'>" tag.
* @return string The html "<script>", "<link>" and "<style>" tags
*/
- protected function makeResourceLoaderLink( $modules, $only, $useESI = false,
+ public function makeResourceLoaderLink( $modules, $only, $useESI = false,
array $extraQuery = array(), $loadCall = false
) {
$modules = (array)$modules;
@@ -3153,7 +3153,7 @@ $templates
* have to be purged on configuration changes.
* @return array
*/
- private function getJSVars() {
+ public function getJSVars() {
global $wgContLang;
$curRevisionId = 0;
@@ -3289,6 +3289,10 @@ $templates
if ( !$this->getTitle()->isJsSubpage() && !$this->getTitle()->isCssSubpage() ) {
return false;
}
+ if ( !$this->getTitle()->isSubpageOf( $this->getUser()->getUserPage() ) ) {
+ // Don't execute another user's CSS or JS on preview (T85855)
+ return false;
+ }
return !count( $this->getTitle()->getUserPermissionsErrors( 'edit', $this->getUser() ) );
}
diff --git a/includes/User.php b/includes/User.php
index 5e5d3eed..a925a3c4 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -773,15 +773,24 @@ class User implements IDBAccessObject {
}
/**
- * Check if this is a valid password for this user. Status will be good if
- * the password is valid, or have an array of error messages if not.
+ * Check if this is a valid password for this user
+ *
+ * Create a Status object based on the password's validity.
+ * The Status should be set to fatal if the user should not
+ * be allowed to log in, and should have any errors that
+ * would block changing the password.
+ *
+ * If the return value of this is not OK, the password
+ * should not be checked. If the return value is not Good,
+ * the password can be checked, but the user should not be
+ * able to set their password to this.
*
* @param string $password Desired password
* @return Status
* @since 1.23
*/
public function checkPasswordValidity( $password ) {
- global $wgMinimalPasswordLength, $wgContLang;
+ global $wgMinimalPasswordLength, $wgMaximalPasswordLength, $wgContLang;
static $blockedLogins = array(
'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589
@@ -801,6 +810,10 @@ class User implements IDBAccessObject {
if ( strlen( $password ) < $wgMinimalPasswordLength ) {
$status->error( 'passwordtooshort', $wgMinimalPasswordLength );
return $status;
+ } elseif ( strlen( $password ) > $wgMaximalPasswordLength ) {
+ // T64685: Password too long, might cause DoS attack
+ $status->fatal( 'passwordtoolong', $wgMaximalPasswordLength );
+ return $status;
} elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
$status->error( 'password-name-match' );
return $status;
@@ -2300,17 +2313,9 @@ class User implements IDBAccessObject {
throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
}
- if ( !$this->isValidPassword( $str ) ) {
- global $wgMinimalPasswordLength;
- $valid = $this->getPasswordValidity( $str );
- if ( is_array( $valid ) ) {
- $message = array_shift( $valid );
- $params = $valid;
- } else {
- $message = $valid;
- $params = array( $wgMinimalPasswordLength );
- }
- throw new PasswordError( wfMessage( $message, $params )->text() );
+ $status = $this->checkPasswordValidity( $str );
+ if ( !$status->isGood() ) {
+ throw new PasswordError( $status->getMessage()->text() );
}
}
@@ -3792,6 +3797,13 @@ class User implements IDBAccessObject {
$this->loadPasswords();
+ // Some passwords will give a fatal Status, which means there is
+ // some sort of technical or security reason for this password to
+ // be completely invalid and should never be checked (e.g., T64685)
+ if ( !$this->checkPasswordValidity( $password )->isOK() ) {
+ return false;
+ }
+
// Certain authentication plugins do NOT want to save
// domain passwords in a mysql database, so we should
// check this (in case $wgAuth->strict() is false).
diff --git a/includes/Xml.php b/includes/Xml.php
index 159f7114..c6c02867 100644
--- a/includes/Xml.php
+++ b/includes/Xml.php
@@ -707,13 +707,15 @@ class Xml {
/**
* Check if a string is well-formed XML.
* Must include the surrounding tag.
+ * This function is a DoS vector if an attacker can define
+ * entities in $text.
*
* @param string $text String to test.
* @return bool
*
* @todo Error position reporting return
*/
- public static function isWellFormed( $text ) {
+ private static function isWellFormed( $text ) {
$parser = xml_parser_create( "UTF-8" );
# case folding violates XML standard, turn it off
diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php
index ba90c260..ec3dc2d9 100644
--- a/includes/api/ApiFormatWddx.php
+++ b/includes/api/ApiFormatWddx.php
@@ -38,15 +38,7 @@ class ApiFormatWddx extends ApiFormatBase {
public function execute() {
$this->markDeprecated();
- // Some versions of PHP have a broken wddx_serialize_value, see
- // PHP bug 45314. Test encoding an affected character (U+00A0)
- // to avoid this.
- $expected =
- "<wddxPacket version='1.0'><header/><data><string>\xc2\xa0</string></data></wddxPacket>";
- if ( function_exists( 'wddx_serialize_value' )
- && !$this->getIsHtml()
- && wddx_serialize_value( "\xc2\xa0" ) == $expected
- ) {
+ if ( !$this->getIsHtml() && !static::useSlowPrinter() ) {
$this->printText( wddx_serialize_value( $this->getResultData() ) );
} else {
// Don't do newlines and indentation if we weren't asked
@@ -63,6 +55,44 @@ class ApiFormatWddx extends ApiFormatBase {
}
}
+ public static function useSlowPrinter() {
+ if ( !function_exists( 'wddx_serialize_value' ) ) {
+ return true;
+ }
+
+ // Some versions of PHP have a broken wddx_serialize_value, see
+ // PHP bug 45314. Test encoding an affected character (U+00A0)
+ // to avoid this.
+ $expected =
+ "<wddxPacket version='1.0'><header/><data><string>\xc2\xa0</string></data></wddxPacket>";
+ if ( wddx_serialize_value( "\xc2\xa0" ) !== $expected ) {
+ return true;
+ }
+
+ // Some versions of HHVM don't correctly encode ampersands.
+ $expected =
+ "<wddxPacket version='1.0'><header/><data><string>&amp;</string></data></wddxPacket>";
+ if ( wddx_serialize_value( '&' ) !== $expected ) {
+ return true;
+ }
+
+ // Some versions of HHVM don't correctly encode empty arrays as subvalues.
+ $expected =
+ "<wddxPacket version='1.0'><header/><data><array length='1'><array length='0'></array></array></data></wddxPacket>";
+ if ( wddx_serialize_value( array( array() ) ) !== $expected ) {
+ return true;
+ }
+
+ // Some versions of HHVM don't correctly encode associative arrays with numeric keys.
+ $expected =
+ "<wddxPacket version='1.0'><header/><data><struct><var name='2'><number>1</number></var></struct></data></wddxPacket>";
+ if ( wddx_serialize_value( array( 2 => 1 ) ) !== $expected ) {
+ return true;
+ }
+
+ return false;
+ }
+
/**
* Recursively go through the object and output its data in WDDX format.
* @param mixed $elemValue
diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php
index 9e8ee94f..df2f0e32 100644
--- a/includes/installer/PostgresUpdater.php
+++ b/includes/installer/PostgresUpdater.php
@@ -384,8 +384,6 @@ class PostgresUpdater extends DatabaseUpdater {
'page(page_id) ON DELETE CASCADE' ),
array( 'changeFkeyDeferrable', 'protected_titles', 'pt_user',
'mwuser(user_id) ON DELETE SET NULL' ),
- array( 'changeFkeyDeferrable', 'recentchanges', 'rc_cur_id',
- 'page(page_id) ON DELETE SET NULL' ),
array( 'changeFkeyDeferrable', 'recentchanges', 'rc_user',
'mwuser(user_id) ON DELETE SET NULL' ),
array( 'changeFkeyDeferrable', 'redirect', 'rd_from', 'page(page_id) ON DELETE CASCADE' ),
@@ -418,6 +416,10 @@ class PostgresUpdater extends DatabaseUpdater {
array( 'addPgField', 'pagelinks', 'pl_from_namespace', 'INTEGER NOT NULL DEFAULT 0' ),
array( 'addPgField', 'templatelinks', 'tl_from_namespace', 'INTEGER NOT NULL DEFAULT 0' ),
array( 'addPgField', 'imagelinks', 'il_from_namespace', 'INTEGER NOT NULL DEFAULT 0' ),
+
+ // 1.24.1 (backport from 1.25)
+ array( 'dropFkey', 'recentchanges', 'rc_cur_id' )
+
);
}
@@ -769,6 +771,24 @@ END;
}
}
+ protected function dropFkey( $table, $field ) {
+ $fi = $this->db->fieldInfo( $table, $field );
+ if ( is_null( $fi ) ) {
+ $this->output( "WARNING! Column '$table.$field' does not exist but it should! " .
+ "Please report this.\n" );
+ return;
+ }
+ $conname = $fi->conname();
+ if ( $fi->conname() ) {
+ $this->output( "Dropping foreign key constraint on '$table.$field'\n" );
+ $conclause = "CONSTRAINT \"$conname\"";
+ $command = "ALTER TABLE $table DROP CONSTRAINT $conname";
+ $this->db->query( $command );
+ } else {
+ $this->output( "...foreign key constraint on '$table.$field' already does not exist\n" );
+ };
+ }
+
protected function changeFkeyDeferrable( $table, $field, $clause ) {
$fi = $this->db->fieldInfo( $table, $field );
if ( is_null( $fi ) ) {
diff --git a/includes/libs/XmlTypeCheck.php b/includes/libs/XmlTypeCheck.php
index aca857e9..31a4e28a 100644
--- a/includes/libs/XmlTypeCheck.php
+++ b/includes/libs/XmlTypeCheck.php
@@ -2,6 +2,11 @@
/**
* XML syntax and type checker.
*
+ * Since 1.24.2, it uses XMLReader instead of xml_parse, which gives us
+ * more control over the expansion of XML entities. When passed to the
+ * callback, entities will be fully expanded, but may report the XML is
+ * invalid if expanding the entities are likely to cause a DoS.
+ *
* 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
@@ -25,7 +30,7 @@ class XmlTypeCheck {
* Will be set to true or false to indicate whether the file is
* well-formed XML. Note that this doesn't check schema validity.
*/
- public $wellFormed = false;
+ public $wellFormed = null;
/**
* Will be set to true if the optional element filter returned
@@ -78,12 +83,7 @@ class XmlTypeCheck {
function __construct( $input, $filterCallback = null, $isFile = true, $options = array() ) {
$this->filterCallback = $filterCallback;
$this->parserOptions = array_merge( $this->parserOptions, $options );
-
- if ( $isFile ) {
- $this->validateFromFile( $input );
- } else {
- $this->validateFromString( $input );
- }
+ $this->validateFromInput( $input, $isFile );
}
/**
@@ -125,140 +125,211 @@ class XmlTypeCheck {
return $this->rootElement;
}
+
/**
- * Get an XML parser with the root element handler.
- * @see XmlTypeCheck::rootElementOpen()
- * @return resource a resource handle for the XML parser
+ * @param string $fname the filename
*/
- private function getParser() {
- $parser = xml_parser_create_ns( 'UTF-8' );
- // case folding violates XML standard, turn it off
- xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
- xml_set_element_handler( $parser, array( $this, 'rootElementOpen' ), false );
- if ( $this->parserOptions['processing_instruction_handler'] ) {
- xml_set_processing_instruction_handler(
- $parser,
- array( $this, 'processingInstructionHandler' )
- );
+ private function validateFromInput( $xml, $isFile ) {
+ $reader = new XMLReader();
+ if ( $isFile ) {
+ $s = $reader->open( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING );
+ } else {
+ $s = $reader->XML( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING );
+ }
+ if ( $s !== true ) {
+ // Couldn't open the XML
+ $this->wellFormed = false;
+ } else {
+ $oldDisable = libxml_disable_entity_loader( true );
+ $reader->setParserProperty( XMLReader::SUBST_ENTITIES, true );
+ try {
+ $this->validate( $reader );
+ } catch ( Exception $e ) {
+ // Calling this malformed, because we didn't parse the whole
+ // thing. Maybe just an external entity refernce.
+ $this->wellFormed = false;
+ $reader->close();
+ libxml_disable_entity_loader( $oldDisable );
+ throw $e;
+ }
+ $reader->close();
+ libxml_disable_entity_loader( $oldDisable );
}
- return $parser;
}
- /**
- * @param string $fname the filename
- */
- private function validateFromFile( $fname ) {
- $parser = $this->getParser();
-
- if ( file_exists( $fname ) ) {
- $file = fopen( $fname, "rb" );
- if ( $file ) {
- do {
- $chunk = fread( $file, 32768 );
- $ret = xml_parse( $parser, $chunk, feof( $file ) );
- if ( $ret == 0 ) {
- $this->wellFormed = false;
- fclose( $file );
- xml_parser_free( $parser );
- return;
+ private function readNext( XMLReader $reader ) {
+ set_error_handler( array( $this, 'XmlErrorHandler' ) );
+ $ret = $reader->read();
+ restore_error_handler();
+ return $ret;
+ }
+
+ public function XmlErrorHandler( $errno, $errstr ) {
+ $this->wellFormed = false;
+ }
+
+ private function validate( $reader ) {
+
+ // First, move through anything that isn't an element, and
+ // handle any processing instructions with the callback
+ do {
+ if( !$this->readNext( $reader ) ) {
+ // Hit the end of the document before any elements
+ $this->wellFormed = false;
+ return;
+ }
+ if ( $reader->nodeType === XMLReader::PI ) {
+ $this->processingInstructionHandler( $reader->name, $reader->value );
+ }
+ } while ( $reader->nodeType != XMLReader::ELEMENT );
+
+ // Process the rest of the document
+ do {
+ switch ( $reader->nodeType ) {
+ case XMLReader::ELEMENT:
+ $name = $this->expandNS(
+ $reader->name,
+ $reader->namespaceURI
+ );
+ if ( $this->rootElement === '' ) {
+ $this->rootElement = $name;
}
- } while ( !feof( $file ) );
+ $empty = $reader->isEmptyElement;
+ $attrs = $this->getAttributesArray( $reader );
+ $this->elementOpen( $name, $attrs );
+ if ( $empty ) {
+ $this->elementClose();
+ }
+ break;
+
+ case XMLReader::END_ELEMENT:
+ $this->elementClose();
+ break;
+
+ case XMLReader::WHITESPACE:
+ case XMLReader::SIGNIFICANT_WHITESPACE:
+ case XMLReader::CDATA:
+ case XMLReader::TEXT:
+ $this->elementData( $reader->value );
+ break;
- fclose( $file );
+ case XMLReader::ENTITY_REF:
+ // Unexpanded entity (maybe external?),
+ // don't send to the filter (xml_parse didn't)
+ break;
+
+ case XMLReader::COMMENT:
+ // Don't send to the filter (xml_parse didn't)
+ break;
+
+ case XMLReader::PI:
+ // Processing instructions can happen after the header too
+ $this->processingInstructionHandler(
+ $reader->name,
+ $reader->value
+ );
+ break;
+ default:
+ // One of DOC, DOC_TYPE, ENTITY, END_ENTITY,
+ // NOTATION, or XML_DECLARATION
+ // xml_parse didn't send these to the filter, so we won't.
}
+
+ } while ( $this->readNext( $reader ) );
+
+ if ( $this->stackDepth !== 0 ) {
+ $this->wellFormed = false;
+ } elseif ( $this->wellFormed === null ) {
+ $this->wellFormed = true;
}
- $this->wellFormed = true;
- xml_parser_free( $parser );
}
/**
- *
- * @param string $string the XML-input-string to be checked.
+ * Get all of the attributes for an XMLReader's current node
+ * @param $r XMLReader
+ * @return array of attributes
*/
- private function validateFromString( $string ) {
- $parser = $this->getParser();
- $ret = xml_parse( $parser, $string, true );
- xml_parser_free( $parser );
- if ( $ret == 0 ) {
- $this->wellFormed = false;
- return;
+ private function getAttributesArray( XMLReader $r ) {
+ $attrs = array();
+ while ( $r->moveToNextAttribute() ) {
+ if ( $r->namespaceURI === 'http://www.w3.org/2000/xmlns/' ) {
+ // XMLReader treats xmlns attributes as normal
+ // attributes, while xml_parse doesn't
+ continue;
+ }
+ $name = $this->expandNS( $r->name, $r->namespaceURI );
+ $attrs[$name] = $r->value;
}
- $this->wellFormed = true;
+ return $attrs;
}
/**
- * @param $parser
- * @param $name
- * @param $attribs
+ * @param $name element or attribute name, maybe with a full or short prefix
+ * @param $namespaceURI the namespaceURI
+ * @return string the name prefixed with namespaceURI
*/
- private function rootElementOpen( $parser, $name, $attribs ) {
- $this->rootElement = $name;
-
- if ( is_callable( $this->filterCallback ) ) {
- xml_set_element_handler(
- $parser,
- array( $this, 'elementOpen' ),
- array( $this, 'elementClose' )
- );
- xml_set_character_data_handler( $parser, array( $this, 'elementData' ) );
- $this->elementOpen( $parser, $name, $attribs );
- } else {
- // We only need the first open element
- xml_set_element_handler( $parser, false, false );
+ private function expandNS( $name, $namespaceURI ) {
+ if ( $namespaceURI ) {
+ $parts = explode( ':', $name );
+ $localname = array_pop( $parts );
+ return "$namespaceURI:$localname";
}
+ return $name;
}
/**
- * @param $parser
* @param $name
* @param $attribs
*/
- private function elementOpen( $parser, $name, $attribs ) {
+ private function elementOpen( $name, $attribs ) {
$this->elementDataContext[] = array( $name, $attribs );
$this->elementData[] = '';
$this->stackDepth++;
}
/**
- * @param $parser
- * @param $name
*/
- private function elementClose( $parser, $name ) {
+ private function elementClose() {
list( $name, $attribs ) = array_pop( $this->elementDataContext );
$data = array_pop( $this->elementData );
$this->stackDepth--;
- if ( call_user_func(
- $this->filterCallback,
- $name,
- $attribs,
- $data
- ) ) {
- // Filter hit!
+ if ( is_callable( $this->filterCallback )
+ && call_user_func(
+ $this->filterCallback,
+ $name,
+ $attribs,
+ $data
+ )
+ ) {
+ // Filter hit
$this->filterMatch = true;
}
}
/**
- * @param $parser
* @param $data
*/
- private function elementData( $parser, $data ) {
- // xml_set_character_data_handler breaks the data on & characters, so
- // we collect any data here, and we'll run the callback in elementClose
+ private function elementData( $data ) {
+ // Collect any data here, and we'll run the callback in elementClose
$this->elementData[ $this->stackDepth - 1 ] .= trim( $data );
}
/**
- * @param $parser
* @param $target
* @param $data
*/
- private function processingInstructionHandler( $parser, $target, $data ) {
- if ( call_user_func( $this->parserOptions['processing_instruction_handler'], $target, $data ) ) {
- // Filter hit!
- $this->filterMatch = true;
+ private function processingInstructionHandler( $target, $data ) {
+ if ( $this->parserOptions['processing_instruction_handler'] ) {
+ if ( call_user_func(
+ $this->parserOptions['processing_instruction_handler'],
+ $target,
+ $data
+ ) ) {
+ // Filter hit!
+ $this->filterMatch = true;
+ }
}
}
}
diff --git a/includes/media/BitmapMetadataHandler.php b/includes/media/BitmapMetadataHandler.php
index dd41c388..1d790155 100644
--- a/includes/media/BitmapMetadataHandler.php
+++ b/includes/media/BitmapMetadataHandler.php
@@ -154,7 +154,7 @@ class BitmapMetadataHandler {
* @throws MWException On invalid file.
*/
static function Jpeg( $filename ) {
- $showXMP = function_exists( 'xml_parser_create_ns' );
+ $showXMP = XMPReader::isSupported();
$meta = new self();
$seg = JpegMetadataExtractor::segmentSplitter( $filename );
@@ -196,7 +196,7 @@ class BitmapMetadataHandler {
* @return array Array for storage in img_metadata.
*/
public static function PNG( $filename ) {
- $showXMP = function_exists( 'xml_parser_create_ns' );
+ $showXMP = XMPReader::isSupported();
$meta = new self();
$array = PNGMetadataExtractor::getMetadata( $filename );
@@ -236,7 +236,7 @@ class BitmapMetadataHandler {
$meta->addMetadata( array( 'GIFFileComment' => $baseArray['comment'] ), 'native' );
}
- if ( $baseArray['xmp'] !== '' && function_exists( 'xml_parser_create_ns' ) ) {
+ if ( $baseArray['xmp'] !== '' && XMPReader::isSupported() ) {
$xmp = new XMPReader();
$xmp->parse( $baseArray['xmp'] );
$xmpRes = $xmp->getResults();
diff --git a/includes/media/JpegMetadataExtractor.php b/includes/media/JpegMetadataExtractor.php
index 8c5b46bb..aaa9930a 100644
--- a/includes/media/JpegMetadataExtractor.php
+++ b/includes/media/JpegMetadataExtractor.php
@@ -48,7 +48,7 @@ class JpegMetadataExtractor {
* @throws MWException If given invalid file.
*/
static function segmentSplitter( $filename ) {
- $showXMP = function_exists( 'xml_parser_create_ns' );
+ $showXMP = XMPReader::isSupported();
$segmentCount = 0;
diff --git a/includes/media/XMP.php b/includes/media/XMP.php
index cdbd5ab2..a3f45e6c 100644
--- a/includes/media/XMP.php
+++ b/includes/media/XMP.php
@@ -80,6 +80,12 @@ class XMPReader {
/** @var int */
private $extendedXMPOffset = 0;
+ /** @var int Flag determining if the XMP is safe to parse **/
+ private $parsable = 0;
+
+ /** @var string Buffer of XML to parse **/
+ private $xmlParsableBuffer = '';
+
/**
* These are various mode constants.
* they are used to figure out what to do
@@ -108,6 +114,12 @@ class XMPReader {
const NS_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
const NS_XML = 'http://www.w3.org/XML/1998/namespace';
+ // States used while determining if XML is safe to parse
+ const PARSABLE_UNKNOWN = 0;
+ const PARSABLE_OK = 1;
+ const PARSABLE_BUFFERING = 2;
+ const PARSABLE_NO = 3;
+
/**
* Constructor.
*
@@ -145,6 +157,9 @@ class XMPReader {
array( $this, 'endElement' ) );
xml_set_character_data_handler( $this->xmlParser, array( $this, 'char' ) );
+
+ $this->parsable = self::PARSABLE_UNKNOWN;
+ $this->xmlParsableBuffer = '';
}
/** Destroy the xml parser
@@ -156,6 +171,13 @@ class XMPReader {
xml_parser_free( $this->xmlParser );
}
+ /**
+ * Check if this instance supports using this class
+ */
+ public static function isSupported() {
+ return function_exists( 'xml_parser_create_ns' ) && class_exists( 'XMLReader' );
+ }
+
/** Get the result array. Do some post-processing before returning
* the array, and transform any metadata that is special-cased.
*
@@ -305,6 +327,27 @@ class XMPReader {
wfRestoreWarnings();
}
+ // Ensure the XMP block does not have an xml doctype declaration, which
+ // could declare entities unsafe to parse with xml_parse (T85848/T71210).
+ if ( $this->parsable !== self::PARSABLE_OK ) {
+ if ( $this->parsable === self::PARSABLE_NO ) {
+ throw new MWException( 'Unsafe doctype declaration in XML.' );
+ }
+
+ $content = $this->xmlParsableBuffer . $content;
+ if ( !$this->checkParseSafety( $content ) ) {
+ if ( !$allOfIt && $this->parsable !== self::PARSABLE_NO ) {
+ // parse wasn't Unsuccessful yet, so return true
+ // in this case.
+ return true;
+ }
+ $msg = ( $this->parsable === self::PARSABLE_NO ) ?
+ 'Unsafe doctype declaration in XML.' :
+ 'No root element found in XML.';
+ throw new MWException( $msg );
+ }
+ }
+
$ok = xml_parse( $this->xmlParser, $content, $allOfIt );
if ( !$ok ) {
$error = xml_error_string( xml_get_error_code( $this->xmlParser ) );
@@ -437,6 +480,59 @@ class XMPReader {
}
}
+ /**
+ * Check if a block of XML is safe to pass to xml_parse, i.e. doesn't
+ * contain a doctype declaration which could contain a dos attack if we
+ * parse it and expand internal entities (T85848).
+ *
+ * @param string $content xml string to check for parse safety
+ * @return bool true if the xml is safe to parse, false otherwise
+ */
+ private function checkParseSafety( $content ) {
+ $reader = new XMLReader();
+ $result = null;
+
+ // For XMLReader to parse incomplete/invalid XML, it has to be open()'ed
+ // instead of using XML().
+ $reader->open(
+ 'data://text/plain,' . urlencode( $content ),
+ null,
+ LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET
+ );
+
+ $oldDisable = libxml_disable_entity_loader( true );
+ $reader->setParserProperty( XMLReader::SUBST_ENTITIES, false );
+
+ // Even with LIBXML_NOWARNING set, XMLReader::read gives a warning
+ // when parsing truncated XML, which causes unit tests to fail.
+ wfSuppressWarnings();
+ while ( $reader->read() ) {
+ if ( $reader->nodeType === XMLReader::ELEMENT ) {
+ // Reached the first element without hitting a doctype declaration
+ $this->parsable = self::PARSABLE_OK;
+ $result = true;
+ break;
+ }
+ if ( $reader->nodeType === XMLReader::DOC_TYPE ) {
+ $this->parsable = self::PARSABLE_NO;
+ $result = false;
+ break;
+ }
+ }
+ wfRestoreWarnings();
+ libxml_disable_entity_loader( $oldDisable );
+
+ if ( !is_null( $result ) ) {
+ return $result;
+ }
+
+ // Reached the end of the parsable xml without finding an element
+ // or doctype. Buffer and try again.
+ $this->parsable = self::PARSABLE_BUFFERING;
+ $this->xmlParsableBuffer = $content;
+ return false;
+ }
+
/** When we hit a closing element in MODE_IGNORE
* Check to see if this is the element we started to ignore,
* in which case we get out of MODE_IGNORE
diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php
index 679492ae..23fdc71a 100644
--- a/includes/specialpage/SpecialPageFactory.php
+++ b/includes/specialpage/SpecialPageFactory.php
@@ -74,7 +74,7 @@ class SpecialPageFactory {
'Wantedtemplates' => 'WantedTemplatesPage',
// List of pages
- 'Allpages' => 'SpecialAllpages',
+ 'Allpages' => 'SpecialAllPages',
'Prefixindex' => 'SpecialPrefixindex',
'Categories' => 'SpecialCategories',
'Listredirects' => 'ListredirectsPage',
@@ -123,7 +123,7 @@ class SpecialPageFactory {
// Data and tools
'Statistics' => 'SpecialStatistics',
- 'Allmessages' => 'SpecialAllmessages',
+ 'Allmessages' => 'SpecialAllMessages',
'Version' => 'SpecialVersion',
'Lockdb' => 'SpecialLockdb',
'Unlockdb' => 'SpecialUnlockdb',
diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php
index ce436525..f983b452 100644
--- a/includes/specials/SpecialActiveusers.php
+++ b/includes/specials/SpecialActiveusers.php
@@ -115,10 +115,16 @@ class ActiveUsersPager extends UsersPager {
) . ')';
}
+ if ( $dbr->implicitGroupby() ) {
+ $options = array( 'GROUP BY' => array( 'qcc_title' ) );
+ } else {
+ $options = array( 'GROUP BY' => array( 'user_name', 'user_id', 'qcc_title' ) );
+ }
+
return array(
'tables' => array( 'querycachetwo', 'user', 'recentchanges' ),
'fields' => array( 'user_name', 'user_id', 'recentedits' => 'COUNT(*)', 'qcc_title' ),
- 'options' => array( 'GROUP BY' => array( 'qcc_title' ) ),
+ 'options' => $options,
'conds' => $conds
);
}
diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php
index 0efebb3e..7d745a50 100644
--- a/includes/specials/SpecialJavaScriptTest.php
+++ b/includes/specials/SpecialJavaScriptTest.php
@@ -26,12 +26,10 @@
*/
class SpecialJavaScriptTest extends SpecialPage {
/**
- * @var array Mapping of framework ids and their initilizer methods
- * in this class. If a framework is requested but not in this array,
- * the 'unknownframework' error is served.
+ * @var array Supported frameworks.
*/
private static $frameworks = array(
- 'qunit' => 'initQUnitTesting',
+ 'qunit',
);
public function __construct() {
@@ -44,43 +42,70 @@ class SpecialJavaScriptTest extends SpecialPage {
$this->setHeaders();
$out->disallowUserJs();
- $out->addModules( 'mediawiki.special.javaScriptTest' );
-
- // Determine framework
- $pars = explode( '/', $par );
- $framework = strtolower( $pars[0] );
-
- // No framework specified
- if ( $par == '' ) {
+ if ( $par === null ) {
+ // No framework specified
+ $out->setStatusCode( 404 );
$out->setPageTitle( $this->msg( 'javascripttest' ) );
- $summary = $this->wrapSummaryHtml(
- $this->msg( 'javascripttest-pagetext-noframework' )->escaped() .
- $this->getFrameworkListHtml(),
- 'noframework'
+ $out->addHTML(
+ $this->msg( 'javascripttest-pagetext-noframework' )->parseAsBlock()
+ . $this->getFrameworkListHtml()
);
- $out->addHtml( $summary );
- } elseif ( isset( self::$frameworks[$framework] ) ) {
- // Matched! Display proper title and initialize the framework
- $out->setPageTitle( $this->msg(
- 'javascripttest-title',
- // Messages: javascripttest-qunit-name
- $this->msg( "javascripttest-$framework-name" )->plain()
- ) );
- $out->setSubtitle( $this->msg( 'javascripttest-backlink' )
- ->rawParams( Linker::linkKnown( $this->getPageTitle() ) ) );
- $this->{self::$frameworks[$framework]}();
- } else {
- // Framework not found, display error
- $out->setPageTitle( $this->msg( 'javascripttest' ) );
- $summary = $this->wrapSummaryHtml(
- '<p class="error">' .
- $this->msg( 'javascripttest-pagetext-unknownframework', $par )->escaped() .
- '</p>' .
- $this->getFrameworkListHtml(),
- 'unknownframework'
+ return;
+ }
+
+ // Determine framework and mode
+ $pars = explode( '/', $par, 2 );
+
+ $framework = $pars[0];
+ if ( !in_array( $framework, self::$frameworks ) ) {
+ // Framework not found
+ $out->setStatusCode( 404 );
+ $out->addHTML(
+ '<div class="error">'
+ . $this->msg( 'javascripttest-pagetext-unknownframework' )
+ ->plaintextParams( $par )->parseAsBlock()
+ . '</div>'
+ . $this->getFrameworkListHtml()
);
- $out->addHtml( $summary );
+ return;
}
+
+ // This special page is disabled by default ($wgEnableJavaScriptTest), and contains
+ // no sensitive data. In order to allow TestSwarm to embed it into a test client window,
+ // we need to allow iframing of this page.
+ $out->allowClickjacking();
+ $out->setSubtitle(
+ $this->msg( 'javascripttest-backlink' )
+ ->rawParams( Linker::linkKnown( $this->getPageTitle() ) )
+ );
+
+ // Custom actions
+ if ( isset( $pars[1] ) ) {
+ $action = $pars[1];
+ if ( !in_array( $action, array( 'export', 'plain' ) ) ) {
+ $out->setStatusCode( 404 );
+ $out->addHTML(
+ '<div class="error">'
+ . $this->msg( 'javascripttest-pagetext-unknownaction' )
+ ->plaintextParams( $action )->parseAsBlock()
+ . '</div>'
+ );
+ return;
+ }
+ $method = $action . ucfirst( $framework );
+ $this->$method();
+ return;
+ }
+
+ $out->addModules( 'mediawiki.special.javaScriptTest' );
+
+ $method = 'view' . ucfirst( $framework );
+ $this->$method();
+ $out->setPageTitle( $this->msg(
+ 'javascripttest-title',
+ // Messages: javascripttest-qunit-name
+ $this->msg( "javascripttest-$framework-name" )->plain()
+ ) );
}
/**
@@ -91,7 +116,7 @@ class SpecialJavaScriptTest extends SpecialPage {
*/
private function getFrameworkListHtml() {
$list = '<ul>';
- foreach ( self::$frameworks as $framework => $initFn ) {
+ foreach ( self::$frameworks as $framework ) {
$list .= Html::rawElement(
'li',
array(),
@@ -109,60 +134,35 @@ class SpecialJavaScriptTest extends SpecialPage {
}
/**
- * Function to wrap the summary.
- * It must be given a valid state as a second parameter or an exception will
- * be thrown.
- * @param string $html The raw HTML.
- * @param string $state State, one of 'noframework', 'unknownframework' or 'frameworkfound'
- * @throws MWException
- * @return string
+ * Wrap HTML contents in a summary container.
+ *
+ * @param string $html HTML contents to be wrapped
+ * @return string HTML
*/
- private function wrapSummaryHtml( $html, $state ) {
- $validStates = array( 'noframework', 'unknownframework', 'frameworkfound' );
-
- if ( !in_array( $state, $validStates ) ) {
- throw new MWException( __METHOD__
- . ' given an invalid state. Must be one of "'
- . join( '", "', $validStates ) . '".'
- );
- }
-
- return "<div id=\"mw-javascripttest-summary\" class=\"mw-javascripttest-$state\">$html</div>";
+ private function wrapSummaryHtml( $html ) {
+ return "<div id=\"mw-javascripttest-summary\">$html</div>";
}
/**
- * Initialize the page for QUnit.
+ * Run the test suite on the Special page.
+ *
+ * Rendered by OutputPage and Skin.
*/
- private function initQUnitTesting() {
+ private function viewQUnit() {
$out = $this->getOutput();
$testConfig = $this->getConfig()->get( 'JavaScriptTestConfig' );
- $out->addModules( 'test.mediawiki.qunit.testrunner' );
- $qunitTestModules = $out->getResourceLoader()->getTestModuleNames( 'qunit' );
- $out->addModules( $qunitTestModules );
+ $modules = $out->getResourceLoader()->getTestModuleNames( 'qunit' );
$summary = $this->msg( 'javascripttest-qunit-intro' )
->params( $testConfig['qunit']['documentation'] )
->parseAsBlock();
- $header = $this->msg( 'javascripttest-qunit-heading' )->escaped();
- $userDir = $this->getLanguage()->getDir();
$baseHtml = <<<HTML
<div class="mw-content-ltr">
-<div id="qunit-header"><span dir="$userDir">$header</span></div>
-<div id="qunit-banner"></div>
-<div id="qunit-testrunner-toolbar"></div>
-<div id="qunit-userAgent"></div>
-<ol id="qunit-tests"></ol>
-<div id="qunit-fixture">test markup, will be hidden</div>
+<div id="qunit"></div>
</div>
HTML;
- $out->addHtml( $this->wrapSummaryHtml( $summary, 'frameworkfound' ) . $baseHtml );
-
- // This special page is disabled by default ($wgEnableJavaScriptTest), and contains
- // no sensitive data. In order to allow TestSwarm to embed it into a test client window,
- // we need to allow iframing of this page.
- $out->allowClickjacking();
// Used in ./tests/qunit/data/testrunner.js, see also documentation of
// $wgJavaScriptTestConfig in DefaultSettings.php
@@ -170,6 +170,102 @@ HTML;
'QUnitTestSwarmInjectJSPath',
$testConfig['qunit']['testswarm-injectjs']
);
+
+ $out->addHtml( $this->wrapSummaryHtml( $summary ) . $baseHtml );
+
+ // The testrunner configures QUnit and essentially depends on it. However, test suites
+ // are reusable in environments that preload QUnit (or a compatibility interface to
+ // another framework). Therefore we have to load it ourselves.
+ $out->addHtml( Html::inlineScript(
+ ResourceLoader::makeLoaderConditionalScript(
+ Xml::encodeJsCall( 'mw.loader.using', array(
+ array( 'jquery.qunit', 'jquery.qunit.completenessTest' ),
+ new XmlJsCode(
+ 'function () {' . Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) . '}'
+ )
+ ) )
+ )
+ ) );
+ }
+
+ /**
+ * Generate self-sufficient JavaScript payload to run the tests elsewhere.
+ *
+ * Includes startup module to request modules from ResourceLoader.
+ *
+ * Note: This modifies the registry to replace 'jquery.qunit' with an
+ * empty module to allow external environment to preload QUnit with any
+ * neccecary framework adapters (e.g. Karma). Loading it again would
+ * re-define QUnit and dereference event handlers from Karma.
+ */
+ private function exportQUnit() {
+ $out = $this->getOutput();
+
+ $out->disable();
+
+ $rl = $out->getResourceLoader();
+
+ $query = array(
+ 'lang' => $this->getLanguage()->getCode(),
+ 'skin' => $this->getSkin()->getSkinName(),
+ 'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false',
+ );
+ $embedContext = new ResourceLoaderContext( $rl, new FauxRequest( $query ) );
+ $query['only'] = 'scripts';
+ $startupContext = new ResourceLoaderContext( $rl, new FauxRequest( $query ) );
+
+ $modules = $rl->getTestModuleNames( 'qunit' );
+
+ // The below is essentially a pure-javascript version of OutputPage::getHeadScripts.
+ $startup = $rl->makeModuleResponse( $startupContext, array(
+ 'startup' => $rl->getModule( 'startup' ),
+ ) );
+ // Embed page-specific mw.config variables.
+ // The current Special page shouldn't be relevant to tests, but various modules (which
+ // are loaded before the test suites), reference mw.config while initialising.
+ $code = ResourceLoader::makeConfigSetScript( $out->getJSVars() );
+ // Embed private modules as they're not allowed to be loaded dynamically
+ $code .= $rl->makeModuleResponse( $embedContext, array(
+ 'user.options' => $rl->getModule( 'user.options' ),
+ 'user.tokens' => $rl->getModule( 'user.tokens' ),
+ ) );
+ $code .= Xml::encodeJsCall( 'mw.loader.load', array( $modules ) );
+
+ header( 'Content-Type: text/javascript; charset=utf-8' );
+ header( 'Cache-Control: private, no-cache, must-revalidate' );
+ header( 'Pragma: no-cache' );
+ echo $startup;
+ echo "\n";
+ // Note: The following has to be wrapped in a script tag because the startup module also
+ // writes a script tag (the one loading mediawiki.js). Script tags are synchronous, block
+ // each other, and run in order. But they don't nest. The code appended after the startup
+ // module runs before the added script tag is parsed and executed.
+ echo Xml::encodeJsCall( 'document.write', array( Html::inlineScript( $code ) ) );
+ }
+
+ private function plainQUnit() {
+ $out = $this->getOutput();
+ $out->disable();
+
+ $url = $this->getPageTitle( 'qunit/export' )->getFullURL( array(
+ 'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false',
+ ) );
+
+ $styles = $out->makeResourceLoaderLink( 'jquery.qunit', ResourceLoaderModule::TYPE_STYLES, false );
+ // Use 'raw' since this is a plain HTML page without ResourceLoader
+ $scripts = $out->makeResourceLoaderLink( 'jquery.qunit', ResourceLoaderModule::TYPE_SCRIPTS, false, array( 'raw' => 'true' ) );
+
+ $head = trim( $styles['html'] . $scripts['html'] );
+ $html = <<<HTML
+<!DOCTYPE html>
+<title>QUnit</title>
+$head
+<div id="qunit"></div>
+HTML;
+ $html .= "\n" . Html::linkedScript( $url );
+
+ header( 'Content-Type: text/html; charset=utf-8' );
+ echo $html;
}
/**
@@ -183,7 +279,7 @@ HTML;
return self::prefixSearchArray(
$search,
$limit,
- array_keys( self::$frameworks )
+ self::$frameworks
);
}
diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php
index 6de7c90d..24b636b1 100644
--- a/includes/specials/SpecialUserlogin.php
+++ b/includes/specials/SpecialUserlogin.php
@@ -538,14 +538,11 @@ class LoginForm extends SpecialPage {
return Status::newFatal( 'badretype' );
}
- # check for minimal password length
- $valid = $u->getPasswordValidity( $this->mPassword );
- if ( $valid !== true ) {
- if ( !is_array( $valid ) ) {
- $valid = array( $valid, $wgMinimalPasswordLength );
- }
-
- return call_user_func_array( 'Status::newFatal', $valid );
+ # check for password validity, return a fatal Status if invalid
+ $validity = $u->checkPasswordValidity( $this->mPassword );
+ if ( !$validity->isGood() ) {
+ $validity->ok = false; // make sure this Status is fatal
+ return $validity;
}
}
diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php
index 89ce2b3a..14959c26 100644
--- a/includes/upload/UploadBase.php
+++ b/includes/upload/UploadBase.php
@@ -1416,27 +1416,22 @@ abstract class UploadBase {
}
}
- # href with embedded svg as target
- if ( $stripped == 'href' && preg_match( '!data:[^,]*image/svg[^,]*,!sim', $value ) ) {
- wfDebug( __METHOD__ . ": Found href to embedded svg "
- . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
-
- return true;
- }
-
- # href with embedded (text/xml) svg as target
- if ( $stripped == 'href' && preg_match( '!data:[^,]*text/xml[^,]*,!sim', $value ) ) {
- wfDebug( __METHOD__ . ": Found href to embedded svg "
- . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
-
- return true;
+ # only allow data: targets that should be safe. This prevents vectors like,
+ # image/svg, text/xml, application/xml, and text/html, which can contain scripts
+ if ( $stripped == 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
+ // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
+ $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
+ if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
+ wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
+ . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
+ return true;
+ }
}
- # Change href with animate from (http://html5sec.org/#137). This doesn't seem
- # possible without embedding the svg, but filter here in case.
- if ( $stripped == 'from'
+ # Change href with animate from (http://html5sec.org/#137).
+ if ( $stripped === 'attributename'
&& $strippedElement === 'animate'
- && !preg_match( '!^https?://!im', $value )
+ && $this->stripXmlNamespace( $value ) == 'href'
) {
wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
. "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
@@ -1528,7 +1523,7 @@ abstract class UploadBase {
private static function checkCssFragment( $value ) {
# Forbid external stylesheets, for both reliability and to protect viewer's privacy
- if ( strpos( $value, '@import' ) !== false ) {
+ if ( stripos( $value, '@import' ) !== false ) {
return true;
}
diff --git a/jsduck.json b/jsduck.json
new file mode 100644
index 00000000..ef92fa12
--- /dev/null
+++ b/jsduck.json
@@ -0,0 +1,40 @@
+{
+ "--title": "MediaWiki core - Documentation",
+ "--categories": "maintenance/jsduck/categories.json",
+ "--eg-iframe": "maintenance/jsduck/eg-iframe.html",
+ "--tags": "maintenance/jsduck/CustomTags.rb",
+ "--warnings": ["-nodoc(class,public)"],
+ "--builtin-classes": true,
+ "--processes": "0",
+ "--warnings-exit-nonzero": true,
+ "--external": "HTMLElement,HTMLDocument,Window,File",
+ "--output": "docs/js",
+ "--": [
+ "maintenance/jsduck/external.js",
+ "resources/src/mediawiki",
+ "resources/src/mediawiki.action",
+ "resources/src/mediawiki.api",
+ "resources/src/mediawiki.language",
+ "resources/src/mediawiki.page",
+ "resources/src/mediawiki.special",
+ "resources/src/jquery/jquery.accessKeyLabel.js",
+ "resources/src/jquery/jquery.arrowSteps.js",
+ "resources/src/jquery/jquery.autoEllipsis.js",
+ "resources/src/jquery/jquery.badge.js",
+ "resources/src/jquery/jquery.byteLength.js",
+ "resources/src/jquery/jquery.byteLimit.js",
+ "resources/src/jquery/jquery.checkboxShiftClick.js",
+ "resources/src/jquery/jquery.client.js",
+ "resources/src/jquery/jquery.colorUtil.js",
+ "resources/src/jquery/jquery.confirmable.js",
+ "resources/src/jquery/jquery.footHovzer.js",
+ "resources/src/jquery/jquery.getAttrs.js",
+ "resources/src/jquery/jquery.hidpi.js",
+ "resources/src/jquery/jquery.localize.js",
+ "resources/src/jquery/jquery.makeCollapsible.js",
+ "resources/src/jquery/jquery.spinner.js",
+ "resources/src/jquery/jquery.tabIndex.js",
+ "resources/lib/oojs",
+ "resources/lib/oojs-ui"
+ ]
+}
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index 9c1a0438..19832b22 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -452,6 +452,7 @@
"wrongpassword": "Incorrect password entered.\nPlease try again.",
"wrongpasswordempty": "Password entered was blank.\nPlease try again.",
"passwordtooshort": "Passwords must be at least {{PLURAL:$1|1 character|$1 characters}}.",
+ "passwordtoolong": "Passwords cannot be longer than {{PLURAL:$1|1 character|$1 characters}}.",
"password-name-match": "Your password must be different from your username.",
"password-login-forbidden": "The use of this username and password has been forbidden.",
"mailmypassword": "Reset password",
@@ -2331,14 +2332,14 @@
"import-logentry-interwiki-detail": "$1 {{PLURAL:$1|revision|revisions}} imported from $2",
"javascripttest": "JavaScript testing",
"javascripttest-backlink": "< $1",
- "javascripttest-title": "Running $1 tests",
+ "javascripttest-title": "$1",
"javascripttest-pagetext-noframework": "This page is reserved for running JavaScript tests.",
"javascripttest-pagetext-unknownframework": "Unknown testing framework \"$1\".",
+ "javascripttest-pagetext-unknownaction": "Unknown action \"$1\".",
"javascripttest-pagetext-frameworks": "Please choose one of the following testing frameworks: $1",
"javascripttest-pagetext-skins": "Choose a skin to run the tests with:",
"javascripttest-qunit-name": "QUnit",
"javascripttest-qunit-intro": "See [$1 testing documentation] on mediawiki.org.",
- "javascripttest-qunit-heading": "MediaWiki JavaScript QUnit test suite",
"accesskey-pt-userpage": ".",
"accesskey-pt-anonuserpage": ".",
"accesskey-pt-mytalk": "n",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index 9562e450..7ee37392 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -612,6 +612,7 @@
"wrongpassword": "Used as error message when the provided password is wrong.\nThis message is used in html.\n{{Identical|Please try again}}",
"wrongpasswordempty": "Error message displayed when entering a blank password.\n{{Identical|Please try again}}",
"passwordtooshort": "This message is shown in [[Special:Preferences]] and [[Special:CreateAccount]].\n\nParameters:\n* $1 - the minimum number of characters in the password",
+ "passwordtoolong": "This message is shown in [[Special:Preferences]], [[Special:CreateAccount]], and [[Special:Userlogin]].\n\nParameters:\n* $1 - the maximum number of characters in the password",
"password-name-match": "Used as error message when password validity check failed.",
"password-login-forbidden": "Error message shown when the user has tried to log in using one of the special username/password combinations used for MediaWiki testing. (See [[mwr:75589]], [[mwr:75605]].)",
"mailmypassword": "Used as label for Submit button in [[Special:PasswordReset]].\n{{Identical|Reset password}}",
@@ -2491,14 +2492,14 @@
"import-logentry-interwiki-detail": "Used as success message and log entry. Parameters:\n* $1 - number of succeeded revisions\n* $2 - interwiki name\nSee also:\n* {{msg-mw|Import-logentry-upload-detail}}",
"javascripttest": "Title of the special page [[Special:JavaScriptTest]].\n\nSee also:\n* {{msg-mw|Javascripttest|title}}\n* {{msg-mw|Javascripttest-pagetext-noframework|summary}}\n* {{msg-mw|Javascripttest-pagetext-unknownframework|error message}}",
"javascripttest-backlink": "{{optional}}\nUsed as subtitle in [[Special:JavaScriptTest]]. Parameters:\n* $1 - page title",
- "javascripttest-title": "Title of the special page when running a test suite. Parameters:\n* $1 is the name of the framework, for example QUnit.",
+ "javascripttest-title": "{{Ignore}}",
"javascripttest-pagetext-noframework": "Used as summary when no framework specified.\n\nSee also:\n* {{msg-mw|Javascripttest|title}}\n* {{msg-mw|Javascripttest-pagetext-noframework|summary}}\n* {{msg-mw|Javascripttest-pagetext-unknownframework|error message}}",
"javascripttest-pagetext-unknownframework": "Error message when given framework ID is not found. Parameters:\n* $1 - the ID of the framework\nSee also:\n* {{msg-mw|Javascripttest|title}}\n* {{msg-mw|Javascripttest-pagetext-noframework|summary}}\n* {{msg-mw|Javascripttest-pagetext-unknownframework|error message}}",
+ "javascripttest-pagetext-unknownaction": "Error message when url specifies an unknown action. Parameters:\n* $1 - the action specified in the url.",
"javascripttest-pagetext-frameworks": "Parameters:\n* $1 - frameworks list which contain a link text {{msg-mw|Javascripttest-qunit-name}}",
"javascripttest-pagetext-skins": "Used as label in [[Special:JavaScriptTest]].",
"javascripttest-qunit-name": "{{Ignore}}",
"javascripttest-qunit-intro": "Used as summary. Parameters:\n* $1 - the configured URL to the documentation\nSee also:\n* {{msg-mw|Javascripttest-qunit-heading}}",
- "javascripttest-qunit-heading": "See also:\n* {{msg-mw|Javascripttest-qunit-intro}}",
"accesskey-pt-userpage": "{{doc-accesskey}}\nSee also:\n<!--* username-->\n* {{msg-mw|Accesskey-pt-userpage}}\n* {{msg-mw|Tooltip-pt-userpage}}",
"accesskey-pt-anonuserpage": "{{doc-accesskey}}",
"accesskey-pt-mytalk": "{{doc-accesskey}}\nSee also:\n* {{msg-mw|Mytalk}}\n* {{msg-mw|Accesskey-pt-mytalk}}\n* {{msg-mw|Tooltip-pt-mytalk}}",
diff --git a/maintenance/jsduck/config.json b/maintenance/jsduck/config.json
deleted file mode 100644
index e97f2923..00000000
--- a/maintenance/jsduck/config.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "--title": "MediaWiki core - Documentation",
- "--categories": "./categories.json",
- "--eg-iframe": "./eg-iframe.html",
- "--tags": "./CustomTags.rb",
- "--warnings": ["-nodoc(class,public)"],
- "--builtin-classes": true,
- "--warnings-exit-nonzero": true,
- "--external": "HTMLElement,HTMLDocument,Window,File",
- "--footer": "Documentation for MediaWiki core. Generated on {DATE} by {JSDUCK} {VERSION}.",
- "--output": "../../docs/js",
- "--": [
- "./external.js",
- "../../resources/src/mediawiki",
- "../../resources/src/mediawiki.action",
- "../../resources/src/mediawiki.api",
- "../../resources/src/mediawiki.language",
- "../../resources/src/mediawiki.page",
- "../../resources/src/mediawiki.special",
- "../../resources/src/jquery/jquery.accessKeyLabel.js",
- "../../resources/src/jquery/jquery.arrowSteps.js",
- "../../resources/src/jquery/jquery.autoEllipsis.js",
- "../../resources/src/jquery/jquery.badge.js",
- "../../resources/src/jquery/jquery.byteLength.js",
- "../../resources/src/jquery/jquery.byteLimit.js",
- "../../resources/src/jquery/jquery.checkboxShiftClick.js",
- "../../resources/src/jquery/jquery.client.js",
- "../../resources/src/jquery/jquery.colorUtil.js",
- "../../resources/src/jquery/jquery.confirmable.js",
- "../../resources/src/jquery/jquery.footHovzer.js",
- "../../resources/src/jquery/jquery.getAttrs.js",
- "../../resources/src/jquery/jquery.hidpi.js",
- "../../resources/src/jquery/jquery.localize.js",
- "../../resources/src/jquery/jquery.makeCollapsible.js",
- "../../resources/src/jquery/jquery.spinner.js",
- "../../resources/src/jquery/jquery.tabIndex.js",
- "../../resources/lib/oojs",
- "../../resources/lib/oojs-ui"
- ]
-}
diff --git a/maintenance/mwjsduck-gen b/maintenance/mwjsduck-gen
index 5247637b..6b7c77b6 100644
--- a/maintenance/mwjsduck-gen
+++ b/maintenance/mwjsduck-gen
@@ -1,25 +1,4 @@
#!/usr/bin/env bash
set -e
-
-JSDUCK_MWVERSION=master
-if [[ "$1" == "--version" && "$2" != "" ]]
-then
- JSDUCK_MWVERSION="$2"
-elif [[ "$*" != "" ]]
-then
- FILENAME=$(basename $0)
- echo "Usage: $FILENAME [--version <mediawiki version>]"
- echo
- exit 1
-fi
-
-MWCORE_DIR=$(cd $(dirname $0)/..; pwd)
-
-jsduck \
---config=$MWCORE_DIR/maintenance/jsduck/config.json \
---footer="Documentation for branch ($JSDUCK_MWVERSION) on {DATE} by {JSDUCK} {VERSION}." \
---processes 0
-
-echo 'JSDuck execution finished.'
-
-ln -s ../../resources $MWCORE_DIR/docs/js/modules
+cd $(dirname $0)/..
+jsduck
diff --git a/maintenance/postgres/tables.sql b/maintenance/postgres/tables.sql
index 400050e7..12e357fc 100644
--- a/maintenance/postgres/tables.sql
+++ b/maintenance/postgres/tables.sql
@@ -421,7 +421,7 @@ CREATE TABLE recentchanges (
rc_minor SMALLINT NOT NULL DEFAULT 0,
rc_bot SMALLINT NOT NULL DEFAULT 0,
rc_new SMALLINT NOT NULL DEFAULT 0,
- rc_cur_id INTEGER NULL REFERENCES page(page_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+ rc_cur_id INTEGER NULL,
rc_this_oldid INTEGER NOT NULL,
rc_last_oldid INTEGER NOT NULL,
rc_type SMALLINT NOT NULL DEFAULT 0,
diff --git a/resources/Resources.php b/resources/Resources.php
index 70f64dd3..ec1c0fc4 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1401,7 +1401,9 @@ return array(
'colon-separator',
'javascripttest-pagetext-skins',
) ),
- 'dependencies' => array( 'jquery.qunit' ),
+ 'dependencies' => array(
+ 'mediawiki.Uri',
+ ),
'position' => 'top',
'targets' => array( 'desktop', 'mobile' ),
),
diff --git a/resources/lib/jquery/jquery.js b/resources/lib/jquery/jquery.js
index d4b67f7e..1c3aa822 100644
--- a/resources/lib/jquery/jquery.js
+++ b/resources/lib/jquery/jquery.js
@@ -1,5 +1,5 @@
/*!
- * jQuery JavaScript Library v1.11.1
+ * jQuery JavaScript Library v1.11.2
* http://jquery.com/
*
* Includes Sizzle.js
@@ -9,7 +9,7 @@
* Released under the MIT license
* http://jquery.org/license
*
- * Date: 2014-05-01T17:42Z
+ * Date: 2014-12-17T15:27Z
*/
(function( global, factory ) {
@@ -64,7 +64,7 @@ var support = {};
var
- version = "1.11.1",
+ version = "1.11.2",
// Define a local copy of jQuery
jQuery = function( selector, context ) {
@@ -269,7 +269,8 @@ jQuery.extend({
// parseFloat NaNs numeric-cast false positives (null|true|false|"")
// ...but misinterprets leading-number strings, particularly hex literals ("0x...")
// subtraction forces infinities to NaN
- return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0;
+ // adding 1 corrects loss of precision from parseFloat (#15100)
+ return !jQuery.isArray( obj ) && (obj - parseFloat( obj ) + 1) >= 0;
},
isEmptyObject: function( obj ) {
@@ -584,14 +585,14 @@ function isArraylike( obj ) {
}
var Sizzle =
/*!
- * Sizzle CSS Selector Engine v1.10.19
+ * Sizzle CSS Selector Engine v2.2.0-pre
* http://sizzlejs.com/
*
- * Copyright 2013 jQuery Foundation, Inc. and other contributors
+ * Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors
* Released under the MIT license
* http://jquery.org/license
*
- * Date: 2014-04-18
+ * Date: 2014-12-16
*/
(function( window ) {
@@ -618,7 +619,7 @@ var i,
contains,
// Instance-specific data
- expando = "sizzle" + -(new Date()),
+ expando = "sizzle" + 1 * new Date(),
preferredDoc = window.document,
dirruns = 0,
done = 0,
@@ -633,7 +634,6 @@ var i,
},
// General-purpose constants
- strundefined = typeof undefined,
MAX_NEGATIVE = 1 << 31,
// Instance methods
@@ -643,12 +643,13 @@ var i,
push_native = arr.push,
push = arr.push,
slice = arr.slice,
- // Use a stripped-down indexOf if we can't use a native one
- indexOf = arr.indexOf || function( elem ) {
+ // Use a stripped-down indexOf as it's faster than native
+ // http://jsperf.com/thor-indexof-vs-for/5
+ indexOf = function( list, elem ) {
var i = 0,
- len = this.length;
+ len = list.length;
for ( ; i < len; i++ ) {
- if ( this[i] === elem ) {
+ if ( list[i] === elem ) {
return i;
}
}
@@ -688,6 +689,7 @@ var i,
")\\)|)",
// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+ rwhitespace = new RegExp( whitespace + "+", "g" ),
rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
@@ -739,6 +741,14 @@ var i,
String.fromCharCode( high + 0x10000 ) :
// Supplemental Plane codepoint (surrogate pair)
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+ },
+
+ // Used for iframes
+ // See setDocument()
+ // Removing the function wrapper causes a "Permission Denied"
+ // error in IE
+ unloadHandler = function() {
+ setDocument();
};
// Optimize for push.apply( _, NodeList )
@@ -781,19 +791,18 @@ function Sizzle( selector, context, results, seed ) {
context = context || document;
results = results || [];
+ nodeType = context.nodeType;
- if ( !selector || typeof selector !== "string" ) {
- return results;
- }
+ if ( typeof selector !== "string" || !selector ||
+ nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
- if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
- return [];
+ return results;
}
- if ( documentIsHTML && !seed ) {
+ if ( !seed && documentIsHTML ) {
- // Shortcuts
- if ( (match = rquickExpr.exec( selector )) ) {
+ // Try to shortcut find operations when possible (e.g., not under DocumentFragment)
+ if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
// Speed-up: Sizzle("#ID")
if ( (m = match[1]) ) {
if ( nodeType === 9 ) {
@@ -825,7 +834,7 @@ function Sizzle( selector, context, results, seed ) {
return results;
// Speed-up: Sizzle(".CLASS")
- } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {
+ } else if ( (m = match[3]) && support.getElementsByClassName ) {
push.apply( results, context.getElementsByClassName( m ) );
return results;
}
@@ -835,7 +844,7 @@ function Sizzle( selector, context, results, seed ) {
if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
nid = old = expando;
newContext = context;
- newSelector = nodeType === 9 && selector;
+ newSelector = nodeType !== 1 && selector;
// qSA works strangely on Element-rooted queries
// We can work around this by specifying an extra ID on the root
@@ -1022,7 +1031,7 @@ function createPositionalPseudo( fn ) {
* @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
*/
function testContext( context ) {
- return context && typeof context.getElementsByTagName !== strundefined && context;
+ return context && typeof context.getElementsByTagName !== "undefined" && context;
}
// Expose support vars for convenience
@@ -1046,9 +1055,8 @@ isXML = Sizzle.isXML = function( elem ) {
* @returns {Object} Returns the current document
*/
setDocument = Sizzle.setDocument = function( node ) {
- var hasCompare,
- doc = node ? node.ownerDocument || node : preferredDoc,
- parent = doc.defaultView;
+ var hasCompare, parent,
+ doc = node ? node.ownerDocument || node : preferredDoc;
// If no document and documentElement is available, return
if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
@@ -1058,9 +1066,7 @@ setDocument = Sizzle.setDocument = function( node ) {
// Set our document
document = doc;
docElem = doc.documentElement;
-
- // Support tests
- documentIsHTML = !isXML( doc );
+ parent = doc.defaultView;
// Support: IE>8
// If iframe document is assigned to "document" variable and if iframe has been reloaded,
@@ -1069,21 +1075,22 @@ setDocument = Sizzle.setDocument = function( node ) {
if ( parent && parent !== parent.top ) {
// IE11 does not have attachEvent, so all must suffer
if ( parent.addEventListener ) {
- parent.addEventListener( "unload", function() {
- setDocument();
- }, false );
+ parent.addEventListener( "unload", unloadHandler, false );
} else if ( parent.attachEvent ) {
- parent.attachEvent( "onunload", function() {
- setDocument();
- });
+ parent.attachEvent( "onunload", unloadHandler );
}
}
+ /* Support tests
+ ---------------------------------------------------------------------- */
+ documentIsHTML = !isXML( doc );
+
/* Attributes
---------------------------------------------------------------------- */
// Support: IE<8
- // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
+ // Verify that getAttribute really returns attributes and not properties
+ // (excepting IE8 booleans)
support.attributes = assert(function( div ) {
div.className = "i";
return !div.getAttribute("className");
@@ -1098,17 +1105,8 @@ setDocument = Sizzle.setDocument = function( node ) {
return !div.getElementsByTagName("*").length;
});
- // Check if getElementsByClassName can be trusted
- support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) {
- div.innerHTML = "<div class='a'></div><div class='a i'></div>";
-
- // Support: Safari<4
- // Catch class over-caching
- div.firstChild.className = "i";
- // Support: Opera<10
- // Catch gEBCN failure to find non-leading classes
- return div.getElementsByClassName("i").length === 2;
- });
+ // Support: IE<9
+ support.getElementsByClassName = rnative.test( doc.getElementsByClassName );
// Support: IE<10
// Check if getElementById returns elements by name
@@ -1122,7 +1120,7 @@ setDocument = Sizzle.setDocument = function( node ) {
// ID find and filter
if ( support.getById ) {
Expr.find["ID"] = function( id, context ) {
- if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
var m = context.getElementById( id );
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document #6963
@@ -1143,7 +1141,7 @@ setDocument = Sizzle.setDocument = function( node ) {
Expr.filter["ID"] = function( id ) {
var attrId = id.replace( runescape, funescape );
return function( elem ) {
- var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
+ var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
return node && node.value === attrId;
};
};
@@ -1152,14 +1150,20 @@ setDocument = Sizzle.setDocument = function( node ) {
// Tag
Expr.find["TAG"] = support.getElementsByTagName ?
function( tag, context ) {
- if ( typeof context.getElementsByTagName !== strundefined ) {
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
return context.getElementsByTagName( tag );
+
+ // DocumentFragment nodes don't have gEBTN
+ } else if ( support.qsa ) {
+ return context.querySelectorAll( tag );
}
} :
+
function( tag, context ) {
var elem,
tmp = [],
i = 0,
+ // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
results = context.getElementsByTagName( tag );
// Filter out possible comments
@@ -1177,7 +1181,7 @@ setDocument = Sizzle.setDocument = function( node ) {
// Class
Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
- if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {
+ if ( documentIsHTML ) {
return context.getElementsByClassName( className );
}
};
@@ -1206,13 +1210,15 @@ setDocument = Sizzle.setDocument = function( node ) {
// setting a boolean content attribute,
// since its presence should be enough
// http://bugs.jquery.com/ticket/12359
- div.innerHTML = "<select msallowclip=''><option selected=''></option></select>";
+ docElem.appendChild( div ).innerHTML = "<a id='" + expando + "'></a>" +
+ "<select id='" + expando + "-\f]' msallowcapture=''>" +
+ "<option selected=''></option></select>";
// Support: IE8, Opera 11-12.16
// Nothing should be selected when empty strings follow ^= or $= or *=
// The test attribute must be unknown in Opera but "safe" for WinRT
// http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
- if ( div.querySelectorAll("[msallowclip^='']").length ) {
+ if ( div.querySelectorAll("[msallowcapture^='']").length ) {
rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
}
@@ -1222,12 +1228,24 @@ setDocument = Sizzle.setDocument = function( node ) {
rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
}
+ // Support: Chrome<29, Android<4.2+, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.7+
+ if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
+ rbuggyQSA.push("~=");
+ }
+
// Webkit/Opera - :checked should return selected option elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
// IE8 throws error here and will not see later tests
if ( !div.querySelectorAll(":checked").length ) {
rbuggyQSA.push(":checked");
}
+
+ // Support: Safari 8+, iOS 8+
+ // https://bugs.webkit.org/show_bug.cgi?id=136851
+ // In-page `selector#id sibing-combinator selector` fails
+ if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) {
+ rbuggyQSA.push(".#.+[+~]");
+ }
});
assert(function( div ) {
@@ -1344,7 +1362,7 @@ setDocument = Sizzle.setDocument = function( node ) {
// Maintain original order
return sortInput ?
- ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
0;
}
@@ -1371,7 +1389,7 @@ setDocument = Sizzle.setDocument = function( node ) {
aup ? -1 :
bup ? 1 :
sortInput ?
- ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
0;
// If the nodes are siblings, we can do a quick check
@@ -1434,7 +1452,7 @@ Sizzle.matchesSelector = function( elem, expr ) {
elem.document && elem.document.nodeType !== 11 ) {
return ret;
}
- } catch(e) {}
+ } catch (e) {}
}
return Sizzle( expr, document, null, [ elem ] ).length > 0;
@@ -1653,7 +1671,7 @@ Expr = Sizzle.selectors = {
return pattern ||
(pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
classCache( className, function( elem ) {
- return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" );
+ return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
});
},
@@ -1675,7 +1693,7 @@ Expr = Sizzle.selectors = {
operator === "^=" ? check && result.indexOf( check ) === 0 :
operator === "*=" ? check && result.indexOf( check ) > -1 :
operator === "$=" ? check && result.slice( -check.length ) === check :
- operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
+ operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
false;
};
@@ -1795,7 +1813,7 @@ Expr = Sizzle.selectors = {
matched = fn( seed, argument ),
i = matched.length;
while ( i-- ) {
- idx = indexOf.call( seed, matched[i] );
+ idx = indexOf( seed, matched[i] );
seed[ idx ] = !( matches[ idx ] = matched[i] );
}
}) :
@@ -1834,6 +1852,8 @@ Expr = Sizzle.selectors = {
function( elem, context, xml ) {
input[0] = elem;
matcher( input, null, xml, results );
+ // Don't keep the element (issue #299)
+ input[0] = null;
return !results.pop();
};
}),
@@ -1845,6 +1865,7 @@ Expr = Sizzle.selectors = {
}),
"contains": markFunction(function( text ) {
+ text = text.replace( runescape, funescape );
return function( elem ) {
return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
};
@@ -2266,7 +2287,7 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS
i = matcherOut.length;
while ( i-- ) {
if ( (elem = matcherOut[i]) &&
- (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
+ (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
seed[temp] = !(results[temp] = elem);
}
@@ -2301,13 +2322,16 @@ function matcherFromTokens( tokens ) {
return elem === checkContext;
}, implicitRelative, true ),
matchAnyContext = addCombinator( function( elem ) {
- return indexOf.call( checkContext, elem ) > -1;
+ return indexOf( checkContext, elem ) > -1;
}, implicitRelative, true ),
matchers = [ function( elem, context, xml ) {
- return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+ var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
(checkContext = context).nodeType ?
matchContext( elem, context, xml ) :
matchAnyContext( elem, context, xml ) );
+ // Avoid hanging onto element (issue #299)
+ checkContext = null;
+ return ret;
} ];
for ( ; i < len; i++ ) {
@@ -2557,7 +2581,7 @@ select = Sizzle.select = function( selector, context, results, seed ) {
// Sort stability
support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
-// Support: Chrome<14
+// Support: Chrome 14-35+
// Always assume duplicates if they aren't passed to the comparison function
support.detectDuplicates = !!hasDuplicate;
@@ -6115,7 +6139,14 @@ var getStyles, curCSS,
if ( window.getComputedStyle ) {
getStyles = function( elem ) {
- return elem.ownerDocument.defaultView.getComputedStyle( elem, null );
+ // Support: IE<=11+, Firefox<=30+ (#15098, #14150)
+ // IE throws on elements created in popups
+ // FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
+ if ( elem.ownerDocument.defaultView.opener ) {
+ return elem.ownerDocument.defaultView.getComputedStyle( elem, null );
+ }
+
+ return window.getComputedStyle( elem, null );
};
curCSS = function( elem, name, computed ) {
@@ -6363,6 +6394,8 @@ function addGetHookIf( conditionFn, hookFn ) {
reliableMarginRightVal =
!parseFloat( ( window.getComputedStyle( contents, null ) || {} ).marginRight );
+
+ div.removeChild( contents );
}
// Support: IE8
@@ -9070,7 +9103,8 @@ jQuery.extend({
}
// We can fire global events as of now if asked to
- fireGlobals = s.global;
+ // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
+ fireGlobals = jQuery.event && s.global;
// Watch for a new set of requests
if ( fireGlobals && jQuery.active++ === 0 ) {
@@ -9329,13 +9363,6 @@ jQuery.each( [ "get", "post" ], function( i, method ) {
};
});
-// Attach a bunch of functions for handling common AJAX events
-jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) {
- jQuery.fn[ type ] = function( fn ) {
- return this.on( type, fn );
- };
-});
-
jQuery._evalUrl = function( url ) {
return jQuery.ajax({
@@ -9561,8 +9588,9 @@ var xhrId = 0,
// Support: IE<10
// Open requests must be manually aborted on unload (#5280)
-if ( window.ActiveXObject ) {
- jQuery( window ).on( "unload", function() {
+// See https://support.microsoft.com/kb/2856746 for more info
+if ( window.attachEvent ) {
+ window.attachEvent( "onunload", function() {
for ( var key in xhrCallbacks ) {
xhrCallbacks[ key ]( undefined, true );
}
@@ -9996,6 +10024,16 @@ jQuery.fn.load = function( url, params, callback ) {
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) {
+ jQuery.fn[ type ] = function( fn ) {
+ return this.on( type, fn );
+ };
+});
+
+
+
+
jQuery.expr.filters.animated = function( elem ) {
return jQuery.grep(jQuery.timers, function( fn ) {
return elem === fn.elem;
diff --git a/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js b/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
index d3e8f299..fb74e4ec 100644
--- a/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
+++ b/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
@@ -6,7 +6,7 @@
// Create useskin dropdown menu and reload onchange to the selected skin
// (only if a framework was found, not on error pages).
- $( '#mw-javascripttest-summary.mw-javascripttest-frameworkfound' ).append( function () {
+ $( '#mw-javascripttest-summary' ).append( function () {
var $html = $( '<p><label for="useskin">'
+ mw.message( 'javascripttest-pagetext-skins' ).escaped()
@@ -25,7 +25,8 @@
// Bind onchange event handler and append to form
$html.append(
$( select ).change( function () {
- window.location = QUnit.url( { useskin: $( this ).val() } );
+ var url = new mw.Uri();
+ location.href = url.extend( { useskin: $( this ).val() } );
} )
);
diff --git a/skins/ArchLinux/FF2Fixes.css b/skins/ArchLinux/FF2Fixes.css
deleted file mode 100644
index c8b65f50..00000000
--- a/skins/ArchLinux/FF2Fixes.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.rtl .external, a.feedlink {
- padding: 0 !important;
- background: none !important;
-}
diff --git a/skins/ArchLinux/arch.css b/skins/ArchLinux/arch.css
index 3b4c38b3..9432d813 100644
--- a/skins/ArchLinux/arch.css
+++ b/skins/ArchLinux/arch.css
@@ -1,71 +1,178 @@
/* general styling */
-body { background: #f6f9fc; }
-body, #content, table { color: #222;}
-h1, h2, h3, h4, h5 { color: #222; }
-h1 { font-weight: bold; }
-pre, code, tt { background-color: #ebf1f5; color: #222; font-family: monospace; }
-pre { border: 1px solid #bcd; overflow: auto; }
-code, tt { padding: 0.3em; }
+body {
+ background: #f6f9fc;
+}
+body,
+#content,
+table,
+h1,
+h2,
+h3,
+h4,
+h5,
+pre,
+code,
+tt {
+ color: #222;
+}
+h1 {
+ font-weight: bold;
+}
+pre,
+code,
+tt {
+ background-color: #ebf1f5;
+ font-family: monospace;
+}
+pre {
+ border: 1px solid #bcd;
+ overflow: auto;
+}
+code,
+tt {
+ /* Inline-block prevents code from wrapping when starting too close to the
+ end of a line; it also lets select the entire code line with a triple
+ click */
+ display: inline-block;
+ padding: 0 0.3em;
+ /* A border would be inherited by the default style sheets, but we don't
+ want it */
+ border-width: 0;
+ border-radius: 0;
+}
+ul,
+.portlet ul {
+ list-style-image: none;
+}
+#bodyContent table {
+ border-collapse: collapse;
+ padding: 2px;
+}
+#bodyContent td {
+ padding: 2px;
+}
/* links (including page tabs and personal toolbar) */
-a, #p-cactions li a { text-decoration: none; outline: none; }
-a:link, #p-cactions li a, #p-personal li a, #bodyContent a.external { color: #07b; }
-#bodyContent > div.mw-content-ltr a, #bodyContent > div.mw-content-rtl a, #wikiPreview > div.mw-content-ltr a, #wikiPreview > div.mw-content-rtl a { font-weight: bold; }
-#bodyContent #toc a, #bodyContent .special li > a, #bodyContent .special li span a, #bodyContent #pagehistory a { font-weight: normal; }
-a:visited, #bodyContent a:visited.external { color: #666; }
-a:focus { color: #e90 !important; }
-a:hover, #p-personal li a:hover, #bodyContent #toc a:hover, #bodyContent a:hover.external { text-decoration: underline; background-color: transparent; color: #999; }
-a:active { color: #e90 !important; }
-a.new, #p-cactions .new a { color: #b00 !important; }
+#bodyContent > div.mw-content-ltr a,
+#bodyContent > div.mw-content-rtl a,
+#wikiPreview > div.mw-content-ltr a,
+#wikiPreview > div.mw-content-rtl a {
+ font-weight: bold;
+}
+#bodyContent #toc a,
+#bodyContent .special li > a,
+#bodyContent .special li span a,
+#bodyContent #pagehistory a {
+ font-weight: normal;
+}
+a:link,
+#toc a,
+#p-cactions li a,
+#p-personal li a,
+#p-cactions li a:visited,
+#p-personal li a:visited,
+#bodyContent a.external,
+#bodyContent a.extiw {
+ text-decoration: none;
+ outline: none;
+ color: #07b;
+}
+a:visited,
+#bodyContent a:visited.external {
+ color: #666;
+}
+a:hover,
+#p-personal li a:hover,
+#bodyContent #toc a:hover,
+#bodyContent a:hover.external {
+ text-decoration: underline;
+ background-color: transparent;
+ color: #999;
+}
+a:focus,
+a:active,
+#toc a:focus,
+#toc a:active,
+#p-cactions li a:focus,
+#p-cactions li a:active,
+#p-personal li a:focus,
+#p-personal li a:active,
+#bodyContent a:focus.external,
+#bodyContent a:active.external,
+#bodyContent a:focus.extiw,
+#bodyContent a:active.extiw {
+ color: #e90 !important;
+}
+a.new,
+#p-cactions .new a,
+#p-personal a.new {
+ color: #b00 !important;
+}
/* bump down the personal toolbar (top menu) */
-#p-personal { top: 70px; }
+#p-personal {
+ top: 70px;
+}
/* bump down the action tabs (page, discuss, edit, etc.) */
-#p-cactions { top: 7.5em; }
-/* first for IE6 */
-#p-cactions { top: 97px; }
-/* and now for the rest */
-html > body #p-cactions { top: 92px; }
+#p-cactions {
+ top: 92px;
+}
/* bump down the main content to make room for navbar */
-#content { top: .8em; }
-#content { top: 10px; }
+#content {
+ top: 10px;
+}
/* shrink the content just enough to show off the borders */
-div#globalWrapper { width: 99%; }
+div#globalWrapper {
+ width: 99%;
+}
/* article Table of Contents */
-#toc, .toc, .mw-warning { background-color: #f9faff; border: 1px solid #d7dfe3; }
+#toc,
+.toc,
+.mw-warning {
+ background-color: #f9faff;
+ border: 1px solid #d7dfe3;
+}
/* sidebar menus and content borders */
-.pBody { border: 1px solid #ddd; }
-div#content { border: 1px solid #ccc; }
+.pBody {
+ border: 1px solid #ddd;
+}
+div#content {
+ border: 1px solid #ccc;
+}
/* disable default mediawiki logo and close the gap it leaves behind */
-#p-logo { display: none !important;}
-/* first for IE6 */
-#column-one { padding-top: 90px; }
-/* and now for the browsers that work like they should */
-html > body #column-one { padding-top: 36px; }
+#p-logo {
+ display: none !important;
+}
+div#column-one {
+ padding-top: 36px;
+}
/* disable footer logos TODO: see if this can be done in LocalSettings.php */
-#f-poweredbyico, #f-copyrightico { display: none; }
+#f-poweredbyico,
+#f-copyrightico {
+ display: none;
+}
/* clean up the footer */
-div#footer { color: #888; background-color: transparent; border-top: none; border-bottom: none; }
+div#footer {
+ color: #888;
+ background-color: transparent;
+ border-top: none;
+ border-bottom: none;
+}
/* bring footer text inline with content */
-/* first for IE6 */
-#footer ul { margin-left: 0; }
-/* and now for the other browsers that work properly */
-html > body #footer ul { margin-left: 170px; }
+#footer ul {
+ margin-left: 170px;
+}
/* highlight current website in the navbar */
-#archnavbar ul li.anb-selected a { color: white !important; }
-
-/* make tables prettier */
-#bodyContent table { border-collapse: collapse; padding: 2px; }
-#bodyContent td { padding: 2px; }
-
-ul, .portlet ul { list-style-image: none; }
+#archnavbar ul li.anb-selected a {
+ color: white !important;
+}
diff --git a/skins/CologneBlue/SkinCologneBlue.php b/skins/CologneBlue/SkinCologneBlue.php
index eb7d50b6..41bc0bae 100644
--- a/skins/CologneBlue/SkinCologneBlue.php
+++ b/skins/CologneBlue/SkinCologneBlue.php
@@ -571,6 +571,9 @@ class CologneBlueTemplate extends BaseTemplate {
$s = "<div id='quickbar'>\n";
foreach ( $bar as $heading => $data ) {
+ // Numeric strings gets an integer when set as key, cast back - T73639
+ $heading = (string)$heading;
+
$portletId = Sanitizer::escapeId( "p-$heading" );
$headingMsg = wfMessage( $idToMessage[$heading] ? $idToMessage[$heading] : $heading );
$headingHTML = "<h3>";
diff --git a/skins/MonoBook/MonoBookTemplate.php b/skins/MonoBook/MonoBookTemplate.php
index c432625f..8637b087 100644
--- a/skins/MonoBook/MonoBookTemplate.php
+++ b/skins/MonoBook/MonoBookTemplate.php
@@ -207,6 +207,9 @@ class MonoBookTemplate extends BaseTemplate {
continue;
}
+ // Numeric strings gets an integer when set as key, cast back - T73639
+ $boxName = (string)$boxName;
+
if ( $boxName == 'SEARCH' ) {
$this->searchBox();
} elseif ( $boxName == 'TOOLBOX' ) {
diff --git a/skins/Vector/SkinVector.php b/skins/Vector/SkinVector.php
index 8f7056d7..565c64bb 100644
--- a/skins/Vector/SkinVector.php
+++ b/skins/Vector/SkinVector.php
@@ -35,8 +35,8 @@ class SkinVector extends SkinTemplate {
*/
private $vectorConfig;
- public function __construct( Config $config ) {
- $this->vectorConfig = $config;
+ public function __construct() {
+ $this->vectorConfig = ConfigFactory::getDefaultInstance()->makeConfig( 'vector' );
}
protected static $bodyClasses = array( 'vector-animateLayout' );
diff --git a/skins/Vector/Vector.php b/skins/Vector/Vector.php
index 16bec178..2ffc3a7b 100644
--- a/skins/Vector/Vector.php
+++ b/skins/Vector/Vector.php
@@ -38,10 +38,7 @@ $GLOBALS['wgAutoloadClasses']['VectorTemplate'] = __DIR__ . '/VectorTemplate.php
$GLOBALS['wgMessagesDirs']['Vector'] = __DIR__ . '/i18n';
// Register skin
-SkinFactory::getDefaultInstance()->register( 'vector', 'Vector', function(){
- $config = ConfigFactory::getDefaultInstance()->makeConfig( 'vector' );
- return new SkinVector( $config );
-} );
+$GLOBALS['wgValidSkinNames']['vector'] = 'Vector';
// Register config
$GLOBALS['wgConfigRegistry']['vector'] = 'GlobalVarConfig::newInstance';
@@ -97,11 +94,14 @@ $GLOBALS['wgResourceModuleSkinStyles']['vector'] = array(
'jquery.ui.button' => 'skinStyles/jquery.ui/jquery.ui.button.css',
'jquery.ui.datepicker' => 'skinStyles/jquery.ui/jquery.ui.datepicker.css',
'jquery.ui.dialog' => 'skinStyles/jquery.ui/jquery.ui.dialog.css',
+ 'jquery.ui.menu' => 'skinStyles/jquery.ui/jquery.ui.menu.css',
'jquery.ui.progressbar' => 'skinStyles/jquery.ui/jquery.ui.progressbar.css',
'jquery.ui.resizable' => 'skinStyles/jquery.ui/jquery.ui.resizable.css',
'jquery.ui.selectable' => 'skinStyles/jquery.ui/jquery.ui.selectable.css',
'jquery.ui.slider' => 'skinStyles/jquery.ui/jquery.ui.slider.css',
+ 'jquery.ui.spinner' => 'skinStyles/jquery.ui/jquery.ui.spinner.css',
'jquery.ui.tabs' => 'skinStyles/jquery.ui/jquery.ui.tabs.css',
+ 'jquery.ui.tooltips' => 'skinStyles/jquery.ui/jquery.ui.tooltips.css',
'mediawiki.notification' => 'skinStyles/mediawiki.notification.less',
'mediawiki.special' => 'skinStyles/mediawiki.special.less',
'mediawiki.special.preferences' => 'skinStyles/mediawiki.special.preferences.less',
diff --git a/skins/Vector/VectorTemplate.php b/skins/Vector/VectorTemplate.php
index 30ba32e5..6e4e2f1e 100644
--- a/skins/Vector/VectorTemplate.php
+++ b/skins/Vector/VectorTemplate.php
@@ -279,6 +279,9 @@ class VectorTemplate extends BaseTemplate {
continue;
}
+ // Numeric strings gets an integer when set as key, cast back - T73639
+ $name = (string)$name;
+
switch ( $name ) {
case 'SEARCH':
break;
diff --git a/skins/Vector/skinStyles/jquery.ui/PATCHES b/skins/Vector/skinStyles/jquery.ui/PATCHES
new file mode 100644
index 00000000..85f663ed
--- /dev/null
+++ b/skins/Vector/skinStyles/jquery.ui/PATCHES
@@ -0,0 +1,25 @@
+jquery.ui.button.css
+* Picked from jQuery UI 1.11.2-alpha instead of 1.9.2.
+* Extra customizations.
+
+jquery.ui.datepicker.css
+* Add @noflip to prevent CSSJanus flipping.
+
+jquery.ui.dialog.css
+* Extra customizations.
+
+jquery.ui.resizable.css
+* Add @noflip to prevent CSSJanus flipping.
+
+jquery.ui.theme.css
+* Add @embed instructions for CSSMin.
+* Change font-size from 1.0em to 0.8em.
+* Join ".ui-icon", ".ui-widget-content .ui-icon" and ".ui-widget-header .ui-icon" rules
+ to optimise image embedding.
+* Removed ".ui-widget-content a { color: #362b36; }" and
+ ".ui-widget-header a { color: #222222; }" due to bug T85857.
+
+images:
+* Add close.png and titlebar-fade.png (used in customizations for
+ jquery.ui.dialog.css)
+* Change chmod from 755 to 644.
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-anim_basic_16x16.gif b/skins/Vector/skinStyles/jquery.ui/images/ui-anim_basic_16x16.gif
deleted file mode 100644
index 085ccaec..00000000
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-anim_basic_16x16.gif
+++ /dev/null
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_100_000000_40x100.png b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_100_000000_40x100.png
new file mode 100644
index 00000000..162ef61b
--- /dev/null
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_100_000000_40x100.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_15_cd0a0a_40x100.png b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_15_cd0a0a_40x100.png
index 09de537f..debc52e6 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_15_cd0a0a_40x100.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_15_cd0a0a_40x100.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_70_000000_40x100.png b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_70_000000_40x100.png
index c06dd561..13032d6d 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_70_000000_40x100.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_flat_70_000000_40x100.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png
index 5308b466..7ccbbd06 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_80_d7ebf9_1x100.png b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_80_d7ebf9_1x100.png
index 0c8997f7..d09a8746 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_80_d7ebf9_1x100.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-hard_80_d7ebf9_1x100.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_e4f1fb_1x100.png b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_e4f1fb_1x100.png
index 31492556..8d46985f 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_e4f1fb_1x100.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_e4f1fb_1x100.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_ffffff_1x100.png b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_ffffff_1x100.png
index 09b23761..26e4666f 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_ffffff_1x100.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_100_ffffff_1x100.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_25_ffef8f_1x100.png b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_25_ffef8f_1x100.png
index 66627c18..d044ef6f 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_25_ffef8f_1x100.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_highlight-soft_25_ffef8f_1x100.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_inset-hard_100_f0f0f0_1x100.png b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_inset-hard_100_f0f0f0_1x100.png
index ccb6dc06..47e1f073 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-bg_inset-hard_100_f0f0f0_1x100.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-bg_inset-hard_100_f0f0f0_1x100.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_2694e8_256x240.png b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_2694e8_256x240.png
index 998ac3bc..252bf0f5 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_2694e8_256x240.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_2694e8_256x240.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_3d80b3_256x240.png b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_3d80b3_256x240.png
index ec129a8b..ff1c26ff 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_3d80b3_256x240.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_3d80b3_256x240.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_666666_256x240.png b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_666666_256x240.png
index a32c57d8..76cecfc0 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_666666_256x240.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_666666_256x240.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_72a7cf_256x240.png b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_72a7cf_256x240.png
index 88fad1a5..9d079149 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_72a7cf_256x240.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_72a7cf_256x240.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_ffffff_256x240.png b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_ffffff_256x240.png
index 29ba7d28..4f624bb2 100644
--- a/skins/Vector/skinStyles/jquery.ui/images/ui-icons_ffffff_256x240.png
+++ b/skins/Vector/skinStyles/jquery.ui/images/ui-icons_ffffff_256x240.png
Binary files differ
diff --git a/skins/Vector/skinStyles/jquery.ui/jquery.ui.autocomplete.css b/skins/Vector/skinStyles/jquery.ui/jquery.ui.autocomplete.css
index da6de452..4ef3497a 100644
--- a/skins/Vector/skinStyles/jquery.ui/jquery.ui.autocomplete.css
+++ b/skins/Vector/skinStyles/jquery.ui/jquery.ui.autocomplete.css
@@ -1,40 +1,19 @@
-/* Autocomplete
-----------------------------------*/
-.ui-autocomplete { position: absolute; cursor: default; }
-.ui-autocomplete-loading { /* @embed */ background: white url('images/ui-anim_basic_16x16.gif') right center no-repeat; }
+/*!
+ * jQuery UI Autocomplete 1.9.2
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete#theming
+ */
+.ui-autocomplete {
+ position: absolute;
+ top: 0;
+ left: 0;
+ cursor: default;
+}
/* workarounds */
* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
-
-/* Menu
-----------------------------------*/
-.ui-menu {
- list-style:none;
- padding: 2px;
- margin: 0;
- display:block;
- float: left;
-}
-.ui-menu .ui-menu {
- margin-top: -3px;
-}
-.ui-menu .ui-menu-item {
- margin:0;
- padding: 0;
- zoom: 1;
- float: left;
- clear: left;
- width: 100%;
-}
-.ui-menu .ui-menu-item a {
- text-decoration:none;
- display:block;
- padding:.2em .4em;
- line-height:1.5;
- zoom:1;
-}
-.ui-menu .ui-menu-item a.ui-state-hover,
-.ui-menu .ui-menu-item a.ui-state-active {
- font-weight: normal;
- margin: -1px;
-}
diff --git a/skins/Vector/skinStyles/jquery.ui/jquery.ui.button.css b/skins/Vector/skinStyles/jquery.ui/jquery.ui.button.css
index 8c2286d1..d3bb7275 100644
--- a/skins/Vector/skinStyles/jquery.ui/jquery.ui.button.css
+++ b/skins/Vector/skinStyles/jquery.ui/jquery.ui.button.css
@@ -1,92 +1,118 @@
-/* Button
-----------------------------------*/
-
+/*!
+ * jQuery UI Button 1.11.2-alpha
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/button/#theming
+ */
.ui-button {
display: inline-block;
position: relative;
padding: 0;
+ line-height: normal;
margin-right: .1em;
- text-decoration: none !important;
cursor: pointer;
+ vertical-align: middle;
text-align: center;
- zoom: 1;
- overflow: visible; /* the overflow property removes extra width in IE */
+ overflow: visible; /* removes extra width in IE */
+}
+.ui-button,
+.ui-button:link,
+.ui-button:visited,
+.ui-button:hover,
+.ui-button:active {
+ text-decoration: none;
+}
+/* to make room for the icon, a width needs to be set here */
+.ui-button-icon-only {
+ width: 2.2em;
+}
+/* button elements seem to need a little more width */
+button.ui-button-icon-only {
+ width: 2.4em;
+}
+.ui-button-icons-only {
+ width: 3.4em;
+}
+button.ui-button-icons-only {
+ width: 3.7em;
}
-/*button text element */
+/* button text element */
.ui-button .ui-button-text {
display: block;
- line-height: 1.4;
- text-shadow: 0 1px 1px #fff;
+ line-height: normal;
}
.ui-button-text-only .ui-button-text {
- padding: 0.3em 1em 0.25em 1em;
+ padding: .4em 1em;
}
.ui-button-icon-only .ui-button-text,
.ui-button-icons-only .ui-button-text {
- padding: 0.3em;
+ padding: .4em;
text-indent: -9999999px;
}
.ui-button-text-icon-primary .ui-button-text,
.ui-button-text-icons .ui-button-text {
- padding: 0.3em 1em 0.25em 2.1em;
+ padding: .4em 1em .4em 2.1em;
}
.ui-button-text-icon-secondary .ui-button-text,
.ui-button-text-icons .ui-button-text {
- padding: 0.3em 2.1em 0.25em 1em;
+ padding: .4em 2.1em .4em 1em;
}
.ui-button-text-icons .ui-button-text {
padding-left: 2.1em;
padding-right: 2.1em;
}
-
/* no icon support for input elements, provide padding by default */
input.ui-button {
- padding: 0.3em 1em;
+ padding: .4em 1em;
}
-/*button icon element(s) */
+/* button icon element(s) */
.ui-button-icon-only .ui-icon,
.ui-button-text-icon-primary .ui-icon,
.ui-button-text-icon-secondary .ui-icon,
.ui-button-text-icons .ui-icon,
-.ui-button-text-icon .ui-icon,
.ui-button-icons-only .ui-icon {
position: absolute;
top: 50%;
- margin-top: -9px;
+ margin-top: -8px;
}
.ui-button-icon-only .ui-icon {
left: 50%;
margin-left: -8px;
}
.ui-button-text-icon-primary .ui-button-icon-primary,
-.ui-button-text-icon .ui-button-icon-primary,
.ui-button-text-icons .ui-button-icon-primary,
.ui-button-icons-only .ui-button-icon-primary {
- left: 0.5em;
+ left: .5em;
}
.ui-button-text-icon-secondary .ui-button-icon-secondary,
-.ui-button-text-icon .ui-button-icon-secondary,
.ui-button-text-icons .ui-button-icon-secondary,
.ui-button-icons-only .ui-button-icon-secondary {
- right: 0.5em;
+ right: .5em;
}
-/*button sets*/
+/* button sets */
.ui-buttonset {
margin-right: 7px;
}
.ui-buttonset .ui-button {
margin-left: 0;
- margin-right: -.4em;
+ margin-right: -.3em;
}
/* workarounds */
+/* reset extra padding in Firefox, see h5bp.com/l */
+input.ui-button::-moz-focus-inner,
button.ui-button::-moz-focus-inner {
border: 0;
- padding: 0; /* reset extra padding in Firefox */
+ padding: 0;
}
+
/* Disables the annoying dashed border Firefox puts on active buttons */
body button.ui-button::-moz-focus-inner {
border: 0;
@@ -187,6 +213,8 @@ body .ui-button:active {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f0f0f0', endColorstr='#d0d0d0', GradientType=0); /* IE6-8 */
}
+/* Customizations for MediaWiki Vector */
+
/* Green buttons */
body .ui-button-green,
body .ui-button-green .ui-button-text {
diff --git a/skins/Vector/skinStyles/jquery.ui/jquery.ui.datepicker.css b/skins/Vector/skinStyles/jquery.ui/jquery.ui.datepicker.css
index 871bf690..b28332ff 100644
--- a/skins/Vector/skinStyles/jquery.ui/jquery.ui.datepicker.css
+++ b/skins/Vector/skinStyles/jquery.ui/jquery.ui.datepicker.css
@@ -1,5 +1,13 @@
-/* Datepicker
-----------------------------------*/
+/*!
+ * jQuery UI Datepicker 1.9.2
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker#theming
+ */
.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
@@ -10,7 +18,7 @@
.ui-datepicker .ui-datepicker-next-hover { right:1px; }
.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
-.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; padding:1px 0; }
+.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
.ui-datepicker select.ui-datepicker-month,
.ui-datepicker select.ui-datepicker-year { width: 49%;}
@@ -18,7 +26,7 @@
.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
.ui-datepicker td { border: 0; padding: 1px; }
.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
-.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .2em 0 0 0; padding: 0 .2em; border-top: 1px solid #DDDDDD; border-left: 0; border-right: 0; border-bottom: 0; }
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
@@ -32,7 +40,7 @@
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
-.ui-datepicker-row-break { clear:both; width:100%; }
+.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
/* RTL support */
/* @noflip */ .ui-datepicker-rtl { direction: rtl; }
@@ -49,8 +57,6 @@
/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
.ui-datepicker-cover {
- display: none; /*sorry for IE5*/
- display/**/: block; /*sorry for IE5*/
position: absolute; /*must have*/
z-index: -1; /*must have*/
filter: mask(); /*must have*/
diff --git a/skins/Vector/skinStyles/jquery.ui/jquery.ui.dialog.css b/skins/Vector/skinStyles/jquery.ui/jquery.ui.dialog.css
index cd85f14e..f7c47a7a 100644
--- a/skins/Vector/skinStyles/jquery.ui/jquery.ui.dialog.css
+++ b/skins/Vector/skinStyles/jquery.ui/jquery.ui.dialog.css
@@ -1,17 +1,28 @@
-/* Dialog
-----------------------------------*/
-.ui-dialog { position: absolute; padding: 0; width: 300px; }
-.ui-dialog .ui-dialog-titlebar { padding: .75em; position: relative; }
-.ui-dialog .ui-dialog-title { float: left; margin: 0; }
-.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .75em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+/*!
+ * jQuery UI Dialog 1.9.2
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog#theming
+ */
+.ui-dialog { position: absolute; top: 0; left: 0; padding: .2em; width: 300px; overflow: hidden; }
+.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; }
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
-.ui-dialog .ui-dialog-content { border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
+.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
.ui-draggable .ui-dialog-titlebar { cursor: move; }
-/* Customizations */
+
+/* Customizations for MediaWiki Vector */
+
body .ui-dialog .ui-dialog-titlebar-close:hover {
text-decoration: none;
}
diff --git a/skins/Vector/skinStyles/jquery.ui/jquery.ui.menu.css b/skins/Vector/skinStyles/jquery.ui/jquery.ui.menu.css
new file mode 100644
index 00000000..83fd84e4
--- /dev/null
+++ b/skins/Vector/skinStyles/jquery.ui/jquery.ui.menu.css
@@ -0,0 +1,30 @@
+/*!
+ * jQuery UI Menu 1.9.2
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu#theming
+ */
+.ui-menu { list-style:none; padding: 2px; margin: 0; display:block; outline: none; }
+.ui-menu .ui-menu { margin-top: -3px; position: absolute; }
+.ui-menu .ui-menu-item { margin: 0; padding: 0; zoom: 1; width: 100%; }
+.ui-menu .ui-menu-divider { margin: 5px -2px 5px -2px; height: 0; font-size: 0; line-height: 0; border-width: 1px 0 0 0; }
+.ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.5; zoom: 1; font-weight: normal; }
+.ui-menu .ui-menu-item a.ui-state-focus,
+.ui-menu .ui-menu-item a.ui-state-active { font-weight: normal; margin: -1px; }
+
+.ui-menu .ui-state-disabled { font-weight: normal; margin: .4em 0 .2em; line-height: 1.5; }
+.ui-menu .ui-state-disabled a { cursor: default; }
+
+/* icon support */
+.ui-menu-icons { position: relative; }
+.ui-menu-icons .ui-menu-item a { position: relative; padding-left: 2em; }
+
+/* left-aligned */
+.ui-menu .ui-icon { position: absolute; top: .2em; left: .2em; }
+
+/* right-aligned */
+.ui-menu .ui-menu-icon { position: static; float: right; }
diff --git a/skins/Vector/skinStyles/jquery.ui/jquery.ui.resizable.css b/skins/Vector/skinStyles/jquery.ui/jquery.ui.resizable.css
index f1bd7c5e..f8822e80 100644
--- a/skins/Vector/skinStyles/jquery.ui/jquery.ui.resizable.css
+++ b/skins/Vector/skinStyles/jquery.ui/jquery.ui.resizable.css
@@ -1,7 +1,15 @@
-/* Resizable
-----------------------------------*/
+/*!
+ * jQuery UI Resizable 1.9.2
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizable#theming
+ */
.ui-resizable { position: relative;}
-.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; }
.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
diff --git a/skins/Vector/skinStyles/jquery.ui/jquery.ui.spinner.css b/skins/Vector/skinStyles/jquery.ui/jquery.ui.spinner.css
new file mode 100644
index 00000000..e89b7206
--- /dev/null
+++ b/skins/Vector/skinStyles/jquery.ui/jquery.ui.spinner.css
@@ -0,0 +1,23 @@
+/*!
+ * jQuery UI Spinner 1.9.2
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Spinner#theming
+ */
+.ui-spinner { position:relative; display: inline-block; overflow: hidden; padding: 0; vertical-align: middle; }
+.ui-spinner-input { border: none; background: none; padding: 0; margin: .2em 0; vertical-align: middle; margin-left: .4em; margin-right: 22px; }
+.ui-spinner-button { width: 16px; height: 50%; font-size: .5em; padding: 0; margin: 0; text-align: center; position: absolute; cursor: default; display: block; overflow: hidden; right: 0; }
+.ui-spinner a.ui-spinner-button { border-top: none; border-bottom: none; border-right: none; } /* more specificity required here to overide default borders */
+.ui-spinner .ui-icon { position: absolute; margin-top: -8px; top: 50%; left: 0; } /* vertical centre icon */
+.ui-spinner-up { top: 0; }
+.ui-spinner-down { bottom: 0; }
+
+/* TR overrides */
+.ui-spinner .ui-icon-triangle-1-s {
+ /* need to fix icons sprite */
+ background-position:-65px -16px;
+}
diff --git a/skins/Vector/skinStyles/jquery.ui/jquery.ui.theme.css b/skins/Vector/skinStyles/jquery.ui/jquery.ui.theme.css
index 6bde5d3e..cccfe4b5 100644
--- a/skins/Vector/skinStyles/jquery.ui/jquery.ui.theme.css
+++ b/skins/Vector/skinStyles/jquery.ui/jquery.ui.theme.css
@@ -1,11 +1,15 @@
-
-
-/*
-* jQuery UI CSS Framework
-* Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
-* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
-* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=sans-serif&fwDefault=normal&fsDefault=1.0em&cornerRadius=3px&bgColorHeader=ffffff&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=100&borderColorHeader=aed0ea&fcHeader=222222&iconColorHeader=72a7cf&bgColorContent=f2f5f7&bgTextureContent=04_highlight_hard.png&bgImgOpacityContent=100&borderColorContent=cccccc&fcContent=362b36&iconColorContent=72a7cf&bgColorDefault=d7ebf9&bgTextureDefault=04_highlight_hard.png&bgImgOpacityDefault=80&borderColorDefault=aed0ea&fcDefault=2779aa&iconColorDefault=3d80b3&bgColorHover=e4f1fb&bgTextureHover=03_highlight_soft.png&bgImgOpacityHover=100&borderColorHover=74b2e2&fcHover=0070a3&iconColorHover=2694e8&bgColorActive=f0f0f0&bgTextureActive=06_inset_hard.png&bgImgOpacityActive=100&borderColorActive=cccccc&fcActive=000000&iconColorActive=666666&bgColorHighlight=ffef8f&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=25&borderColorHighlight=f9dd34&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=cd0a0a&bgTextureError=01_flat.png&bgImgOpacityError=15&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffffff&bgColorOverlay=000000&bgTextureOverlay=21_glow_ball.png&bgImgOpacityOverlay=100&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=70&opacityShadow=20&thicknessShadow=7px&offsetTopShadow=-7px&offsetLeftShadow=-7px&cornerRadiusShadow=8px
-*/
+/*!
+ * jQuery UI CSS Framework 1.9.2
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ *
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=sans-serif&fwDefault=normal&fsDefault=1.0em&cornerRadius=3px&bgColorHeader=ffffff&bgTextureHeader=highlight_soft&bgImgOpacityHeader=100&borderColorHeader=aed0ea&fcHeader=222222&iconColorHeader=72a7cf&bgColorContent=f2f5f7&bgTextureContent=highlight_hard&bgImgOpacityContent=100&borderColorContent=cccccc&fcContent=362b36&iconColorContent=72a7cf&bgColorDefault=d7ebf9&bgTextureDefault=highlight_hard&bgImgOpacityDefault=80&borderColorDefault=aed0ea&fcDefault=2779aa&iconColorDefault=3d80b3&bgColorHover=e4f1fb&bgTextureHover=highlight_soft&bgImgOpacityHover=100&borderColorHover=74b2e2&fcHover=0070a3&iconColorHover=2694e8&bgColorActive=f0f0f0&bgTextureActive=inset_hard&bgImgOpacityActive=100&borderColorActive=cccccc&fcActive=000000&iconColorActive=666666&bgColorHighlight=ffef8f&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=25&borderColorHighlight=f9dd34&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=cd0a0a&bgTextureError=flat&bgImgOpacityError=15&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffffff&bgColorOverlay=000000&bgTextureOverlay=flat&bgImgOpacityOverlay=100&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=70&opacityShadow=20&thicknessShadow=7px&offsetTopShadow=-7px&offsetLeftShadow=-7px&cornerRadiusShadow=8px
+ */
/* Component containers
@@ -13,41 +17,43 @@
.ui-widget { font-family: sans-serif; font-size: 0.8em; }
.ui-widget .ui-widget { font-size: 1em; }
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: sans-serif; font-size: 1em; }
-.ui-widget-content { border: 1px solid #cccccc; /* @embed */ background: #f2f5f7 url(images/ui-bg_highlight-hard_100_f2f5f7_1x100.png) 50% top repeat-x; color: #362b36; }
-.ui-widget-header { border-bottom: 1px solid #bbbbbb; line-height: 1em; /* @embed */ background: #ffffff url(images/ui-bg_highlight-soft_100_ffffff_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
+.ui-widget-content { border: 1px solid #cccccc; /* @embed */ background: #f2f5f7 url("images/ui-bg_highlight-hard_100_f2f5f7_1x100.png") 50% top repeat-x; color: #362b36; }
+.ui-widget-header { border: 1px solid #aed0ea; /* @embed */ background: #ffffff url("images/ui-bg_highlight-soft_100_ffffff_1x100.png") 50% 50% repeat-x; color: #222222; font-weight: bold; }
/* Interaction states
----------------------------------*/
-.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #aed0ea; /* @embed */ background: #d7ebf9 url(images/ui-bg_highlight-hard_80_d7ebf9_1x100.png) 50% 50% repeat-x; font-weight: normal; color: #2779aa; }
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #aed0ea; /* @embed */ background: #d7ebf9 url("images/ui-bg_highlight-hard_80_d7ebf9_1x100.png") 50% 50% repeat-x; font-weight: normal; color: #2779aa; }
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #2779aa; text-decoration: none; }
-.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #74b2e2; /* @embed */ background: #e4f1fb url(images/ui-bg_highlight-soft_100_e4f1fb_1x100.png) 50% 50% repeat-x; font-weight: normal; color: #0070a3; }
-.ui-state-hover a, .ui-state-hover a:hover { color: #0070a3; text-decoration: none; }
-.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #cccccc; background: #f0f0f0 /* @embed */ url(images/ui-bg_inset-hard_100_f0f0f0_1x100.png) 50% 50% repeat-x; font-weight: normal; color: #000000; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #74b2e2; /* @embed */ background: #e4f1fb url("images/ui-bg_highlight-soft_100_e4f1fb_1x100.png") 50% 50% repeat-x; font-weight: normal; color: #0070a3; }
+.ui-state-hover a, .ui-state-hover a:hover, .ui-state-hover a:link, .ui-state-hover a:visited { color: #0070a3; text-decoration: none; }
+.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #cccccc; background: #f0f0f0 /* @embed */ url("images/ui-bg_inset-hard_100_f0f0f0_1x100.png") 50% 50% repeat-x; font-weight: normal; color: #000000; }
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #000000; text-decoration: none; }
-.ui-widget :active { outline: none; }
/* Interaction Cues
----------------------------------*/
-.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #f9dd34; background: #ffef8f /* @embed */ url(images/ui-bg_highlight-soft_25_ffef8f_1x100.png) 50% top repeat-x; color: #363636; }
+.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #f9dd34; background: #ffef8f /* @embed */ url("images/ui-bg_highlight-soft_25_ffef8f_1x100.png") 50% top repeat-x; color: #363636; }
.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; }
-.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #cd0a0a /* @embed */ url(images/ui-bg_flat_15_cd0a0a_40x100.png) 50% 50% repeat-x; color: #ffffff; }
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #cd0a0a /* @embed */ url("images/ui-bg_flat_15_cd0a0a_40x100.png") 50% 50% repeat-x; color: #ffffff; }
.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; }
.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; }
.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+.ui-state-disabled .ui-icon { filter:Alpha(Opacity=35); } /* For IE8 - See #6059 */
/* Icons
----------------------------------*/
/* states and images */
.ui-icon { width: 16px; height: 16px; }
-.ui-icon, .ui-widget-content .ui-icon, .ui-widget-header .ui-icon { /* @embed */ background-image: url(images/ui-icons_72a7cf_256x240.png); }
-.ui-state-default .ui-icon { /* @embed */ background-image: url(images/ui-icons_3d80b3_256x240.png); }
-.ui-state-hover .ui-icon, .ui-state-focus .ui-icon { /* @embed */ background-image: url(images/ui-icons_2694e8_256x240.png); }
-.ui-state-active .ui-icon { /* @embed */ background-image: url(images/ui-icons_666666_256x240.png); }
-.ui-state-highlight .ui-icon { /* @embed */ background-image: url(images/ui-icons_2e83ff_256x240.png); }
-.ui-state-error .ui-icon, .ui-state-error-text .ui-icon { /* @embed */ background-image: url(images/ui-icons_ffffff_256x240.png); }
+.ui-icon,
+.ui-widget-content .ui-icon,
+.ui-widget-header .ui-icon { /* @embed */ background-image: url("images/ui-icons_72a7cf_256x240.png"); }
+.ui-state-default .ui-icon { /* @embed */ background-image: url("images/ui-icons_3d80b3_256x240.png"); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon { /* @embed */ background-image: url("images/ui-icons_2694e8_256x240.png"); }
+.ui-state-active .ui-icon { /* @embed */ background-image: url("images/ui-icons_666666_256x240.png"); }
+.ui-state-highlight .ui-icon { /* @embed */ background-image: url("images/ui-icons_2e83ff_256x240.png"); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon { /* @embed */ background-image: url("images/ui-icons_ffffff_256x240.png"); }
/* positioning */
.ui-icon-carat-1-n { background-position: 0 0; }
@@ -176,8 +182,8 @@
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
-.ui-icon-radio-off { background-position: -96px -144px; }
-.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
@@ -231,16 +237,11 @@
----------------------------------*/
/* Corner radius */
-.ui-corner-tl { border-top-left-radius: 0; }
-.ui-corner-tr { border-top-right-radius: 0; }
-.ui-corner-bl { border-bottom-left-radius: 0; }
-.ui-corner-br { border-bottom-right-radius: 0; }
-.ui-corner-top { border-top-left-radius: 0; border-top-right-radius: 0; }
-.ui-corner-bottom { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
-.ui-corner-right { border-top-right-radius: 0; border-bottom-right-radius: 0; }
-.ui-corner-left { border-top-left-radius: 0; border-bottom-left-radius: 0; }
-.ui-corner-all { border-radius: 0; }
+.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; -khtml-border-top-left-radius: 3px; border-top-left-radius: 3px; }
+.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 3px; -webkit-border-top-right-radius: 3px; -khtml-border-top-right-radius: 3px; border-top-right-radius: 3px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; -khtml-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 3px; -webkit-border-bottom-right-radius: 3px; -khtml-border-bottom-right-radius: 3px; border-bottom-right-radius: 3px; }
/* Overlays */
-.ui-widget-overlay { background: #000000; opacity: .75;filter:Alpha(Opacity=75); }
-.ui-widget-shadow { margin: -7px 0 0 -7px; padding: 7px; /* @embed */ background: #000000 url(images/ui-bg_flat_70_000000_40x100.png) 50% 50% repeat-x; opacity: .20;filter:Alpha(Opacity=20); border-radius: 8px; }
+.ui-widget-overlay { /* @embed */ background: #000000 url("images/ui-bg_flat_100_000000_40x100.png") 50% 50% repeat-x; opacity: .5;filter:Alpha(Opacity=50); }
+.ui-widget-shadow { margin: -7px 0 0 -7px; padding: 7px; /* @embed */ background: #000000 url("images/ui-bg_flat_70_000000_40x100.png") 50% 50% repeat-x; opacity: .2;filter:Alpha(Opacity=20); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }
diff --git a/skins/Vector/skinStyles/jquery.ui/jquery.ui.tooltip.css b/skins/Vector/skinStyles/jquery.ui/jquery.ui.tooltip.css
new file mode 100644
index 00000000..88b0d02e
--- /dev/null
+++ b/skins/Vector/skinStyles/jquery.ui/jquery.ui.tooltip.css
@@ -0,0 +1,21 @@
+/*!
+ * jQuery UI Tooltip 1.9.2
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+.ui-tooltip {
+ padding: 8px;
+ position: absolute;
+ z-index: 9999;
+ max-width: 300px;
+ -webkit-box-shadow: 0 0 5px #aaa;
+ box-shadow: 0 0 5px #aaa;
+}
+/* Fades and background-images don't work well together in IE6, drop the image */
+* html .ui-tooltip {
+ background-image: none;
+}
+body .ui-tooltip { border-width: 2px; }
diff --git a/tests/.htaccess b/tests/.htaccess
new file mode 100644
index 00000000..3a428827
--- /dev/null
+++ b/tests/.htaccess
@@ -0,0 +1 @@
+Deny from all
diff --git a/tests/TestsAutoLoader.php b/tests/TestsAutoLoader.php
new file mode 100644
index 00000000..2e8fed44
--- /dev/null
+++ b/tests/TestsAutoLoader.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * AutoLoader for the testing suite.
+ *
+ * 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
+ * @ingroup Testing
+ */
+
+global $wgAutoloadClasses;
+$testDir = __DIR__;
+
+$wgAutoloadClasses += array(
+
+ # tests
+ 'DbTestPreviewer' => "$testDir/testHelpers.inc",
+ 'DbTestRecorder' => "$testDir/testHelpers.inc",
+ 'DelayedParserTest' => "$testDir/testHelpers.inc",
+ 'ParserTestResult' => "$testDir/parser/ParserTestResult.php",
+ 'TestFileIterator' => "$testDir/testHelpers.inc",
+ 'TestRecorder' => "$testDir/testHelpers.inc",
+ 'ITestRecorder' => "$testDir/testHelpers.inc",
+ 'DjVuSupport' => "$testDir/testHelpers.inc",
+ 'TidySupport' => "$testDir/testHelpers.inc",
+
+ # tests/phpunit
+ 'MediaWikiTestCase' => "$testDir/phpunit/MediaWikiTestCase.php",
+ 'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php",
+ 'MediaWikiLangTestCase' => "$testDir/phpunit/MediaWikiLangTestCase.php",
+ 'ResourceLoaderTestCase' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+ 'ResourceLoaderTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+ 'ResourceLoaderFileModuleTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+ 'ResourceLoaderWikiModuleTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+ 'TestUser' => "$testDir/phpunit/includes/TestUser.php",
+ 'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php",
+
+ # tests/phpunit/includes
+ 'BlockTest' => "$testDir/phpunit/includes/BlockTest.php",
+ 'RevisionStorageTest' => "$testDir/phpunit/includes/RevisionStorageTest.php",
+ 'WikiPageTest' => "$testDir/phpunit/includes/WikiPageTest.php",
+
+ # tests/phpunit/includes/api
+ 'ApiFormatTestBase' => "$testDir/phpunit/includes/api/format/ApiFormatTestBase.php",
+ 'ApiTestCase' => "$testDir/phpunit/includes/api/ApiTestCase.php",
+ 'ApiTestContext' => "$testDir/phpunit/includes/api/ApiTestContext.php",
+ 'MockApi' => "$testDir/phpunit/includes/api/MockApi.php",
+ 'MockApiQueryBase' => "$testDir/phpunit/includes/api/MockApiQueryBase.php",
+ 'UserWrapper' => "$testDir/phpunit/includes/api/UserWrapper.php",
+ 'RandomImageGenerator' => "$testDir/phpunit/includes/api/RandomImageGenerator.php",
+
+ # tests/phpunit/includes/changes
+ 'TestRecentChangesHelper' => "$testDir/phpunit/includes/changes/TestRecentChangesHelper.php",
+
+ # tests/phpunit/includes/content
+ 'DummyContentHandlerForTesting' => "$testDir/phpunit/includes/content/ContentHandlerTest.php",
+ 'DummyContentForTesting' => "$testDir/phpunit/includes/content/ContentHandlerTest.php",
+ 'ContentHandlerTest' => "$testDir/phpunit/includes/content/ContentHandlerTest.php",
+ 'JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php",
+ 'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php",
+ 'WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php",
+
+ # tests/phpunit/includes/db
+ 'ORMRowTest' => "$testDir/phpunit/includes/db/ORMRowTest.php",
+ 'ORMTableTest' => "$testDir/phpunit/includes/db/ORMTableTest.php",
+ 'PageORMTableForTesting' => "$testDir/phpunit/includes/db/ORMTableTest.php",
+ 'DatabaseTestHelper' => "$testDir/phpunit/includes/db/DatabaseTestHelper.php",
+
+ # tests/phpunit/includes/passwords
+ 'PasswordTestCase' => "$testDir/phpunit/includes/password/PasswordTestCase.php",
+
+ # tests/phpunit/languages
+ 'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php",
+
+ # tests/phpunit/includes/libs
+ 'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php",
+
+ # tests/phpunit/maintenance
+ 'DumpTestCase' => "$testDir/phpunit/maintenance/DumpTestCase.php",
+
+ # tests/phpunit/media
+ 'FakeDimensionFile' => "$testDir/phpunit/includes/media/FakeDimensionFile.php",
+ 'MediaWikiMediaTestCase' => "$testDir/phpunit/includes/media/MediaWikiMediaTestCase.php",
+
+ # tests/phpunit/mocks
+ 'MockFSFile' => "$testDir/phpunit/mocks/filebackend/MockFSFile.php",
+ 'MockFileBackend' => "$testDir/phpunit/mocks/filebackend/MockFileBackend.php",
+ 'MockBitmapHandler' => "$testDir/phpunit/mocks/media/MockBitmapHandler.php",
+ 'MockImageHandler' => "$testDir/phpunit/mocks/media/MockImageHandler.php",
+ 'MockSvgHandler' => "$testDir/phpunit/mocks/media/MockSvgHandler.php",
+ 'MockDjVuHandler' => "$testDir/phpunit/mocks/media/MockDjVuHandler.php",
+
+ # tests/parser
+ 'NewParserTest' => "$testDir/phpunit/includes/parser/NewParserTest.php",
+ 'MediaWikiParserTest' => "$testDir/phpunit/includes/parser/MediaWikiParserTest.php",
+ 'ParserTest' => "$testDir/parser/parserTest.inc",
+ 'ParserTestParserHook' => "$testDir/parser/parserTestsParserHook.php",
+
+ # tests/phpunit/includes/site
+ 'SiteTest' => "$testDir/phpunit/includes/site/SiteTest.php",
+ 'TestSites' => "$testDir/phpunit/includes/site/TestSites.php",
+);
diff --git a/tests/browser/Gemfile.lock b/tests/browser/Gemfile.lock
new file mode 100644
index 00000000..1ea4eb55
--- /dev/null
+++ b/tests/browser/Gemfile.lock
@@ -0,0 +1,82 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ builder (3.2.2)
+ childprocess (0.5.3)
+ ffi (~> 1.0, >= 1.0.11)
+ cucumber (1.3.16)
+ builder (>= 2.1.2)
+ diff-lcs (>= 1.1.3)
+ gherkin (~> 2.12)
+ multi_json (>= 1.7.5, < 2.0)
+ multi_test (>= 0.1.1)
+ data_magic (0.19)
+ faker (>= 1.1.2)
+ yml_reader (>= 0.3)
+ diff-lcs (1.2.5)
+ domain_name (0.5.20)
+ unf (>= 0.0.5, < 1.0.0)
+ faker (1.4.3)
+ i18n (~> 0.5)
+ faraday (0.9.0)
+ multipart-post (>= 1.2, < 3)
+ faraday-cookie_jar (0.0.6)
+ faraday (>= 0.7.4)
+ http-cookie (~> 1.0.0)
+ ffi (1.9.3)
+ gherkin (2.12.2)
+ multi_json (~> 1.3)
+ headless (1.0.2)
+ http-cookie (1.0.2)
+ domain_name (~> 0.5)
+ i18n (0.6.11)
+ json (1.8.1)
+ mediawiki_api (0.2.1)
+ faraday (~> 0.9, >= 0.9.0)
+ faraday-cookie_jar (~> 0.0, >= 0.0.6)
+ mediawiki_selenium (0.3.2)
+ cucumber (~> 1.3, >= 1.3.10)
+ headless (~> 1.0, >= 1.0.1)
+ json (~> 1.8, >= 1.8.1)
+ mediawiki_api (~> 0.2, >= 0.2.1)
+ page-object (~> 1.0)
+ rest-client (~> 1.6, >= 1.6.7)
+ rspec-expectations (~> 2.14, >= 2.14.4)
+ syntax (~> 1.2, >= 1.2.0)
+ mime-types (2.3)
+ multi_json (1.10.1)
+ multi_test (0.1.1)
+ multipart-post (2.0.0)
+ netrc (0.7.7)
+ page-object (1.0.2)
+ page_navigation (>= 0.9)
+ selenium-webdriver (>= 2.42.0)
+ watir-webdriver (>= 0.6.9)
+ page_navigation (0.9)
+ data_magic (>= 0.14)
+ rest-client (1.7.2)
+ mime-types (>= 1.16, < 3.0)
+ netrc (~> 0.7)
+ rspec-expectations (2.99.2)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rubyzip (1.1.6)
+ selenium-webdriver (2.42.0)
+ childprocess (>= 0.5.0)
+ multi_json (~> 1.0)
+ rubyzip (~> 1.0)
+ websocket (~> 1.0.4)
+ syntax (1.2.0)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.6)
+ watir-webdriver (0.6.10)
+ selenium-webdriver (>= 2.18.0)
+ websocket (1.0.7)
+ yml_reader (0.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ mediawiki_api
+ mediawiki_selenium
diff --git a/tests/browser/README.mediawiki b/tests/browser/README.mediawiki
new file mode 100644
index 00000000..22657627
--- /dev/null
+++ b/tests/browser/README.mediawiki
@@ -0,0 +1,64 @@
+Purpose:
+
+The purpose of these tests is to validate that a newly installed (or updated, or hacked, or whatever) mediawiki instance presents to the user a set of expected features, regardless of what language the wiki is in, or where it is installed, or what extensions it might have.
+
+The tests are based on the basic definition of a wiki, a website where anyone
+
+* can read a page
+* can create a page
+* can edit a page
+* can link one page to another page
+
+Install:
+
+Ruby 1.9.3 or higher is required
+Firefox browser is required
+::
+ cd /tests/browser
+ gem update --system
+ gem install bundler
+ bundle install
+
+Run the tests:
+
+Edit the environment_variables file with appropriate values for your wiki
+$source environment_variables (example shown in bash shell)
+
+bundle exec cucumber features/
+
+Note that the acceptance tests will create three pages in your wiki entitled "Editing Test Page", "Link Source Test Page", and "Link Target Test Page". These pages may be deleted at any time. If you wish to re-run the tests at any time, these test pages will be re-created or reset to their original contents at the time that the tests run.
+
+For more information about running Selenium tests please see
+https://github.com/wikimedia/mediawiki-selenium
+
+Details:
+
+create_account.feature
+* Checks three different ways to arrive on page allowing the user to create an account
+
+create_and_follow_wiki_link.feature:
+* uses the mediawiki API to create a link target page
+* uses the mediawiki API to create a link source page
+* navigates a browser to the link source page
+* clicks the link in that page to the link target page
+* validates that the browser has in fact followed the link to the target page correctly
+
+edit_page.feature:
+* uses the mediawiki API to create an editable page on the wiki
+* navigates a browser to the page
+* clicks the Edit button to invoke the basic editor
+* edits the page with a particular string containing a static part and also a quasi-unique random part
+* saves the edited page
+* checks that the saved page contains the particular string with which the page was edited
+
+main_page.feature:
+* navigates a browser to the default landing page of the wiki
+* checks for the View History link on the landing page
+* checks for the full set of of sidebar links that should exist on every mediawiki wiki
+
+view_history.feature
+* similar to edit_page.feature but checks for an older version of the edited page
+
+Notes:
+
+Tested on beta labs hewiki, dewiki, enwiki, and on a local installation of mediawiki \ No newline at end of file
diff --git a/tests/browser/environment_variables b/tests/browser/environment_variables
new file mode 100644
index 00000000..25c45775
--- /dev/null
+++ b/tests/browser/environment_variables
@@ -0,0 +1,5 @@
+export MEDIAWIKI_URL=http://localhost/wiki/
+export MEDIAWIKI_API_URL=http://localhost/w/api.php
+export MEDIAWIKI_USER=Selenium_user
+export MEDIAWIKI_PASSWORD=Selenium_password
+export BROWSER=firefox
diff --git a/tests/browser/features/create_account.feature b/tests/browser/features/create_account.feature
new file mode 100644
index 00000000..0b4e83a5
--- /dev/null
+++ b/tests/browser/features/create_account.feature
@@ -0,0 +1,12 @@
+@chrome @clean @firefox @phantomjs
+Feature: Create account
+
+ Scenario Outline: Go to Create account page
+ Given I go to Create account page at <path>
+ Then form has Create account button
+
+ Examples:
+ | path |
+ | Special:CreateAccount |
+ | Special:UserLogin/signup |
+ | Special:UserLogin?type=signup |
diff --git a/tests/browser/features/create_and_follow_wiki_link.feature b/tests/browser/features/create_and_follow_wiki_link.feature
new file mode 100644
index 00000000..a0aa624e
--- /dev/null
+++ b/tests/browser/features/create_and_follow_wiki_link.feature
@@ -0,0 +1,9 @@
+@chrome @clean @firefox @login @phantomjs
+Feature: Create Page With Wiki Link
+
+ Scenario: Create Page With Wiki Link
+ Given I create page "Link Target Test Page" with content "Link Target Test Page"
+ And I go to the "Link Source Test Page" page with content "This is a [[Link Target Test Page|link to the test target page]] right here."
+ When I click the Link Target link
+ Then I should be on the Link Target Test Page
+ And the page content should contain "Link Target Test Page"
diff --git a/tests/browser/features/edit_page.feature b/tests/browser/features/edit_page.feature
new file mode 100644
index 00000000..b905795e
--- /dev/null
+++ b/tests/browser/features/edit_page.feature
@@ -0,0 +1,11 @@
+@chrome @clean @firefox @login @phantomjs
+Feature: Edit Page
+
+ Scenario: Create and edit page
+ Given I go to the "Editing Test Page" page with content "This is a page to test editing"
+ When I click Edit
+ And I edit the page with "Edited and a random string"
+ And I click Preview
+ And I click Show Changes
+ And I save the edit
+ Then the edited page content should contain "Edited and a random string"
diff --git a/tests/browser/features/file.feature b/tests/browser/features/file.feature
new file mode 100644
index 00000000..0bd36ed6
--- /dev/null
+++ b/tests/browser/features/file.feature
@@ -0,0 +1,23 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+@chrome @clean @firefox @internet_explorer_6 @internet_explorer_7 @internet_explorer_8 @internet_explorer_9 @internet_explorer_10 @phantomjs
+Feature: File
+
+ Scenario: Anonymous goes to file that does not exist
+ Given I am at file that does not exist
+ Then page should show that no such file exists
+
+ @login
+ Scenario: Logged-in user goes to file that does not exist
+ Given I am logged in
+ And I am at file that does not exist
+ Then page should show that no such file exists
diff --git a/tests/browser/features/login.feature b/tests/browser/features/login.feature
new file mode 100644
index 00000000..c34d23d3
--- /dev/null
+++ b/tests/browser/features/login.feature
@@ -0,0 +1,42 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+@chrome @clean @firefox @internet_explorer_6 @internet_explorer_7 @internet_explorer_8 @internet_explorer_9 @internet_explorer_10 @phantomjs
+Feature: Log in
+
+ Background:
+ Given I am at Log in page
+
+ Scenario: Go to Log in page
+ Then Username element should be there
+ And Password element should be there
+ And Log in element should be there
+
+ Scenario: Log in without entering credentials
+ When I log in without entering credentials
+ Then error box should be visible
+
+ Scenario: Log in without entering password
+ When I log in without entering password
+ Then error box should be visible
+
+ Scenario: Log in with incorrect username
+ When I log in with incorrect username
+ Then error box should be visible
+
+ Scenario: Log in with incorrect password
+ When I log in with incorrect password
+ Then error box should be visible
+
+ @login
+ Scenario: Log in with valid credentials
+ When I am logged in
+ Then error box should not be visible
diff --git a/tests/browser/features/main_page_links.feature b/tests/browser/features/main_page_links.feature
new file mode 100644
index 00000000..3613c828
--- /dev/null
+++ b/tests/browser/features/main_page_links.feature
@@ -0,0 +1,19 @@
+@chrome @clean @firefox @phantomjs
+Feature: Main Page View History Links
+
+ Background:
+ Given I open the main wiki URL
+
+ Scenario: Main Page View History links exist
+ Then I should see a link for View History
+
+ Scenario: Main Page Sidebar Links
+ Then I should see a link for Recent changes
+ And I should see a link for Random page
+ And I should see a link for Help
+ And I should see a link for What links here
+ And I should see a link for Related changes
+ And I should see a link for Special pages
+ And I should see a link for Printable version
+ And I should see a link for Permanent link
+ And I should see a link for Page information
diff --git a/tests/browser/features/preferences.feature b/tests/browser/features/preferences.feature
new file mode 100644
index 00000000..9e3abfde
--- /dev/null
+++ b/tests/browser/features/preferences.feature
@@ -0,0 +1,60 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+@chrome @clean @firefox @internet_explorer_6 @internet_explorer_7 @internet_explorer_8 @internet_explorer_9 @internet_explorer_10 @login @phantomjs
+Feature: Preferences
+
+ Scenario: Preferences Appearance
+ Given I am logged in
+ When I navigate to Preferences
+ And I click Appearance
+ Then I can select skins
+ And I can select image size
+ And I can select thumbnail size
+ And I can select Threshold for stub link
+ And I can select underline preferences
+ And I have advanced options checkboxes
+ And I can click Save
+ And I can restore default settings
+ And I can select date format
+ And I can see time offset section
+ And I can see local time
+ And I can select my time zone
+
+
+ Scenario: Preferences Editing
+ Given I am logged in
+ When I navigate to Preferences
+ And I click Editing
+ Then I can select edit area font style
+ And I can select section editing via edit links
+ And I can select section editing by right clicking
+ And I can select section editing by double clicking
+ And I can select to prompt me when entering a blank edit summary
+ And I can select to warn me when I leave an edit page with unsaved changes
+ And I can select show edit toolbar
+ And I can select show preview on first edit
+ And I can select show preview before edit box
+ And I can select live preview
+
+
+ Scenario: Preferences User profile
+ Given I am logged in
+ When I navigate to Preferences
+ And I click User profile
+ Then I can see my Basic informations
+ And I can change my language
+ And I can change my gender
+ And I can see my signature
+ And I can change my signature
+ And I can see my email
+ And I can click Save
+ And I can restore default settings
diff --git a/tests/browser/features/step_definitions/create_account_steps.rb b/tests/browser/features/step_definitions/create_account_steps.rb
new file mode 100644
index 00000000..7fa29843
--- /dev/null
+++ b/tests/browser/features/step_definitions/create_account_steps.rb
@@ -0,0 +1,18 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+Given(/^I go to Create account page at (.+)$/) do |path|
+ visit(CreateAccountPage, :using_params => {:page_title => path})
+end
+
+Then(/^form has Create account button$/) do
+ on(CreateAccountPage).create_account_element.should exist
+end
diff --git a/tests/browser/features/step_definitions/create_and_follow_wiki_link_steps.rb b/tests/browser/features/step_definitions/create_and_follow_wiki_link_steps.rb
new file mode 100644
index 00000000..ba41f7fb
--- /dev/null
+++ b/tests/browser/features/step_definitions/create_and_follow_wiki_link_steps.rb
@@ -0,0 +1,28 @@
+Given(/^I go to the "(.+)" page with content "(.+)"$/) do |page_title, page_content|
+ @wikitext = page_content
+ on(APIPage).create page_title, page_content
+ step "I am on the #{page_title} page"
+end
+
+Given(/^I am on the (.+) page$/) do |article|
+ article = article.gsub(/ /, '_')
+ visit(ZtargetPage, :using_params => {:article_name => article})
+end
+
+Given(/^I create page "(.*?)" with content "(.*?)"$/) do |page_title, page_content|
+ on(APIPage).create page_title, page_content
+end
+
+
+When(/^I click the Link Target link$/) do
+ on(ZtargetPage).link_target_page_link
+end
+
+Then(/^I should be on the Link Target Test Page$/) do
+ @browser.url.should match /Link_Target_Test_Page/
+end
+
+Then(/^the page content should contain "(.*?)"$/) do |content|
+ on(ZtargetPage).page_content.should match content
+end
+
diff --git a/tests/browser/features/step_definitions/edit_page_steps.rb b/tests/browser/features/step_definitions/edit_page_steps.rb
new file mode 100644
index 00000000..5ab02bec
--- /dev/null
+++ b/tests/browser/features/step_definitions/edit_page_steps.rb
@@ -0,0 +1,24 @@
+When(/^I click Edit$/) do
+ on(MainPage).edit_link
+end
+
+When(/^I click Preview$/) do
+ on(EditPage).preview_button
+end
+
+When(/^I click Show Changes$/) do
+ on(EditPage).show_changes_button
+end
+
+When(/^I edit the page with "(.*?)"$/) do |edit_content|
+ on(EditPage).edit_page_content_element.send_keys(edit_content + @random_string)
+end
+
+When(/^I save the edit$/) do
+ on(EditPage).save_button
+end
+
+Then(/^the edited page content should contain "(.*?)"$/) do |content|
+ on(MainPage).page_content.should match(content + @random_string)
+end
+
diff --git a/tests/browser/features/step_definitions/file_steps.rb b/tests/browser/features/step_definitions/file_steps.rb
new file mode 100644
index 00000000..a2ed1bfc
--- /dev/null
+++ b/tests/browser/features/step_definitions/file_steps.rb
@@ -0,0 +1,18 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+Given(/^I am at file that does not exist$/) do
+ visit(FileDoesNotExistPage, using_params: {page_name: @random_string})
+end
+
+Then(/^page should show that no such file exists$/) do
+ on(FileDoesNotExistPage).file_does_not_exist_message_element.should be_visible
+end
diff --git a/tests/browser/features/step_definitions/login_steps.rb b/tests/browser/features/step_definitions/login_steps.rb
new file mode 100644
index 00000000..b654b2d3
--- /dev/null
+++ b/tests/browser/features/step_definitions/login_steps.rb
@@ -0,0 +1,65 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+Given(/^I am at Log in page$/) do
+ visit LoginPage
+end
+
+When(/^I log in with incorrect password$/) do
+ on(LoginPage).login_with(ENV["MEDIAWIKI_USER"], "incorrect password", false)
+end
+
+When(/^I log in with incorrect username$/) do
+ on(LoginPage).login_with("incorrect username", ENV["MEDIAWIKI_PASSWORD"], false)
+end
+
+When(/^I log in without entering credentials$/) do
+ on(LoginPage).login_with("", "", false)
+end
+
+When(/^I log in without entering password$/) do
+ on(LoginPage).login_with(ENV["MEDIAWIKI_USER"], "", false)
+end
+
+Then(/^error box should be visible$/) do
+ on(LoginErrorPage).error_box_element.should be_visible
+end
+
+Then(/^error box should not be visible$/) do
+ on(LoginErrorPage).error_box_element.should_not be_visible
+end
+
+Then(/^feedback should be (.+)$/) do |feedback|
+ on(LoginPage) do |page|
+ page.feedback_element.when_present.click
+ page.feedback.should match Regexp.escape(feedback)
+ end
+end
+
+Then(/^Log in element should be there$/) do
+ on(LoginPage).login_element.should exist
+end
+
+Then(/^main page should open$/) do
+ @browser.url.should == on(MainPage).class.url
+end
+
+Then(/^Password element should be there$/) do
+ on(LoginPage).password_element.should exist
+end
+
+Then(/^there should be a link to (.+)$/) do |text|
+ on(LoginPage).username_displayed_element.when_present.text.should == text
+end
+
+Then(/^Username element should be there$/) do
+ on(LoginPage).username_element.should exist
+end
diff --git a/tests/browser/features/step_definitions/main_page_links_steps.rb b/tests/browser/features/step_definitions/main_page_links_steps.rb
new file mode 100644
index 00000000..c76fd2ba
--- /dev/null
+++ b/tests/browser/features/step_definitions/main_page_links_steps.rb
@@ -0,0 +1,47 @@
+Given(/^I open the main wiki URL$/) do
+ visit(MainPage)
+end
+
+Then(/^I should see a link for View History$/) do
+ on(MainPage).view_history_link_element.should be_visible
+end
+
+Then(/^I should see a link for Edit$/) do
+ on(MainPage).edit_link_element.should be_visible
+end
+
+Then(/^I should see a link for Recent changes$/) do
+ on(MainPage).recent_changes_link_element.should be_visible
+end
+
+Then(/^I should see a link for Random page$/) do
+ on(MainPage).random_page_link_element.should be_visible
+end
+
+Then(/^I should see a link for Help$/) do
+ on(MainPage).help_link_element.should be_visible
+end
+
+Then(/^I should see a link for What links here$/) do
+ on(MainPage).what_links_here_link_element.should be_visible
+end
+
+Then(/^I should see a link for Related changes$/) do
+ on(MainPage).related_changes_link_element.should be_visible
+end
+
+Then(/^I should see a link for Special pages$/) do
+ on(MainPage).special_pages_link_element.should be_visible
+end
+
+Then(/^I should see a link for Printable version$/) do
+ on(MainPage).printable_version_link_element.should be_visible
+end
+
+Then(/^I should see a link for Permanent link$/) do
+ on(MainPage).permanent_link_link_element.should be_visible
+end
+
+Then(/^I should see a link for Page information$/) do
+ on(MainPage).page_information_link_element.should be_visible
+end
diff --git a/tests/browser/features/step_definitions/preferences_appearance_steps.rb b/tests/browser/features/step_definitions/preferences_appearance_steps.rb
new file mode 100644
index 00000000..0046af69
--- /dev/null
+++ b/tests/browser/features/step_definitions/preferences_appearance_steps.rb
@@ -0,0 +1,85 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+When(/^I click Appearance$/) do
+ visit(PreferencesPage).appearance_link_element.when_present.click
+end
+
+When(/^I navigate to Preferences$/) do
+ visit(PreferencesPage)
+end
+
+Then(/^I can click Save$/) do
+ on(PreferencesPage).save_button_element.should exist
+end
+
+Then(/^I can restore default settings$/) do
+ on(PreferencesAppearancePage).restore_default_link_element.should exist
+end
+
+Then(/^I can see local time$/) do
+ on(PreferencesAppearancePage).local_time_span_element.should exist
+end
+
+Then(/^I can see time offset section$/) do
+ on(PreferencesAppearancePage).time_offset_table_element.should be_visible
+end
+
+Then(/^I can select date format$/) do
+ on(PreferencesAppearancePage) do |page|
+ page.no_preference_radio_element.should exist
+ page.mo_day_year_radio_element.should exist
+ page.day_mo_year_radio_element.should exist
+ page.year_mo_day_radio_element.should exist
+ page.iso_8601_radio_element.should exist
+ end
+end
+
+Then(/^I can select image size$/) do
+ on(PreferencesAppearancePage).size_select_element.should exist
+end
+
+Then(/^I can select my time zone$/) do
+ on(PreferencesAppearancePage) do |page|
+ page.time_offset_select_element.should exist
+ page.other_offset_element.should exist
+ end
+end
+
+Then(/^I can select skins$/) do
+ on(PreferencesAppearancePage) do |page|
+ page.cologne_blue_element.should exist
+ page.modern_element.should exist
+ page.monobook_element.should exist
+ page.vector_element.should exist
+ end
+end
+
+Then(/^I can select Threshold for stub link$/) do
+ on(PreferencesAppearancePage).threshold_select_element.should exist
+end
+
+Then(/^I can select thumbnail size$/) do
+ on(PreferencesAppearancePage).thumb_select_element.should exist
+end
+
+Then(/^I can select underline preferences$/) do
+ on(PreferencesAppearancePage).underline_select_element.should exist
+end
+
+Then(/^I have advanced options checkboxes$/) do
+ on(PreferencesAppearancePage) do |page|
+ page.hidden_categories_check_element.should exist
+ page.auto_number_check_element.should exist
+ end
+end
+
+
diff --git a/tests/browser/features/step_definitions/preferences_editing_steps.rb b/tests/browser/features/step_definitions/preferences_editing_steps.rb
new file mode 100644
index 00000000..ad29a745
--- /dev/null
+++ b/tests/browser/features/step_definitions/preferences_editing_steps.rb
@@ -0,0 +1,54 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+When(/^I click Editing$/) do
+ visit(PreferencesPage).editing_link_element.when_present.click
+end
+
+Then(/^I can select edit area font style$/) do
+ on(PreferencesEditingPage).edit_area_font_style_select_element.when_present.should exist
+end
+
+Then(/^I can select live preview$/) do
+ on(PreferencesEditingPage).live_preview_check_element.when_present.should exist
+end
+
+Then(/^I can select section editing by double clicking$/) do
+ on(PreferencesEditingPage).edit_section_double_click_check_element.when_present.should exist
+end
+
+Then(/^I can select section editing by right clicking$/) do
+ on(PreferencesEditingPage).edit_section_right_click_check_element.when_present.should exist
+end
+
+Then(/^I can select section editing via edit links$/) do
+ on(PreferencesEditingPage).edit_section_edit_link_element.when_present.should exist
+end
+
+Then(/^I can select show edit toolbar$/) do
+ on(PreferencesEditingPage).show_edit_toolbar_check_element.when_present.should exist
+end
+
+Then(/^I can select show preview before edit box$/) do
+ on(PreferencesEditingPage).preview_on_top_check_element.when_present.should exist
+end
+
+Then(/^I can select show preview on first edit$/) do
+ on(PreferencesEditingPage).preview_on_first_check_element.when_present.should exist
+end
+
+Then(/^I can select to prompt me when entering a blank edit summary$/) do
+ on(PreferencesEditingPage).forced_edit_summary_check_element.when_present.should exist
+end
+
+Then(/^I can select to warn me when I leave an edit page with unsaved changes$/) do
+ on(PreferencesEditingPage).unsaved_changes_check_element.when_present.should exist
+end
diff --git a/tests/browser/features/step_definitions/preferences_user_profile_steps.rb b/tests/browser/features/step_definitions/preferences_user_profile_steps.rb
new file mode 100644
index 00000000..529af66d
--- /dev/null
+++ b/tests/browser/features/step_definitions/preferences_user_profile_steps.rb
@@ -0,0 +1,43 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+When(/^I click User profile$/) do
+ visit(PreferencesPage).user_profile_link_element.when_present.click
+end
+
+Then(/^I can change my gender$/) do
+ on(PreferencesUserProfilePage) do |page|
+ page.gender_undefined_radio_element.should exist
+ page.gender_male_radio_element.should exist
+ page.gender_female_radio_element.should exist
+ end
+end
+
+Then(/^I can change my language$/) do
+ on(PreferencesUserProfilePage).lang_select_element.should exist
+end
+
+Then(/^I can change my signature$/) do
+ on(PreferencesUserProfilePage).signature_field_element.should exist
+end
+
+Then(/^I can see my Basic informations$/) do
+ on(PreferencesUserProfilePage).basic_info_table_element.should exist
+end
+
+Then(/^I can see my email$/) do
+ on(PreferencesUserProfilePage).email_table_element.should exist
+end
+
+Then(/^I can see my signature$/) do
+ on(PreferencesUserProfilePage).signature_table_element.should exist
+end
+
diff --git a/tests/browser/features/step_definitions/view_history_steps.rb b/tests/browser/features/step_definitions/view_history_steps.rb
new file mode 100644
index 00000000..1ecc0085
--- /dev/null
+++ b/tests/browser/features/step_definitions/view_history_steps.rb
@@ -0,0 +1,8 @@
+When(/^I click View History$/) do
+ on(ViewHistoryPage).view_history_link
+end
+
+Then(/^I should see a link to a previous version of the page$/) do
+ on(ViewHistoryPage).old_version_link_element.should be_visible
+end
+
diff --git a/tests/browser/features/support/env.rb b/tests/browser/features/support/env.rb
new file mode 100644
index 00000000..7c122366
--- /dev/null
+++ b/tests/browser/features/support/env.rb
@@ -0,0 +1,2 @@
+require "mediawiki_api"
+require "mediawiki_selenium"
diff --git a/tests/browser/features/support/hooks.rb b/tests/browser/features/support/hooks.rb
new file mode 100644
index 00000000..85309f39
--- /dev/null
+++ b/tests/browser/features/support/hooks.rb
@@ -0,0 +1,2 @@
+# Needed for cucumber --dry-run -f stepdefs
+require 'page-object'
diff --git a/tests/browser/features/support/modules/url_module.rb b/tests/browser/features/support/modules/url_module.rb
new file mode 100644
index 00000000..6c329e87
--- /dev/null
+++ b/tests/browser/features/support/modules/url_module.rb
@@ -0,0 +1,10 @@
+module URL
+ def self.url(name)
+ if ENV["MEDIAWIKI_URL"]
+ mediawiki_url = ENV["MEDIAWIKI_URL"]
+ else
+ mediawiki_url = "http://127.0.0.1:80/w/index.php"
+ end
+ "#{mediawiki_url}#{name}"
+ end
+end
diff --git a/tests/browser/features/support/pages/create_account_page.rb b/tests/browser/features/support/pages/create_account_page.rb
new file mode 100644
index 00000000..380bccbc
--- /dev/null
+++ b/tests/browser/features/support/pages/create_account_page.rb
@@ -0,0 +1,19 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+class CreateAccountPage
+ include PageObject
+
+ include URL
+ page_url URL.url("<%=params[:page_title]%>")
+
+ button(:create_account, id: "wpCreateaccount")
+end
diff --git a/tests/browser/features/support/pages/edit_page.rb b/tests/browser/features/support/pages/edit_page.rb
new file mode 100644
index 00000000..b619c342
--- /dev/null
+++ b/tests/browser/features/support/pages/edit_page.rb
@@ -0,0 +1,8 @@
+class EditPage
+ include PageObject
+
+ text_area(:edit_page_content, id: "wpTextbox1")
+ button(:preview_button, id: "wpPreview")
+ button(:show_changes_button, id: "wpDiff")
+ button(:save_button, id: "wpSave")
+end \ No newline at end of file
diff --git a/tests/browser/features/support/pages/file_does_not_exist_page.rb b/tests/browser/features/support/pages/file_does_not_exist_page.rb
new file mode 100644
index 00000000..c8491f3b
--- /dev/null
+++ b/tests/browser/features/support/pages/file_does_not_exist_page.rb
@@ -0,0 +1,19 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+class FileDoesNotExistPage
+ include PageObject
+
+ include URL
+ page_url URL.url("File:<%=params[:page_name]%>")
+
+ div(:file_does_not_exist_message, id: "mw-imagepage-nofile")
+end
diff --git a/tests/browser/features/support/pages/login_error_page.rb b/tests/browser/features/support/pages/login_error_page.rb
new file mode 100644
index 00000000..4fc9ca7f
--- /dev/null
+++ b/tests/browser/features/support/pages/login_error_page.rb
@@ -0,0 +1,5 @@
+class LoginErrorPage
+ include PageObject
+
+ div(:error_box, class: "errorbox")
+end \ No newline at end of file
diff --git a/tests/browser/features/support/pages/main_page.rb b/tests/browser/features/support/pages/main_page.rb
new file mode 100644
index 00000000..7d96c2b2
--- /dev/null
+++ b/tests/browser/features/support/pages/main_page.rb
@@ -0,0 +1,19 @@
+class MainPage
+ include PageObject
+
+ include URL
+ page_url URL.url("")
+
+ a(:edit_link, href: /action=edit/)
+ li(:help_link, id: "n-help")
+ div(:page_content, id: "content")
+ li(:page_information_link, id: "t-info")
+ li(:permanent_link_link, id: "t-permalink")
+ a(:printable_version_link, href: /printable=yes/)
+ li(:random_page_link, id: "n-randompage")
+ li(:recent_changes_link, id: "n-recentchanges")
+ li(:related_changes_link, id: "t-recentchangeslinked")
+ li(:special_pages_link, id: "t-specialpages")
+ a(:view_history_link, href: /action=history/)
+ li(:what_links_here_link, id: "t-whatlinkshere")
+end \ No newline at end of file
diff --git a/tests/browser/features/support/pages/preferences_appearance_page.rb b/tests/browser/features/support/pages/preferences_appearance_page.rb
new file mode 100644
index 00000000..c24e3862
--- /dev/null
+++ b/tests/browser/features/support/pages/preferences_appearance_page.rb
@@ -0,0 +1,41 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+class PreferencesAppearancePage
+ include PageObject
+
+ include URL
+ page_url URL.url("Special:Preferences#mw-prefsection-rendering")
+
+ checkbox(:auto_number_check, id: "mw-input-wpnumberheadings")
+ radio_button(:cologne_blue, id: "mw-input-wpskin-cologneblue")
+ radio_button(:day_mo_year_radio, id: "mw-input-wpdate-dmy")
+ checkbox(:dont_show_aft_check, id: "mw-input-wparticlefeedback-disable")
+ checkbox(:exclude_from_experiments_check, id: "mw-input-wpvector-noexperiments")
+ checkbox(:hidden_categories_check, id: "mw-input-wpshowhiddencats")
+ radio_button(:iso_8601_radio, id: "mw-input-wpdate-ISO_8601")
+ span(:local_time_span, id: "wpLocalTime")
+ radio_button(:mo_day_year_radio, id: "mw-input-wpdate-mdy")
+ radio_button(:modern, id: "mw-input-wpskin-modern")
+ radio_button(:monobook, id: "mw-input-wpskin-monobook")
+ radio_button(:no_preference_radio, id: "mw-input-wpdate-default")
+ text_field(:other_offset, id: "mw-input-wptimecorrection-other")
+ a(:restore_default_link, href: /reset/)
+ select_list(:size_select, id: "mw-input-wpimagesize")
+ select_list(:threshold_select, id: "mw-input-wpstubthreshold")
+ select_list(:time_offset_select, id: "mw-input-wptimecorrection")
+ table(:time_offset_table, id: "mw-htmlform-timeoffset")
+ select_list(:thumb_select, id: "mw-input-wpthumbsize")
+ select_list(:underline_select, id: "mw-input-wpunderline")
+ radio_button(:vector, id: "mw-input-wpskin-vector")
+ radio_button(:year_mo_day_radio, id: "mw-input-wpdate-ymd")
+end
+
diff --git a/tests/browser/features/support/pages/preferences_editing_page.rb b/tests/browser/features/support/pages/preferences_editing_page.rb
new file mode 100644
index 00000000..aed9c41d
--- /dev/null
+++ b/tests/browser/features/support/pages/preferences_editing_page.rb
@@ -0,0 +1,28 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+class PreferencesEditingPage
+ include PageObject
+
+ include URL
+ page_url URL.url("Special:Preferences#mw-prefsection-rendering")
+
+ select_list(:edit_area_font_style_select, id: "mw-input-wpeditfont")
+ checkbox(:edit_section_double_click_check, id: "mw-input-wpeditondblclick")
+ checkbox(:edit_section_edit_link, id: "mw-input-wpeditsectiononrightclick")
+ checkbox(:edit_section_right_click_check, id: "mw-input-wpeditsectiononrightclick")
+ checkbox(:forced_edit_summary_check, id: "mw-input-wpforceeditsummary")
+ checkbox(:live_preview_check, id: "mw-input-wpuselivepreview")
+ checkbox(:preview_on_first_check, id: "mw-input-wppreviewonfirst")
+ checkbox(:preview_on_top_check, id: "mw-input-wppreviewontop")
+ checkbox(:show_edit_toolbar_check, id: "mw-input-wpshowtoolbar")
+ checkbox(:unsaved_changes_check, id: "mw-input-wpuseeditwarning")
+end
diff --git a/tests/browser/features/support/pages/preferences_page.rb b/tests/browser/features/support/pages/preferences_page.rb
new file mode 100644
index 00000000..919ba27f
--- /dev/null
+++ b/tests/browser/features/support/pages/preferences_page.rb
@@ -0,0 +1,22 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+class PreferencesPage
+ include PageObject
+
+ include URL
+ page_url URL.url("Special:Preferences")
+
+ a(:appearance_link, id: "preftab-rendering")
+ a(:editing_link, id: "preftab-editing")
+ a(:user_profile_link, id: "preftab-personal")
+ button(:save_button, id: "prefcontrol")
+end
diff --git a/tests/browser/features/support/pages/preferences_user_profile_page.rb b/tests/browser/features/support/pages/preferences_user_profile_page.rb
new file mode 100644
index 00000000..28e10b97
--- /dev/null
+++ b/tests/browser/features/support/pages/preferences_user_profile_page.rb
@@ -0,0 +1,28 @@
+#
+# This file is subject to the license terms in the LICENSE file found in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/LICENSE. No part of
+# qa-browsertests, including this file, may be copied, modified, propagated, or
+# distributed except according to the terms contained in the LICENSE file.
+#
+# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the
+# qa-browsertests top-level directory and at
+# https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
+#
+class PreferencesUserProfilePage
+ include PageObject
+
+ include URL
+ page_url URL.url("Special:Preferences#mw-prefsection-personal")
+
+ table(:basic_info_table, id: "mw-htmlform-info")
+ link(:change_password_link, text: "Change password")
+ table(:email_table, id: "mw-htmlform-email")
+ radio_button(:gender_female_radio, id: "mw-input-wpgender-male")
+ radio_button(:gender_male_radio, id: "mw-input-wpgender-female")
+ radio_button(:gender_undefined_radio, id: "mw-input-wpgender-unknown")
+ select_list(:lang_select, id: "mw-input-wplanguage")
+ checkbox(:remember_password_check, id: "mw-input-wprememberpassword")
+ text_field(:signature_field, id: "mw-input-wpnickname")
+ table(:signature_table, id: "mw-htmlform-signature")
+end
diff --git a/tests/browser/features/support/pages/view_history_page.rb b/tests/browser/features/support/pages/view_history_page.rb
new file mode 100644
index 00000000..66895986
--- /dev/null
+++ b/tests/browser/features/support/pages/view_history_page.rb
@@ -0,0 +1,7 @@
+class ViewHistoryPage
+ include PageObject
+
+ a(:view_history_link, href: /action=history/)
+ a(:old_version_link, href: /oldid=/)
+
+end \ No newline at end of file
diff --git a/tests/browser/features/support/pages/ztargetpage.rb b/tests/browser/features/support/pages/ztargetpage.rb
new file mode 100644
index 00000000..c1f46eca
--- /dev/null
+++ b/tests/browser/features/support/pages/ztargetpage.rb
@@ -0,0 +1,7 @@
+class ZtargetPage < MainPage
+ include URL
+ page_url URL.url("<%=params[:article_name]%>")
+ include PageObject
+
+ a(:link_target_page_link, text: "link to the test target page")
+end \ No newline at end of file
diff --git a/tests/browser/features/view_history.feature b/tests/browser/features/view_history.feature
new file mode 100644
index 00000000..ba61ebda
--- /dev/null
+++ b/tests/browser/features/view_history.feature
@@ -0,0 +1,11 @@
+@chrome @clean @firefox @phantomjs
+Feature: View History
+
+ Scenario: Edit page and view history
+ Given I go to the "History Test Page" page with content "This is a page that will have history"
+ When I click Edit
+ And I edit the page with "Edited and a random string"
+ And I save the edit
+ And the edited page content should contain "Edited and a random string"
+ And I click View History
+ Then I should see a link to a previous version of the page
diff --git a/tests/parser/ParserTestResult.php b/tests/parser/ParserTestResult.php
new file mode 100644
index 00000000..7d9415a2
--- /dev/null
+++ b/tests/parser/ParserTestResult.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @copyright Copyright © 2013, Antoine Musso
+ * @copyright Copyright © 2013, Wikimedia Foundation Inc.
+ * @license GNU GPL v2
+ *
+ * @file
+ */
+
+/**
+ * Represent the result of a parser test.
+ *
+ * @since 1.22
+ */
+class ParserTestResult {
+ /**
+ * Description of the parser test.
+ *
+ * This is usually the text used to describe a parser test in the .txt
+ * files. It is initialized on a construction and you most probably
+ * never want to change it.
+ */
+ public $description;
+ /** Text that was expected */
+ public $expected;
+ /** Actual text rendered */
+ public $actual;
+
+ /**
+ * @param string $description A short text describing the parser test
+ * usually the text in the parser test .txt file. The description
+ * is later available using the property $description.
+ */
+ public function __construct( $description ) {
+ $this->description = $description;
+ }
+
+ /**
+ * Whether the test passed
+ * @return bool
+ */
+ public function isSuccess() {
+ return $this->expected === $this->actual;
+ }
+}
diff --git a/tests/parser/README b/tests/parser/README
new file mode 100644
index 00000000..8b413376
--- /dev/null
+++ b/tests/parser/README
@@ -0,0 +1,8 @@
+Parser tests are run using our PHPUnit test suite in tests/phpunit:
+
+ $ cd tests/phpunit
+ ./phpunit.php --group Parser
+
+You can optionally filter by title using --regex. I.e. :
+
+ ./phpunit.php --group Parser --regex="Bug 6200"
diff --git a/tests/parser/extraParserTests.txt b/tests/parser/extraParserTests.txt
new file mode 100644
index 00000000..bef8f506
--- /dev/null
+++ b/tests/parser/extraParserTests.txt
Binary files differ
diff --git a/tests/parser/parserTest.inc b/tests/parser/parserTest.inc
new file mode 100644
index 00000000..a9df6832
--- /dev/null
+++ b/tests/parser/parserTest.inc
@@ -0,0 +1,1655 @@
+<?php
+/**
+ * Helper code for the MediaWiki parser test suite. Some code is duplicated
+ * in PHPUnit's NewParserTests.php, so you'll probably want to update both
+ * at the same time.
+ *
+ * Copyright © 2004, 2010 Brion Vibber <brion@pobox.com>
+ * https://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
+ *
+ * @todo Make this more independent of the configuration (and if possible the database)
+ * @todo document
+ * @file
+ * @ingroup Testing
+ */
+
+/**
+ * @ingroup Testing
+ */
+class ParserTest {
+ /**
+ * @var bool $color whereas output should be colorized
+ */
+ private $color;
+
+ /**
+ * @var bool $showOutput Show test output
+ */
+ private $showOutput;
+
+ /**
+ * @var bool $useTemporaryTables Use temporary tables for the temporary database
+ */
+ private $useTemporaryTables = true;
+
+ /**
+ * @var bool $databaseSetupDone True if the database has been set up
+ */
+ private $databaseSetupDone = false;
+
+ /**
+ * Our connection to the database
+ * @var DatabaseBase
+ */
+ private $db;
+
+ /**
+ * Database clone helper
+ * @var CloneDatabase
+ */
+ private $dbClone;
+
+ /**
+ * @var DjVuSupport
+ */
+ private $djVuSupport;
+
+ /**
+ * @var TidySupport
+ */
+ private $tidySupport;
+
+ private $maxFuzzTestLength = 300;
+ private $fuzzSeed = 0;
+ private $memoryLimit = 50;
+ private $uploadDir = null;
+
+ public $regex = "";
+ private $savedGlobals = array();
+
+ /**
+ * Sets terminal colorization and diff/quick modes depending on OS and
+ * command-line options (--color and --quick).
+ * @param array $options
+ */
+ public function __construct( $options = array() ) {
+ # Only colorize output if stdout is a terminal.
+ $this->color = !wfIsWindows() && Maintenance::posix_isatty( 1 );
+
+ if ( isset( $options['color'] ) ) {
+ switch ( $options['color'] ) {
+ case 'no':
+ $this->color = false;
+ break;
+ case 'yes':
+ default:
+ $this->color = true;
+ break;
+ }
+ }
+
+ $this->term = $this->color
+ ? new AnsiTermColorer()
+ : new DummyTermColorer();
+
+ $this->showDiffs = !isset( $options['quick'] );
+ $this->showProgress = !isset( $options['quiet'] );
+ $this->showFailure = !(
+ isset( $options['quiet'] )
+ && ( isset( $options['record'] )
+ || isset( $options['compare'] ) ) ); // redundant output
+
+ $this->showOutput = isset( $options['show-output'] );
+
+ if ( isset( $options['filter'] ) ) {
+ $options['regex'] = $options['filter'];
+ }
+
+ if ( isset( $options['regex'] ) ) {
+ if ( isset( $options['record'] ) ) {
+ echo "Warning: --record cannot be used with --regex, disabling --record\n";
+ unset( $options['record'] );
+ }
+ $this->regex = $options['regex'];
+ } else {
+ # Matches anything
+ $this->regex = '';
+ }
+
+ $this->setupRecorder( $options );
+ $this->keepUploads = isset( $options['keep-uploads'] );
+
+ if ( isset( $options['seed'] ) ) {
+ $this->fuzzSeed = intval( $options['seed'] ) - 1;
+ }
+
+ $this->runDisabled = isset( $options['run-disabled'] );
+ $this->runParsoid = isset( $options['run-parsoid'] );
+
+ $this->djVuSupport = new DjVuSupport();
+ $this->tidySupport = new TidySupport();
+ if ( !$this->tidySupport->isEnabled() ) {
+ echo "Warning: tidy is not installed, skipping some tests\n";
+ }
+
+ $this->hooks = array();
+ $this->functionHooks = array();
+ $this->transparentHooks = array();
+ self::setUp();
+ }
+
+ static function setUp() {
+ global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc,
+ $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, $wgEnableParserCache,
+ $wgExtraNamespaces, $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo,
+ $wgExtraInterlanguageLinkPrefixes, $wgLocalInterwikis,
+ $parserMemc, $wgThumbnailScriptPath, $wgScriptPath,
+ $wgArticlePath, $wgScript, $wgStylePath, $wgExtensionAssetsPath,
+ $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgLockManagers;
+
+ $wgScript = '/index.php';
+ $wgScriptPath = '/';
+ $wgArticlePath = '/wiki/$1';
+ $wgStylePath = '/skins';
+ $wgExtensionAssetsPath = '/extensions';
+ $wgThumbnailScriptPath = false;
+ $wgLockManagers = array( array(
+ 'name' => 'fsLockManager',
+ 'class' => 'FSLockManager',
+ 'lockDirectory' => wfTempDir() . '/test-repo/lockdir',
+ ), array(
+ 'name' => 'nullLockManager',
+ 'class' => 'NullLockManager',
+ ) );
+ $wgLocalFileRepo = array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => new FSFileBackend( array(
+ 'name' => 'local-backend',
+ 'wikiId' => wfWikiId(),
+ 'containerPaths' => array(
+ 'local-public' => wfTempDir() . '/test-repo/public',
+ 'local-thumb' => wfTempDir() . '/test-repo/thumb',
+ 'local-temp' => wfTempDir() . '/test-repo/temp',
+ 'local-deleted' => wfTempDir() . '/test-repo/deleted',
+ )
+ ) )
+ );
+ $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
+ $wgNamespaceAliases['Image'] = NS_FILE;
+ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
+ # add a namespace shadowing a interwiki link, to test
+ # proper precedence when resolving links. (bug 51680)
+ $wgExtraNamespaces[100] = 'MemoryAlpha';
+
+ // XXX: tests won't run without this (for CACHE_DB)
+ if ( $wgMainCacheType === CACHE_DB ) {
+ $wgMainCacheType = CACHE_NONE;
+ }
+ if ( $wgMessageCacheType === CACHE_DB ) {
+ $wgMessageCacheType = CACHE_NONE;
+ }
+ if ( $wgParserCacheType === CACHE_DB ) {
+ $wgParserCacheType = CACHE_NONE;
+ }
+
+ $wgEnableParserCache = false;
+ DeferredUpdates::clearPendingUpdates();
+ $wgMemc = wfGetMainCache(); // checks $wgMainCacheType
+ $messageMemc = wfGetMessageCacheStorage();
+ $parserMemc = wfGetParserCacheStorage();
+
+ $wgUser = new User;
+ $context = new RequestContext;
+ $wgLang = $context->getLanguage();
+ $wgOut = $context->getOutput();
+ $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
+ $wgRequest = $context->getRequest();
+
+ if ( $wgStyleDirectory === false ) {
+ $wgStyleDirectory = "$IP/skins";
+ }
+
+ self::setupInterwikis();
+ $wgLocalInterwikis = array( 'local', 'mi' );
+ // "extra language links"
+ // see https://gerrit.wikimedia.org/r/111390
+ array_push( $wgExtraInterlanguageLinkPrefixes, 'mul' );
+ }
+
+ /**
+ * Insert hardcoded interwiki in the lookup table.
+ *
+ * This function insert a set of well known interwikis that are used in
+ * the parser tests. They can be considered has fixtures are injected in
+ * the interwiki cache by using the 'InterwikiLoadPrefix' hook.
+ * Since we are not interested in looking up interwikis in the database,
+ * the hook completely replace the existing mechanism (hook returns false).
+ */
+ public static function setupInterwikis() {
+ # Hack: insert a few Wikipedia in-project interwiki prefixes,
+ # for testing inter-language links
+ Hooks::register( 'InterwikiLoadPrefix', function ( $prefix, &$iwData ) {
+ static $testInterwikis = array(
+ 'local' => array(
+ 'iw_url' => 'http://doesnt.matter.org/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ),
+ 'wikipedia' => array(
+ 'iw_url' => 'http://en.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ),
+ 'meatball' => array(
+ 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ),
+ 'memoryalpha' => array(
+ 'iw_url' => 'http://www.memory-alpha.org/en/index.php/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ),
+ 'zh' => array(
+ 'iw_url' => 'http://zh.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ 'es' => array(
+ 'iw_url' => 'http://es.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ 'fr' => array(
+ 'iw_url' => 'http://fr.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ 'ru' => array(
+ 'iw_url' => 'http://ru.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ 'mi' => array(
+ 'iw_url' => 'http://mi.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ 'mul' => array(
+ 'iw_url' => 'http://wikisource.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ );
+ if ( array_key_exists( $prefix, $testInterwikis ) ) {
+ $iwData = $testInterwikis[$prefix];
+ }
+
+ // We only want to rely on the above fixtures
+ return false;
+ } );// hooks::register
+ }
+
+ /**
+ * Remove the hardcoded interwiki lookup table.
+ */
+ public static function tearDownInterwikis() {
+ Hooks::clear( 'InterwikiLoadPrefix' );
+ }
+
+ public function setupRecorder( $options ) {
+ if ( isset( $options['record'] ) ) {
+ $this->recorder = new DbTestRecorder( $this );
+ $this->recorder->version = isset( $options['setversion'] ) ?
+ $options['setversion'] : SpecialVersion::getVersion();
+ } elseif ( isset( $options['compare'] ) ) {
+ $this->recorder = new DbTestPreviewer( $this );
+ } else {
+ $this->recorder = new TestRecorder( $this );
+ }
+ }
+
+ /**
+ * Remove last character if it is a newline
+ * @group utility
+ * @param string $s
+ * @return string
+ */
+ public static function chomp( $s ) {
+ if ( substr( $s, -1 ) === "\n" ) {
+ return substr( $s, 0, -1 );
+ } else {
+ return $s;
+ }
+ }
+
+ /**
+ * Run a fuzz test series
+ * Draw input from a set of test files
+ * @param array $filenames
+ */
+ function fuzzTest( $filenames ) {
+ $GLOBALS['wgContLang'] = Language::factory( 'en' );
+ $dict = $this->getFuzzInput( $filenames );
+ $dictSize = strlen( $dict );
+ $logMaxLength = log( $this->maxFuzzTestLength );
+ $this->setupDatabase();
+ ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
+
+ $numTotal = 0;
+ $numSuccess = 0;
+ $user = new User;
+ $opts = ParserOptions::newFromUser( $user );
+ $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
+
+ 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 );
+ $fail = false;
+ } catch ( Exception $exception ) {
+ $fail = true;
+ }
+
+ if ( $fail ) {
+ echo "Test failed with seed {$this->fuzzSeed}\n";
+ echo "Input:\n";
+ printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
+ echo "$exception\n";
+ } else {
+ $numSuccess++;
+ }
+
+ $numTotal++;
+ $this->teardownGlobals();
+ $parser->__destruct();
+
+ if ( $numTotal % 100 == 0 ) {
+ $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
+ echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
+ if ( $usage > 90 ) {
+ echo "Out of memory:\n";
+ $memStats = $this->getMemoryBreakdown();
+
+ foreach ( $memStats as $name => $usage ) {
+ echo "$name: $usage\n";
+ }
+ $this->abort();
+ }
+ }
+ }
+ }
+
+ /**
+ * Get an input dictionary from a set of parser test files
+ * @param array $filenames
+ * @return string
+ */
+ function getFuzzInput( $filenames ) {
+ $dict = '';
+
+ foreach ( $filenames as $filename ) {
+ $contents = file_get_contents( $filename );
+ preg_match_all(
+ '/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s',
+ $contents,
+ $matches
+ );
+
+ foreach ( $matches[1] as $match ) {
+ $dict .= $match . "\n";
+ }
+ }
+
+ return $dict;
+ }
+
+ /**
+ * Get a memory usage breakdown
+ * @return array
+ */
+ 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;
+ }
+
+ function abort() {
+ $this->abort();
+ }
+
+ /**
+ * Run a series of tests listed in the given text files.
+ * Each test consists of a brief description, wikitext input,
+ * and the expected HTML output.
+ *
+ * Prints status updates on stdout and counts up the total
+ * number and percentage of passed tests.
+ *
+ * @param array $filenames Array of strings
+ * @return bool True if passed all tests, false if any tests failed.
+ */
+ public function runTestsFromFiles( $filenames ) {
+ $ok = false;
+
+ // be sure, ParserTest::addArticle has correct language set,
+ // so that system messages gets into the right language cache
+ $GLOBALS['wgLanguageCode'] = 'en';
+ $GLOBALS['wgContLang'] = Language::factory( 'en' );
+
+ $this->recorder->start();
+ try {
+ $this->setupDatabase();
+ $ok = true;
+
+ foreach ( $filenames as $filename ) {
+ $tests = new TestFileIterator( $filename, $this );
+ $ok = $this->runTests( $tests ) && $ok;
+ }
+
+ $this->teardownDatabase();
+ $this->recorder->report();
+ } catch ( DBError $e ) {
+ echo $e->getMessage();
+ }
+ $this->recorder->end();
+
+ return $ok;
+ }
+
+ function runTests( $tests ) {
+ $ok = true;
+
+ foreach ( $tests as $t ) {
+ $result =
+ $this->runTest( $t['test'], $t['input'], $t['result'], $t['options'], $t['config'] );
+ $ok = $ok && $result;
+ $this->recorder->record( $t['test'], $result );
+ }
+
+ if ( $this->showProgress ) {
+ print "\n";
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Get a Parser object
+ *
+ * @param string $preprocessor
+ * @return Parser
+ */
+ function getParser( $preprocessor = null ) {
+ global $wgParserConf;
+
+ $class = $wgParserConf['class'];
+ $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
+
+ foreach ( $this->hooks as $tag => $callback ) {
+ $parser->setHook( $tag, $callback );
+ }
+
+ foreach ( $this->functionHooks as $tag => $bits ) {
+ list( $callback, $flags ) = $bits;
+ $parser->setFunctionHook( $tag, $callback, $flags );
+ }
+
+ foreach ( $this->transparentHooks as $tag => $callback ) {
+ $parser->setTransparentTagHook( $tag, $callback );
+ }
+
+ wfRunHooks( 'ParserTestParser', array( &$parser ) );
+
+ return $parser;
+ }
+
+ /**
+ * Run a given wikitext input through a freshly-constructed wiki parser,
+ * and compare the output against the expected results.
+ * Prints status and explanatory messages to stdout.
+ *
+ * @param string $desc Test's description
+ * @param string $input Wikitext to try rendering
+ * @param string $result Result to output
+ * @param array $opts Test's options
+ * @param string $config Overrides for global variables, one per line
+ * @return bool
+ */
+ public function runTest( $desc, $input, $result, $opts, $config ) {
+ if ( $this->showProgress ) {
+ $this->showTesting( $desc );
+ }
+
+ $opts = $this->parseOptions( $opts );
+ $context = $this->setupGlobals( $opts, $config );
+
+ $user = $context->getUser();
+ $options = ParserOptions::newFromContext( $context );
+
+ if ( isset( $opts['djvu'] ) ) {
+ if ( !$this->djVuSupport->isEnabled() ) {
+ return $this->showSkipped();
+ }
+ }
+
+ 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'] ) ) {
+ $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 );
+ $output->setTOCEnabled( !isset( $opts['notoc'] ) );
+ $out = $output->getText();
+ if ( isset( $opts['tidy'] ) ) {
+ if ( !$this->tidySupport->isEnabled() ) {
+ return $this->showSkipped();
+ }
+ $out = MWTidy::tidy( $out );
+ $out = preg_replace( '/\s+$/', '', $out );
+ }
+
+ if ( isset( $opts['showtitle'] ) ) {
+ if ( $output->getTitleText() ) {
+ $title = $output->getTitleText();
+ }
+
+ $out = "$title\n$out";
+ }
+
+ if ( isset( $opts['ill'] ) ) {
+ $out = implode( ' ', $output->getLanguageLinks() );
+ } elseif ( isset( $opts['cat'] ) ) {
+ $outputPage = $context->getOutput();
+ $outputPage->addCategoryLinks( $output->getCategories() );
+ $cats = $outputPage->getCategoryLinks();
+
+ if ( isset( $cats['normal'] ) ) {
+ $out = implode( ' ', $cats['normal'] );
+ } else {
+ $out = '';
+ }
+ }
+ }
+
+ $this->teardownGlobals();
+
+ $testResult = new ParserTestResult( $desc );
+ $testResult->expected = $result;
+ $testResult->actual = $out;
+
+ return $this->showTestResult( $testResult );
+ }
+
+ /**
+ * Refactored in 1.22 to use ParserTestResult
+ * @param ParserTestResult $testResult
+ * @return bool
+ */
+ function showTestResult( ParserTestResult $testResult ) {
+ if ( $testResult->isSuccess() ) {
+ $this->showSuccess( $testResult );
+ return true;
+ } else {
+ $this->showFailure( $testResult );
+ return false;
+ }
+ }
+
+ /**
+ * Use a regex to find out the value of an option
+ * @param string $key Name of option val to retrieve
+ * @param array $opts Options array to look in
+ * @param mixed $default Default value returned if not found
+ * @return mixed
+ */
+ private static function getOptionValue( $key, $opts, $default ) {
+ $key = strtolower( $key );
+
+ if ( isset( $opts[$key] ) ) {
+ return $opts[$key];
+ } else {
+ return $default;
+ }
+ }
+
+ private function parseOptions( $instring ) {
+ $opts = array();
+ // foo
+ // foo=bar
+ // foo="bar baz"
+ // foo=[[bar baz]]
+ // foo=bar,"baz quux"
+ // foo={...json...}
+ $defs = '(?(DEFINE)
+ (?<qstr> # Quoted string
+ "
+ (?:[^\\\\"] | \\\\.)*
+ "
+ )
+ (?<json>
+ \{ # Open bracket
+ (?:
+ [^"{}] | # Not a quoted string or object, or
+ (?&qstr) | # A quoted string, or
+ (?&json) # A json object (recursively)
+ )*
+ \} # Close bracket
+ )
+ (?<value>
+ (?:
+ (?&qstr) # Quoted val
+ |
+ \[\[
+ [^]]* # Link target
+ \]\]
+ |
+ [\w-]+ # Plain word
+ |
+ (?&json) # JSON object
+ )
+ )
+ )';
+ $regex = '/' . $defs . '\b
+ (?<k>[\w-]+) # Key
+ \b
+ (?:\s*
+ = # First sub-value
+ \s*
+ (?<v>
+ (?&value)
+ (?:\s*
+ , # Sub-vals 1..N
+ \s*
+ (?&value)
+ )*
+ )
+ )?
+ /x';
+ $valueregex = '/' . $defs . '(?&value)/x';
+
+ if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
+ foreach ( $matches as $bits ) {
+ $key = strtolower( $bits['k'] );
+ if ( !isset( $bits['v'] ) ) {
+ $opts[$key] = true;
+ } else {
+ preg_match_all( $valueregex, $bits['v'], $vmatches );
+ $opts[$key] = array_map( array( $this, 'cleanupOption' ), $vmatches[0] );
+ if ( count( $opts[$key] ) == 1 ) {
+ $opts[$key] = $opts[$key][0];
+ }
+ }
+ }
+ }
+ return $opts;
+ }
+
+ private function cleanupOption( $opt ) {
+ if ( substr( $opt, 0, 1 ) == '"' ) {
+ return stripcslashes( substr( $opt, 1, -1 ) );
+ }
+
+ if ( substr( $opt, 0, 2 ) == '[[' ) {
+ return substr( $opt, 2, -2 );
+ }
+
+ if ( substr( $opt, 0, 1 ) == '{' ) {
+ return FormatJson::decode( $opt, true );
+ }
+ return $opt;
+ }
+
+ /**
+ * Set up the global variables for a consistent environment for each test.
+ * Ideally this should replace the global configuration entirely.
+ * @param string $opts
+ * @param string $config
+ * @return RequestContext
+ */
+ private function setupGlobals( $opts = '', $config = '' ) {
+ global $IP;
+
+ # 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://example.org',
+ 'wgServerName' => 'example.org',
+ 'wgScript' => '/index.php',
+ 'wgScriptPath' => '/',
+ 'wgArticlePath' => '/wiki/$1',
+ 'wgActionPaths' => array(),
+ 'wgLockManagers' => array( array(
+ 'name' => 'fsLockManager',
+ 'class' => 'FSLockManager',
+ 'lockDirectory' => $this->uploadDir . '/lockdir',
+ ), array(
+ 'name' => 'nullLockManager',
+ 'class' => 'NullLockManager',
+ ) ),
+ 'wgLocalFileRepo' => array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => new FSFileBackend( array(
+ 'name' => 'local-backend',
+ 'wikiId' => wfWikiId(),
+ 'containerPaths' => array(
+ 'local-public' => $this->uploadDir,
+ 'local-thumb' => $this->uploadDir . '/thumb',
+ 'local-temp' => $this->uploadDir . '/temp',
+ 'local-deleted' => $this->uploadDir . '/delete',
+ )
+ ) )
+ ),
+ 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
+ 'wgUploadNavigationUrl' => false,
+ 'wgStylePath' => '/skins',
+ 'wgSitename' => 'MediaWiki',
+ 'wgLanguageCode' => $lang,
+ 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'parsertest_' : 'pt_',
+ 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ),
+ 'wgLang' => null,
+ 'wgContLang' => null,
+ 'wgNamespacesWithSubpages' => array( 0 => isset( $opts['subpage'] ) ),
+ 'wgMaxTocLevel' => $maxtoclevel,
+ 'wgCapitalLinks' => true,
+ 'wgNoFollowLinks' => true,
+ 'wgNoFollowDomainExceptions' => array(),
+ 'wgThumbnailScriptPath' => false,
+ 'wgUseImageResize' => true,
+ 'wgSVGConverter' => 'null',
+ 'wgSVGConverters' => array( 'null' => 'echo "1">$output' ),
+ 'wgLocaltimezone' => 'UTC',
+ 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ),
+ 'wgThumbLimits' => array( self::getOptionValue( 'thumbsize', $opts, 180 ) ),
+ '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,
+ 'wgHtml5' => true,
+ 'wgWellFormedXml' => true,
+ 'wgAllowMicrodataAttributes' => true,
+ 'wgAdaptiveMessageCache' => true,
+ 'wgDisableLangConversion' => false,
+ 'wgDisableTitleConversion' => false,
+ // Tidy options.
+ // We always set 'wgUseTidy' to false when parsing, but certain
+ // test-running modes still use tidy if available, so ensure
+ // that the tidy-related options are all set to their defaults.
+ 'wgUseTidy' => false,
+ 'wgAlwaysUseTidy' => false,
+ 'wgDebugTidy' => false,
+ 'wgTidyConf' => $IP . '/includes/tidy.conf',
+ 'wgTidyOpts' => '',
+ 'wgTidyInternal' => $this->tidySupport->isInternal(),
+ );
+
+ if ( $config ) {
+ $configLines = explode( "\n", $config );
+
+ foreach ( $configLines as $line ) {
+ list( $var, $value ) = explode( '=', $line, 2 );
+
+ $settings[$var] = eval( "return $value;" );
+ }
+ }
+
+ $this->savedGlobals = array();
+
+ /** @since 1.20 */
+ wfRunHooks( 'ParserTestGlobals', array( &$settings ) );
+
+ foreach ( $settings as $var => $val ) {
+ if ( array_key_exists( $var, $GLOBALS ) ) {
+ $this->savedGlobals[$var] = $GLOBALS[$var];
+ }
+
+ $GLOBALS[$var] = $val;
+ }
+
+ $GLOBALS['wgContLang'] = Language::factory( $lang );
+ $GLOBALS['wgMemc'] = new EmptyBagOStuff;
+
+ $context = new RequestContext();
+ $GLOBALS['wgLang'] = $context->getLanguage();
+ $GLOBALS['wgOut'] = $context->getOutput();
+ $GLOBALS['wgUser'] = $context->getUser();
+
+ // We (re)set $wgThumbLimits to a single-element array above.
+ $context->getUser()->setOption( 'thumbsize', 0 );
+
+ global $wgHooks;
+
+ $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
+ $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
+
+ MagicWord::clearCache();
+
+ return $context;
+ }
+
+ /**
+ * List of temporary tables to create, without prefix.
+ * Some of these probably aren't necessary.
+ * @return array
+ */
+ private function listTables() {
+ $tables = array( 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions',
+ 'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks',
+ 'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks',
+ 'site_stats', 'hitcounter', 'ipblocks', 'image', 'oldimage',
+ 'recentchanges', 'watchlist', 'interwiki', 'logging',
+ 'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo',
+ 'archive', 'user_groups', 'page_props', 'category', 'msg_resource', 'msg_resource_links'
+ );
+
+ if ( in_array( $this->db->getType(), array( 'mysql', 'sqlite', 'oracle' ) ) ) {
+ array_push( $tables, 'searchindex' );
+ }
+
+ // Allow extensions to add to the list of tables to duplicate;
+ // may be necessary if they hook into page save or other code
+ // which will require them while running tests.
+ wfRunHooks( 'ParserTestTables', array( &$tables ) );
+
+ return $tables;
+ }
+
+ /**
+ * Set up a temporary set of wiki tables to work with for the tests.
+ * Currently this will only be done once per run, and any changes to
+ * the db will be visible to later tests in the run.
+ */
+ public function setupDatabase() {
+ global $wgDBprefix;
+
+ if ( $this->databaseSetupDone ) {
+ return;
+ }
+
+ $this->db = wfGetDB( DB_MASTER );
+ $dbType = $this->db->getType();
+
+ if ( $wgDBprefix === 'parsertest_' || ( $dbType == 'oracle' && $wgDBprefix === 'pt_' ) ) {
+ throw new MWException( 'setupDatabase should be called before setupGlobals' );
+ }
+
+ $this->databaseSetupDone = true;
+
+ # SqlBagOStuff broke when using temporary tables on r40209 (bug 15892).
+ # It seems to have been fixed since (r55079?), but regressed at some point before r85701.
+ # This works around it for now...
+ ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
+
+ # CREATE TEMPORARY TABLE breaks if there is more than one server
+ if ( wfGetLB()->getServerCount() != 1 ) {
+ $this->useTemporaryTables = false;
+ }
+
+ $temporary = $this->useTemporaryTables || $dbType == 'postgres';
+ $prefix = $dbType != 'oracle' ? 'parsertest_' : 'pt_';
+
+ $this->dbClone = new CloneDatabase( $this->db, $this->listTables(), $prefix );
+ $this->dbClone->useTemporaryTables( $temporary );
+ $this->dbClone->cloneTableStructure();
+
+ if ( $dbType == 'oracle' ) {
+ $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
+ # Insert 0 user to prevent FK violations
+
+ # Anonymous user
+ $this->db->insert( 'user', array(
+ 'user_id' => 0,
+ 'user_name' => 'Anonymous' ) );
+ }
+
+ # Update certain things in site_stats
+ $this->db->insert( 'site_stats',
+ array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ) );
+
+ # Reinitialise the LocalisationCache to match the database state
+ Language::getLocalisationCache()->unloadAll();
+
+ # Clear the message cache
+ MessageCache::singleton()->clear();
+
+ // Remember to update newParserTests.php after changing the below
+ // (and it uses a slightly different syntax just for teh lulz)
+ $this->uploadDir = $this->setupUploadDir();
+ $user = User::createNew( 'WikiSysop' );
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
+ # note that the size/width/height/bits/etc of the file
+ # are actually set by inspecting the file itself; the arguments
+ # to recordUpload2 have no effect. That said, we try to make things
+ # match up so it is less confusing to readers of the code & tests.
+ $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', array(
+ 'size' => 7881,
+ 'width' => 1941,
+ 'height' => 220,
+ 'bits' => 8,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '1', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20010115123500' ), $user );
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) );
+ # again, note that size/width/height below are ignored; see above.
+ $image->recordUpload2( '', 'Upload of some lame thumbnail', 'Some lame thumbnail', array(
+ 'size' => 22589,
+ 'width' => 135,
+ 'height' => 135,
+ 'bits' => 8,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/png',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '2', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20130225203040' ), $user );
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) );
+ $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', array(
+ 'size' => 12345,
+ 'width' => 240,
+ 'height' => 180,
+ 'bits' => 0,
+ 'media_type' => MEDIATYPE_DRAWING,
+ 'mime' => 'image/svg+xml',
+ '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( '3', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20010115123500' ), $user );
+
+ # A DjVu file
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) );
+ $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', array(
+ 'size' => 3249,
+ 'width' => 2480,
+ 'height' => 3508,
+ 'bits' => 0,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/vnd.djvu',
+ 'metadata' => '<?xml version="1.0" ?>
+<!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd">
+<DjVuXML>
+<HEAD></HEAD>
+<BODY><OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+</BODY>
+</DjVuXML>',
+ 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20010115123600' ), $user );
+ }
+
+ public function teardownDatabase() {
+ if ( !$this->databaseSetupDone ) {
+ $this->teardownGlobals();
+ return;
+ }
+ $this->teardownUploadDir( $this->uploadDir );
+
+ $this->dbClone->destroy();
+ $this->databaseSetupDone = false;
+
+ if ( $this->useTemporaryTables ) {
+ if ( $this->db->getType() == 'sqlite' ) {
+ # Under SQLite the searchindex table is virtual and need
+ # to be explicitly destroyed. See bug 29912
+ # See also MediaWikiTestCase::destroyDB()
+ wfDebug( __METHOD__ . " explicitly destroying sqlite virtual table parsertest_searchindex\n" );
+ $this->db->query( "DROP TABLE `parsertest_searchindex`" );
+ }
+ # Don't need to do anything
+ $this->teardownGlobals();
+ return;
+ }
+
+ $tables = $this->listTables();
+
+ foreach ( $tables as $table ) {
+ if ( $this->db->getType() == 'oracle' ) {
+ $this->db->query( "DROP TABLE pt_$table DROP CONSTRAINTS" );
+ } else {
+ $this->db->query( "DROP TABLE `parsertest_$table`" );
+ }
+ }
+
+ if ( $this->db->getType() == 'oracle' ) {
+ $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
+ }
+
+ $this->teardownGlobals();
+ }
+
+ /**
+ * Create a dummy uploads directory which will contain a couple
+ * of files in order to pass existence tests.
+ *
+ * @return string The directory
+ */
+ private 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', null, __METHOD__ );
+ copy( "$IP/tests/phpunit/data/parser/headbg.jpg", "$dir/3/3a/Foobar.jpg" );
+ wfMkdirParents( $dir . '/e/ea', null, __METHOD__ );
+ copy( "$IP/tests/phpunit/data/parser/wiki.png", "$dir/e/ea/Thumb.png" );
+ wfMkdirParents( $dir . '/0/09', null, __METHOD__ );
+ copy( "$IP/tests/phpunit/data/parser/headbg.jpg", "$dir/0/09/Bad.jpg" );
+ wfMkdirParents( $dir . '/f/ff', null, __METHOD__ );
+ file_put_contents( "$dir/f/ff/Foobar.svg",
+ '<?xml version="1.0" encoding="utf-8"?>' .
+ '<svg xmlns="http://www.w3.org/2000/svg"' .
+ ' version="1.1" width="240" height="180"/>' );
+ wfMkdirParents( $dir . '/5/5f', null, __METHOD__ );
+ copy( "$IP/tests/phpunit/data/parser/LoremIpsum.djvu", "$dir/5/5f/LoremIpsum.djvu" );
+
+ return $dir;
+ }
+
+ /**
+ * Restore default values and perform any necessary clean-up
+ * after each test runs.
+ */
+ private function teardownGlobals() {
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+ LockManagerGroup::destroySingletons();
+ LinkCache::singleton()->clear();
+
+ foreach ( $this->savedGlobals as $var => $val ) {
+ $GLOBALS[$var] = $val;
+ }
+ }
+
+ /**
+ * Remove the dummy uploads directory
+ * @param string $dir
+ */
+ 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/1000px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/100px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/177px-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/206px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/265px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/274px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/30px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/353px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/40px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/442px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/450px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/600px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/75px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg",
+
+ "$dir/e/ea/Thumb.png",
+
+ "$dir/0/09/Bad.jpg",
+
+ "$dir/5/5f/LoremIpsum.djvu",
+ "$dir/thumb/5/5f/LoremIpsum.djvu/page2-2480px-LoremIpsum.djvu.jpg",
+ "$dir/thumb/5/5f/LoremIpsum.djvu/page2-3720px-LoremIpsum.djvu.jpg",
+ "$dir/thumb/5/5f/LoremIpsum.djvu/page2-4960px-LoremIpsum.djvu.jpg",
+
+ "$dir/f/ff/Foobar.svg",
+ "$dir/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png",
+ "$dir/thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png",
+ "$dir/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png",
+ "$dir/thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png",
+ "$dir/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png",
+ "$dir/thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png",
+ "$dir/thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.png",
+ "$dir/thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.png",
+ "$dir/thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.png",
+
+ "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
+ )
+ );
+
+ self::deleteDirs(
+ array(
+ "$dir/3/3a",
+ "$dir/3",
+ "$dir/thumb/3/3a/Foobar.jpg",
+ "$dir/thumb/3/3a",
+ "$dir/thumb/3",
+ "$dir/e/ea",
+ "$dir/e",
+ "$dir/f/ff/",
+ "$dir/f/",
+ "$dir/thumb/f/ff/Foobar.svg",
+ "$dir/thumb/f/ff/",
+ "$dir/thumb/f/",
+ "$dir/0/09/",
+ "$dir/0/",
+ "$dir/5/5f",
+ "$dir/5",
+ "$dir/thumb/5/5f/LoremIpsum.djvu",
+ "$dir/thumb/5/5f",
+ "$dir/thumb/5",
+ "$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 array $files 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 array $dirs Full paths to directories to delete.
+ */
+ private static function deleteDirs( $dirs ) {
+ foreach ( $dirs as $dir ) {
+ if ( is_dir( $dir ) ) {
+ rmdir( $dir );
+ }
+ }
+ }
+
+ /**
+ * "Running test $desc..."
+ * @param string $desc
+ */
+ protected function showTesting( $desc ) {
+ print "Running test $desc... ";
+ }
+
+ /**
+ * Print a happy success message.
+ *
+ * Refactored in 1.22 to use ParserTestResult
+ *
+ * @param ParserTestResult $testResult
+ * @return bool
+ */
+ protected function showSuccess( ParserTestResult $testResult ) {
+ if ( $this->showProgress ) {
+ print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n";
+ }
+
+ return true;
+ }
+
+ /**
+ * Print a failure message and provide some explanatory output
+ * about what went wrong if so configured.
+ *
+ * Refactored in 1.22 to use ParserTestResult
+ *
+ * @param ParserTestResult $testResult
+ * @return bool
+ */
+ protected function showFailure( ParserTestResult $testResult ) {
+ if ( $this->showFailure ) {
+ if ( !$this->showProgress ) {
+ # In quiet mode we didn't show the 'Testing' message before the
+ # test, in case it succeeded. Show it now:
+ $this->showTesting( $testResult->description );
+ }
+
+ print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n";
+
+ if ( $this->showOutput ) {
+ print "--- Expected ---\n{$testResult->expected}\n";
+ print "--- Actual ---\n{$testResult->actual}\n";
+ }
+
+ if ( $this->showDiffs ) {
+ print $this->quickDiff( $testResult->expected, $testResult->actual );
+ if ( !$this->wellFormed( $testResult->actual ) ) {
+ print "XML error: $this->mXmlError\n";
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Print a skipped message.
+ *
+ * @return bool
+ */
+ protected function showSkipped() {
+ if ( $this->showProgress ) {
+ print $this->term->color( '1;33' ) . 'SKIPPED' . $this->term->reset() . "\n";
+ }
+
+ return true;
+ }
+
+ /**
+ * Run given strings through a diff and return the (colorized) output.
+ * Requires writable /tmp directory and a 'diff' command in the PATH.
+ *
+ * @param string $input
+ * @param string $output
+ * @param string $inFileTail Tailing for the input file name
+ * @param string $outFileTail Tailing for the output file name
+ * @return string
+ */
+ protected function quickDiff( $input, $output,
+ $inFileTail = 'expected', $outFileTail = 'actual'
+ ) {
+ # Windows, or at least the fc utility, is retarded
+ $slash = wfIsWindows() ? '\\' : '/';
+ $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand();
+
+ $infile = "$prefix-$inFileTail";
+ $this->dumpToFile( $input, $infile );
+
+ $outfile = "$prefix-$outFileTail";
+ $this->dumpToFile( $output, $outfile );
+
+ $shellInfile = wfEscapeShellArg( $infile );
+ $shellOutfile = wfEscapeShellArg( $outfile );
+
+ global $wgDiff3;
+ // we assume that people with diff3 also have usual diff
+ $shellCommand = ( wfIsWindows() && !$wgDiff3 ) ? 'fc' : 'diff -au';
+
+ $diff = wfShellExec( "$shellCommand $shellInfile $shellOutfile" );
+
+ unlink( $infile );
+ unlink( $outfile );
+
+ return $this->colorDiff( $diff );
+ }
+
+ /**
+ * Write the given string to a file, adding a final newline.
+ *
+ * @param string $data
+ * @param string $filename
+ */
+ private function dumpToFile( $data, $filename ) {
+ $file = fopen( $filename, "wt" );
+ fwrite( $file, $data . "\n" );
+ fclose( $file );
+ }
+
+ /**
+ * Colorize unified diff output if set for ANSI color output.
+ * Subtractions are colored blue, additions red.
+ *
+ * @param string $text
+ * @return string
+ */
+ protected function colorDiff( $text ) {
+ return preg_replace(
+ array( '/^(-.*)$/m', '/^(\+.*)$/m' ),
+ array( $this->term->color( 34 ) . '$1' . $this->term->reset(),
+ $this->term->color( 31 ) . '$1' . $this->term->reset() ),
+ $text );
+ }
+
+ /**
+ * Show "Reading tests from ..."
+ *
+ * @param string $path
+ */
+ public function showRunFile( $path ) {
+ print $this->term->color( 1 ) .
+ "Reading tests from \"$path\"..." .
+ $this->term->reset() .
+ "\n";
+ }
+
+ /**
+ * Insert a temporary test article
+ * @param string $name The title, including any prefix
+ * @param string $text The article text
+ * @param int $line The input line number, for reporting errors
+ * @param bool $ignoreDuplicate Whether to silently ignore duplicate pages
+ */
+ public static function addArticle( $name, $text, $line = 'unknown', $ignoreDuplicate = '' ) {
+ global $wgCapitalLinks;
+
+ $oldCapitalLinks = $wgCapitalLinks;
+ $wgCapitalLinks = true; // We only need this from SetupGlobals() See r70917#c8637
+
+ $text = self::chomp( $text );
+ $name = self::chomp( $name );
+
+ $title = Title::newFromText( $name );
+
+ if ( is_null( $title ) ) {
+ throw new MWException( "invalid title '$name' at line $line\n" );
+ }
+
+ $page = WikiPage::factory( $title );
+ $page->loadPageData( 'fromdbmaster' );
+
+ if ( $page->exists() ) {
+ if ( $ignoreDuplicate == 'ignoreduplicate' ) {
+ return;
+ } else {
+ throw new MWException( "duplicate article '$name' at line $line\n" );
+ }
+ }
+
+ $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW );
+
+ $wgCapitalLinks = $oldCapitalLinks;
+ }
+
+ /**
+ * 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 string $name
+ * @return bool True if tag hook is present
+ */
+ public function requireHook( $name ) {
+ global $wgParser;
+
+ $wgParser->firstCallInit(); // make sure hooks are loaded.
+
+ if ( isset( $wgParser->mTagHooks[$name] ) ) {
+ $this->hooks[$name] = $wgParser->mTagHooks[$name];
+ } else {
+ echo " This test suite requires the '$name' hook extension, skipping.\n";
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 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 string $name
+ * @return bool True if function hook is present
+ */
+ public function requireFunctionHook( $name ) {
+ global $wgParser;
+
+ $wgParser->firstCallInit(); // make sure hooks are loaded.
+
+ if ( isset( $wgParser->mFunctionHooks[$name] ) ) {
+ $this->functionHooks[$name] = $wgParser->mFunctionHooks[$name];
+ } else {
+ echo " This test suite requires the '$name' function hook extension, skipping.\n";
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 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 string $name
+ * @return bool True if function hook is present
+ */
+ public function requireTransparentHook( $name ) {
+ global $wgParser;
+
+ $wgParser->firstCallInit(); // make sure hooks are loaded.
+
+ if ( isset( $wgParser->mTransparentTagHooks[$name] ) ) {
+ $this->transparentHooks[$name] = $wgParser->mTransparentTagHooks[$name];
+ } else {
+ echo " This test suite requires the '$name' transparent hook extension, skipping.\n";
+ return false;
+ }
+
+ return true;
+ }
+
+ private function wellFormed( $text ) {
+ $html =
+ Sanitizer::hackDocType() .
+ '<html>' .
+ $text .
+ '</html>';
+
+ $parser = xml_parser_create( "UTF-8" );
+
+ # case folding violates XML standard, turn it off
+ xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+ if ( !xml_parse( $parser, $html, true ) ) {
+ $err = xml_error_string( xml_get_error_code( $parser ) );
+ $position = xml_get_current_byte_index( $parser );
+ $fragment = $this->extractFragment( $html, $position );
+ $this->mXmlError = "$err at byte $position:\n$fragment";
+ xml_parser_free( $parser );
+
+ return false;
+ }
+
+ xml_parser_free( $parser );
+
+ return true;
+ }
+
+ private function extractFragment( $text, $position ) {
+ $start = max( 0, $position - 10 );
+ $before = $position - $start;
+ $fragment = '...' .
+ $this->term->color( 34 ) .
+ substr( $text, $start, $before ) .
+ $this->term->color( 0 ) .
+ $this->term->color( 31 ) .
+ $this->term->color( 1 ) .
+ substr( $text, $position, 1 ) .
+ $this->term->color( 0 ) .
+ $this->term->color( 34 ) .
+ substr( $text, $position + 1, 9 ) .
+ $this->term->color( 0 ) .
+ '...';
+ $display = str_replace( "\n", ' ', $fragment );
+ $caret = ' ' .
+ str_repeat( ' ', $before ) .
+ $this->term->color( 31 ) .
+ '^' .
+ $this->term->color( 0 );
+
+ return "$display\n$caret";
+ }
+
+ static function getFakeTimestamp( &$parser, &$ts ) {
+ $ts = 123; //parsed as '1970-01-01T00:02:03Z'
+ return true;
+ }
+}
diff --git a/tests/parser/parserTests.txt b/tests/parser/parserTests.txt
new file mode 100644
index 00000000..f915922f
--- /dev/null
+++ b/tests/parser/parserTests.txt
@@ -0,0 +1,21904 @@
+# MediaWiki Parser test cases
+# Some taken from http://meta.wikimedia.org/wiki/Parser_testing
+# All (C) their respective authors and released under the GPL
+#
+# The syntax should be fairly self-explanatory.
+#
+# Currently supported test options:
+# One of the following three:
+#
+# (default) generate HTML output
+# pst apply pre-save transform
+# msg apply message transform
+#
+# Plus any combination of these:
+#
+# cat add category links
+# ill add inter-language links
+# subpage enable subpages (disabled by default)
+# noxml don't check for XML well formdness
+# title=[[XXX]] run test using article title XXX
+# language=XXX set content language to XXX for this test
+# variant=XXX set the variant of language for this test (eg zh-tw)
+# disabled do not run test
+# parsoid parsoid-specific options (not run by PHP parser unless
+# the test includes an html/php section)
+# php php-only test (not run by the parsoid parser unless
+# the test includes an html/parsoid section)
+# showtitle make the first line the title
+# comment run through Linker::formatComment() instead of main parser
+# local format section links in edit comment text as local links
+# notoc disable table of contents
+# thumbsize=NNN set the default thumb size to NNNpx for this test
+#
+# You can also set the following parser properties via test options:
+# wgEnableUploads, wgAllowExternalImages, wgMaxTocLevel,
+# wgLinkHolderBatchSize, wgRawHtml
+#
+# For testing purposes, temporary articles can created:
+# !!article / NAMESPACE:TITLE / !!text / ARTICLE TEXT / !!endarticle
+# where '/' denotes a newline.
+
+# This is the standard article assumed to exist.
+!! article
+Main Page
+!! text
+blah blah
+!! endarticle
+
+!!article
+Foo
+!!text
+FOO
+!!endarticle
+
+!!article
+Template:Foo
+!!text
+FOO
+!!endarticle
+
+!! article
+Template:Blank
+!! text
+!! endarticle
+
+!! article
+Template:pipe
+!! text
+|
+!! endarticle
+
+!!article
+MediaWiki:bad image list
+!!text
+* [[File:Bad.jpg]] except [[Nasty page]]
+!!endarticle
+
+!! article
+Template:inner list
+!! text
+* item 1
+!! endarticle
+
+!! article
+Template:tbl-start
+!! text
+{|
+!! endarticle
+
+!! article
+Template:tbl-end
+!! text
+|}
+!! endarticle
+
+!! article
+Template:echo
+!! text
+{{{1}}}
+!! endarticle
+
+!! article
+Template:echo_with_span
+!! text
+<span>{{{1}}}</span>
+!! endarticle
+
+!! article
+Template:echo_with_div
+!! text
+<div>{{{1}}}</div>
+!! endarticle
+
+!! article
+Template:attr_str
+!! text
+{{{1}}}="{{{2}}}"
+!! endarticle
+
+!! article
+Template:table_attribs
+!! text
+<noinclude>
+|</noinclude>style="color: red"| Foo
+!! endarticle
+
+!! article
+Template:table_cells
+!! text
+{{table_attribs}} || Bar || Baz
+!! endarticle
+
+!! article
+Template:image_attribs
+!! text
+<noinclude>
+[[File:foobar.jpg|</noinclude>right|Caption text<noinclude>]]</noinclude>
+!! endarticle
+
+!! article
+A?b
+!! text
+Weirdo titles!
+!! endarticle
+
+!!article
+Template:Bullet
+!!text
+* Bar
+!!endarticle
+
+!!article
+Template:OpenTable
+!!text
+{|
+!!endarticle
+
+###
+### Basic tests
+###
+!! test
+Blank input
+!! wikitext
+!! html
+!! end
+
+
+!! test
+Simple paragraph
+!! wikitext
+This is a simple paragraph.
+!! html
+<p>This is a simple paragraph.
+</p>
+!! end
+
+!! test
+Paragraphs with extra newline spacing
+!! wikitext
+foo
+
+bar
+
+
+baz
+
+
+
+booz
+!! html
+<p>foo
+</p><p>bar
+</p><p><br />
+baz
+</p><p><br />
+</p><p>booz
+</p>
+!! end
+
+!! test
+Paragraphs with newline spacing with comment lines in between
+!! wikitext
+----
+a
+<!--foo-->
+b
+----
+a
+<!--foo--><!--More than 1 comment, still stripped-->
+b
+----
+a
+ <!--foo--> <!----> <!-- bar -->
+b
+----
+a
+<!--foo-->
+
+b
+----
+a
+
+<!--foo-->
+b
+----
+a
+<!--foo-->
+
+
+b
+----
+a
+
+
+<!--foo-->
+b
+----
+!! html
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+</p><p>b
+</p>
+<hr />
+<p>a
+</p><p>b
+</p>
+<hr />
+<p>a
+</p><p><br />
+b
+</p>
+<hr />
+<p>a
+</p><p><br />
+b
+</p>
+<hr />
+
+!! end
+
+!! test
+Paragraphs with newline spacing with non-empty white-space lines in between
+!! wikitext
+----
+a
+
+b
+----
+a
+
+
+b
+----
+!! html
+<hr />
+<p>a
+</p><p>b
+</p>
+<hr />
+<p>a
+</p><p><br />
+b
+</p>
+<hr />
+
+!! end
+
+!! test
+Paragraphs with newline spacing with non-empty mixed comment and white-space lines in between
+!! wikitext
+----
+a
+ <!--foo-->
+b
+----
+a
+ <!--foo--><!--More than 1 comment doesn't disable stripping of this line!-->
+b
+----
+a
+
+<!--foo-->
+ <!--bar-->
+b
+----
+a
+
+ <!--foo-->
+ <!--bar-->
+
+b
+----
+!! html
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+</p><p>b
+</p>
+<hr />
+<p>a
+</p><p><br />
+b
+</p>
+<hr />
+
+!! end
+
+!! test
+Extra newlines: More paragraphs with indented comment
+!! wikitext
+a
+
+ <!--boo-->
+
+b
+!! html
+<p>a
+</p><p><br />
+b
+</p>
+!!end
+
+!! test
+Extra newlines followed by heading
+!! wikitext
+a
+
+
+
+=b=
+[[a]]
+
+
+=b=
+!! html
+<p>a
+</p><p><br />
+</p>
+<h1><span class="mw-headline" id="b">b</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: b">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<p><a href="/index.php?title=A&amp;action=edit&amp;redlink=1" class="new" title="A (page does not exist)">a</a>
+</p><p><br />
+</p>
+<h1><span class="mw-headline" id="b_2">b</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: b">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+
+!! end
+
+!! test
+Extra newlines between heading and content are swallowed
+!! wikitext
+=b=
+
+
+
+[[a]]
+!! html
+<h1><span class="mw-headline" id="b">b</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: b">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<p><a href="/index.php?title=A&amp;action=edit&amp;redlink=1" class="new" title="A (page does not exist)">a</a>
+</p>
+!! end
+
+!! test
+Parsing an URL
+!! wikitext
+http://fr.wikipedia.org/wiki/🍺
+<!-- EasterEgg we love beer, better be able be able to link to it -->
+!! html
+<p><a rel="nofollow" class="external free" href="http://fr.wikipedia.org/wiki/🍺">http://fr.wikipedia.org/wiki/🍺</a>
+</p>
+!! end
+
+# Note that the html+tidy output removes the spaces after the <li>,
+# which is a bug (http://sourceforge.net/p/tidy/bugs/945/, etc).
+# This is an issue for all tests with lists. We intentionally do
+# *not* add html+tidy clauses for these, as we don't want to
+# document/test the broken behavior. (Parsoid matches the non-tidy
+# output in these cases.)
+
+!! test
+Simple list
+!! wikitext
+* Item 1
+* Item 2
+!! html
+<ul><li> Item 1</li>
+<li> Item 2</li></ul>
+
+!! end
+
+!! test
+Italics and bold
+!! wikitext
+* plain
+* plain''italic''plain
+* plain''italic''plain''italic''plain
+* plain'''bold'''plain
+* plain'''bold'''plain'''bold'''plain
+* plain''italic''plain'''bold'''plain
+* plain'''bold'''plain''italic''plain
+* plain''italic'''bold-italic'''italic''plain
+* plain'''bold''bold-italic''bold'''plain
+* plain'''''bold-italic'''italic''plain
+* plain'''''bold-italic''bold'''plain
+* plain''italic'''bold-italic'''''plain
+* plain'''bold''bold-italic'''''plain
+* plain l'''italic''plain
+* plain l''''bold''' plain
+!! html
+<ul><li> plain</li>
+<li> plain<i>italic</i>plain</li>
+<li> plain<i>italic</i>plain<i>italic</i>plain</li>
+<li> plain<b>bold</b>plain</li>
+<li> plain<b>bold</b>plain<b>bold</b>plain</li>
+<li> plain<i>italic</i>plain<b>bold</b>plain</li>
+<li> plain<b>bold</b>plain<i>italic</i>plain</li>
+<li> plain<i>italic<b>bold-italic</b>italic</i>plain</li>
+<li> plain<b>bold<i>bold-italic</i>bold</b>plain</li>
+<li> plain<i><b>bold-italic</b>italic</i>plain</li>
+<li> plain<b><i>bold-italic</i>bold</b>plain</li>
+<li> plain<i>italic<b>bold-italic</b></i>plain</li>
+<li> plain<b>bold<i>bold-italic</i></b>plain</li>
+<li> plain l'<i>italic</i>plain</li>
+<li> plain l'<b>bold</b> plain</li></ul>
+
+!! end
+
+# this example taken from the [[simple:Moon]] article (bug 47326)
+!! test
+Italics and possessives (1)
+!! wikitext
+obtained by ''[[Lunar Prospector]]'''s gamma-ray spectrometer
+!! html
+<p>obtained by <i><a href="/index.php?title=Lunar_Prospector&amp;action=edit&amp;redlink=1" class="new" title="Lunar Prospector (page does not exist)">Lunar Prospector</a>'</i>s gamma-ray spectrometer
+</p>
+!! end
+
+# this example taken from [[en:Flaming Pie]] (bug 49926)
+!! test
+Italics and possessives (2)
+!! wikitext
+'''''Flaming Pie''''' is ... released in 1997. In ''Flaming Pie'''s liner notes
+!! html
+<p><i><b>Flaming Pie</b></i> is ... released in 1997. In <i>Flaming Pie'</i>s liner notes
+</p>
+!! end
+
+# this example taken from [[en:Dictionary]] (bug 49926)
+!! test
+Italics and possessives (3)
+!! wikitext
+The first monolingual dictionary written in a Romance language was ''Sebastián Covarrubias''' ''Tesoro de la lengua castellana o española'', published in 1611 in Madrid. In 1612 the first edition of the ''Vocabolario dell'[[Accademia della Crusca]]'', for Italian, was published. In 1690 in Rotterdam was published, posthumously, the ''Dictionnaire Universel''.
+!! html
+<p>The first monolingual dictionary written in a Romance language was <i>Sebastián Covarrubias'</i> <i>Tesoro de la lengua castellana o española</i>, published in 1611 in Madrid. In 1612 the first edition of the <i>Vocabolario dell'<a href="/index.php?title=Accademia_della_Crusca&amp;action=edit&amp;redlink=1" class="new" title="Accademia della Crusca (page does not exist)">Accademia della Crusca</a></i>, for Italian, was published. In 1690 in Rotterdam was published, posthumously, the <i>Dictionnaire Universel</i>.
+</p>
+!! end
+
+
+###
+### 2-quote opening sequence tests
+###
+!! test
+Italics and bold: 2-quote opening sequence: (2,2)
+!! wikitext
+''foo''
+!! html
+<p><i>foo</i>
+</p>
+!!end
+
+
+!! test
+Italics and bold: 2-quote opening sequence: (2,3)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo'''
+!! html/*
+<p><i>foo'</i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 2-quote opening sequence: (2,3) w/ nowiki
+!! wikitext
+''<nowiki>foo'</nowiki>''
+!! html
+<p><i>foo'</i>
+</p>
+!! end
+
+
+!! test
+Italics and bold: 2-quote opening sequence: (2,4)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo''''
+!! html/*
+<p><i>foo''</i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 2-quote opening sequence: (2,4) w/ nowiki
+!! wikitext
+''<nowiki>foo''</nowiki>''
+!! html
+<p><i>foo''</i>
+</p>
+!! end
+
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: 2-quote opening sequence: (2,5)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo'''''
+!! html/php
+<p><i>foo</i>
+</p>
+!! html/parsoid
+<p><i>foo</i><b></b>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 2-quote opening sequence: (2,5+3) w/ nowiki
+!! wikitext
+''foo'''''<nowiki/>'''
+!! html/php
+<p><i>foo</i>
+</p>
+!! html/parsoid
+<p><i>foo</i><b></b>
+</p>
+!! end
+
+
+###
+### 3-quote opening sequence tests
+###
+
+!! test
+Italics and bold: 3-quote opening sequence: (3,2)
+!! wikitext
+'''foo''
+!! html
+<p>'<i>foo</i>
+</p>
+!!end
+
+
+!! test
+Italics and bold: 3-quote opening sequence: (3,3)
+!! wikitext
+'''foo'''
+!! html
+<p><b>foo</b>
+</p>
+!!end
+
+
+!! test
+Italics and bold: 3-quote opening sequence: (3,4)
+!! options
+parsoid=wt2html
+!! wikitext
+'''foo''''
+!! html/*
+<p><b>foo'</b>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 3-quote opening sequence: (3,4) w/ nowiki
+!! wikitext
+'''<nowiki>foo'</nowiki>'''
+!! html
+<p><b>foo'</b>
+</p>
+!! end
+
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: 3-quote opening sequence: (3,5)
+!! options
+parsoid=wt2html
+!! wikitext
+'''foo'''''
+!! html/php
+<p><b>foo</b>
+</p>
+!! html/parsoid
+<p><b>foo</b><i></i>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 3-quote opening sequence: (3,5+2) w/ nowiki
+!! wikitext
+'''foo'''''<nowiki/>''
+!! html/php
+<p><b>foo</b>
+</p>
+!! html/parsoid
+<p><b>foo</b><i></i>
+</p>
+!! end
+
+
+###
+### 4-quote opening sequence tests
+###
+
+!! test
+Italics and bold: 4-quote opening sequence: (4,2)
+!! options
+parsoid=wt2html
+!! wikitext
+''''foo''
+!! html/*
+<p>''<i>foo</i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 4-quote opening sequence: (4,2) w/ nowiki
+!! wikitext
+<nowiki>''</nowiki>''foo''
+!! html
+<p>''<i>foo</i>
+</p>
+!! end
+
+
+!! test
+Italics and bold: 4-quote opening sequence: (4,3)
+!! wikitext
+''''foo'''
+!! html
+<p>'<b>foo</b>
+</p>
+!!end
+
+
+!! test
+Italics and bold: 4-quote opening sequence: (4,4)
+!! options
+parsoid=wt2html
+!! wikitext
+''''foo''''
+!! html/*
+<p>'<b>foo'</b>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 4-quote opening sequence: (4,4) w/ nowiki
+!! wikitext
+''''<nowiki>foo'</nowiki>'''
+!! html
+<p>'<b>foo'</b>
+</p>
+!! end
+
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: 4-quote opening sequence: (4,5)
+!! options
+parsoid=wt2html
+!! wikitext
+''''foo'''''
+!! html/php
+<p>'<b>foo</b>
+</p>
+!! html/parsoid
+<p>'<b>foo</b><i></i>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 4-quote opening sequence: (4,5+2) w/ nowiki
+!! wikitext
+''''foo'''''<nowiki/>''
+!! html/php
+<p>'<b>foo</b>
+</p>
+!! html/parsoid
+<p>'<b>foo</b><i></i>
+</p>
+!! end
+
+
+###
+### 5-quote opening sequence tests
+###
+
+!! test
+Italics and bold: 5-quote opening sequence: (5,2)
+!! options
+parsoid=wt2html
+!! wikitext
+'''''foo''
+!! html/*
+<p><b><i>foo</i></b>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+# skipping wt2html and html2html because it wants to put <i> before <b>
+!! test
+Italics and bold: 5-quote opening sequence: (5,2+3)
+!! options
+parsoid=wt2wt,html2wt
+!! wikitext
+'''''foo'''''
+!! html
+<p><b><i>foo</i></b>
+</p>
+!! end
+
+!! test
+Italics and bold: 5-quote opening sequence: (5,3)
+!! options
+parsoid=wt2html
+!! wikitext
+'''''foo'''
+!! html/*
+<p><i><b>foo</b></i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 5-quote opening sequence: (5,3+2)
+!! wikitext
+'''''foo'''''
+!! html
+<p><i><b>foo</b></i>
+</p>
+!! end
+
+
+!! test
+Italics and bold: 5-quote opening sequence: (5,4)
+!! options
+parsoid=wt2html
+!! wikitext
+'''''foo''''
+!! html/*
+<p><i><b>foo'</b></i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 5-quote opening sequence: (5,4+2) w/ nowiki
+!! wikitext
+'''''<nowiki>foo'</nowiki>'''''
+!! html
+<p><i><b>foo'</b></i>
+</p>
+!! end
+
+
+!! test
+Italics and bold: 5-quote opening sequence: (5,5)
+!! wikitext
+'''''foo'''''
+!! html
+<p><i><b>foo</b></i>
+</p>
+!!end
+
+###
+### multiple quote sequences in a line
+###
+!! test
+Italics and bold: multiple quote sequences: (2,4,2)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo''''bar''
+!! html/*
+<p><i>foo'<b>bar</b></i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (2,4,2+3) w/ nowiki
+!! wikitext
+''<nowiki>foo'</nowiki>'''bar'''''
+!! html
+<p><i>foo'<b>bar</b></i>
+</p>
+!! end
+
+
+!! test
+Italics and bold: multiple quote sequences: (2,4,3)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo''''bar'''
+!! html/*
+<p><i>foo'<b>bar</b></i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (2,4,3+2) w/ nowiki
+!! wikitext
+''<nowiki>foo'</nowiki>'''bar'''''
+!! html
+<p><i>foo'<b>bar</b></i>
+</p>
+!! end
+
+
+!! test
+Italics and bold: multiple quote sequences: (2,4,4)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo''''bar''''
+!! html/*
+<p><i>foo'<b>bar'</b></i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (2,4,4+2) w/ nowiki
+!! wikitext
+''<nowiki>foo'</nowiki>'''<nowiki>bar'</nowiki>'''''
+!! html
+<p><i>foo'<b>bar'</b></i>
+</p>
+!! end
+
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: multiple quote sequences: (3,4,2)
+!! options
+parsoid=wt2html
+!! wikitext
+'''foo''''bar''
+!! html/php
+<p><b>foo'</b>bar
+</p>
+!! html/parsoid
+<p><b>foo'</b>bar<i></i>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (3,4,2+2) w/ nowiki
+!! options
+parsoid
+!! wikitext
+'''<nowiki>foo'</nowiki>'''bar''<nowiki/>''
+!! html/php
+<p><b>foo'</b>bar
+</p>
+!! html/parsoid
+<p><b><span typeof="mw:Nowiki">foo'</span></b>bar<i></i>
+</p>
+!! end
+
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: multiple quote sequences: (3,4,3)
+!! options
+parsoid=wt2html
+!! wikitext
+'''foo''''bar'''
+!! html/php
+<p><b>foo'</b>bar
+</p>
+!! html/parsoid
+<p><b>foo'</b>bar<b></b>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (3,4,3+3) w/ nowiki
+!! wikitext
+'''<nowiki>foo'</nowiki>'''bar'''<nowiki/>'''
+!! html/php
+<p><b>foo'</b>bar
+</p>
+!! html/parsoid
+<p><b><span typeof="mw:Nowiki">foo'</span></b>bar<b></b>
+</p>
+!! end
+
+###
+### other quote tests
+###
+!! test
+Italics and bold: other quote tests: (2,3,5)
+!! wikitext
+''this is about '''foo's family'''''
+!! html
+<p><i>this is about <b>foo's family</b></i>
+</p>
+!!end
+
+
+!! test
+Italics and bold: other quote tests: (2,(3,3),2)
+!! wikitext
+''this is about '''foo's''' family''
+!! html
+<p><i>this is about <b>foo's</b> family</i>
+</p>
+!!end
+
+
+!! test
+Italics and bold: other quote tests: (3,2,3,2)
+!! options
+parsoid=wt2html
+!! wikitext
+'''this is about ''foo'''s family''
+!! html/*
+<p><b>this is about <i>foo</i></b><i>s family</i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+# add 'parsoid' option to use 'parsoid' normalization of the placeholder
+!! test
+Italics and bold: other quote tests: (3,2,3+2+2,2)
+!! options
+parsoid
+!! wikitext
+'''this is about ''foo'''''<nowiki/>''s family''
+!! html/*
+<p><b>this is about <i>foo</i></b><i>s family</i>
+</p>
+!! end
+
+
+!! test
+Italics and bold: other quote tests: (3,2,3,3)
+!! options
+!! wikitext
+'''this is about ''foo'''s family'''
+!! html
+<p>'<i>this is about </i>foo<b>s family</b>
+</p>
+!!end
+
+
+!! test
+Italics and bold: other quote tests: (3,(2,2),3)
+!! wikitext
+'''this is about ''foo's'' family'''
+!! html
+<p><b>this is about <i>foo's</i> family</b>
+</p>
+!!end
+
+
+!! test
+Italicized possessive
+!! wikitext
+The ''[[Main Page]]'''s talk page.
+!! html
+<p>The <i><a href="/wiki/Main_Page" title="Main Page">Main Page</a>'</i>s talk page.
+</p>
+!! end
+
+!! test
+Parsoid only: Quote balancing context should be restricted to td/th cells on the same wikitext line
+(Requires tidy for PHP parser output to be fixed up)
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+{|
+!''a!!''b
+|''a||''b
+|}
+!! html/php+tidy
+<table>
+<tr>
+<th><i>a</i></th>
+<th><i>b</i></th>
+<td><i>a</i></td>
+<td><i>b</i></td>
+</tr>
+</table>
+!! html/parsoid
+<table>
+<tbody><tr><th><i>a</i></th><th><i>b</i></th>
+<td><i>a</i></td><td><i>b</i></td></tr>
+</tbody></table>
+!! end
+
+###
+### Non-html5 tags
+###
+
+!! test
+Non-html5 tags should be accepted
+!! wikitext
+<center>''foo''</center>
+<big>''foo''</big>
+<font>''foo''</font>
+<strike>''foo''</strike>
+<tt>''foo''</tt>
+!! html
+<center><i>foo</i></center>
+<p><big><i>foo</i></big>
+<font><i>foo</i></font>
+<strike><i>foo</i></strike>
+<tt><i>foo</i></tt>
+</p>
+!! end
+
+!! test
+<wbr> is valid wikitext (bug 52468)
+!! wikitext
+<wbr>
+!! html
+<p><wbr />
+</p>
+!! end
+
+# <strike> is HTML4, <s> is HTML4/5.
+!! test
+<s> or <strike> for strikethrough
+!! wikitext
+<strike>strike</strike>
+
+<s>s</s>
+!! html
+<p><strike>strike</strike>
+</p><p><s>s</s>
+</p>
+!! end
+
+## a not permitted
+## i,b,br omitted
+!! test
+Text-level semantic html elements in wikitext
+!! wikitext
+<em>text</em>
+<strong>text</strong>
+<small>text</small>
+<s>text</s>
+<cite>text</cite>
+<q>text</q>
+<dfn>text</dfn>
+<abbr>text</abbr>
+<data>text</data>
+<time>text</time>
+<code>text</code>
+<var>text</var>
+<samp>text</samp>
+<kbd>text</kbd>
+<sub>text</sub>
+<u>text</u>
+<mark>text</mark>
+<ruby><rb>明日</rb><rp>(</rp><rt>Ashita</rt><rp> </rp><rtc>あした</rtc><rp>)</rp></ruby>
+<bdi>text</bdi>
+<bdo>text</bdo>
+<span>text</span>
+<wbr />
+!! html
+<p><em>text</em>
+<strong>text</strong>
+<small>text</small>
+<s>text</s>
+<cite>text</cite>
+<q>text</q>
+<dfn>text</dfn>
+<abbr>text</abbr>
+<data>text</data>
+<time>text</time>
+<code>text</code>
+<var>text</var>
+<samp>text</samp>
+<kbd>text</kbd>
+<sub>text</sub>
+<u>text</u>
+<mark>text</mark>
+<ruby><rb>明日</rb><rp>(</rp><rt>Ashita</rt><rp> </rp><rtc>あした</rtc><rp>)</rp></ruby>
+<bdi>text</bdi>
+<bdo>text</bdo>
+<span>text</span>
+<wbr />
+</p>
+!! end
+
+# test cases taken from
+# http://www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element
+!! test
+Ruby markup (W3C-style)
+!! wikitext
+; Mono-ruby for individual base characters
+: <ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby>
+; Group ruby
+: <ruby>今日<rt>きょう</rt></ruby>
+; Jukugo ruby
+: <ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby>
+; Inline ruby
+: <ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby>
+; Double-sided ruby
+: <ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby>
+<ruby>
+<rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang="fr"><rt>Cœur</rt></rtc>
+<rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang="fr"><rt>Trèfle</rt></rtc>
+<rb>✶</rb><rtc><rt>Star</rt></rtc><rtc lang="fr"><rt>Étoile</rt></rtc>
+</ruby>
+!! html
+<dl><dt> Mono-ruby for individual base characters</dt>
+<dd> <ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby></dd>
+<dt> Group ruby</dt>
+<dd> <ruby>今日<rt>きょう</rt></ruby></dd>
+<dt> Jukugo ruby</dt>
+<dd> <ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby></dd>
+<dt> Inline ruby</dt>
+<dd> <ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby></dd>
+<dt> Double-sided ruby</dt>
+<dd> <ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby></dd></dl>
+<p><ruby>
+<rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang="fr"><rt>Cœur</rt></rtc>
+<rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang="fr"><rt>Trèfle</rt></rtc>
+<rb>✶</rb><rtc><rt>Star</rt></rtc><rtc lang="fr"><rt>Étoile</rt></rtc>
+</ruby>
+</p>
+!! end
+
+# There is a tidy bug here: http://sourceforge.net/p/tidy/bugs/946/
+!! test
+Non-word characters don't terminate tag names (bug 17663, 40670, 52022)
+!! wikitext
+<b→> doesn't work! </b→>
+
+<bä> doesn't work! </bä>
+
+<boo> works fine </boo>
+
+<s.foo>s.foo</s.foo>
+
+<sub-ID#1>
+!! html
+<p>&lt;b→&gt; doesn't work! &lt;/b→&gt;
+</p><p>&lt;bä&gt; doesn't work! &lt;/bä&gt;
+</p><p>&lt;boo&gt; works fine &lt;/boo&gt;
+</p><p>&lt;s.foo&gt;s.foo&lt;/s.foo&gt;
+</p><p>&lt;sub-ID#1&gt;
+</p>
+!! end
+
+!! test
+Isolated close tags should be treated as literal text (bug 52760)
+!! wikitext
+</b>
+
+<s.foo>s</s>
+!! html
+<p>&lt;/b&gt;
+</p><p>&lt;s.foo&gt;s&lt;/s&gt;
+</p>
+!! end
+
+###
+### Special characters
+###
+
+!! test
+Bare pipe character (bug 52363)
+!! wikitext
+|
+!! html
+<p>|
+</p>
+!! end
+
+!! test
+Bare pipe character from a template (bug 52363)
+!! wikitext
+{{pipe}}
+!! html
+<p>|
+</p>
+!! end
+
+###
+### <nowiki> test cases
+###
+
+!! test
+<nowiki> unordered list
+!! wikitext
+<nowiki>* This is not an unordered list item.</nowiki>
+!! html
+<p>* This is not an unordered list item.
+</p>
+!! end
+
+!! test
+<nowiki> spacing
+!! wikitext
+<nowiki>Lorem ipsum dolor
+
+sed abit.
+ sed nullum.
+
+:and a colon
+</nowiki>
+!! html
+<p>Lorem ipsum dolor
+
+sed abit.
+ sed nullum.
+
+:and a colon
+
+</p>
+!! end
+
+!! test
+nowiki 3
+!! wikitext
+:There is not nowiki.
+:There is <nowiki>nowiki</nowiki>.
+
+#There is not nowiki.
+#There is <nowiki>nowiki</nowiki>.
+
+*There is not nowiki.
+*There is <nowiki>nowiki</nowiki>.
+!! html
+<dl><dd>There is not nowiki.</dd>
+<dd>There is nowiki.</dd></dl>
+<ol><li>There is not nowiki.</li>
+<li>There is nowiki.</li></ol>
+<ul><li>There is not nowiki.</li>
+<li>There is nowiki.</li></ul>
+
+!! end
+
+!! test
+Entities inside <nowiki>
+!! wikitext
+<nowiki>&lt;</nowiki>
+!! html
+<p>&lt;
+</p>
+!! end
+
+!! test
+Entities inside template parameters
+!! options
+parsoid
+!! wikitext
+{{echo|&ndash;}}
+!! html
+<p><span typeof="mw:Transclusion mw:Entity" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&amp;ndash;"}},"i":0}}]}'>&ndash;</span>
+</p>
+!! end
+
+!! test
+Properly escape nowiki when combined with other wiki markup
+!! options
+parsoid=html2wt
+!! wikitext
+<nowiki>* &lt;/nowiki&gt;</nowiki> tag
+!! html
+<p>* &lt;/nowiki&gt; tag</p>
+!! end
+
+###
+### Comments
+###
+!! test
+Comments and Indent-Pre
+!! wikitext
+<!-- comment 1 --> asdf
+
+<!-- comment 1 --> asdf
+<!-- comment 2 -->
+
+<!-- comment 1 --> asdf
+<!-- comment 2 -->xyz
+
+<!-- comment 1 --> asdf
+<!-- comment 2 --> xyz
+!! html
+<pre>asdf
+</pre>
+<pre>asdf
+</pre>
+<pre>asdf
+</pre>
+<p>xyz
+</p>
+<pre>asdf
+xyz
+</pre>
+!! end
+
+!! test
+Comment test 2a
+!! wikitext
+asdf
+<!-- comment 1 -->
+jkl
+!! html
+<p>asdf
+jkl
+</p>
+!! end
+
+!! test
+Comment test 2b
+!! wikitext
+asdf
+<!-- comment 1 -->
+
+jkl
+!! html
+<p>asdf
+</p><p>jkl
+</p>
+!! end
+
+!! test
+Comment test 3
+!! wikitext
+asdf
+<!-- comment 1 -->
+<!-- comment 2 -->
+jkl
+!! html
+<p>asdf
+jkl
+</p>
+!! end
+
+!! test
+Comment test 4
+!! wikitext
+asdf<!-- comment 1 -->jkl
+!! html
+<p>asdfjkl
+</p>
+!! end
+
+!! test
+Comment spacing
+!! wikitext
+a
+ <!-- foo --> b <!-- bar -->
+c
+!! html
+<p>a
+</p>
+<pre> b
+</pre>
+<p>c
+</p>
+!! end
+
+!! test
+Comment whitespace
+!! wikitext
+<!-- returns a single newline, not nothing, since the newline after > is not stripped -->
+!! html
+
+!! end
+
+!! test
+Comment semantics and delimiters
+!! wikitext
+<!-- --><!----><!-----><!------>
+!! html
+
+!! end
+
+!! test
+Comment semantics and delimiters, redux
+!! wikitext
+<!-- In SGML every "foo" here would actually show up in the text -- foo -- bar
+-- foo -- funky huh? ... -->
+!! html
+
+!! end
+
+!! test
+Comment semantics and delimiters: directors cut
+!! wikitext
+<!-- ... However we like to keep things simple and somewhat XML-ish so we eat
+everything starting with < followed by !-- until the first -- and > we see,
+that wouldn't be valid XML however, since in XML -- has to terminate a comment
+-->-->
+!! html
+<p>--&gt;
+</p>
+!! end
+
+!! test
+Comment semantics: nesting
+!! wikitext
+<!--<!-- no, we're not going to do anything fancy here -->-->
+!! html
+<p>--&gt;
+</p>
+!! end
+
+!! test
+Comment semantics: unclosed comment at end
+!! wikitext
+<!--This comment will run out to the end of the document
+!! html
+
+!! end
+
+!! test
+Comment in template title
+!! wikitext
+{{f<!---->oo}}
+!! html
+<p>FOO
+</p>
+!! end
+
+!! test
+Comment on its own line post-expand
+!! wikitext
+a
+{{blank}}<!---->
+b
+!! html
+<p>a
+</p><p>b
+</p>
+!! end
+
+!! test
+Comment on its own line post-expand with non-significant whitespace
+!! wikitext
+a
+ {{blank}} <!---->
+b
+!! html
+<p>a
+</p><p>b
+</p>
+!! end
+
+!! test
+Multiple comments should still parse as SOL-transparent
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+<!--c1-->*a
+<!--c2--><!--c3--><!--c4-->*b
+!! html
+<ul>
+<li>a
+</li>
+<li>b
+</li>
+</ul>
+!! end
+
+###
+### paragraph wrapping tests
+###
+!! test
+No block tags
+!! wikitext
+a
+
+b
+!! html
+<p>a
+</p><p>b
+</p>
+!! end
+
+!! test
+Block tag on one line (<div>)
+!! wikitext
+a <div>foo</div>
+
+b
+!! html
+a <div>foo</div>
+<p>b
+</p>
+!! html+tidy
+<p>a</p>
+<div>foo</div>
+<p>b</p>
+!! end
+
+!! test
+Block tag on one line (<blockquote>)
+!! wikitext
+a <blockquote>foo</blockquote>
+
+b
+!! html
+a <blockquote>foo</blockquote>
+<p>b
+</p>
+!! html+tidy
+<p>a</p>
+<blockquote>
+<p>foo</p>
+</blockquote>
+<p>b</p>
+!! end
+
+!! test
+Block tag on both lines (<div>)
+!! wikitext
+a <div>foo</div>
+
+b <div>foo</div>
+!! html
+a <div>foo</div>
+b <div>foo</div>
+
+!! html+tidy
+<p>a</p>
+<div>foo</div>
+<p>b</p>
+<div>foo</div>
+!! end
+
+!! test
+Block tag on both lines (<blockquote>)
+!! wikitext
+a <blockquote>foo</blockquote>
+
+b <blockquote>foo</blockquote>
+!! html
+a <blockquote>foo</blockquote>
+b <blockquote>foo</blockquote>
+
+!! html+tidy
+<p>a</p>
+<blockquote>
+<p>foo</p>
+</blockquote>
+<p>b</p>
+<blockquote>
+<p>foo</p>
+</blockquote>
+!! end
+
+!! test
+Multiple lines without block tags
+!! wikitext
+<div>foo</div> a
+b
+c
+d<!--foo--> e
+x <div>foo</div> z
+!! html
+<div>foo</div> a
+<p>b
+c
+d e
+</p>
+x <div>foo</div> z
+
+!! html+tidy
+<div>foo</div>
+<p>a</p>
+<p>b c d e</p>
+<p>x</p>
+<div>foo</div>
+<p>z</p>
+!! end
+
+!! test
+Empty lines between lines with block tags
+!! wikitext
+<div></div>
+
+
+<div></div>a
+
+b
+<div>a</div>b
+
+<div>b</div>d
+
+
+<div>e</div>
+!! html
+<div></div>
+<p><br />
+</p>
+<div></div>a
+<p>b
+</p>
+<div>a</div>b
+<div>b</div>d
+<p><br />
+</p>
+<div>e</div>
+
+!! html+tidy
+<p><br /></p>
+<p>a</p>
+<p>b</p>
+<div>a</div>
+<p>b</p>
+<div>b</div>
+<p>d</p>
+<p><br /></p>
+<div>e</div>
+!! end
+
+## PHP parser emits output which is broken
+## XXX The parsoid output doesn't match the tidy output.
+!! test
+Unclosed HTML p-tags should be handled properly
+!! wikitext
+<div><p>foo</div>
+a
+
+b
+!! html/php+tidy
+<div>
+<p>foo&lt;/div&gt;</p>
+<p>a</p>
+b</div>
+!! html/parsoid
+<div data-parsoid='{"stx":"html"}'><p data-parsoid='{"stx":"html", "autoInsertedEnd":true}'>foo</p></div>
+<p>a</p>
+<p>b</p>
+!! end
+
+###
+### Preformatted text
+###
+!! test
+Preformatted text
+!! wikitext
+ This is some
+ Preformatted text
+ With ''italic''
+ And '''bold'''
+ And a [[Main Page|link]]
+!! html
+<pre>This is some
+Preformatted text
+With <i>italic</i>
+And <b>bold</b>
+And a <a href="/wiki/Main_Page" title="Main Page">link</a>
+</pre>
+!! end
+
+!! test
+Tabs don't trigger preformatted text
+!! wikitext
+ This is not
+ preformatted text.
+ This is preformatted text.
+ So is this.
+!! html
+<p> This is not
+ preformatted text.
+</p>
+<pre>This is preformatted text.
+ So is this.
+</pre>
+!! end
+
+!! test
+Ident preformatting with inline content
+!! wikitext
+ a
+ ''b''
+!! html
+<pre>a
+<i>b</i>
+</pre>
+!! end
+
+!! test
+<pre> with <nowiki> inside (compatibility with 1.6 and earlier)
+!! wikitext
+<pre><nowiki>
+<b>
+<cite>
+<em>
+</nowiki></pre>
+!! html
+<pre>
+&lt;b&gt;
+&lt;cite&gt;
+&lt;em&gt;
+</pre>
+
+!! end
+
+!! test
+Regression with preformatted in <center>
+!! wikitext
+<center>
+ Blah
+</center>
+!! html
+<center>
+<pre>Blah
+</pre>
+</center>
+
+!! end
+
+!! test
+Bug 52763: Preformatted in <blockquote>
+!! wikitext
+<blockquote>
+ Blah
+{|
+|
+ indented cell (no pre-wrapping!)
+|}
+</blockquote>
+!! html
+<blockquote>
+<p> Blah
+</p>
+<table>
+<tr>
+<td>
+<p> indented cell (no pre-wrapping!)
+</p>
+</td></tr></table>
+</blockquote>
+
+!! end
+
+!! test
+Bug 51086: Double newlines in blockquotes should be turned into paragraphs
+!! wikitext
+<blockquote>
+Foo
+
+Bar
+</blockquote>
+!! html
+<blockquote>
+<p>Foo
+</p><p>Bar
+</p>
+</blockquote>
+
+!! end
+
+!! test
+Bug 15491: <ins>/<del> in blockquote
+!! wikitext
+<blockquote>
+Foo <del>bar</del> <ins>baz</ins> quux
+</blockquote>
+!! html
+<blockquote>
+<p>Foo <del>bar</del> <ins>baz</ins> quux
+</p>
+</blockquote>
+
+!! end
+
+# Note that the p-wrapping is newline sensitive, which could be
+# considered a bug: tidy will wrap only the 'Foo' in the example
+# below in a <p> tag. (see comment 23-25 of bug #6200)
+!! test
+Bug 15491: <ins>/<del> in blockquote (2)
+!! wikitext
+<blockquote>Foo <del>bar</del> <ins>baz</ins> quux
+</blockquote>
+!! html
+<blockquote>Foo <del>bar</del> <ins>baz</ins> quux
+</blockquote>
+
+!! html+tidy
+<blockquote>
+<p>Foo</p>
+<del>bar</del> <ins>baz</ins> quux</blockquote>
+!! end
+
+!! test
+<pre> with attributes (bug 3202)
+!! wikitext
+<pre style="background: blue; color:white">Bluescreen of WikiDeath</pre>
+!! html
+<pre style="background: blue; color:white">Bluescreen of WikiDeath</pre>
+
+!! end
+
+!! test
+<pre> with width attribute (bug 3202)
+!! wikitext
+<pre width="8">Narrow screen goodies</pre>
+!! html
+<pre width="8">Narrow screen goodies</pre>
+
+!! end
+
+!! test
+<pre> with forbidden attribute (bug 3202)
+!! wikitext
+<pre width="8" onmouseover="alert(document.cookie)">Narrow screen goodies</pre>
+!! html
+<pre width="8">Narrow screen goodies</pre>
+
+!! end
+
+!! test
+Entities inside <pre>
+!! wikitext
+<pre>&lt;</pre>
+!! html
+<pre>&lt;</pre>
+
+!! end
+
+!! test
+<pre> with forbidden attribute values (bug 3202)
+!! wikitext
+<pre width="8" style="border-width: expression(alert(document.cookie))">Narrow screen goodies</pre>
+!! html
+<pre width="8" style="/* insecure input */">Narrow screen goodies</pre>
+
+!! end
+
+!! test
+<nowiki> inside <pre> (bug 13238)
+!! wikitext
+<pre>
+<nowiki>
+</pre>
+<pre>
+<nowiki></nowiki>
+</pre>
+<pre><nowiki><nowiki></nowiki>Foo<nowiki></nowiki></nowiki></pre>
+!! html
+<pre>
+&lt;nowiki&gt;
+</pre>
+<pre>
+
+</pre>
+<pre>&lt;nowiki&gt;Foo&lt;/nowiki&gt;</pre>
+
+!! end
+
+!! test
+<nowiki> and <pre> preference (first one wins)
+!! wikitext
+<pre>
+<nowiki>
+</pre>
+</nowiki>
+</pre>
+
+<nowiki>
+<pre>
+<nowiki>
+</pre>
+</nowiki>
+</pre>
+
+!! html
+<pre>
+&lt;nowiki&gt;
+</pre>
+<p>&lt;/nowiki&gt;
+&lt;/pre&gt;
+</p><p>
+&lt;pre&gt;
+&lt;nowiki&gt;
+&lt;/pre&gt;
+
+&lt;/pre&gt;
+</p>
+!! end
+
+!! test
+</pre> inside nowiki
+!! wikitext
+<nowiki></pre></nowiki>
+!! html
+<p>&lt;/pre&gt;
+</p>
+!! end
+
+!! test
+Empty pre; pre inside other HTML tags (bug 54946)
+!! wikitext
+a
+
+<div><pre>
+foo
+</pre></div>
+<pre></pre>
+!! html
+<p>a
+</p>
+<div><pre>
+foo
+</pre></div>
+<pre></pre>
+
+!! html+tidy
+<p>a</p>
+<div>
+<pre>
+foo
+</pre></div>
+!! end
+
+!! test
+HTML pre followed by indent-pre
+!! wikitext
+<pre>foo</pre>
+ bar
+!! html
+<pre>foo</pre>
+<pre>bar
+</pre>
+!! end
+
+!!test
+Block tag pre
+!!options
+parsoid
+!! wikitext
+<p><pre>foo</pre></p>
+!! html
+<p data-parsoid='{"stx":"html","autoInsertedEnd":true}'></p><pre data-parsoid='{"stx":"html"}'>foo</pre><p data-parsoid='{"autoInsertedStart":true,"stx":"html"}'></p>
+!!end
+
+!!test
+Templates: Indent-Pre: 1a. Templates that break a line should suppress <pre>
+!! wikitext
+ {{echo|}}
+!! html
+
+!!end
+
+!!test
+Templates: Indent-Pre: 1b. Templates that break a line should suppress <pre>
+!! wikitext
+ {{echo|
+foo}}
+!! html
+<p>foo
+</p>
+!!end
+
+!! test
+Templates: Indent-Pre: 1c: Wrapping should be based on expanded content
+!! wikitext
+ {{echo|a
+b}}
+!! html
+<pre>a
+</pre>
+<p>b
+</p>
+!!end
+
+!! test
+Templates: Indent-Pre: 1d: Wrapping should be based on expanded content
+!! wikitext
+ {{echo|a
+b
+c
+ d
+e
+}}
+!! html
+<pre>a
+</pre>
+<p>b
+c
+</p>
+<pre>d
+</pre>
+<p>e
+</p>
+!!end
+
+!!test
+Templates: Indent-Pre: 1e. Wrapping should be based on expanded content
+!! wikitext
+{{echo| foo}}
+
+{{echo| foo}}{{echo| bar}}
+
+{{echo| foo}}
+{{echo| bar}}
+
+{{echo|<!--cmt--> foo}}
+
+<!--cmt-->{{echo| foo}}
+
+{{echo|{{echo| }}bar}}
+!! html
+<pre>foo
+</pre>
+<pre>foo bar
+</pre>
+<pre>foo
+bar
+</pre>
+<pre>foo
+</pre>
+<pre>foo
+</pre>
+<pre>bar
+</pre>
+!!end
+
+!! test
+Templates: Indent-Pre: 1f: Wrapping should be based on expanded content
+!! wikitext
+{{echo| }}a
+
+{{echo|
+ }}a
+
+{{echo|
+ b}}
+
+{{echo|a
+ }}b
+
+{{echo|a
+}} b
+!! html
+<pre>a
+</pre>
+<p><br />
+</p>
+<pre>a
+</pre>
+<p><br />
+</p>
+<pre>b
+</pre>
+<p>a
+</p>
+<pre>b
+</pre>
+<p>a
+</p>
+<pre>b
+</pre>
+!!end
+
+!! test
+Things that look like <pre> tags aren't treated as such
+!! wikitext
+Barack Obama <President> of the United States
+<President></President>
+!! html
+<p>Barack Obama &lt;President&gt; of the United States
+&lt;President&gt;&lt;/President&gt;
+</p>
+!! end
+
+## PHP parser discards the "<pre " string
+!! test
+Handle broken pre-like tags (bug 64025)
+!! options
+parsoid=wt2html
+!! wikitext
+{{echo|<pre <pre>x</pre>}}
+
+<table><pre </table>
+!! html/php
+<pre>x</pre>
+<table><pre></pre></table>
+
+!! html/parsoid
+<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;pre &lt;pre>x&lt;/pre>"}},"i":0}}]}'>&lt;pre </span>
+<pre>x</pre>
+
+<span>&lt;pre </span>
+<table></table>
+!! end
+
+!! test
+Parsoid: handle pre with space after attribute
+!! options
+parsoid=wt2html
+!! wikitext
+<pre style="width:50%;" >{{echo|foo}}</pre>
+!! html
+<pre style="width:50%;">{{echo|foo}}</pre>
+!! end
+
+# TODO / maybe: fix wt2wt for this
+!! test
+Parsoid: Don't paragraph-wrap fosterable content
+!! options
+parsoid=wt2html
+!! wikitext
+{|
+<td></td>
+<td></td>
+
+
+
+|}
+!! html
+<table>
+
+<tbody>
+<tr>
+<td></td>
+
+<td></td></tr>
+
+
+
+</tbody></table>
+!! end
+
+!! test
+Parsoid: Don't paragraph-wrap fosterable content even if table syntax is unbalanced
+!! options
+parsoid=wt2html
+!! wikitext
+{|
+<td>
+<td>
+</td>
+
+
+
+|}
+!! html
+<table>
+
+<tbody>
+<tr>
+<td></td>
+
+<td>
+</td></tr>
+
+
+
+</tbody></table>
+!! end
+
+
+#--------------------------------------------------------------------
+# Transclusion parameter whitespace stripping tests
+# Behavior is different for positional and named parameters
+#--------------------------------------------------------------------
+!! test
+Templates: Strip leading and trailing whitespace from named-param values
+!! wikitext
+{{echo|1= a }}
+
+{{echo|1= {{echo|b}} }}
+
+{{echo| 1 =
+ c }}
+
+{{echo| 1 =
+* d
+}}
+!! html
+<p>a
+</p><p>b
+</p><p>c
+</p>
+<ul><li> d</li></ul>
+
+!! end
+
+!! test
+Templates: Don't strip whitespace from positional-param values
+!! wikitext
+{{echo|a }}
+
+{{echo|{{echo|b}} }}
+
+{{echo| c
+}}
+
+{{echo| {{echo|d}}
+}}
+
+{{echo|
+ e}}
+
+{{echo|
+* f}}
+
+{{echo|
+ }}g
+!! html
+<p>a
+</p><p>b
+</p>
+<pre>c
+</pre>
+<p><br />
+</p>
+<pre>d
+</pre>
+<p><br />
+</p>
+<pre>e
+</pre>
+<p><br />
+</p>
+<ul><li> f</li></ul>
+<p><br />
+</p>
+<pre>g
+</pre>
+!! end
+
+!! test
+Templates: Handle empty comment-and-ws-only lines correctly
+!! wikitext
+{{echo|foo
+<!--should be ignored-->
+ <!--should be ignored as well-->
+bar}}
+!! html
+<p>foo
+bar
+</p>
+!! end
+
+!! test
+Templates: Handle comments in the target
+!! wikitext
+{{echo
+<!-- should be ignored -->
+|foo}}
+
+{{echo<!-- should be ignored -->
+|foo}}
+
+{{echo<!-- should be ignored -->|foo}}
+
+{{<!-- should be ignored -->echo|foo}}
+!!html/parsoid
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo\n&lt;!-- should be ignored -->\n","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo&lt;!-- should be ignored -->\n","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo&lt;!-- should be ignored -->","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+!!end
+
+#--------------------------------------------------------------------
+# Transclusion parameter escaping tests
+#--------------------------------------------------------------------
+!! test
+Templates: Parsoid parameter escaping test 1
+!! options
+parsoid
+!! wikitext
+{{echo|[foo]|{{echo|[bar]}}}}
+!! html
+<p about="#mwt1" typeof="mw:Transclusion"
+data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[foo]"},"2":{"wt":"{{echo|[bar]}}"}},"i":0}}]}'>[foo]</p>
+!! end
+
+!! test
+Parsoid: Pipes in external links in template parameter
+!! options
+parsoid
+!! wikitext
+{{echo|[{{echo|http://example.com}} link]}}
+!! html
+<p><a rel="mw:ExtLink" href="http://example.com" about="#mwt31" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{echo|http://example.com}} link]"}},"i":0}}]}'>link</a></p>
+!! end
+
+!! test
+Parsoid: pipe in transclusion parameter
+!! options
+parsoid
+!! wikitext
+{{echo|http://foo.com/a&#124;b}}
+!! html
+<p><a rel="mw:ExtLink" href="http://foo.com/a|b" about="#mwt1"
+typeof="mw:Transclusion"
+data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"http://foo.com/a&amp;#124;b"}},"i":0}}]}'>http://foo.com/a|b</a></p>
+!! end
+
+!! test
+Parsoid: Pipe in external link target and content in template parameter
+!! options
+parsoid=html2wt,wt2wt
+!! wikitext
+{{echo|[http://foo.com/a&#124;b a&#124;b]}}
+!! html
+<p><a rel="mw:ExtLink" href="http://foo.com/a|b" about="#mwt1"
+typeof="mw:Transclusion"
+data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},
+"params":{"1":{"wt":"[http://foo.com/a|b a|b]"}},"i":0}}]}'>a|b</a></p>
+!! end
+
+!! test
+Parsoid: Pipe in template with nested template in external link target in template parameter (seriously)
+!! options
+parsoid
+!! wikitext
+{{echo|[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]}}
+!! html
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]"}},"i":0}}]}'>[Main Page bar]</p>
+!! end
+
+!! test
+Templates: Don't escape already nowiki-escaped text in template parameters
+!! options
+parsoid=html2wt,wt2wt
+!! wikitext
+{{echo|foo<nowiki>|</nowiki>bar}}
+{{echo|<nowiki>&lt;div&gt;</nowiki>}}
+{{echo|<nowiki></nowiki>}}
+!! html
+<p><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo<nowiki>|</nowiki>bar"}},"i":0}}]}'}'>foo</span><span typeof="mw:Nowiki" about="#mwt1">|</span><span about="#mwt1">bar</span>
+<span typeof="mw:Transclusion mw:Nowiki" about="#mwt2" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<nowiki>&amp;lt;div&amp;gt;</nowiki>"}},"i":0}}]}'><span typeof="mw:Entity">&lt;</span>div<span typeof="mw:Entity">&gt;</span></span>
+<span typeof="mw:Transclusion mw:Nowiki" about="#mwt3" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<nowiki></nowiki>"}},"i":0}}]}'></span>
+</p>
+!! end
+
+## Bug 52824
+!! test
+Templates: '=' char in nested transclusions should not trigger nowiki escapes or conversion to named param
+!! options
+parsoid=html2wt,wt2wt
+!! wikitext
+{{echo|{{echo|1=bar}}}}
+!! html
+<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{echo|1=bar}}"}},"i":0}}]}'>bar</p>
+!! end
+
+## Bug 56733
+!! test
+Templates parameters with special tokenizing behavior dont get modified because of arg escaping
+!! options
+parsoid
+!! wikitext
+{{echo|a : b}}
+!! html
+<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a : b"}},"i":0}}]}'>a<span typeof="mw:Placeholder" data-parsoid='{"isDisplayHack":true}'> </span>: b</p>
+!! end
+
+###
+### Parsoid-centric tests for testing RT edge cases for pre
+###
+
+!!test
+1a. Indent-Pre and Comments
+!! wikitext
+ a
+<!--a-->
+c
+!! html
+<pre>a
+</pre>
+<p>c
+</p>
+!!end
+
+!!test
+1b. Indent-Pre and Comments
+!! wikitext
+ a
+ <!--a-->
+c
+!! html
+<pre>a
+</pre>
+<p>c
+</p>
+!!end
+
+!!test
+1c. Indent-Pre and Comments
+!! wikitext
+<!--a--> a
+
+ <!--a--> a
+!! html
+<pre> a
+</pre>
+<pre> a
+</pre>
+!!end
+
+!!test
+1d. Indent-Pre and Comments
+(Pre-handler currently cannot distinguish between comment/ws order and normalizes them to [comment,ws] order)
+!! wikitext
+<!--a--> a
+
+ <!--b-->b
+!! html
+<pre>a
+</pre>
+<pre>b
+</pre>
+!!end
+
+!!test
+2a. Indent-Pre and tables
+!! wikitext
+ {|
+ |-
+ !h1!!h2
+ |foo||bar
+ |}
+!! html
+<table>
+
+<tr>
+<th>h1</th>
+<th>h2
+</th>
+<td>foo</td>
+<td>bar
+</td></tr></table>
+
+!!end
+
+!!test
+2b. Indent-Pre and tables
+!! wikitext
+ {|
+ |-
+|foo
+|}
+!! html
+<table>
+
+<tr>
+<td>foo
+</td></tr></table>
+
+!!end
+
+!!test
+2c. Indent-Pre and tables (bug 42252)
+!! wikitext
+{|
+ |+ foo
+ ! | bar
+|}
+!! html
+<table>
+<caption> foo
+</caption>
+<tr>
+<th> bar
+</th></tr></table>
+
+!!end
+
+!!test
+2d. Indent-Pre and tables
+!! wikitext
+ a
+ {|
+ | b
+ |}
+!! html/php
+<pre>a
+</pre>
+<table>
+<tr>
+<td> b
+</td></tr></table>
+
+!! html/parsoid
+<pre>a</pre>
+
+<table>
+
+<tbody>
+<tr>
+<td> b</td></tr>
+ </tbody></table>
+!!end
+
+!!test
+2e. Indent-Pre and table-line syntax
+!! wikitext
+ a
+ | b
+ | c
+!! html/php
+<pre>a
+| b
+| c
+</pre>
+!!end
+
+!!test
+2f. Indent-pre started by table-line syntax
+!! wikitext
+a
+ | b
+ | c
+!! html/php
+<p>a
+</p>
+<pre>| b
+| c
+</pre>
+!! html/parsoid
+<p>a</p>
+<pre>
+| b
+| c</pre>
+!!end
+
+!!test
+3a. Indent-Pre and block tags (single-line html)
+!! wikitext
+ a <p> foo </p>
+ b <div> foo </div>
+ c <blockquote> foo </blockquote>
+ <span> foo </span>
+!! html
+ a <p> foo </p>
+ b <div> foo </div>
+ c <blockquote> foo </blockquote>
+<pre><span> foo </span>
+</pre>
+!! html+tidy
+<p>a</p>
+<p>foo</p>
+<p>b</p>
+<div>foo</div>
+<p>c</p>
+<blockquote>
+<p>foo</p>
+</blockquote>
+<pre>
+<span> foo </span>
+</pre>
+!! end
+
+!!test
+3b. Indent-Pre and block tags (multi-line html)
+!! wikitext
+ a <span>foo</span>
+ b <div> foo </div>
+!! html
+<pre>a <span>foo</span>
+</pre>
+ b <div> foo </div>
+
+!! html+tidy
+<pre>
+a <span>foo</span>
+</pre>
+<p>b</p>
+<div>foo</div>
+!!end
+
+!!test
+3c. Indent-Pre and block tags (pre-content on separate line)
+!! wikitext
+<p>
+ foo
+</p>
+
+<div>
+ foo
+</div>
+
+<center>
+ foo
+</center>
+
+<blockquote>
+ foo
+</blockquote>
+
+<blockquote>
+<pre>
+foo
+</pre>
+</blockquote>
+
+<table><tr><td>
+ foo
+</td></tr></table>
+
+<ul><li>
+ foo
+</li></ul>
+
+!! html
+<p>
+ foo
+</p>
+<div>
+<pre>foo
+</pre>
+</div>
+<center>
+<pre>foo
+</pre>
+</center>
+<blockquote>
+<p> foo
+</p>
+</blockquote>
+<blockquote>
+<pre>
+foo
+</pre>
+</blockquote>
+<table><tr><td>
+<pre>foo
+</pre>
+</td></tr></table>
+<ul><li>
+ foo
+</li></ul>
+
+!!end
+
+!!test
+4. Indent-Pre and extension tags
+!! wikitext
+ a <gallery>
+File:foobar.jpg
+</gallery>
+!! html
+ a <ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+</ul>
+
+!! html+tidy
+<p>a</p>
+<ul class="gallery mw-gallery-traditional">
+<li class="gallerybox" style="width: 155px">
+<div style="width: 155px">
+<div class="thumb" style="width: 150px;">
+<div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div>
+</div>
+<div class="gallerytext"></div>
+</div>
+</li>
+</ul>
+!!end
+
+!! test
+Table wikitext syntax outside wiki-tables
+!! wikitext
+a
+! not a table heading
+|- not a table row
+| not a table cell
+| class="foo bar" | baz
+b
+|}
+|-
+c
+!! html
+<p>a
+! not a table heading
+|- not a table row
+| not a table cell
+| class="foo bar" | baz
+b
+|}
+|-
+c
+</p>
+!! end
+
+!!test
+Render paragraphs when indent-pre is suppressed in blocklevels
+!! wikitext
+<blockquote>
+ foo
+
+ bar
+</blockquote>
+!! html
+<blockquote>
+<p> foo
+</p><p> bar
+</p>
+</blockquote>
+
+!!end
+
+!!test
+4. Multiple spaces at start-of-line
+!! wikitext
+ <p> foo </p>
+ foo
+ {|
+|foo
+|}
+!! html
+ <p> foo </p>
+<pre> foo
+</pre>
+<table>
+<tr>
+<td>foo
+</td></tr></table>
+
+!!end
+
+## NOTE: the leading white-space chars on empty line are significant
+!! test
+5a. White-space in indent-pre
+!! wikitext
+ a<br />
+
+ b
+!! html
+<pre>a<br />
+
+b
+</pre>
+!! end
+
+## NOTE: the leading white-space chars on empty line are significant
+!! test
+5b. White-space in indent-pre
+!! wikitext
+ a
+
+ b
+
+
+ c
+!! html
+<pre>a
+
+b
+
+
+c
+</pre>
+!! end
+
+!! test
+5c. White-space in indent-pre
+!! wikitext
+ ''a''
+ ''b''
+ ''c''
+!! html
+<pre><i>a</i>
+ <i>b</i>
+ <i>c</i>
+</pre>
+!! end
+
+!! test
+6. Pre-blocks should extend across lines with leading WS even when there is no wrappable content
+!! wikitext
+ a
+
+ <!-- continue -->
+ b
+
+ c
+
+d
+!! html
+<pre>a
+
+b
+</pre>
+<pre>c
+
+</pre>
+<p>d
+</p>
+!! end
+
+!! test
+7a. Indent-pre and category links
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+ [[Category:foo]] <!-- No pre-wrapping -->
+{{echo| [[Category:foo]]}} <!-- No pre-wrapping -->
+!! html
+ <link rel="mw:PageProp/Category" href="./Category:Foo"> <!-- No pre-wrapping -->
+<span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":" [[Category:foo]]"}},"i":0}}]}'> </span>
+<link rel="mw:PageProp/Category" href="./Category:Foo" about="#mwt1"> <!-- No pre-wrapping -->
+!! end
+
+!! test
+7b. Indent-pre and category links
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+ [[Category:foo]] a
+ [[Category:foo]] {{echo|b}}
+!! html
+<pre>
+<link rel="mw:PageProp/Category" href="./Category:Foo"> a
+
+<link rel="mw:PageProp/Category" href="./Category:Foo"> <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"b"}},"i":0}}]}'>b</span></pre>
+!! end
+
+###
+### HTML-pre (some to spec PHP parser behavior and some Parsoid-RT-centric)
+###
+
+!!test
+HTML-pre: 1. embedded newlines
+!! wikitext
+<pre>foo</pre>
+
+<pre>
+foo
+</pre>
+
+<pre>
+
+foo
+</pre>
+
+<pre>
+
+
+foo
+</pre>
+!! html
+<pre>foo</pre>
+<pre>
+foo
+</pre>
+<pre>
+
+foo
+</pre>
+<pre>
+
+
+foo
+</pre>
+
+!! html/parsoid
+<pre data-parsoid='{"stx":"html"}'>foo</pre>
+
+<pre data-parsoid='{"stx":"html","strippedNL":"\n"}'>
+foo
+</pre>
+
+<pre data-parsoid='{"stx":"html"}'>
+
+foo
+</pre>
+
+<pre data-parsoid='{"stx":"html"}'>
+
+
+foo
+</pre>
+!!end
+
+!! test
+HTML-pre: big spaces
+!! wikitext
+<pre>
+
+
+
+
+haha
+
+
+
+
+haha
+
+
+
+
+</pre>
+!! html
+<pre>
+
+
+
+
+haha
+
+
+
+
+haha
+
+
+
+
+</pre>
+
+!! html/parsoid
+<pre data-parsoid='{"stx":"html"}'>
+
+
+
+
+haha
+
+
+
+
+haha
+
+
+
+
+</pre>
+!! end
+
+!!test
+HTML-pre: 2: indented text
+!! wikitext
+<pre>
+ foo
+</pre>
+!! html
+<pre>
+ foo
+</pre>
+
+!!end
+
+!!test
+HTML-pre: 3: other wikitext
+!! wikitext
+<pre>
+* foo
+# bar
+= no-h =
+'' no-italic ''
+[[ NoLink ]]
+</pre>
+!! html
+<pre>
+* foo
+# bar
+= no-h =
+'' no-italic ''
+[[ NoLink ]]
+</pre>
+
+!!end
+
+###
+### Definition lists
+###
+!! test
+Simple definition
+!! wikitext
+; name : Definition
+!! html
+<dl><dt> name&#160;</dt>
+<dd> Definition</dd></dl>
+
+!! end
+
+!! test
+Definition list for indentation only
+!! wikitext
+: Indented text
+!! html
+<dl><dd> Indented text</dd></dl>
+
+!! end
+
+!! test
+Definition list with no space
+!! wikitext
+;name:Definition
+!! html
+<dl><dt>name</dt>
+<dd>Definition</dd></dl>
+
+!!end
+
+!! test
+Definition list with URL link
+!! wikitext
+; http://example.com/ : definition
+!! html
+<dl><dt> <a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a>&#160;</dt>
+<dd> definition</dd></dl>
+
+!! end
+
+!! test
+Definition list with bracketed URL link
+!! wikitext
+;[http://www.example.com/ Example]:Something about it
+!! html
+<dl><dt><a rel="nofollow" class="external text" href="http://www.example.com/">Example</a></dt>
+<dd>Something about it</dd></dl>
+
+!! end
+
+!! test
+Definition list with wikilink containing colon
+!! wikitext
+; [[Help:FAQ]]: The least-read page on Wikipedia
+!! html
+<dl><dt> <a href="/index.php?title=Help:FAQ&amp;action=edit&amp;redlink=1" class="new" title="Help:FAQ (page does not exist)">Help:FAQ</a></dt>
+<dd> The least-read page on Wikipedia</dd></dl>
+
+!! end
+
+# At Brion's and JeLuF's insistence... :)
+!! test
+Definition list with news link containing colon
+!! wikitext
+; news:alt.wikipedia.rox: This isn't even a real newsgroup!
+!! html
+<dl><dt> <a rel="nofollow" class="external free" href="news:alt.wikipedia.rox">news:alt.wikipedia.rox</a></dt>
+<dd> This isn't even a real newsgroup!</dd></dl>
+
+!! end
+
+!! test
+Malformed definition list with colon
+!! wikitext
+; news:alt.wikipedia.rox -- don't crash or enter an infinite loop
+!! html
+<dl><dt> <a rel="nofollow" class="external free" href="news:alt.wikipedia.rox">news:alt.wikipedia.rox</a> -- don't crash or enter an infinite loop</dt></dl>
+
+!! end
+
+!! test
+Definition lists: colon in external link text
+!! wikitext
+; [http://www.wikipedia2.org/ Wikipedia : The Next Generation]: OK, I made that up
+!! html
+<dl><dt> <a rel="nofollow" class="external text" href="http://www.wikipedia2.org/">Wikipedia&#160;: The Next Generation</a></dt>
+<dd> OK, I made that up</dd></dl>
+
+!! end
+
+!! test
+Definition lists: colon in HTML attribute
+!! wikitext
+;<b style="display: inline">bold</b>
+!! html
+<dl><dt><b style="display: inline">bold</b></dt></dl>
+
+!! end
+
+!! test
+Definition lists: self-closed tag
+!! wikitext
+;one<br/>two : two-line fun
+!! html
+<dl><dt>one<br />two&#160;</dt>
+<dd> two-line fun</dd></dl>
+
+!! end
+
+!! test
+Bug 11748: Literal closing tags
+!! wikitext
+<dl>
+<dt>test 1</dt>
+<dd>test test test test test</dd>
+<dt>test 2</dt>
+<dd>test test test test test</dd>
+</dl>
+!! html
+<dl>
+<dt>test 1</dt>
+<dd>test test test test test</dd>
+<dt>test 2</dt>
+<dd>test test test test test</dd>
+</dl>
+
+!! end
+
+!! test
+Definition and unordered list using wiki syntax nested in unordered list using html tags.
+!! wikitext
+<ul><li>
+; term : description
+* unordered
+</li></ul>
+!! html
+<ul><li>
+<dl><dt> term&#160;</dt>
+<dd> description</dd></dl>
+<ul><li> unordered</li></ul>
+</li></ul>
+
+!! end
+
+!! test
+
+Definition list with empty definition and following paragraph
+!! wikitext
+; term:
+Paragraph text
+!! html
+<dl><dt> term</dt>
+<dd></dd></dl>
+<p>Paragraph text
+</p>
+!! end
+
+!! test
+Nested definition lists using html syntax
+!! wikitext
+<dl><dt>x</dt>
+<dd>a</dd>
+<dd>b</dd></dl>
+
+!! end
+
+!! test
+Definition Lists: No nesting: Multiple dd's
+!! wikitext
+;x
+:a
+:b
+!! html
+<dl><dt>x</dt>
+<dd>a</dd>
+<dd>b</dd></dl>
+
+!! end
+
+!! test
+Definition Lists: Indentation: Regular
+!! wikitext
+:i1
+::i2
+:::i3
+!! html
+<dl><dd>i1
+<dl><dd>i2
+<dl><dd>i3</dd></dl></dd></dl></dd></dl>
+
+!! end
+
+!! test
+Definition Lists: Indentation: Missing 1st level
+!! wikitext
+::i2
+:::i3
+!! html
+<dl><dd><dl><dd>i2
+<dl><dd>i3</dd></dl></dd></dl></dd></dl>
+
+!! end
+
+!! test
+Definition Lists: Indentation: Multi-level indent
+!! wikitext
+:::i3
+!! html
+<dl><dd><dl><dd><dl><dd>i3</dd></dl></dd></dl></dd></dl>
+
+!! end
+
+!! test
+Definition Lists: Hacky use to indent tables
+!! wikitext
+::{|
+|foo
+|bar
+|}
+this text
+should be left alone
+!! html
+<dl><dd><dl><dd><table>
+<tr>
+<td>foo
+</td>
+<td>bar
+</td></tr></table></dd></dl></dd></dl>
+<p>this text
+should be left alone
+</p>
+!! end
+
+!! test
+Definition Lists: Hacky use to indent tables, with comments (bug 63979)
+!! wikitext
+<!-- foo -->
+::{|
+|foo
+|bar
+|}<!-- bar -->
+this text
+should be left alone
+!! html/parsoid
+<!-- foo -->
+<dl><dd><dl><dd><table><tr>
+<td>foo</td>
+<td>bar</td>
+</tr></table><!-- bar --></dd></dl></dd></dl>
+<p>this text
+should be left alone</p>
+!! end
+
+!! test
+Definition Lists: Hacky use to indent tables, with comment before table
+!! wikitext
+::<!-- foo -->{|
+|foo
+|}
+!! html/parsoid
+<dl><dd><dl><dd><!-- foo --><table><tr>
+<td>foo</td>
+</tr></table></dd></dl></dd></dl>
+!! end
+
+# Bug 52473
+!! test
+Definition Lists: Hacky use to indent tables (WS-insensitive)
+!! options
+parsoid
+!! wikitext
+: {|
+|a
+|}
+!! html
+<dl>
+<dd> <table><tr><td>a</td></tr></table> </dd>
+</dl>
+!! end
+## The PHP parser treats : items (dd) without a corresponding ; item (dt)
+## as an empty dt item. It also ignores all but the last ";" when followed
+## by ":" later on. So, ";" are not ignored in ";;;t3" but are ignored in
+## ";;;t3 :d1". So, PHP parser behavior is a little inconsistent wrt multiple
+## ";"s.
+##
+## Ex: ";;t2 ::d2" is transformed into:
+##
+## <dl>
+## <dt>t2 </dt>
+## <dd>
+## <dl>
+## <dt></dt>
+## <dd>d2</dd>
+## </dl>
+## </dd>
+## </dl>
+##
+## But, Parsoid treats "; :" as a tight atomic unit and excess ":" as plain text
+## So, the same wikitext above (;;t2 ::d2) is transformed into:
+##
+## <dl>
+## <dt>
+## <dl>
+## <dt>t2 </dt>
+## <dd>:d2</dd>
+## </dl>
+## </dt>
+## </dl>
+##
+## All Parsoid only definition list tests have this difference.
+##
+## See also: https://bugzilla.wikimedia.org/show_bug.cgi?id=6569
+## and http://lists.wikimedia.org/pipermail/wikitext-l/2011-November/000483.html
+
+!! test
+Table / list interaction: indented table with lists in table contents
+!! wikitext
+:{|
+|-
+| a
+* b
+|-
+| c
+* d
+|}
+!! html
+<dl><dd><table>
+
+<tr>
+<td> a
+<ul><li> b</li></ul>
+</td></tr>
+<tr>
+<td> c
+<ul><li> d</li></ul>
+</td></tr></table></dd></dl>
+
+!! end
+
+!!test
+Table / list interaction: lists nested in tables nested in indented lists
+!! wikitext
+:{|
+|
+:a
+:b
+|
+*c
+*d
+|}
+
+*e
+*f
+!! html
+<dl><dd><table>
+<tr>
+<td>
+<dl><dd>a</dd>
+<dd>b</dd></dl>
+</td>
+<td>
+<ul><li>c</li>
+<li>d</li></ul>
+</td></tr></table></dd></dl>
+<ul><li>e</li>
+<li>f</li></ul>
+
+!!end
+
+!! test
+Definition Lists: Nesting: Multi-level (Parsoid only)
+!! options
+parsoid
+!! wikitext
+;t1 :d1
+;;t2 ::d2
+;;;t3 :::d3
+!! html
+<dl>
+ <dt>t1 </dt>
+ <dd>d1</dd>
+ <dt>
+ <dl>
+ <dt>t2 </dt>
+ <dd>:d2</dd>
+ <dt>
+ <dl>
+ <dt>t3 </dt>
+ <dd>::d3</dd>
+ </dl>
+ </dt>
+ </dl>
+ </dt>
+</dl>
+
+
+!! end
+
+
+!! test
+Definition Lists: Nesting: Test 2 (Parsoid only)
+!! options
+parsoid
+!! wikitext
+;t1
+::d2
+!! html
+<dl>
+ <dt>t1</dt>
+ <dd>
+ <dl>
+ <dd>d2</dd>
+ </dl>
+ </dd>
+</dl>
+
+!! end
+
+
+!! test
+Definition Lists: Nesting: Test 3 (Parsoid only)
+!! options
+parsoid
+!! wikitext
+:;t1
+::::d2
+!! html
+<dl>
+ <dd>
+ <dl>
+ <dt>t1</dt>
+ <dd>
+ <dl>
+ <dd>
+ <dl>
+ <dd>d2</dd>
+ </dl>
+ </dd>
+ </dl>
+ </dd>
+ </dl>
+ </dd>
+</dl>
+
+!! end
+
+
+!! test
+Definition Lists: Nesting: Test 4
+!! wikitext
+::;t3
+:::d3
+!! html
+<dl><dd><dl><dd><dl><dt>t3</dt>
+<dd>d3</dd></dl></dd></dl></dd></dl>
+
+!! end
+
+
+## The Parsoid team believes the following three test exposes a
+## bug in the PHP parser. (Parsoid team thinks the PHP parser is
+## wrong to close the <dl> after the <dt> containing the <ul>.)
+## It also exposes a "misfeature" in tidy, which doesn't like
+## <dl> tags with a single <dt> child; it converts the <dt> into
+## a <dd> in that case. (Parsoid leaves the <dt> alone!)
+!! test
+Definition Lists: Mixed Lists: Test 1
+!! wikitext
+:;* foo
+::* bar
+:; baz
+!! html/php
+<dl><dd><dl><dt><ul><li> foo</li>
+<li> bar</li></ul></dt></dl>
+<dl><dt> baz</dt></dl></dd></dl>
+
+!! html/php+tidy
+<dl>
+<dd>
+<dl>
+<dd>
+<ul>
+<li>foo</li>
+<li>bar</li>
+</ul>
+</dd>
+</dl>
+<dl>
+<dt>baz</dt>
+</dl>
+</dd>
+</dl>
+!! html/parsoid
+<dl>
+<dd><dl>
+<dt><ul>
+<li> foo
+</li>
+</ul></dt>
+<dd><ul>
+<li> bar
+</li>
+</ul></dd>
+<dt> baz</dt>
+</dl></dd>
+</dl>
+!! end
+
+!! test
+Definition Lists: Mixed Lists: Test 2
+!! wikitext
+*: d1
+*: d2
+!! html
+<ul><li><dl><dd> d1</dd>
+<dd> d2</dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 3
+!! wikitext
+*::: d1
+*::: d2
+!! html
+<ul><li><dl><dd><dl><dd><dl><dd> d1</dd>
+<dd> d2</dd></dl></dd></dl></dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 4
+!! wikitext
+*;d1 :d2
+*;d3 :d4
+!! html
+<ul><li><dl><dt>d1&#160;</dt>
+<dd>d2</dd>
+<dt>d3&#160;</dt>
+<dd>d4</dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 5
+!! wikitext
+*:d1
+*:: d2
+!! html
+<ul><li><dl><dd>d1
+<dl><dd> d2</dd></dl></dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 6
+!! wikitext
+#*:d1
+#*::: d3
+!! html
+<ol><li><ul><li><dl><dd>d1
+<dl><dd><dl><dd> d3</dd></dl></dd></dl></dd></dl></li></ul></li></ol>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 7
+!! wikitext
+:* d1
+:* d2
+!! html
+<dl><dd><ul><li> d1</li>
+<li> d2</li></ul></dd></dl>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 8
+!! wikitext
+:* d1
+::* d2
+!! html
+<dl><dd><ul><li> d1</li></ul>
+<dl><dd><ul><li> d2</li></ul></dd></dl></dd></dl>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 9
+!! wikitext
+*;foo :bar
+!! html
+<ul><li><dl><dt>foo&#160;</dt>
+<dd>bar</dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 10
+!! wikitext
+*#;foo :bar
+!! html
+<ul><li><ol><li><dl><dt>foo&#160;</dt>
+<dd>bar</dd></dl></li></ol></li></ul>
+
+!! end
+
+# The Parsoid team disagrees with the PHP parser's seemingly-random
+# rules regarding dd/dt on the next two tests. Parsoid is more
+# consistent, and recognizes the shared nesting and keeps the
+# still-open tags around until the nesting is complete.
+# (And tidy again converts <dt> to <dd> before 'bar'.)
+
+!! test
+Definition Lists: Mixed Lists: Test 11
+!! wikitext
+*#*#;*;;foo :bar
+*#*#;boo :baz
+!! html/php
+<ul><li><ol><li><ul><li><ol><li><dl><dt>foo&#160;</dt>
+<dd><ul><li><dl><dt><dl><dt>bar</dt></dl></dd></dl></li></ul></dd></dl>
+<dl><dt>boo&#160;</dt>
+<dd>baz</dd></dl></li></ol></li></ul></li></ol></li></ul>
+
+!! html/php+tidy
+<ul>
+<li>
+<ol>
+<li>
+<ul>
+<li>
+<ol>
+<li>
+<dl>
+<dt>foo&#160;</dt>
+<dd>
+<ul>
+<li>
+<dl>
+<dd>
+<dl>
+<dt>bar</dt>
+</dl>
+</dd>
+</dl>
+</li>
+</ul>
+</dd>
+</dl>
+<dl>
+<dt>boo&#160;</dt>
+<dd>baz</dd>
+</dl>
+</li>
+</ol>
+</li>
+</ul>
+</li>
+</ol>
+</li>
+</ul>
+!! html/parsoid
+<ul>
+<li>
+<ol>
+<li>
+<ul>
+<li>
+<ol>
+<li>
+<dl>
+<dt>
+<ul>
+<li>
+<dl>
+<dt>
+<dl>
+<dt>foo<span typeof="mw:Placeholder" data-parsoid='{"src":" "}'>&nbsp;</span></dt>
+<dd data-parsoid='{"stx":"row"}'>bar</dd>
+</dl></dt>
+</dl></li>
+</ul></dt>
+<dt>boo<span typeof="mw:Placeholder" data-parsoid='{"src":" "}'>&nbsp;</span></dt>
+<dd data-parsoid='{"stx":"row"}'>baz</dd>
+</dl></li>
+</ol></li>
+</ul></li>
+</ol></li>
+</ul>
+!! end
+
+
+# Another case where tidy converts a <dt> to a <dd> (but Parsoid doesn't).
+!! test
+Definition Lists: Weird Ones: Test 1
+!! wikitext
+*#;*::;; foo : bar (who uses this?)
+!! html/php
+<ul><li><ol><li><dl><dt> foo&#160;</dt>
+<dd><ul><li><dl><dd><dl><dd><dl><dt><dl><dt> bar (who uses this?)</dt></dl></dd></dl></dd></dl></dd></dl></li></ul></dd></dl></li></ol></li></ul>
+
+!! html/php+tidy
+<ul>
+<li>
+<ol>
+<li>
+<dl>
+<dt>foo&#160;</dt>
+<dd>
+<ul>
+<li>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dt>bar (who uses this?)</dt>
+</dl>
+</dd>
+</dl>
+</dd>
+</dl>
+</dd>
+</dl>
+</li>
+</ul>
+</dd>
+</dl>
+</li>
+</ol>
+</li>
+</ul>
+!! html/parsoid
+<ul>
+<li>
+<ol>
+<li>
+<dl>
+<dt>
+<ul>
+<li>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dt>
+<dl>
+<dt> foo<span typeof="mw:Placeholder" data-parsoid='{"src":" "}'>&nbsp;</span></dt>
+<dd data-parsoid='{"stx":"row"}'> bar (who uses this?)</dd>
+</dl></dt>
+</dl></dd>
+</dl></dd>
+</dl></li>
+</ul></dt>
+</dl></li>
+</ol></li>
+</ul>
+!! end
+
+###
+### External links
+###
+!! test
+External links: non-bracketed
+!! wikitext
+Non-bracketed: http://example.com
+!! html
+<p>Non-bracketed: <a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>
+</p>
+!! end
+
+# parsoid doesn't explicitly mark autonumbered links, see bug 53505
+!! test
+External links: numbered
+!! wikitext
+Numbered: [http://example.com]
+Numbered: [http://example.net]
+Numbered: [http://example.com]
+!! html/php
+<p>Numbered: <a rel="nofollow" class="external autonumber" href="http://example.com">[1]</a>
+Numbered: <a rel="nofollow" class="external autonumber" href="http://example.net">[2]</a>
+Numbered: <a rel="nofollow" class="external autonumber" href="http://example.com">[3]</a>
+</p>
+!! html/parsoid
+<p>Numbered: <a rel="mw:ExtLink" href="http://example.com"></a>
+Numbered: <a rel="mw:ExtLink" href="http://example.net"></a>
+Numbered: <a rel="mw:ExtLink" href="http://example.com"></a></p>
+!!end
+
+!! test
+External links: specified text
+!! wikitext
+Specified text: [http://example.com link]
+!! html
+<p>Specified text: <a rel="nofollow" class="external text" href="http://example.com">link</a>
+</p>
+!!end
+
+!! test
+External links: trail
+!! wikitext
+Linktrails should not work for external links: [http://example.com link]s
+!! html
+<p>Linktrails should not work for external links: <a rel="nofollow" class="external text" href="http://example.com">link</a>s
+</p>
+!! end
+
+!! test
+External links: dollar sign in URL
+!! wikitext
+http://example.com/1$2345
+!! html
+<p><a rel="nofollow" class="external free" href="http://example.com/1$2345">http://example.com/1$2345</a>
+</p>
+!! end
+
+# parsoid doesn't explicitly mark autonumbered links, see bug 53505
+!! test
+External links: dollar sign in URL (autonumber)
+!! wikitext
+[http://example.com/1$2345]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://example.com/1$2345">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/1$2345"></a></p>
+!!end
+
+!! test
+External links: open square bracket forbidden in URL (bug 4377)
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+http://example.com/1[2345
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/1">http://example.com/1</a>[2345
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/1">http://example.com/1</a>[2345</p>
+!! end
+
+!! test
+External links: open square bracket forbidden in URL (named) (bug 4377)
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+[http://example.com/1[2345]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://example.com/1">[2345</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/1">[2345</a></p>
+!!end
+
+# parsoid adds a space before the link name
+!! test
+External links: open square bracket forbidden in URL (named) (bug 4377)
+Parsoid variant.
+!! wikitext
+[http://example.com/1 [2345]
+!! html
+<p><a rel="nofollow" class="external text" href="http://example.com/1">[2345</a>
+</p>
+!!end
+
+!! test
+External links: nowiki in URL link text (bug 6230)
+!! wikitext
+[http://example.com/ <nowiki>''example site''</nowiki>]
+!! html
+<p><a rel="nofollow" class="external text" href="http://example.com/">''example site''</a>
+</p>
+!! end
+
+!! test
+External links: newline forbidden in text (bug 6230 regression check)
+!! wikitext
+[http://example.com/ first
+second]
+!! html
+<p>[<a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a> first
+second]
+</p>
+!!end
+
+!! test
+External links: Pipe char between url and text
+!! wikitext
+[http://example.com | link]
+!! html
+<p><a rel="nofollow" class="external text" href="http://example.com">| link</a>
+</p>
+!!end
+
+!! test
+External links: protocol-relative URL in brackets
+!! wikitext
+[//example.com/ Test]
+!! html
+<p><a rel="nofollow" class="external text" href="//example.com/">Test</a>
+</p>
+!! end
+
+# parsoid doesn't explicitly mark autonumbered links, see bug 53505
+!! test
+External links: protocol-relative URL in brackets without text
+!! wikitext
+[//example.com]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="//example.com">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="//example.com"></a></p>
+!! end
+
+!! test
+External links: protocol-relative URL in free text is left alone
+!! wikitext
+//example.com/Foo
+!! html
+<p>//example.com/Foo
+</p>
+!!end
+
+!! test
+External links: protocol-relative URL in the middle of a word is left alone (bug 30269)
+!! wikitext
+foo//example.com/Foo
+!! html
+<p>foo//example.com/Foo
+</p>
+!! end
+
+!! test
+External links: with no contents
+!! wikitext
+[http://en.wikipedia.org/wiki/Foo]
+
+[[wikipedia:Foo|Bar]]
+
+[[wikipedia:Foo|<span>Bar</span>]]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://en.wikipedia.org/wiki/Foo">[1]</a>
+</p><p><a href="http://en.wikipedia.org/wiki/Foo" class="extiw" title="wikipedia:Foo">Bar</a>
+</p><p><a href="http://en.wikipedia.org/wiki/Foo" class="extiw" title="wikipedia:Foo"><span>Bar</span></a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo"></a></p>
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo">Bar</a></p>
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo"><span>Bar</span></a></p>
+!! end
+
+!! test
+External image
+!! wikitext
+External image: http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png
+!! html
+<p>External image: <img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" />
+</p>
+!! end
+
+!! test
+External image from https
+!! wikitext
+External image from https: https://meta.wikimedia.org/upload/f/f1/Ncwikicol.png
+!! html
+<p>External image from https: <img src="https://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" />
+</p>
+!! end
+
+!! test
+External image (when not allowed)
+!! options
+wgAllowExternalImages=0
+!! wikitext
+External image: http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png
+!! html
+<p>External image: <a rel="nofollow" class="external free" href="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png">http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png</a>
+</p>
+!! end
+
+!! test
+Link to non-http image, no img tag
+!! wikitext
+Link to non-http image, no img tag: ftp://example.com/test.jpg
+!! html
+<p>Link to non-http image, no img tag: <a rel="nofollow" class="external free" href="ftp://example.com/test.jpg">ftp://example.com/test.jpg</a>
+</p>
+!! end
+
+!! test
+External links: terminating separator
+!! wikitext
+Terminating separator: http://example.com/thing,
+!! html
+<p>Terminating separator: <a rel="nofollow" class="external free" href="http://example.com/thing">http://example.com/thing</a>,
+</p>
+!! end
+
+!! test
+External links: intervening separator
+!! wikitext
+Intervening separator: http://example.com/1,2,3
+!! html
+<p>Intervening separator: <a rel="nofollow" class="external free" href="http://example.com/1,2,3">http://example.com/1,2,3</a>
+</p>
+!! end
+
+!! test
+External links: old bug with URL in query
+!! wikitext
+Old bug with URL in query: [http://example.com/thing?url=http://example.com link]
+!! html
+<p>Old bug with URL in query: <a rel="nofollow" class="external text" href="http://example.com/thing?url=http://example.com">link</a>
+</p>
+!! end
+
+!! test
+External links: old URL-in-URL bug, mixed protocols
+!! wikitext
+And again with mixed protocols: [ftp://example.com?url=http://example.com link]
+!! html
+<p>And again with mixed protocols: <a rel="nofollow" class="external text" href="ftp://example.com?url=http://example.com">link</a>
+</p>
+!!end
+
+!! test
+External links: URL in text
+!! wikitext
+URL in text: [http://example.com http://example.com]
+!! html
+<p>URL in text: <a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>
+</p>
+!! end
+
+!! test
+External links: Clickable images
+!! wikitext
+ja-style clickable images: [http://example.com http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png]
+!! html
+<p>ja-style clickable images: <a rel="nofollow" class="external text" href="http://example.com"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" /></a>
+</p>
+!!end
+
+!! test
+External links: raw ampersand
+!! wikitext
+Old &amp; use: http://x&y
+!! html
+<p>Old &amp; use: <a rel="nofollow" class="external free" href="http://x&amp;y">http://x&amp;y</a>
+</p>
+!! end
+
+!! test
+External links: encoded ampersand
+!! wikitext
+Old &amp; use: http://x&amp;y
+!! html/php
+<p>Old &amp; use: <a rel="nofollow" class="external free" href="http://x&amp;y">http://x&amp;y</a>
+</p>
+!! html/parsoid
+<p>Old <span typeof="mw:Entity">&amp;</span> use: <a rel="mw:ExtLink" href="http://x&amp;y">http://x&amp;y</a></p>
+!! end
+
+!! test
+External links: encoded equals (bug 6102)
+!! wikitext
+http://example.com/?foo&#61;bar
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/?foo=bar">http://example.com/?foo=bar</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/?foo=bar">http://example.com/?foo=bar</a></p>
+!! end
+
+##
+## Note that parsoid doesn't explicit mark autonumbered links, nor
+## does it number them. As discussed in bug 53505, we can identify
+## autonumbered links via CSS.
+##
+
+!! test
+External links: [raw ampersand]
+!! wikitext
+Old &amp; use: [http://x&y]
+!! html/php
+<p>Old &amp; use: <a rel="nofollow" class="external autonumber" href="http://x&amp;y">[1]</a>
+</p>
+!! html/parsoid
+<p>Old <span typeof="mw:Entity">&amp;</span> use: <a rel="mw:ExtLink" href="http://x&amp;y"></a></p>
+!! end
+
+# note that parsoid html is identical to [raw ampersand] case; so html2wt
+# mode will return the [raw ampersand] wikitext
+!! test
+External links: [encoded ampersand]
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+Old &amp; use: [http://x&amp;y]
+!! html/php
+<p>Old &amp; use: <a rel="nofollow" class="external autonumber" href="http://x&amp;y">[1]</a>
+</p>
+!! html/parsoid
+<p>Old <span typeof="mw:Entity">&amp;</span> use: <a rel="mw:ExtLink" href="http://x&amp;y"></a></p>
+!! end
+
+!! test
+External links: [raw equals]
+!! wikitext
+[http://example.com/?foo=bar]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://example.com/?foo=bar">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/?foo=bar"></a></p>
+!! end
+
+# note that parsoid html is identical to [raw equals] case; so html2wt
+# mode will return the [raw equals] wikitext
+!! test
+External links: [encoded equals] (bug 6102)
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[http://example.com/?foo&#61;bar]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://example.com/?foo=bar">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/?foo=bar"></a></p>
+!! end
+
+# xxx parsoid strips the IDN character, so the round-trip tests will
+# obviously fail and are disabled. --cscott
+!! test
+External links: [IDN ignored character reference in hostname; strip it right off]
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[http://e&zwnj;xample.com/]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://example.com/">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/"></a></p>
+!! end
+
+# FIXME: This test (the IDN characters in the text of a link) is an inconsistency.
+# Where an external link could easily circumvent the sanitization of the text of
+# a link like this (where an IDN-ignore character is in the URL somewhere), this
+# test demands a higher standard. That's a bit strange.
+#
+# Example:
+#
+# http://e‌xample.com -> [http://example.com|http://example.com]
+# [http://example.com|http://e‌xample.com] -> [http://example.com|http://e‌xample.com]
+#
+# The first example is sanitized, but the second is not. Any security benefits
+# from this production are trivial to circumvent. Either remove this test and
+# let the parser(s) do their thing unaccosted, or fix the inconsistency and change
+# the test accordingly.
+#
+# All our love,
+# The Parsoid team.
+# xxx parsoid strips the IDN character, so the round-trip tests will
+# obviously fail and are disabled. --cscott
+!! test
+External links: IDN ignored character reference in hostname; strip it right off
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+http://e&zwnj;xample.com/
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/">http://example.com/</a></p>
+!! end
+
+!! test
+External links: www.jpeg.org (bug 554)
+!! wikitext
+http://www.jpeg.org
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.jpeg.org">http://www.jpeg.org</a>
+</p>
+!! end
+
+# parsoid doesn't explicitly mark autonumbered links, see bug 53505
+!! test
+External links: URL within URL (original bug 2)
+!! wikitext
+[http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp"></a></p>
+!! end
+
+!! test
+BUG 361: URL inside bracketed URL
+!! wikitext
+[http://www.example.com/foo http://www.example.com/bar]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/foo">http://www.example.com/bar</a>
+</p>
+!! end
+
+!! test
+BUG 361: URL within URL, not bracketed
+!! wikitext
+http://www.example.com/foo?=http://www.example.com/bar
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.example.com/foo?=http://www.example.com/bar">http://www.example.com/foo?=http://www.example.com/bar</a>
+</p>
+!! end
+
+!! test
+BUG 289: ">"-token in URL-tail
+!! wikitext
+http://www.example.com/<hello>
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.example.com/">http://www.example.com/</a>&lt;hello&gt;
+</p>
+!!end
+
+!! test
+BUG 289: literal ">"-token in URL-tail
+!! wikitext
+http://www.example.com/<b>html</b>
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.example.com/">http://www.example.com/</a><b>html</b>
+</p>
+!!end
+
+!! test
+BUG 289: ">"-token in bracketed URL
+!! wikitext
+[http://www.example.com/<hello> stuff]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/">&lt;hello&gt; stuff</a>
+</p>
+!!end
+
+!! test
+BUG 289: literal ">"-token in bracketed URL
+!! wikitext
+[http://www.example.com/<b>html</b> stuff]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/"><b>html</b> stuff</a>
+</p>
+!!end
+
+!! test
+BUG 289: literal double quote at end of URL
+!! wikitext
+http://www.example.com/"hello"
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.example.com/">http://www.example.com/</a>"hello"
+</p>
+!!end
+
+!! test
+BUG 289: literal double quote in bracketed URL
+!! wikitext
+[http://www.example.com/"hello" stuff]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/">"hello" stuff</a>
+</p>
+!!end
+
+!! test
+External links: multiple legal whitespace is fine, Magnus. Don't break it please. (bug 5081)
+!! wikitext
+[http://www.example.com test]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com">test</a>
+</p>
+!! end
+
+!! test
+External links: link text with spaces
+!! wikitext
+[http://www.example.com a b c]
+[http://www.example.com ''a'' ''b'']
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com">a b c</a>
+<a rel="nofollow" class="external text" href="http://www.example.com"><i>a</i> <i>b</i></a>
+</p>
+!! end
+
+!! test
+External links: wiki links within external link (Bug 3695)
+!! wikitext
+[http://example.com [[wikilink]] embedded in ext link]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://example.com"></a><a href="/index.php?title=Wikilink&amp;action=edit&amp;redlink=1" class="new" title="Wikilink (page does not exist)">wikilink</a><a rel="nofollow" class="external text" href="http://example.com"> embedded in ext link</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com"></a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p>
+!! end
+
+!! test
+BUG 787: Links with one slash after the url protocol are invalid
+!! wikitext
+http:/example.com
+
+[http:/example.com title]
+!! html
+<p>http:/example.com
+</p><p>[http:/example.com title]
+</p>
+!! end
+
+!! test
+Bracketed external links with template-generated invalid target
+!! wikitext
+[{{echo|http:/example.com}} title]
+!! html
+<p>[http:/example.com title]
+</p>
+!! end
+
+!! test
+Bug 2702: Mismatched <i>, <b> and <a> tags are invalid
+!! wikitext
+''[http://example.com text'']
+[http://example.com '''text]'''
+''Something [http://example.com in italic'']
+''Something [http://example.com mixed''''', even bold]'''
+'''''Now [http://example.com both''''']
+!! html
+<p><a rel="nofollow" class="external text" href="http://example.com"><i>text</i></a>
+<a rel="nofollow" class="external text" href="http://example.com"><b>text</b></a>
+<i>Something </i><a rel="nofollow" class="external text" href="http://example.com"><i>in italic</i></a>
+<i>Something </i><a rel="nofollow" class="external text" href="http://example.com"><i>mixed</i><b>, even bold</b></a>
+<i><b>Now </b></i><a rel="nofollow" class="external text" href="http://example.com"><i><b>both</b></i></a>
+</p>
+!! end
+
+
+!! test
+Bug 4781: %26 in URL
+!! wikitext
+http://www.example.com/?title=AT%26T
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a></p>
+!! end
+
+# According to http://dev.w3.org/html5/spec/Overview.html#parsing-urls a plain
+# % is actually legal in HTML5. Any change in output would need testing though.
+!! test
+Bug 4781, 5267: %25 in URL
+!! wikitext
+http://www.example.com/?title=100%25_Bran
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://www.example.com/?title=100%25_Bran">http://www.example.com/?title=100%25_Bran</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=100%25_Bran">http://www.example.com/?title=100%25_Bran</a></p>
+!! end
+
+!! test
+Bug 4781, 5267: %28, %29 in URL
+!! wikitext
+http://www.example.com/?title=Ben-Hur_%281959_film%29
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">http://www.example.com/?title=Ben-Hur_%281959_film%29</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">http://www.example.com/?title=Ben-Hur_%281959_film%29</a></p>
+!! end
+
+
+!! test
+Bug 4781: %26 in autonumber URL
+!! wikitext
+[http://www.example.com/?title=AT%26T]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=AT%26T">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T"></a></p>
+!! end
+
+!! test
+Bug 4781, 5267: %26 in autonumber URL
+!! wikitext
+[http://www.example.com/?title=100%25_Bran]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=100%25_Bran">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=100%25_Bran"></a></p>
+!! end
+
+!! test
+Bug 4781, 5267: %28, %29 in autonumber URL
+!! wikitext
+[http://www.example.com/?title=Ben-Hur_%281959_film%29]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=Ben-Hur_%281959_film%29"></a></p>
+!! end
+
+
+!! test
+Bug 4781: %26 in bracketed URL
+!! wikitext
+[http://www.example.com/?title=AT%26T link]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://www.example.com/?title=AT%26T">link</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T">link</a></p>
+!! end
+
+!! test
+Bug 4781, 5267: %25 in bracketed URL
+!! wikitext
+[http://www.example.com/?title=100%25_Bran link]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/?title=100%25_Bran">link</a>
+</p>
+!! end
+
+!! test
+Bug 4781, 5267: %28, %29 in bracketed URL
+!! wikitext
+[http://www.example.com/?title=Ben-Hur_%281959_film%29 link]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">link</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">link</a></p>
+!! end
+
+!! test
+External link containing a period in the anchor. (bug 63947)
+!! wikitext
+[//foo.org/bar#baz. bang]
+
+[//foo.org/bar. bang]
+!! html/php
+<p><a rel="nofollow" class="external text" href="//foo.org/bar#baz.">bang</a>
+</p><p><a rel="nofollow" class="external text" href="//foo.org/bar.">bang</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="//foo.org/bar#baz.">bang</a></p>
+<p><a rel="mw:ExtLink" href="//foo.org/bar.">bang</a></p>
+!! end
+
+!! test
+External link containing a single quote. (bug 63947)
+!! wikitext
+[//foo.org/bar'baz]
+
+[//foo.org/bar'baz bang]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="//foo.org/bar'baz">[1]</a>
+</p><p><a rel="nofollow" class="external text" href="//foo.org/bar'baz">bang</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="//foo.org/bar'baz"></a></p>
+<p><a rel="mw:ExtLink" href="//foo.org/bar'baz">bang</a></p>
+!! end
+
+
+!! test
+External link containing a period in the anchor. (bug 63947)
+!! wikitext
+[//foo.org/bar#baz. bang]
+
+[//foo.org/bar. bang]
+!! html/php
+<p><a rel="nofollow" class="external text" href="//foo.org/bar#baz.">bang</a>
+</p><p><a rel="nofollow" class="external text" href="//foo.org/bar.">bang</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="//foo.org/bar#baz.">bang</a></p>
+<p><a rel="mw:ExtLink" href="//foo.org/bar.">bang</a></p>
+!! end
+
+!! test
+External link containing a single quote. (bug 63947)
+!! wikitext
+[//foo.org/bar'baz]
+
+[//foo.org/bar'baz bang]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="//foo.org/bar'baz">[1]</a>
+</p><p><a rel="nofollow" class="external text" href="//foo.org/bar'baz">bang</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="//foo.org/bar'baz"></a></p>
+<p><a rel="mw:ExtLink" href="//foo.org/bar'baz">bang</a></p>
+!! end
+
+
+!! test
+External link containing double-single-quotes in text '' (bug 4598 sanity check)
+!! wikitext
+Some [http://example.com/ pretty ''italics'' and stuff]!
+!! html
+<p>Some <a rel="nofollow" class="external text" href="http://example.com/">pretty <i>italics</i> and stuff</a>!
+</p>
+!! end
+
+!! test
+External link containing double-single-quotes in text embedded in italics (bug 4598 sanity check)
+!! wikitext
+''Some [http://example.com/ pretty ''italics'' and stuff]!''
+!! html
+<p><i>Some </i><a rel="nofollow" class="external text" href="http://example.com/"><i>pretty </i>italics<i> and stuff</i></a><i>!</i>
+</p>
+!! end
+
+!! test
+External link containing double-single-quotes with no space separating the url from text in italics
+!! wikitext
+[http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm''La muerte de Casagemas'' (1901) en el sitio de [[Museo Picasso (París)|Museo Picasso]].]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de <a href="/index.php?title=Museo_Picasso_(Par%C3%ADs)&amp;action=edit&amp;redlink=1" class="new" title="Museo Picasso (París) (page does not exist)">Museo Picasso</a>.</a>
+</p>
+!! html/php+tidy
+<p><a rel="nofollow" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de</a> <a href="/index.php?title=Museo_Picasso_(Par%C3%ADs)&amp;action=edit&amp;redlink=1" class="new" title="Museo Picasso (París) (page does not exist)">Museo Picasso</a>.</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de </a><a rel="mw:WikiLink" href="./Museo_Picasso_(París)" title="Museo Picasso (París)">Museo Picasso</a><span>.</span></p>
+!! end
+
+!! test
+External link with comments in link text
+!! wikitext
+[http://www.google.com Google <!-- comment -->]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.google.com">Google </a>
+</p>
+!! end
+
+!! test
+URL-encoding in URL functions (single parameter)
+!! wikitext
+{{localurl:Some page|amp=&}}
+!! html
+<p>/index.php?title=Some_page&amp;amp=&amp;
+</p>
+!! end
+
+!! test
+URL-encoding in URL functions (multiple parameters)
+!! wikitext
+{{localurl:Some page|q=?&amp=&}}
+!! html
+<p>/index.php?title=Some_page&amp;q=?&amp;amp=&amp;
+</p>
+!! end
+
+!! test
+Brackets in urls
+!! wikitext
+http://example.com/index.php?foozoid%5B%5D=bar
+
+http://example.com/index.php?foozoid&#x5B;&#x5D;=bar
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a>
+</p><p><a rel="nofollow" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a></p>
+
+<p><a rel="mw:ExtLink" href="http://example.com/index.php?foozoid[]=bar">http://example.com/index.php?foozoid[]=bar</a></p>
+!! end
+
+!! test
+IPv6 urls (bug 21261)
+!! options
+disabled
+!! wikitext
+http://[2404:130:0:1000::187:2]/index.php
+!! html
+<p><a rel="nofollow" class="external free" href="http://[2404:130:0:1000::187:2]/index.php">http://[2404:130:0:1000::187:2]/index.php</a>
+</p>
+!! end
+
+!! test
+Non-extlinks in brackets
+!! wikitext
+[foo]
+[foo bar]
+[foo ''bar'']
+[fool's] errand
+[fool's errand]
+[{{echo|foo}}]
+[{{echo|foo}} bar]
+[{{echo|foo}} ''bar'']
+[{{echo|foo}}l's] errand
+[{{echo|foo}}l's errand]
+[url={{echo|foo}}]
+[url=http://example.com]
+!! html
+<p>[foo]
+[foo bar]
+[foo <i>bar</i>]
+[fool's] errand
+[fool's errand]
+[foo]
+[foo bar]
+[foo <i>bar</i>]
+[fool's] errand
+[fool's errand]
+[url=foo]
+[url=<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>]
+</p>
+!! end
+
+!! test
+Percent encoding in external links
+!! wikitext
+[https://github.com/search?l=&q=ResourceLoader+%40wikimedia Search]
+!! html/php
+<p><a rel="nofollow" class="external text" href="https://github.com/search?l=&amp;q=ResourceLoader+%40wikimedia">Search</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink"
+href="https://github.com/search?l=&amp;q=ResourceLoader+%40wikimedia">Search</a></p>
+!! end
+
+!! test
+Use url link syntax for links where the content is equal the link target
+!! wikitext
+http://example.com
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com">http://example.com</a></p>
+!! end
+
+!! test
+Parenthesis in external links, especially URL links
+!! wikitext
+http://example.com)
+
+http://example.com/test)
+
+http://example.com/(test)
+
+http://example.com/((test)
+
+(http://example.com/(test))
+
+(http://example.com/(test)))))
+
+http://example.com/a)b
+
+[http://example.com) foo]
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>)
+</p><p><a rel="nofollow" class="external free" href="http://example.com/test">http://example.com/test</a>)
+</p><p><a rel="nofollow" class="external free" href="http://example.com/(test)">http://example.com/(test)</a>
+</p><p><a rel="nofollow" class="external free" href="http://example.com/((test)">http://example.com/((test)</a>
+</p><p>(<a rel="nofollow" class="external free" href="http://example.com/(test))">http://example.com/(test))</a>
+</p><p>(<a rel="nofollow" class="external free" href="http://example.com/(test)))))">http://example.com/(test)))))</a>
+</p><p><a rel="nofollow" class="external free" href="http://example.com/a)b">http://example.com/a)b</a>
+</p><p><a rel="nofollow" class="external text" href="http://example.com)">foo</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com">http://example.com</a>)</p>
+<p><a rel="mw:ExtLink" href="http://example.com/test">http://example.com/test</a>)</p>
+<p><a rel="mw:ExtLink" href="http://example.com/(test)">http://example.com/(test)</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/((test)">http://example.com/((test)</a></p>
+<p>(<a rel="mw:ExtLink" href="http://example.com/(test))">http://example.com/(test))</a></p>
+<p>(<a rel="mw:ExtLink" href="http://example.com/(test)))))">http://example.com/(test)))))</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/a)b">http://example.com/a)b</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com)">foo</a></p>
+!! end
+
+!! test
+Parenthesis in external links, w/ transclusion or comment
+!! wikitext
+(http://example.com/{{echo|hi}})
+
+(http://example.com<!-- hi -->)
+!! html/php
+<p>(<a rel="nofollow" class="external free" href="http://example.com/hi">http://example.com/hi</a>)
+</p><p>(<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>)
+</p>
+!! html/parsoid
+<p>(<a data-mw='{"attribs":[[{"txt":"href"},{"html":"http://example.com/&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;hi&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;,&amp;quot;spc&amp;quot;:[&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;]}]],&amp;quot;dsr&amp;quot;:[20,31,null,null]}\">hi&lt;/span>"}]]}' typeof="mw:ExpandedAttrs" about="#mwt2" rel="mw:ExtLink" href="http://example.com/hi" data-parsoid='{"stx":"url","a":{"href":"http://example.com/hi"},"sa":{"href":"http://example.com/{{echo|hi}}"}}'>http://example.com/hi</a>)</p>
+
+<p>(<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url","a":{"href":"http://example.com"},"sa":{"href":"http://example.com&lt;!-- hi -->"}}'>http://example.com</a>)</p>
+!! end
+
+!! test
+Replace invalid link targets when serializing
+!! options
+parsoid=html2wt
+!! html
+<a rel="mw:WikiLink" href="./]] foo [[bar">Manual</a>
+!! wikitext
+[[MediaWiki:Badtitletext|Manual]]
+!! end
+
+###
+### Quotes
+###
+
+!! test
+Quotes
+!! wikitext
+Normal text. '''Bold text.''' Normal text. ''Italic text.''
+
+Normal text. '''''Bold italic text.''''' Normal text.
+!! html
+<p>Normal text. <b>Bold text.</b> Normal text. <i>Italic text.</i>
+</p><p>Normal text. <i><b>Bold italic text.</b></i> Normal text.
+</p>
+!! end
+
+
+# Parsoid inserts an empty bold tag pair at the end of the line, that the PHP
+# parser strips. The wikitext contains just the first half of the bold
+# quote pair.
+!! test
+Unclosed and unmatched quotes
+!! wikitext
+'''''Bold italic text '''with bold deactivated''' in between.'''''
+
+'''''Bold italic text ''with italic deactivated'' in between.'''''
+
+'''Bold text..
+
+..spanning two paragraphs (should not work).'''
+
+'''Bold tag left open
+
+''Italic tag left open
+
+Normal text.
+
+<!-- Unmatching number of opening, closing tags: -->
+'''This year''''s election ''should'' beat '''last year''''s.
+
+''Tom'''s car is bigger than ''Susan'''s.
+
+Plain ''italic'''s plain
+!! html/php
+<p><i><b>Bold italic text </b>with bold deactivated<b> in between.</b></i>
+</p><p><b><i>Bold italic text </i>with italic deactivated<i> in between.</i></b>
+</p><p><b>Bold text..</b>
+</p><p>..spanning two paragraphs (should not work).
+</p><p><b>Bold tag left open</b>
+</p><p><i>Italic tag left open</i>
+</p><p>Normal text.
+</p><p><b>This year'</b>s election <i>should</i> beat <b>last year'</b>s.
+</p><p><i>Tom<b>s car is bigger than </b></i><b>Susan</b>s.
+</p><p>Plain <i>italic'</i>s plain
+</p>
+!! html/parsoid
+<p><i><b>Bold italic text </b>with bold deactivated<b> in between.</b></i>
+</p><p><b><i>Bold italic text </i>with italic deactivated<i> in between.</i></b>
+</p><p><b>Bold text..</b>
+</p><p>..spanning two paragraphs (should not work).<b></b>
+</p><p><b>Bold tag left open</b>
+</p><p><i>Italic tag left open</i>
+</p><p>Normal text.
+</p><p><b>This year'</b>s election <i>should</i> beat <b>last year'</b>s.
+</p><p><i>Tom<b>s car is bigger than </b></i><b>Susan</b>s.
+</p><p>Plain <i>italic'</i>s plain
+</p>
+!! end
+
+###
+### Tables
+###
+### some content taken from http://meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide:_Using_tables
+###
+
+# This should not produce <table></table> as <table><tr><td></td></tr></table>
+# is the bare minimum required by the spec, see:
+# http://www.w3.org/TR/xhtml-modularization/dtd_module_defs.html#a_module_Basic_Tables
+# Parsoid team replies: empty table tags are legal in HTML5
+!! test
+A table with no data.
+!! options
+parsoid=wt2html
+!! wikitext
+{||}
+!! html/php
+
+!! html/parsoid
+<table></table>
+
+!! end
+
+!! test
+A table with stray table end tags on start tag line (wt2html)
+!! options
+parsoid=wt2html
+!! wikitext
+{|style="color: red;"|}
+
+{|style="color: red;" |}
+|foo
+|}
+
+{|style="color: red;"|} id="foo"
+|foo
+|}
+
+{|style="color: red;" |} id="foo"
+|foo
+|}
+!! html
+<table style="color: red;"></table>
+
+<table style="color: red;">
+<tbody><tr>
+<td>foo</td>
+</tr></tbody>
+</table>
+
+<table style="color: red;" id="foo">
+<tbody><tr>
+<td>foo</td>
+</tr></tbody>
+</table>
+
+<table style="color: red;" id="foo">
+<tbody><tr>
+<td>foo</td>
+</tr></tbody>
+</table>
+
+!! end
+
+!! test
+A table with no data (take 2)
+!! wikitext
+{|
+|}
+!! html/parsoid
+<table></table>
+!! end
+
+# A table with nothing but a caption is invalid XHTML, we might want to render
+# this as <p>caption</p>
+# Parsoid team replies: table with only a caption is legal in HTML5
+!! test
+A table with nothing but a caption
+!! wikitext
+{|
+|+ caption
+|}
+!! html/php
+<table>
+<caption> caption
+</caption><tr><td></td></tr></table>
+
+!! html/parsoid
+<table><caption> caption</caption></table>
+!! end
+
+!! test
+A table with caption with default-spaced attributes and a table row
+!! wikitext
+{|
+|+ style="color: red;" | caption1
+|-
+| foo
+|}
+!! html
+<table>
+<caption style="color: red;"> caption1
+</caption>
+<tr>
+<td> foo
+</td></tr></table>
+
+!! end
+
+!! test
+A table with captions with non-default spaced attributes and a table row
+!! wikitext
+{|
+|+style="color: red;"|caption2
+|+ style="color: red;"| caption3
+|-
+| foo
+|}
+!! html
+<table>
+<caption style="color: red;">caption2
+</caption>
+<caption style="color: red;"> caption3
+</caption>
+<tr>
+<td> foo
+</td></tr></table>
+
+!! end
+
+!! test
+Table td-cell syntax variations
+!! wikitext
+{|
+| foo bar foo | baz
+| foo bar foo || baz
+| style='color:red;' | baz
+| style='color:red;' || baz
+|}
+!! html
+<table>
+<tr>
+<td> baz
+</td>
+<td> foo bar foo </td>
+<td> baz
+</td>
+<td style="color:red;"> baz
+</td>
+<td> style='color:red;' </td>
+<td> baz
+</td></tr></table>
+
+!! end
+
+!! test
+Simple table
+!! wikitext
+{|
+| 1 || 2
+|-
+| 3 || 4
+|}
+!! html
+<table>
+<tr>
+<td> 1 </td>
+<td> 2
+</td></tr>
+<tr>
+<td> 3 </td>
+<td> 4
+</td></tr></table>
+
+!! end
+
+!! test
+Simple table but with multiple dashes for row wikitext
+!! wikitext
+{|
+| foo
+|-----
+| bar
+|}
+!! html
+<table>
+<tr>
+<td> foo
+</td></tr>
+<tr>
+<td> bar
+</td></tr></table>
+
+!! end
+!! test
+Multiplication table
+!! wikitext
+{| border="1" cellpadding="2"
+|+Multiplication table
+|-
+! &times; !! 1 !! 2 !! 3
+|-
+! 1
+| 1 || 2 || 3
+|-
+! 2
+| 2 || 4 || 6
+|-
+! 3
+| 3 || 6 || 9
+|-
+! 4
+| 4 || 8 || 12
+|-
+! 5
+| 5 || 10 || 15
+|}
+!! html
+<table border="1" cellpadding="2">
+<caption>Multiplication table
+</caption>
+<tr>
+<th> &#215; </th>
+<th> 1 </th>
+<th> 2 </th>
+<th> 3
+</th></tr>
+<tr>
+<th> 1
+</th>
+<td> 1 </td>
+<td> 2 </td>
+<td> 3
+</td></tr>
+<tr>
+<th> 2
+</th>
+<td> 2 </td>
+<td> 4 </td>
+<td> 6
+</td></tr>
+<tr>
+<th> 3
+</th>
+<td> 3 </td>
+<td> 6 </td>
+<td> 9
+</td></tr>
+<tr>
+<th> 4
+</th>
+<td> 4 </td>
+<td> 8 </td>
+<td> 12
+</td></tr>
+<tr>
+<th> 5
+</th>
+<td> 5 </td>
+<td> 10 </td>
+<td> 15
+</td></tr></table>
+
+!! end
+
+!! test
+Accept "||" in table headings
+!! wikitext
+{|
+!h1 || h2
+|}
+!! html
+<table>
+<tr>
+<th>h1 </th>
+<th> h2
+</th></tr></table>
+
+!! end
+
+!! test
+Accept "!!" in table data
+!! wikitext
+{|
+| Foo!! ||
+|}
+!! html
+<table>
+<tr>
+<td> Foo!! </td>
+<td>
+</td></tr></table>
+
+!! html/parsoid
+<table data-parsoid='{}'>
+<tbody data-parsoid='{}'><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'> Foo!! </td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'></td></tr>
+</tbody></table>
+!! end
+
+!! test
+Accept "||" in indented table headings
+!! wikitext
+:{|
+!h1 || h2
+|}
+!! html
+<dl><dd><table>
+<tr>
+<th>h1 </th>
+<th> h2
+</th></tr></table></dd></dl>
+
+!! end
+
+!! test
+Accept empty attributes in td/th cells (td/th cells starting with leading ||)
+!! wikitext
+{|
+!| h1
+|| a
+|}
+!! html
+<table>
+<tr>
+<th> h1
+</th>
+<td> a
+</td></tr></table>
+
+!! end
+
+!!test
+Accept "| !" at start of line in tables (ignore !-attribute)
+!! wikitext
+{|
+|-
+| !style="color:red" | bar
+|}
+!! html
+<table>
+
+<tr>
+<td> bar
+</td></tr></table>
+
+!!end
+
+!!test
+Allow +/- in 2nd and later cells in a row, in 1st cell when td-attrs are present, or in 1st cell when there is a space between "|" and +/-
+!! wikitext
+{|
+|-
+|style='color:red;'|+1
+|style='color:blue;'|-1
+|-
+| 1 || 2 || 3
+| 1 ||+2 ||-3
+|-
+| +1
+| -1
+|}
+!! html
+<table>
+
+<tr>
+<td style="color:red;">+1
+</td>
+<td style="color:blue;">-1
+</td></tr>
+<tr>
+<td> 1 </td>
+<td> 2 </td>
+<td> 3
+</td>
+<td> 1 </td>
+<td>+2 </td>
+<td>-3
+</td></tr>
+<tr>
+<td> +1
+</td>
+<td> -1
+</td></tr></table>
+
+!!end
+
+!! test
+Table rowspan
+!! wikitext
+{| border=1
+| Cell 1, row 1
+|rowspan=2| Cell 2, row 1 (and 2)
+| Cell 3, row 1
+|-
+| Cell 1, row 2
+| Cell 3, row 2
+|}
+!! html
+<table border="1">
+<tr>
+<td> Cell 1, row 1
+</td>
+<td rowspan="2"> Cell 2, row 1 (and 2)
+</td>
+<td> Cell 3, row 1
+</td></tr>
+<tr>
+<td> Cell 1, row 2
+</td>
+<td> Cell 3, row 2
+</td></tr></table>
+
+!! end
+
+!! test
+Nested table
+!! wikitext
+{| border=1
+| &alpha;
+|
+{| bgcolor=#ABCDEF border=2
+|nested
+|-
+|table
+|}
+|the original table again
+|}
+!! html
+<table border="1">
+<tr>
+<td> &#945;
+</td>
+<td>
+<table bgcolor="#ABCDEF" border="2">
+<tr>
+<td>nested
+</td></tr>
+<tr>
+<td>table
+</td></tr></table>
+</td>
+<td>the original table again
+</td></tr></table>
+
+!! end
+
+!! test
+Invalid attributes in table cell (bug 1830)
+!! wikitext
+{|
+|Cell:|broken
+|}
+!! html
+<table>
+<tr>
+<td>broken
+</td></tr></table>
+
+!! end
+
+
+# The "|}" to close the table is missing from the input, so parsoid's
+# *2wt modes will fail.
+!! test
+Table security: embedded pipes (http://lists.wikimedia.org/mailman/htdig/wikitech-l/2006-April/022293.html)
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+{|
+| |[ftp://|x||]" onmouseover="alert(document.cookie)">test
+!! html/php
+<table>
+<tr>
+<td>[<a rel="nofollow" class="external free" href="ftp://%7Cx">ftp://%7Cx</a></td>
+<td>]" onmouseover="alert(document.cookie)"&gt;test
+</td>
+</tr>
+</table>
+
+!! html/parsoid
+<table><tbody>
+<tr>
+<td><a rel="mw:ExtLink" href="ftp://|x||"></a>" onmouseover="alert(document.cookie)">test</td></tr></tbody></table>
+!! end
+
+
+!! test
+Indented table markup mixed with indented pre content (proposed in bug 6200)
+!! wikitext
+ <table>
+ <tr>
+ <td>
+ Text that should be rendered preformatted
+ </td>
+ </tr>
+ </table>
+!! html
+ <table>
+ <tr>
+ <td>
+<pre>Text that should be rendered preformatted
+</pre>
+ </td>
+ </tr>
+ </table>
+
+!! end
+
+!! test
+Template-generated table cell attributes and cell content
+!! wikitext
+{|
+|{{table_attribs}}
+| {{table_attribs}}
+|}
+!! html
+<table>
+<tr>
+<td style="color: red"> Foo
+</td>
+<td style="color: red"> Foo
+</td></tr></table>
+
+!! end
+
+!! test
+Template-generated table cell attributes and cell content (2)
+!! wikitext
+{|
+|align=center {{table_attribs}}
+|}
+!! html
+<table>
+<tr>
+<td align="center" style="color: red"> Foo
+</td></tr></table>
+
+!! end
+
+!! test
+Template-generated table cell attributes and cell content (3)
+!! wikitext
+{|
+|align=center {{table_cells}}
+|}
+!! html
+<table>
+<tr>
+<td align="center" style="color: red"> Foo </td>
+<td> Bar </td>
+<td> Baz
+</td></tr></table>
+
+!! end
+
+!! test
+Table with row followed by newlines and table heading
+!! wikitext
+{|
+|-
+
+! foo
+|}
+!! html
+<table>
+
+
+<tr>
+<th> foo
+</th></tr></table>
+
+!! end
+
+!! test
+Table with empty line following the start tag
+!! wikitext
+{|
+
+|-
+| foo
+|}
+!! html
+<table>
+
+
+<tr>
+<td> foo
+</td></tr></table>
+
+!! end
+
+# FIXME: Preserve the attribute properly (with an empty string as value) in
+# the PHP parser. Parsoid implements the behavior below.
+!! test
+Table attributes with empty value
+!! wikitext
+{|
+| style=| hello
+|}
+!! html/parsoid
+<table>
+<tbody>
+<tr>
+<td style=""> hello
+</td></tr></tbody></table>
+
+!! end
+
+!! test
+Wikitext table with a lot of comments
+!! wikitext
+{|
+<!-- c0 -->
+| foo
+<!-- c1 -->
+|- <!-- c2 -->
+<!-- c3 -->
+|<!-- c4 -->
+<!-- c5 -->
+|}
+!! html
+<table>
+<tr>
+<td> foo
+</td></tr>
+<tr>
+<td>
+</td></tr></table>
+
+!! end
+
+!! test
+Wikitext table with double-line table cell
+!! wikitext
+{|
+|a
+b
+|}
+!! html
+<table>
+<tr>
+<td>a
+<p>b
+</p>
+</td></tr></table>
+
+!! end
+
+!! test
+Table cell with a single comment
+!! wikitext
+{|
+| <!-- c1 -->
+| a
+|}
+!! html
+<table>
+<tr>
+<td>
+</td>
+<td> a
+</td></tr></table>
+
+!! end
+
+!! test
+Table-cell after a comment-only-empty-line
+!! wikitext
+{|
+|a
+<!--c1-->
+<!--c2-->| b
+|}
+!! html
+<table>
+<tr>
+<td>a
+</td>
+<td> b
+</td></tr></table>
+
+!! html/parsoid
+<table>
+<tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'>a</td>
+<!--c1-->
+<!--c2--><td data-parsoid='{"autoInsertedEnd":true}'> b</td></tr>
+</tbody></table>
+
+!! end
+
+!! test
+Build table with {{!}}
+!! wikitext
+{{{!}} class="wikitable"
+! header
+! second header
+{{!}}- style="color:red;"
+{{!}} data {{!}}{{!}} style="color:red;" {{!}} second data
+{{!}}}
+!! html
+<table class="wikitable">
+<tr>
+<th> header
+</th>
+<th> second header
+</th></tr>
+<tr style="color:red;">
+<td> data </td>
+<td style="color:red;"> second data
+</td></tr></table>
+
+!! end
+
+# The expected HTML structure in this test is debatable. The PHP parser does
+# not parse this kind of table at all. The main focus for Parsoid is on
+# round-tripping, so this output is ok for now. TODO: revisit!
+!! test
+Wikitext table with html-syntax row
+!! wikitext
+{|
+|-
+<td>foo</td>
+|}
+!! html/parsoid
+<table>
+<tbody>
+<tr>
+<td>foo</td></tr></tbody></table>
+!! end
+
+## Note that Parsoid output differs from PHP and PHP+tidy here.
+## The lack of <tr> tags in the PHP output is arguably a bug in the
+## PHP parser, which tidy then compounds by fostering the content
+## entirely out of the table. Parsoid recognizes the table context
+## and generates <tr> and <td> wrappers as needed. Hopefully nobody
+## depends on PHP's treatment of broken table markup!
+!! test
+Implicit <td> after a |-
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+{|
+|-
+a
+|}
+!! html/php
+<table>
+
+a
+</table>
+
+!! html/php+tidy
+<p>a</p>
+!! html/parsoid
+<table>
+<tr><td>a</td></tr>
+</table>
+!! end
+
+# Again, Parsoid adds implicit <td>s here, PHP and Tidy strip the b out.
+!! test
+<pre> tags should be recognized in an explicit <td> context, but not in an implicit <td> context
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+{|
+|-
+|
+ a
+|-
+ b
+|}
+!! html/php
+<table>
+
+<tr>
+<td>
+<pre>a
+</pre>
+</td></tr>
+ b
+</table>
+
+!! html/php+tidy
+<p>b</p>
+<table>
+<tr>
+<td>
+<pre>
+a
+</pre></td>
+</tr>
+</table>
+!! html/parsoid
+<table>
+<tbody>
+<tr><td><pre>a</pre></td></tr>
+<tr><td> b</td></tr>
+</tbody>
+</table>
+!! end
+
+# PHP + Tidy strips the list out of the table; Parsoid wraps it.
+!! test
+Lists should be recognized in an implicit <td> context
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+{|
+|-
+*a
+|}
+!! html/php
+<table>
+
+<ul><li>a</li></ul>
+</table>
+
+!! html/php+tidy
+<ul>
+<li>a</li>
+</ul>
+!! html/parsoid
+<table>
+<tr>
+<td><ul>
+<li>a</li>
+</ul></td>
+</tr>
+</table>
+!! end
+
+!! test
+Parsoid: Round-trip tables directly followed by content (bug 51219)
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+{|
+|foo
+|} bar
+
+{|
+|baz
+|}<b>quux</b>
+!! html
+<table><tbody>
+<tr>
+<td>foo</td></tr></tbody></table> bar
+<table>
+<tbody>
+<tr>
+<td>baz</td></tr></tbody></table><b>quux</b>
+!! end
+
+!! test
+Parsoid: Default to a newline after tables in new content (bug 51219)
+!! options
+parsoid=html2wt
+!! wikitext
+{|
+|foo
+|}
+<nowiki> </nowiki>bar
+{|
+|baz
+|}
+'''quux'''
+!! html
+<table><tbody>
+<tr><td>foo</td></tr></tbody></table> bar
+<table><tbody>
+<tr><td>baz</td></tr></tbody></table><b>quux</b>
+!! end
+
+!! test
+Parsoid: newline inducing block nodes don't suppress <nowiki>
+!! options
+parsoid=html2wt
+!! wikitext
+<nowiki> </nowiki>a
+
+= foo =
+!! html
+ a<h1>foo</h1>
+!! end
+
+!! test
+Parsoid: Row-syntax table headings followed by comment & table cells
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+{|
+! foo || bar
+<!-- foo --> || baz || quux
+|}
+!! html/php
+<table>
+<tr>
+<th> foo </th>
+<th> bar
+</th>
+<td> baz </td>
+<td> quux
+</td></tr></table>
+
+!! html/parsoid
+<table>
+<tbody><tr><th> foo </th><th> bar
+<!-- foo --> </th><td> baz </td><td> quux</td></tr>
+</tbody></table>
+!! end
+
+
+# PHP throws away the (semi-broken) "foo" class here; Parsoid
+# preserves it.
+!!test
+Parsoid: Recover better from broken table attributes
+!!options
+parsoid=wt2html
+!!wikitext
+{| class="foo
+| class="bar" |
+foo
+|}
+!!html/php+tidy
+<table>
+<tr>
+<td class="bar">
+<p>foo</p>
+</td>
+</tr>
+</table>
+!!html/parsoid
+<table class="foo">
+<tr>
+<td class="bar">
+<p>foo</p></td></tr>
+</tbody></table>
+!!end
+
+!! test
+Strip unsupported table tags
+!! options
+parsoid=html2wt
+!! html
+<table>
+<thead>
+<tr>
+<th>Month</th>
+<th>Savings</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>January</td>
+<td>$100</td>
+</tr>
+<tr>
+<td>February</td>
+<td>$80</td>
+</tr>
+</tbody>
+<tfoot>
+<tr>
+<td>Sum</td>
+<td>$180</td>
+</tr>
+</tfoot>
+</table>
+!! wikitext
+{|
+
+!Month
+!Savings
+
+|January
+|$100
+
+|-
+|February
+|$80
+
+|Sum
+|$180
+
+|}
+!! end
+
+###
+### Internal links
+###
+!! test
+Plain link, capitalized
+!! wikitext
+[[Main Page]]
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">Main Page</a>
+</p>
+!! end
+
+!! test
+Plain link, uncapitalized
+!! wikitext
+[[main Page]]
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">main Page</a>
+</p>
+!! end
+
+!! test
+Piped link
+!! wikitext
+[[Main Page|The Main Page]]
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">The Main Page</a>
+</p>
+!! end
+
+!! test
+Piped link with comment in link text
+!! wikitext
+[[Main Page|The Main<!--front--> Page]]
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">The Main Page</a>
+</p>
+!! end
+
+!! test
+Piped link with multiple pipe characters in link text
+!! wikitext
+[[Main Page||The|Main|Page|]]
+!! html/php
+<p><a href="/wiki/Main_Page" title="Main Page">|The|Main|Page|</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="Main_Page" title="Main Page">|The|Main|Page|</a></p>
+!! end
+
+!! test
+Broken link
+!! wikitext
+[[Zigzagzogzagzig]]
+!! html
+<p><a href="/index.php?title=Zigzagzogzagzig&amp;action=edit&amp;redlink=1" class="new" title="Zigzagzogzagzig (page does not exist)">Zigzagzogzagzig</a>
+</p>
+!! end
+
+!! test
+Broken link with fragment
+!! wikitext
+[[Zigzagzogzagzig#zug]]
+!! html
+<p><a href="/index.php?title=Zigzagzogzagzig&amp;action=edit&amp;redlink=1" class="new" title="Zigzagzogzagzig (page does not exist)">Zigzagzogzagzig#zug</a>
+</p>
+!! end
+
+!! test
+Special page link with fragment
+!! wikitext
+[[Special:Version#anchor]]
+!! html
+<p><a href="/wiki/Special:Version#anchor" title="Special:Version">Special:Version#anchor</a>
+</p>
+!! end
+
+!! test
+Nonexistent special page link with fragment
+!! wikitext
+[[Special:ThisNameWillHopefullyNeverBeUsed#anchor]]
+!! html
+<p><a href="/wiki/Special:ThisNameWillHopefullyNeverBeUsed" class="new" title="Special:ThisNameWillHopefullyNeverBeUsed (page does not exist)">Special:ThisNameWillHopefullyNeverBeUsed#anchor</a>
+</p>
+!! end
+
+!! test
+Link with prefix
+!! wikitext
+xxx[[main Page]], xxx[[Main Page]], Xxx[[main Page]] XXX[[main Page]], XXX[[Main Page]]
+!! html
+<p>xxx<a href="/wiki/Main_Page" title="Main Page">main Page</a>, xxx<a href="/wiki/Main_Page" title="Main Page">Main Page</a>, Xxx<a href="/wiki/Main_Page" title="Main Page">main Page</a> XXX<a href="/wiki/Main_Page" title="Main Page">main Page</a>, XXX<a href="/wiki/Main_Page" title="Main Page">Main Page</a>
+</p>
+!! end
+
+!! test
+Link with suffix
+!! wikitext
+[[Main Page]]xxx, [[Main Page]]XXX, [[Main Page]]!!!
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">Main Pagexxx</a>, <a href="/wiki/Main_Page" title="Main Page">Main Page</a>XXX, <a href="/wiki/Main_Page" title="Main Page">Main Page</a>!!!
+</p>
+!! end
+
+!! article
+prefixed article
+!! text
+Some text
+!! endarticle
+
+!! test
+Bug 43661: Piped links with identical prefixes
+!! wikitext
+[[prefixed article|prefixed articles with spaces]]
+
+[[prefixed article|prefixed articlesaoeu]]
+
+[[Main Page|Main Page test]]
+!! html
+<p><a href="/wiki/Prefixed_article" title="Prefixed article">prefixed articles with spaces</a>
+</p><p><a href="/wiki/Prefixed_article" title="Prefixed article">prefixed articlesaoeu</a>
+</p><p><a href="/wiki/Main_Page" title="Main Page">Main Page test</a>
+</p>
+!! end
+
+
+!! test
+Link with HTML entity in suffix / tail
+!! wikitext
+[[Main Page]]&quot;, [[Main Page]]&#97;
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">Main Page</a>&quot;, <a href="/wiki/Main_Page" title="Main Page">Main Page</a>&#97;
+</p>
+!! end
+
+!! test
+Link with 3 brackets
+!! wikitext
+[[[Main Page]]]
+!! html
+<p>[[[Main Page]]]
+</p>
+!! end
+
+!! test
+Link with 4 brackets
+!! wikitext
+[[[[Main Page]]]]
+!! html
+<p>[[<a href="/wiki/Main_Page" title="Main Page">Main Page</a>]]
+</p>
+!! end
+
+!! test
+Piped link with 3 brackets
+!! wikitext
+[[[main page|the main page]]]
+!! html
+<p>[[[main page|the main page]]]
+</p>
+!! end
+
+!! test
+Piped link with extlink-like text
+!! wikitext
+[[Main Page|[bar]]]
+[[Main Page|This is a [bar]]]
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">[bar]</a>
+<a href="/wiki/Main_Page" title="Main Page">This is a [bar]</a>
+</p>
+!! end
+
+!! test
+Link with multiple pipes
+!! wikitext
+[[Main Page|The|Main|Page]]
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">The|Main|Page</a>
+</p>
+!! end
+
+# Note that parsoid does not munge anchor text; all non-space
+# characters are valid in HTML5 ids.
+!! test
+Anchor containing a #. (bug 63430)
+!! wikitext
+[[Main Page#And#Link]]
+!! html/php
+<p><a href="/wiki/Main_Page#And.23Link" title="Main Page">Main Page#And#Link</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Main_Page#And%23Link" title="Main Page">Main Page#And#Link</a></p>
+!! end
+
+!! test
+Link to namespaces
+!! wikitext
+[[Talk:Parser testing]], [[Meta:Disclaimers]]
+!! html
+<p><a href="/index.php?title=Talk:Parser_testing&amp;action=edit&amp;redlink=1" class="new" title="Talk:Parser testing (page does not exist)">Talk:Parser testing</a>, <a href="/index.php?title=Meta:Disclaimers&amp;action=edit&amp;redlink=1" class="new" title="Meta:Disclaimers (page does not exist)">Meta:Disclaimers</a>
+</p>
+!! end
+
+!! test
+Link with space in namespace
+!! wikitext
+[[User talk:Foo bar]]
+!! html
+<p><a href="/index.php?title=User_talk:Foo_bar&amp;action=edit&amp;redlink=1" class="new" title="User talk:Foo bar (page does not exist)">User talk:Foo bar</a>
+</p>
+!! end
+
+!! article
+MemoryAlpha:AlphaTest
+!! text
+This is an article in the MemoryAlpha namespace
+(which shadows the memoryalpha interwiki link).
+!! endarticle
+
+!! test
+Namespace takes precedence over interwiki link (bug 51680)
+!! wikitext
+[[MemoryAlpha:AlphaTest]]
+!! html
+<p><a href="/wiki/MemoryAlpha:AlphaTest" title="MemoryAlpha:AlphaTest">MemoryAlpha:AlphaTest</a>
+</p>
+!! end
+
+# The previous test doesn't work correctly in html2*, due to not recognizing the
+# link as an internal one. This one checks for the correct behavior.
+!! test
+Link to namespace preferred over interwiki with correct rel attribute
+!! options
+parsoid=html2wt,html2html
+!! wikitext
+[[MemoryAlpha:AlphaTest]]
+!! html
+<p><a rel="mw:WikiLink" href="./MemoryAlpha:AlphaTest" title="MemoryAlpha:AlphaTest">MemoryAlpha:AlphaTest</a>
+</p>
+!! end
+
+!! test
+Piped link to namespace
+!! wikitext
+[[Meta:Disclaimers|The disclaimers]]
+!! html
+<p><a href="/index.php?title=Meta:Disclaimers&amp;action=edit&amp;redlink=1" class="new" title="Meta:Disclaimers (page does not exist)">The disclaimers</a>
+</p>
+!! end
+
+!! test
+Link containing }
+!! wikitext
+[[Usually caused by a typo (oops}]]
+!! html
+<p>[[Usually caused by a typo (oops}]]
+</p>
+!! end
+
+!! article
+7% Solution
+!! text
+Just a test of an article title containing a percent.
+!! endarticle
+
+!! test
+Link containing % (not as a hex sequence)
+!! wikitext
+[[7% Solution]]
+!! html/php
+<p><a href="/wiki/7%25_Solution" title="7% Solution">7% Solution</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a></p>
+!! end
+
+# note that the parsoid HTML is identical to the previous test output,
+# so the previous test ensures that the html2wt mode will generate the
+# "not as a hex sequence" wikitext.
+!! test
+Link containing % as a single hex sequence interpreted to char
+!! options
+parsoid=wt2wt,wt2html,html2html
+!! wikitext
+[[7%25 Solution]]
+!! html/php
+<p><a href="/wiki/7%25_Solution" title="7% Solution">7% Solution</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a></p>
+!!end
+
+!! test
+Link containing % as a double hex sequence interpreted to hex sequence
+!! wikitext
+[[7%2525 Solution]]
+!! html
+<p>[[7%2525 Solution]]
+</p>
+!!end
+
+# note that parsoid does not munge anchor text; all non-space
+# characters are valid in HTML5 anchors.
+!! test
+Link containing "#<" and "#>" % as a hex sequences- these are valid section anchors
+Example for such a section: == < ==
+!! wikitext
+[[%23%3c]][[%23%3e]]
+!! html/php
+<p><a href="#.3C">#&lt;</a><a href="#.3E">#&gt;</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Main%20Page#%3C" title="Main Page">#&lt;</a><a rel="mw:WikiLink" href="./Main%20Page#%3E" title="Main Page">#></a></p>
+!! end
+
+!! test
+Link containing "<#" and ">#" as a hex sequences
+!! wikitext
+[[%3c%23]][[%3e%23]]
+!! html
+<p>[[%3c%23]][[%3e%23]]
+</p>
+!! end
+
+!! test
+Link containing an equals sign
+!! wikitext
+[[Special:BookSources/isbn=4-00-026157-6]]
+!! html/php
+<p><a href="/wiki/Special:BookSources/isbn%3D4-00-026157-6" title="Special:BookSources/isbn=4-00-026157-6">Special:BookSources/isbn=4-00-026157-6</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Special:BookSources/isbn=4-00-026157-6" title="Special:BookSources/isbn=4-00-026157-6">Special:BookSources/isbn=4-00-026157-6</a></p>
+!! end
+
+!! article
+Foo~bar
+!! text
+Just a test of an article title containing a tilde.
+!! endarticle
+
+# note that links containing signatures, like [[Foo~~~~]], are
+# massaged by the pre-save transform (PST) and so the tildes are never
+# seen by the parser.
+!! test
+Link containing a tilde
+!! wikitext
+[[Foo~bar]]
+!! html/php
+<p><a href="/wiki/Foo%7Ebar" title="Foo~bar">Foo~bar</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Foo~bar" title="Foo~bar">Foo~bar</a></p>
+!! end
+
+!! test
+Link containing double-single-quotes '' (bug 4598)
+!! wikitext
+[[Lista d''e paise d''o munno]]
+!! html/php
+<p><a href="/index.php?title=Lista_d%27%27e_paise_d%27%27o_munno&amp;action=edit&amp;redlink=1" class="new" title="Lista d''e paise d''o munno (page does not exist)">Lista d''e paise d''o munno</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Lista_d''e_paise_d''o_munno" title="Lista d''e paise d''o munno">Lista d''e paise d''o munno</a></p>
+!! end
+
+!! test
+Link containing double-single-quotes '' in text (bug 4598 sanity check)
+!! wikitext
+Some [[Link|pretty ''italics'' and stuff]]!
+!! html/php
+<p>Some <a href="/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">pretty <i>italics</i> and stuff</a>!
+</p>
+!! html/parsoid
+<p>Some <a rel="mw:WikiLink" href="Link" title="Link">pretty <i>italics</i> and stuff</a>!</p>
+!! end
+
+!! test
+Link containing double-single-quotes '' in text embedded in italics (bug 4598 sanity check)
+!! wikitext
+''Some [[Link|pretty ''italics'' and stuff]]!''
+!! html
+<p><i>Some <a href="/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">pretty <i>italics</i> and stuff</a>!</i>
+</p>
+!! end
+
+!! test
+Link with double quotes in title part (literal) and alternate part (interpreted)
+!! wikitext
+[[File:Denys Savchenko ''Pentecoste''.jpg]]
+
+[[''Pentecoste'']]
+
+[[''Pentecoste''|Pentecoste]]
+
+[[''Pentecoste''|''Pentecoste'']]
+!! html/php
+<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=Denys_Savchenko_%27%27Pentecoste%27%27.jpg" class="new" title="File:Denys Savchenko &#39;&#39;Pentecoste&#39;&#39;.jpg">File:Denys Savchenko <i>Pentecoste</i>.jpg</a>
+</p><p><a href="/index.php?title=%27%27Pentecoste%27%27&amp;action=edit&amp;redlink=1" class="new" title="''Pentecoste'' (page does not exist)">''Pentecoste''</a>
+</p><p><a href="/index.php?title=%27%27Pentecoste%27%27&amp;action=edit&amp;redlink=1" class="new" title="''Pentecoste'' (page does not exist)">Pentecoste</a>
+</p><p><a href="/index.php?title=%27%27Pentecoste%27%27&amp;action=edit&amp;redlink=1" class="new" title="''Pentecoste'' (page does not exist)"><i>Pentecoste</i></a>
+</p>
+!! html/parsoid
+<meta typeof="mw:Placeholder"/>
+<p><a rel="mw:WikiLink" href="''Pentecoste''" title="''Pentecoste''">''Pentecoste''</a></p>
+<p><a rel="mw:WikiLink" href="''Pentecoste''" title="''Pentecoste''">Pentecoste</a></p>
+<p><a rel="mw:WikiLink" href="''Pentecoste''" title="''Pentecoste''"><i>Pentecoste</i></a></p>
+!! end
+
+!! test
+Broken image links with HTML captions (bug 39700)
+!! wikitext
+[[File:Nonexistent|<script></script>]]
+[[File:Nonexistent|100px|<script></script>]]
+[[File:Nonexistent|&lt;]]
+[[File:Nonexistent|a<i>b</i>c]]
+!! html
+<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">&lt;script&gt;&lt;/script&gt;</a>
+<a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">&lt;script&gt;&lt;/script&gt;</a>
+<a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">&lt;</a>
+<a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">abc</a>
+</p>
+!! end
+
+!! test
+Plain link to URL
+!! wikitext
+[[http://www.example.com]]
+!! html/php
+<p>[<a rel="nofollow" class="external autonumber" href="http://www.example.com">[1]</a>]
+</p>
+!! html/parsoid
+<p>[<a rel="mw:ExtLink" href="http://www.example.com"></a>]</p>
+!! end
+
+!! test
+Plain link to URL with link text
+!! wikitext
+[[http://www.example.com Link text]]
+!! html
+<p>[<a rel="nofollow" class="external text" href="http://www.example.com">Link text</a>]
+</p>
+!! end
+
+!! test
+Plain link to protocol-relative URL
+!! wikitext
+[[//www.example.com]]
+!! html/php
+<p>[<a rel="nofollow" class="external autonumber" href="//www.example.com">[1]</a>]
+</p>
+!! html/parsoid
+<p>[<a rel="mw:ExtLink" href="//www.example.com"></a>]</p>
+!! end
+
+!! test
+Plain link to protocol-relative URL with link text
+!! wikitext
+[[//www.example.com Link text]]
+!! html
+<p>[<a rel="nofollow" class="external text" href="//www.example.com">Link text</a>]
+</p>
+!! end
+
+!! test
+Plain link to page with question mark in title
+!! wikitext
+[[A?b]]
+
+[[A?b|Baz]]
+!! html
+<p><a href="/wiki/A%3Fb" title="A?b">A?b</a>
+</p><p><a href="/wiki/A%3Fb" title="A?b">Baz</a>
+</p>
+!! end
+
+
+# I'm fairly sure the expected result here is wrong.
+# We want these to be URL links, not pseudo-pages with URLs for titles....
+# However the current output is also pretty screwy.
+#
+# ----
+# I'm changing it to match the current output--it arguably makes more
+# sense in the light of the test above. Old expected result was:
+#<p>Piped link to URL: <a href="/index.php?title=Http://www.example.com&amp;action=edit" class="new">an example URL</a>
+#</p>
+# But I think this test is bordering on "garbage in, garbage out" anyway.
+# -- wtm
+!! test
+Piped link to URL
+!! wikitext
+Piped link to URL: [[http://www.example.com|an example URL]]
+!! html/php
+<p>Piped link to URL: [<a rel="nofollow" class="external text" href="http://www.example.com%7Can">example URL</a>]
+</p>
+!! html/parsoid
+<p>Piped link to URL: [<a rel="mw:ExtLink" href="http://www.example.com|an">example URL</a>]</p>
+!! end
+
+!! test
+BUG 2: [[page|http://url/]] should link to page, not http://url/
+!! wikitext
+[[Main Page|http://url/]]
+!! html/php
+<p><a href="/wiki/Main_Page" title="Main Page">http://url/</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Main_Page" title="Main Page">http://url/</a></p>
+!! end
+
+# Parsoid does not mark self-links, by design.
+!! test
+BUG 337: Escaped self-links should be bold
+!! options
+title=[[Bug462]]
+!! wikitext
+[[Bu&#103;462]] [[Bug462]]
+!! html/php
+<p><strong class="selflink">Bu&#103;462</strong> <strong class="selflink">Bug462</strong>
+</p>
+!! html/php+tidy
+<p><strong class="selflink">Bug462</strong> <strong class="selflink">Bug462</strong></p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Bug462" title="Bug462">Bug462</a> <a rel="mw:WikiLink" href="./Bug462" title="Bug462">Bug462</a></p>
+!! end
+
+!! test
+Self-link to section should not be bold
+!! options
+title=[[Main Page]]
+!! wikitext
+[[Main Page#section]]
+!! html
+<p><a href="/wiki/Main_Page#section" title="Main Page">Main Page#section</a>
+</p>
+!! end
+
+!! article
+00
+!! text
+This is 00.
+!! endarticle
+
+!!test
+Self-link to numeric title
+!!options
+title=[[0]]
+!! wikitext
+[[0]]
+!! html
+<p><strong class="selflink">0</strong>
+</p>
+!!end
+
+!!test
+Link to numeric-equivalent title
+!!options
+title=[[0]]
+!! wikitext
+[[00]]
+!! html
+<p><a href="/wiki/00" title="00">00</a>
+</p>
+!!end
+
+!! test
+<nowiki> inside a link
+!! wikitext
+[[Main<nowiki> Page</nowiki>]] [[Main Page|the main page <nowiki>[it's not very good]</nowiki>]]
+!! html
+<p>[[Main Page]] <a href="/wiki/Main_Page" title="Main Page">the main page [it's not very good]</a>
+</p>
+!! end
+
+!! test
+Non-breaking spaces in title
+!! wikitext
+[[&nbsp; Main &nbsp; Page &nbsp;]]
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">&#160; Main &#160; Page &#160;</a>
+</p>
+!!end
+
+!! test
+Internal link with ca linktrail, surrounded by bold apostrophes (bug 27473 primary issue)
+!! options
+language=ca
+!! wikitext
+'''[[Main Page]]'''
+!! html
+<p><b><a href="/wiki/Main_Page" title="Main Page">Main Page</a></b>
+</p>
+!! end
+
+!! test
+Internal link with ca linktrail, surrounded by italic apostrophes (bug 27473 primary issue)
+!! options
+language=ca
+!! wikitext
+''[[Main Page]]''
+!! html
+<p><i><a href="/wiki/Main_Page" title="Main Page">Main Page</a></i>
+</p>
+!! end
+
+!! test
+Internal link with en linktrail: no apostrophes (bug 27473)
+!! options
+language=en
+!! wikitext
+[[Something]]'nice
+!! html
+<p><a href="/index.php?title=Something&amp;action=edit&amp;redlink=1" class="new" title="Something (page does not exist)">Something</a>'nice
+</p>
+!! end
+
+!! test
+Internal link with ca linktrail with apostrophes (bug 27473)
+!! options
+language=ca
+!! wikitext
+[[Something]]'nice
+!! html
+<p><a href="/index.php?title=Something&amp;action=edit&amp;redlink=1" class="new" title="Something (encara no existeix)">Something'nice</a>
+</p>
+!! end
+
+!! test
+Internal link with kaa linktrail with apostrophes (bug 27473)
+!! options
+language=kaa
+!! wikitext
+[[Something]]'nice
+!! html
+<p><a href="/index.php?title=Something&amp;action=edit&amp;redlink=1" class="new" title="Something (bet ele jaratılmag'an)">Something'nice</a>
+</p>
+!! end
+
+!! test
+Link with multiple ":" in a subpage-supporting namespace (bug 63636)
+!! wikitext
+[[User:Foo/Test/63636:Bar|Test]]
+!! html/php
+<p><a href="/index.php?title=User:Foo/Test/63636:Bar&amp;action=edit&amp;redlink=1" class="new" title="User:Foo/Test/63636:Bar (page does not exist)">Test</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./User:Foo/Test/63636:Bar" title="User:Foo/Test/63636:Bar">Test</a></p>
+!! end
+
+!! test
+Purely hash wikilink
+!! options
+title=[[User:test/123]]
+!! wikitext
+[[#a|b]]
+!! html/php
+<p><a href="#a">b</a>
+</p>
+!! html/parsoid
+<p data-parsoid='{}'><a rel="mw:WikiLink" href="../User:Test/123#a" data-parsoid='{"stx":"piped","a":{"href":"../User:Test/123#a"},"sa":{"href":"#a"}}'>b</a></p>
+!! end
+
+!! test
+1. Interaction of linktrail and template encapsulation
+!! options
+parsoid
+!! wikitext
+{{echo|[[Foo]]}}l
+!! html
+<p><a rel="mw:WikiLink" href="Foo" title="Foo" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[Foo]]"}},"i":0}},"l"]}'>Fool</a></p>
+!! end
+
+!! test
+2. Interaction of linktrail and template encapsulation
+!! options
+parsoid
+!! wikitext
+{{echo|Some [[Fool]]}}s
+!! html
+<p data-parsoid='{}'><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"Some [[Fool]]"}},"i":0}},"s"]}' data-parsoid='{"pi":[[{"k":"1","spc":["","","",""]}]]}'>Some </span><a rel="mw:WikiLink" href="./Fool" title="Fool" about="#mwt1" data-parsoid='{"stx":"simple","a":{"href":"./Fool"},"sa":{"href":"Fool"},"tail":"s"}'>Fools</a></p>
+!! end
+
+!! test
+3. Interaction of linktrail and template encapsulation
+!! options
+parsoid
+!! wikitext
+{{echo|Some [[Fool]]s are '''bold and foolish'''}}
+!! html
+<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"Some [[Fool]]s are &#39;&#39;&#39;bold and foolish&#39;&#39;&#39;"}},"i":0}}]}' data-parsoid='{"pi":[[{"k":"1","spc":["","","",""]}]]}'>Some <a rel="mw:WikiLink" href="./Fool" title="Fool" data-parsoid='{"stx":"simple","a":{"href":"./Fool"},"sa":{"href":"Fool"},"tail":"s"}'>Fools</a> are <b data-parsoid="{}">bold and foolish</b></p>
+!! end
+
+!! article
+Söfnuður
+!! text
+Test.
+!! endarticle
+
+!! test
+Internal link with is link prefix
+!! options
+language=is
+!! wikitext
+Aðrir mótmælenda[[söfnuður|söfnuðir]] og
+!! html
+<p>Aðrir <a href="/wiki/S%C3%B6fnu%C3%B0ur" title="Söfnuður">mótmælendasöfnuðir</a> og
+</p>
+!! end
+
+!! article
+Mótmælendatrú
+!! text
+Test.
+!! endarticle
+
+!! test
+Internal link with is link trail and link prefix
+!! options
+language=is
+!! wikitext
+[[mótmælendatrú|xxx]]ar
+[[mótmælendatrú]]ar
+mótmælenda[[söfnuður]]
+mótmælenda[[söfnuður|söfnuðir]]
+mótmælenda[[söfnuður|söfnuðir]]xxx
+!! html
+<p><a href="/wiki/M%C3%B3tm%C3%A6lendatr%C3%BA" title="Mótmælendatrú">xxxar</a>
+<a href="/wiki/M%C3%B3tm%C3%A6lendatr%C3%BA" title="Mótmælendatrú">mótmælendatrúar</a>
+<a href="/wiki/S%C3%B6fnu%C3%B0ur" title="Söfnuður">mótmælendasöfnuður</a>
+<a href="/wiki/S%C3%B6fnu%C3%B0ur" title="Söfnuður">mótmælendasöfnuðir</a>
+<a href="/wiki/S%C3%B6fnu%C3%B0ur" title="Söfnuður">mótmælendasöfnuðirxxx</a>
+</p>
+!! end
+
+!! test
+Parsoid link trail escaping
+!! options
+parsoid=html2wt,html2html
+!! wikitext
+[[apple]]<nowiki/>s
+!! html
+<p><a rel="mw:WikiLink" href="Apple" title="Apple">apple</a>s</p>
+!! end
+
+!! test
+Parsoid link prefix escaping
+!! options
+language=is
+parsoid=html2wt,html2html
+!! wikitext
+Aðrir mótmælenda<nowiki/>[[söfnuður]]
+!! html
+<p>Aðrir mótmælenda<a rel="mw:WikiLink" href="Söfnuður" title="Söfnuður">söfnuður</a></p>
+!! end
+
+!! test
+Parsoid-centric test: Whitespace in ext- and wiki-links should be preserved
+!! wikitext
+[[Foo| bar]]
+
+[[Foo| ''bar'']]
+
+[http://wp.org foo]
+
+[http://wp.org ''foo'']
+!! html
+<p><a href="/wiki/Foo" title="Foo"> bar</a>
+</p><p><a href="/wiki/Foo" title="Foo"> <i>bar</i></a>
+</p><p><a rel="nofollow" class="external text" href="http://wp.org">foo</a>
+</p><p><a rel="nofollow" class="external text" href="http://wp.org"><i>foo</i></a>
+</p>
+!! end
+
+!! test
+Parsoid: Scoped parsing should handle mixed transclusions and plain text
+!! options
+parsoid
+!! wikitext
+[[Foo|{{echo|a}} b {{echo|c}}]]
+!! html
+<p><a rel="mw:WikiLink" href="Foo" title="Foo"><span about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a"}},"i":0}}]}'>a</span> b <span about="#mwt3" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"c"}},"i":0}}]}'>c</span></a></p>
+!! end
+
+!! test
+Link with angle bracket after anchor
+!! wikitext
+[[Foo#<bar>]]
+!! html/php
+<p><a href="/wiki/Foo#.3Cbar.3E" title="Foo">Foo#&lt;bar&gt;</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Foo#%3Cbar%3E" title="Foo" data-parsoid='{"stx":"simple","a":{"href":"./Foo#%3Cbar%3E"},"sa":{"href":"Foo#&lt;bar>"}}'>Foo#&lt;bar></a></p>
+!! end
+
+###
+### Interwiki links (see maintenance/interwiki.sql)
+###
+
+!! test
+Inline interwiki link
+!! wikitext
+[[MeatBall:SoftSecurity]]
+!! html
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?SoftSecurity" class="extiw" title="meatball:SoftSecurity">MeatBall:SoftSecurity</a>
+</p>
+!! end
+
+!! test
+Inline interwiki link with empty title (bug 2372)
+!! wikitext
+[[MeatBall:]]
+!! html
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl" class="extiw" title="meatball:">MeatBall:</a>
+</p>
+!! end
+
+!! test
+Interwiki link encoding conversion (bug 1636)
+!! wikitext
+*[[Wikipedia:ro:Olteni&#0355;a]]
+*[[Wikipedia:ro:Olteni&#355;a]]
+!! html
+<ul><li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteni&#355;a</a></li>
+<li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteni&#355;a</a></li></ul>
+
+!! html+tidy
+<ul>
+<li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a></li>
+<li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a></li>
+</ul>
+!! end
+
+!! test
+Interwiki link with fragment (bug 2130)
+!! wikitext
+[[MeatBall:SoftSecurity#foo]]
+!! html
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?SoftSecurity#foo" class="extiw" title="meatball:SoftSecurity">MeatBall:SoftSecurity#foo</a>
+</p>
+!! end
+
+# Ideally the wikipedia: prefix here should be proto-relative too
+!! test
+Different interwiki prefixes mapping to the same URL
+!! wikitext
+[[:en:Foo]]
+
+[[:en:Foo|Foo]]
+
+[[wikipedia:Foo]]
+
+[[:wikipedia:Foo|Foo]]
+
+[[wikipedia:en:Foo]]
+
+[[:wikipedia:en:Foo]]
+
+[[ wikiPEdia :Foo]]
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="//en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"simple","a":{"href":"//en.wikipedia.org/wiki/Foo"},"sa":{"href":":en:Foo"},"isIW":true}'>en:Foo</a></p>
+
+<p><a rel="mw:ExtLink" href="//en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"piped","a":{"href":"//en.wikipedia.org/wiki/Foo"},"sa":{"href":":en:Foo"},"isIW":true}'>Foo</a></p>
+
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":"wikipedia:Foo"},"isIW":true}'>wikipedia:Foo</a></p>
+
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"piped","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":":wikipedia:Foo"},"isIW":true}'>Foo</a></p>
+
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/en:Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/en:Foo"},"sa":{"href":"wikipedia:en:Foo"},"isIW":true}'>wikipedia:en:Foo</a></p>
+
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/en:Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/en:Foo"},"sa":{"href":":wikipedia:en:Foo"},"isIW":true}'>wikipedia:en:Foo</a></p>
+
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":" wikiPEdia :Foo"},"isIW":true}'> wikiPEdia :Foo</a></p>
+!! end
+
+!! test
+Interwiki links that cannot be represented in wiki syntax
+!! wikitext
+[[meatball:ok]]
+[[meatball:ok#foo|ok with fragment]]
+[[meatball:ok_as_well?|ok ending with ? mark]]
+[http://de.wikipedia.org/wiki/Foo?action=history has query]
+[http://de.wikipedia.org/wiki/#foo is just fragment]
+
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?ok">meatball:ok</a>
+<a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?ok#foo">ok with fragment</a>
+<a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?ok_as_well?">ok ending with ? mark</a>
+<a rel="mw:ExtLink" href="http://de.wikipedia.org/wiki/Foo?action=history">has query</a>
+<a rel="mw:ExtLink" href="http://de.wikipedia.org/wiki/#foo">is just fragment</a></p>
+!! end
+
+!! test
+Interwiki links: trail
+!! options
+parsoid
+!! wikitext
+[[wikipedia:Foo|Ba]]r
+!! html
+<p data-parsoid='{}'><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"piped","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":"wikipedia:Foo"},"isIW":true,"tail":"r"}'>Bar</a></p>
+!! end
+
+!! test
+Local interwiki link
+!! wikitext
+[[local:Template:Foo]]
+!! html
+<p><a href="/wiki/Template:Foo" title="Template:Foo">local:Template:Foo</a>
+</p>
+!! end
+
+!! test
+Local interwiki link: self-link to current page
+!! options
+title=[[Main Page]]
+!! wikitext
+[[local:Main Page]]
+!! html
+<p><strong class="selflink">local:Main Page</strong>
+</p>
+!! end
+
+!! test
+Local interwiki link: prefix only (bug 64167)
+!! wikitext
+[[local:]]
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">local:</a>
+</p>
+!! end
+
+!! test
+Local interwiki link: with additional interwiki prefix (bug 61357)
+!! wikitext
+[[local:meatball:Hello]]
+!! html
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?Hello" class="extiw" title="meatball:Hello">local:meatball:Hello</a>
+</p>
+!! end
+
+###
+### Interlanguage links
+### Language links (so that searching for '### language' matches..)
+###
+
+!! test
+Interlanguage link
+!! wikitext
+Blah blah blah
+[[zh:Chinese]]
+!! html/php
+<p>Blah blah blah
+</p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
+!! end
+
+!! test
+Interlanguage link with spacing
+!! wikitext
+Blah blah blah
+[[ zh : Chinese ]]
+!! html/php
+<p>Blah blah blah
+</p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
+!! end
+
+!! test
+Double interlanguage link
+!! wikitext
+Blah blah blah
+[[es:Spanish]]
+[[zh:Chinese]]
+!! html/php
+<p>Blah blah blah
+</p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Spanish"/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
+!! end
+
+!! test
+Interlanguage link variations
+!! wikitext
+Blah blah blah
+[[ es :Spanish]]
+[[ ZH :Chinese]]
+!! html/php
+<p>Blah blah blah
+</p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Spanish" data-parsoid='{"stx":"simple","a":{"href":"//es.wikipedia.org/wiki/Spanish"},"sa":{"href":" es :Spanish"}}'/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese" data-parsoid='{"stx":"simple","a":{"href":"//zh.wikipedia.org/wiki/Chinese"},"sa":{"href":" ZH :Chinese"}}'/>
+!! end
+
+!! test
+Interlanguage link, with prefix links
+!! options
+language=ln
+!! wikitext
+Blah blah blah
+[[zh:Chinese]]
+!! html/php
+<p>Blah blah blah
+</p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
+!! end
+
+!! test
+Double interlanguage link, with prefix links (bug 8897)
+!! options
+language=ln
+!! wikitext
+Blah blah blah
+[[es:Spanish]]
+[[zh:Chinese]]
+!! html/php
+<p>Blah blah blah
+</p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Spanish"/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
+!! end
+
+!! test
+"Extra" interlanguage links (bug 32189 / gerrit 111390)
+!! wikitext
+Blah blah blah
+[[mul:Article]]
+!! html/php
+<p>Blah blah blah
+</p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" title="Multilingual" href="//wikisource.org/wiki/Article"/></p>
+!! end
+
+!! test
+Parsoid-specific test: Wikilinks with &nbsp; should RT properly
+!! options
+language=ln
+!! wikitext
+[[WW&nbsp;II]]
+!! html
+<p><a href="/index.php?title=WW_II&amp;action=edit&amp;redlink=1" class="new" title="WW II (lonkásá ezalí tɛ̂)">WW&#160;II</a>
+</p>
+!! end
+
+!! test
+Parsoid bug 53221: Wikilinks should be properly entity-escaped
+!! options
+parsoid=html2wt
+!! wikitext
+He&amp;nbsp;llo [[Foo|He&amp;nbsp;llo]]
+
+He&amp;nbsp;llo [[He&amp;nbsp;llo]]
+!! html
+<p>He&amp;nbsp;llo <a href="Foo" rel="mw:WikiLink">He&amp;nbsp;llo</a></p>
+<p>He&amp;nbsp;llo <a href="He&amp;nbsp;llo" rel="mw:WikiLink">He&amp;nbsp;llo</a></p>
+!! end
+
+!! test
+Parsoid: handle constructor well
+!! options
+parsoid
+!! wikitext
+[[constructor]]
+
+[[constructor:foo]]
+!! html
+<p><a rel="mw:WikiLink" href="./Constructor" title="Constructor" data-parsoid="{&quot;stx&quot;:&quot;simple&quot;,&quot;a&quot;:{&quot;href&quot;:&quot;./Constructor&quot;},&quot;sa&quot;:{&quot;href&quot;:&quot;constructor&quot;}}">constructor</a></p>
+
+<p><a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid="{&quot;stx&quot;:&quot;simple&quot;,&quot;a&quot;:{&quot;href&quot;:&quot;./Foo&quot;},&quot;sa&quot;:{&quot;href&quot;:&quot;constructor:foo&quot;}}">constructor:foo</a></p>
+!! end
+
+!! test
+Parsoid: recognize interlanguage links without a target page
+!! options
+parsoid
+!! wikitext
+[[ko:]]
+!! html
+<p><link rel="mw:PageProp/Language" href="http://ko.wikipedia.org/wiki/"></p>
+!! end
+
+!! test
+Parsoid: recognize interwiki links without a target page
+!! options
+parsoid
+!! wikitext
+[[:ko:]]
+!! html
+<p><a rel="mw:ExtLink" href="//ko.wikipedia.org/wiki/">ko:</a></p>
+!! end
+
+!! test
+Parsoid: Bug #45209, handle interwiki links pointing to the current wiki as plain wiki links
+!! options
+parsoid
+!! wikitext
+[[en:Foo]]
+!! html
+<p><a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid='{"stx":"simple","a":{"href":"./Foo"},"sa":{"href":"en:Foo"}}'>Foo</a></p>
+!! end
+
+!! test
+Interlanguage link with preceding local interwiki link (bug 68085)
+!! wikitext
+Blah blah blah
+[[local:es:Spanish]]
+!! html
+<p>Blah blah blah
+<a href="http://es.wikipedia.org/wiki/Spanish" class="extiw" title="es:Spanish">local:es:Spanish</a>
+</p>
+!! end
+
+!! test
+Looks like an interlanguage link, but is actually a local interwiki
+!! wikitext
+Blah blah blah
+[[mi:Template:Foo]]
+!! html
+<p>Blah blah blah
+<a href="/wiki/Template:Foo" title="Template:Foo">mi:Template:Foo</a>
+</p>
+!! end
+
+###
+### Redirects, Parsoid-only
+###
+!! test
+1. Simple redirect to page
+!! options
+parsoid
+!! wikitext
+#REDIRECT [[Main Page]]
+!! html
+<link rel="mw:PageProp/redirect" href="./Main_Page">
+!! end
+
+# Only wt2html and html2html since "Main_Page" will serialize to "Main Page"
+!! test
+2. Other redirect variants
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+#REDIRECT [[Main_Page]]
+#REDIRECT [[<nowiki>[[Bar]]</nowiki>]]
+!! html
+<link rel="mw:PageProp/redirect" href="./Main_Page">
+<link rel="mw:PageProp/redirect" href="./%5B%5BBar%5D%5D">
+!! end
+
+!! test
+Empty redirect
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+#REDIRECT [[]]
+!! html
+<ol>
+<li>REDIRECT [[]]</li></ol>
+!! end
+
+!! test
+Optional colon in #REDIRECT
+!! options
+# the colon is archaic syntax. we support it for wt2html, but we
+# don't care that it roundtrips back to the modern syntax.
+parsoid=wt2html,html2html
+!! wikitext
+#REDIRECT:[[Main Page]]
+!! html
+<link rel="mw:PageProp/redirect" href="./Main_Page">
+!! end
+
+!! test
+Whitespace in #REDIRECT with optional colon
+!! options
+# the colon and gratuitous whitespace is archaic syntax. we support
+# it for wt2html, but we don't care that it roundtrips back to the
+# modern syntax (without extra whitespace)
+parsoid=wt2html,html2html
+!! wikitext
+
+ #REDIRECT
+:
+[[Main Page]]
+!! html
+<link rel="mw:PageProp/redirect" href="./Main_Page">
+!! end
+
+!! test
+Piped link in #REDIRECT
+!! options
+# content after piped link is ignored. we support this syntax,
+# but don't care that the piped link is lost when we roundtrip this.
+parsoid=wt2html
+!! wikitext
+#REDIRECT [[Main Page|bar]]
+!! html
+<link rel="mw:PageProp/redirect" href="./Main_Page">
+!! end
+
+!! test
+Redirect to category
+!! options
+parsoid=wt2html
+!! wikitext
+#REDIRECT [[Category:Foo]]
+!! html
+<link rel="mw:PageProp/redirect" href="./Category:Foo"><link rel="mw:PageProp/Category" href="./Category:Foo">
+!! end
+
+!! test
+Redirect to category with URL encoding
+!! options
+parsoid=wt2html
+!! wikitext
+#REDIRECT [[Category%3AFoo]]
+!! html
+<link rel="mw:PageProp/redirect" href="./Category:Foo"><link rel="mw:PageProp/Category" href="./Category:Foo">
+!! end
+
+!! test
+Redirect to category page
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+#REDIRECT [[:Category:Foo]]
+!! html
+<p><a rel="mw:WikiLink" href="Category:Foo" title="Category:Foo">Category:Foo</a></p>
+!! end
+
+!! test
+Redirect to image page (1)
+!! options
+parsoid
+!! wikitext
+#REDIRECT [[File:Wiki.png]]
+!! html
+<link rel="mw:PageProp/redirect" href="./File:Wiki.png">
+!! end
+
+!! test
+Redirect to image page (2)
+!! options
+parsoid
+!! wikitext
+#REDIRECT [[Image:Wiki.png]]
+!! html
+<link rel="mw:PageProp/redirect" href="./File:Wiki.png">
+!! end
+
+!! test
+Redirect to language
+!! options
+parsoid
+!! wikitext
+#REDIRECT [[en:File:Wiki.png]]
+!! html
+<link rel="mw:PageProp/redirect" href="File:Wiki.png">
+!! end
+
+!! test
+Redirect to interwiki
+!! options
+parsoid
+!! wikitext
+#REDIRECT [[meatball:File:Wiki.png]]
+!! html
+<link rel="mw:PageProp/redirect" href="File:Wiki.png">
+!! end
+
+!! test
+Non-English #REDIRECT
+!! options
+parsoid
+language=is
+!! wikitext
+#TILVÍSUN [[Main Page]]
+!! html
+<link rel="mw:PageProp/redirect" href="./Main_Page">
+!! end
+
+!! test
+New redirect
+!! options
+parsoid=html2wt
+!! wikitext
+Foo
+#REDIRECT [[Foo]]
+!! html
+<p>Foo<link rel="mw:PageProp/redirect" href="./Foo"></p>
+!! end
+
+##
+## XHTML tidiness
+###
+
+!! test
+<br> to <br />
+!! wikitext
+1<br>2<br />3
+!! html
+<p>1<br />2<br />3
+</p>
+!! end
+
+!! test
+Broken br tag sanitization
+!! wikitext
+</br>
+!! html/php
+<p>&lt;/br&gt;
+</p>
+!! end
+
+# TODO: Fix html2html mode (bug 51055)!
+# This </br> handling was added as part of bug 50831; but it
+# differs from how PHP+tidy handles this. We should investigate
+# this.
+!! test
+Parsoid: Broken br tag recognition
+!! options
+parsoid=wt2html
+!! wikitext
+</br>
+
+<br/ >
+!! html/php+tidy
+<p>&lt;/br&gt;</p>
+<p><br /></p>
+!! html/parsoid
+<p><br></p>
+<p><br/></p>
+!! end
+
+!! test
+Incorrecly removing closing slashes from correctly formed XHTML
+!! wikitext
+<br style="clear:both;" />
+!! html
+<p><br style="clear:both;" />
+</p>
+!! end
+
+!! test
+Failing to transform badly formed HTML into correct XHTML
+!! wikitext
+<br style="clear: left;">
+<br style="clear: right;">
+<br style="clear: both;">
+!! html
+<p><br style="clear: left;" />
+<br style="clear: right;" />
+<br style="clear: both;" />
+</p>
+!!end
+
+!! test
+Handling html with a div self-closing tag
+!! wikitext
+<div title />
+<div title/>
+<div title/ >
+<div title=bar />
+<div title=bar/>
+<div title=bar/ >
+!! html
+<p>&lt;div title /&gt;
+&lt;div title/&gt;
+</p>
+<div>
+<p>&lt;div title=bar /&gt;
+&lt;div title=bar/&gt;
+</p>
+<div title="bar/"></div>
+</div>
+
+!! end
+
+!! test
+Handling html with a br self-closing tag
+!! wikitext
+<br title />
+<br title/>
+<br title/ >
+<br title=bar />
+<br title=bar/>
+<br title=bar/ >
+!! html/php
+<p><br title="title" />
+<br title="title" />
+<br />
+<br title="bar" />
+<br title="bar" />
+<br title="bar/" />
+</p>
+!! html/parsoid
+<p><br title="" />
+<br title="" />
+<br />
+<br title="bar" />
+<br title="bar" />
+<br title="bar/" />
+</p>
+!! end
+
+!! test
+Horizontal ruler (should it add that extra space?)
+!! wikitext
+<hr>
+<hr >
+foo <hr
+> bar
+!! html
+<hr />
+<hr />
+foo <hr /> bar
+
+!! end
+
+!! test
+Horizontal ruler -- 4+ dashes render hr
+!! wikitext
+----
+!! html
+<hr />
+
+!! end
+
+!! test
+Horizontal ruler -- eats additional dashes on the same line
+!! wikitext
+---------
+!! html
+<hr />
+
+!! end
+
+!! test
+Horizontal ruler -- does not collapse dashes on consecutive lines
+!! wikitext
+----
+----
+!! html
+<hr />
+<hr />
+
+!! end
+
+!! test
+Horizontal ruler -- <4 dashes render as plain text
+!! wikitext
+---
+!! html
+<p>---
+</p>
+!! end
+
+!! test
+Horizontal ruler -- Supports content following dashes on same line
+!! wikitext
+---- Foo
+!! html
+<hr /> Foo
+
+!! html+tidy
+<hr />
+<p>Foo</p>
+!! end
+
+###
+### Block-level elements
+###
+!! test
+Common list
+!! wikitext
+*Common list
+* item 2
+*item 3
+!! html
+<ul><li>Common list</li>
+<li> item 2</li>
+<li>item 3</li></ul>
+
+!! end
+
+!! test
+Numbered list
+!! wikitext
+#Numbered list
+#item 2
+# item 3
+!! html
+<ol><li>Numbered list</li>
+<li>item 2</li>
+<li> item 3</li></ol>
+
+!! end
+
+!! test
+Mixed list
+!! wikitext
+*Mixed list
+*# with numbers
+** and bullets
+*# and numbers
+*bullets again
+**bullet level 2
+***bullet level 3
+***#Number on level 4
+**bullet level 2
+**#Number on level 3
+**#Number on level 3
+*#number level 2
+*Level 1
+*** Level 3
+#** Level 3, but ordered
+!! html
+<ul><li>Mixed list
+<ol><li> with numbers</li></ol>
+<ul><li> and bullets</li></ul>
+<ol><li> and numbers</li></ol></li>
+<li>bullets again
+<ul><li>bullet level 2
+<ul><li>bullet level 3
+<ol><li>Number on level 4</li></ol></li></ul></li>
+<li>bullet level 2
+<ol><li>Number on level 3</li>
+<li>Number on level 3</li></ol></li></ul>
+<ol><li>number level 2</li></ol></li>
+<li>Level 1
+<ul><li><ul><li> Level 3</li></ul></li></ul></li></ul>
+<ol><li><ul><li><ul><li> Level 3, but ordered</li></ul></li></ul></li></ol>
+
+!! end
+
+!! test
+Nested lists 1
+!! wikitext
+*foo
+**bar
+!! html
+<ul><li>foo
+<ul><li>bar</li></ul></li></ul>
+
+!! end
+
+!! test
+Nested lists 2
+!! wikitext
+**foo
+*bar
+!! html
+<ul><li><ul><li>foo</li></ul></li>
+<li>bar</li></ul>
+
+!! end
+
+!! test
+Nested lists 3 (first element empty)
+!! wikitext
+*
+**bar
+!! html
+<ul><li>
+<ul><li>bar</li></ul></li></ul>
+
+!! end
+
+!! test
+Nested lists 4 (first element empty)
+!! wikitext
+**
+*bar
+!! html
+<ul><li><ul><li></li></ul></li>
+<li>bar</li></ul>
+
+!! end
+
+!! test
+Nested lists 5 (both elements empty)
+!! wikitext
+**
+*
+!! html
+<ul><li><ul><li></li></ul></li>
+<li></li></ul>
+
+!! end
+
+!! test
+Nested lists 6 (both elements empty)
+!! wikitext
+*
+**
+!! html
+<ul><li>
+<ul><li></li></ul></li></ul>
+
+!! end
+
+!! test
+Nested lists 7 (skip initial nesting levels)
+!! wikitext
+*** foo
+!! html
+<ul><li><ul><li><ul><li> foo</li></ul></li></ul></li></ul>
+
+!! end
+
+!! test
+Nested lists 8 (multiple nesting transitions)
+!! wikitext
+* foo
+*** bar
+** baz
+* boo
+!! html
+<ul><li> foo
+<ul><li><ul><li> bar</li></ul></li>
+<li> baz</li></ul></li>
+<li> boo</li></ul>
+
+!! end
+
+!! test
+1. Lists with start-of-line-transparent tokens before bullets: Comments
+!! wikitext
+*foo
+*<!--cmt-->bar
+<!--cmt-->*baz
+!! html
+<ul><li>foo</li>
+<li>bar</li>
+<li>baz</li></ul>
+
+!! end
+
+!! test
+2. Lists with start-of-line-transparent tokens before bullets: Template close
+!! wikitext
+*foo {{echo|bar
+}}*baz
+!! html
+<ul><li>foo bar</li>
+<li>baz</li></ul>
+
+!! end
+
+!! test
+List items are not parsed correctly following a <pre> block (bug 785)
+!! wikitext
+* <pre>foo</pre>
+* <pre>bar</pre>
+* zar
+!! html
+<ul><li> <pre>foo</pre></li>
+<li> <pre>bar</pre></li>
+<li> zar</li></ul>
+
+!! end
+
+!! test
+List items from template
+!! wikitext
+
+{{inner list}}
+* item 2
+
+* item 0
+{{inner list}}
+* item 2
+
+* item 0
+* notSOL{{inner list}}
+* item 2
+!! html
+<ul><li> item 1</li>
+<li> item 2</li></ul>
+<ul><li> item 0</li>
+<li> item 1</li>
+<li> item 2</li></ul>
+<ul><li> item 0</li>
+<li> notSOL</li>
+<li> item 1</li>
+<li> item 2</li></ul>
+
+!! end
+
+!! test
+List interrupted by empty line or heading
+!! wikitext
+* foo
+
+** bar
+== A heading ==
+* Another list item
+!! html
+<ul><li> foo</li></ul>
+<ul><li><ul><li> bar</li></ul></li></ul>
+<h2><span class="mw-headline" id="A_heading">A heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: A heading">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<ul><li> Another list item</li></ul>
+
+!!end
+
+!!test
+Multiple list tags generated by templates
+!! wikitext
+{{echo|<li>}}a
+{{echo|<li>}}b
+{{echo|<li>}}c
+!! html
+<li>a
+<li>b
+<li>c</li>
+</li>
+</li>
+
+!! html+tidy
+<ul>
+<li>a</li>
+<li>b</li>
+<li>c</li>
+</ul>
+!!end
+
+!!test
+Single-comment whitespace lines dont break lists, and neither do multi-comment whitespace lines
+!! wikitext
+*a
+<!--This line will NOT split the list-->
+*b
+ <!--This line will NOT split the list either-->
+*c
+ <!--foo--> <!----> <!--This line NOT split the list either-->
+*d
+!! html
+<ul><li>a</li>
+<li>b</li>
+<li>c</li>
+<li>d</li></ul>
+
+!!end
+
+!!test
+Replacing whitespace with tabs still doesn't break the list (gerrit 78327)
+!! wikitext
+*a
+<!--This line will NOT split the list-->
+*b
+ <!--This line will NOT split the list either-->
+*c
+ <!--foo--> <!----> <!--This line NOT split the list
+ either-->
+*d
+!! html
+<ul><li>a</li>
+<li>b</li>
+<li>c</li>
+<li>d</li></ul>
+
+!!end
+
+!!test
+Test the li-hack
+(The PHP parser relies on Tidy for the hack)
+!!options
+parsoid=wt2html,wt2wt
+!! wikitext
+* foo
+* <li>li-hack
+* {{echo|<li>templated li-hack}}
+* <!--foo--> <li> unsupported li-hack with preceding comments
+
+<ul>
+<li><li>not a li-hack
+</li>
+</ul>
+!! html+tidy
+<ul>
+<li>foo</li>
+<li>li-hack</li>
+<li>templated li-hack</li>
+<li>unsupported li-hack with preceding comments</li>
+</ul>
+<ul>
+<li>not a li-hack</li>
+</ul>
+!!end
+
+!! test
+Parsoid: Make sure nested lists are serialized on their own line even if HTML contains no newlines
+!! options
+parsoid
+!! wikitext
+# foo
+## bar
+* foo
+** bar
+: foo
+:: bar
+!! html
+<ol>
+<li> foo<ol>
+<li> bar</li>
+</ol></li>
+</ol><ul>
+<li> foo<ul>
+<li> bar</li>
+</ul></li>
+</ul><dl>
+<dd> foo<dl>
+<dd> bar</dd>
+</dl></dd>
+</dl>
+!! end
+
+!! test
+Parsoid: Test of whitespace serialization with Templated bullets
+!! options
+parsoid
+!! wikitext
+* {{bullet}}
+!! html
+<ul>
+<li> </li><li about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"bullet","href":"./Template:Bullet"},"params":{},"i":0}}]}'> Bar</li>
+</ul>
+!! end
+
+# ------------------------------------------------------------------------
+# The next set of tests are about Parsoid's ability to handle badly nested
+# tags (parse, minimize scope of fixup, and roundtrip back)
+# ------------------------------------------------------------------------
+
+!! test
+Unbalanced closing block tags break a list
+(php parser relies on Tidy to fix up)
+!! wikitext
+<div>
+*a</div><div>
+*b</div>
+!! html+tidy
+<div>
+<ul>
+<li>a</li>
+</ul>
+</div>
+<div>
+<ul>
+<li>b</li>
+</ul>
+</div>
+!! end
+
+# Parsoid fails this test, but it might be tricky to support properly.
+# See bug 68395.
+!! test
+Unbalanced closing non-block tags don't break a list
+(php parser relies on Tidy to fix up)
+!! wikitext
+<span>
+*a</span><span>
+*b</span>
+!! html/php+tidy
+<ul>
+<li><span>a</span></li>
+<li><span>b</span></li>
+</ul>
+!! html/parsoid
+<span>
+<ul>
+<li>a<span></span>
+</li>
+<li>b
+</li>
+</ul>
+</span>
+!! end
+
+!! test
+Unclosed formatting tags that straddle lists are closed and reopened
+(php parser relies on Tidy to fix up)
+!! wikitext
+# <s> a
+# b </s>
+!! html/php+tidy
+<ol>
+<li><s>a</s></li>
+<li><s>b</s></li>
+</ol>
+!! html/parsoid
+<ol>
+<li> <s> a </s>
+</li>
+<li> <s> b </s>
+</li>
+</ol>
+!! end
+
+# Parsoid fails this test, but it might be tricky to support properly.
+# See bug 68395.
+!!test
+List embedded in a non-block tag
+(Ugly Parsoid output -- worth fixing; PHP parser relies on Tidy)
+!! wikitext
+<small>
+* foo
+</small>
+!! html/php+tidy
+<ul>
+<li><small>foo</small></li>
+</ul>
+!! html/parsoid
+<small>
+<ul>
+<li> foo</li>
+</ul>
+</small>
+!!end
+
+# This is a bug in the PHP parser + tidy combination.
+# (The </tr> tag gets parsed as text and html-escaped by PHP,
+# and then fostered out of the table by tidy.)
+# We believe the Parsoid output to be correct.
+!! test
+Table with missing opening <tr> tag
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+<table>
+<td>foo</td>
+</tr>
+</table>
+!! html/php+tidy
+<p>&lt;/tr&gt;</p>
+<table>
+<tr>
+<td>foo</td>
+</tr>
+</table>
+!! html/parsoid
+<table>
+<tr>
+<td>foo</td>
+</tr>
+</table>
+!! end
+
+###
+### Magic Words
+###
+
+# Note that the current date is hard-coded as
+# 1970-01-01T00:02:03Z (a Thursday)
+# when running parser tests. The timezone is also fixed to GMT, so
+# local date will be identical to current date.
+
+!! test
+Magic Word: {{CURRENTDAY}}
+!! wikitext
+{{CURRENTDAY}}
+!! html
+<p>1
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTDAY2}}
+!! wikitext
+{{CURRENTDAY2}}
+!! html
+<p>01
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTDAYNAME}}
+!! wikitext
+{{CURRENTDAYNAME}}
+!! html
+<p>Thursday
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTDOW}}
+!! wikitext
+{{CURRENTDOW}}
+!! html
+<p>4
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTMONTH}}
+!! wikitext
+{{CURRENTMONTH}}
+!! html
+<p>01
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTMONTH1}}
+!! wikitext
+{{CURRENTMONTH1}}
+!! html
+<p>1
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTMONTHABBREV}}
+!! wikitext
+{{CURRENTMONTHABBREV}}
+!! html
+<p>Jan
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTMONTHNAME}}
+!! wikitext
+{{CURRENTMONTHNAME}}
+!! html
+<p>January
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTMONTHNAMEGEN}}
+!! wikitext
+{{CURRENTMONTHNAMEGEN}}
+!! html
+<p>January
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTTIME}}
+!! wikitext
+{{CURRENTTIME}}
+!! html
+<p>00:02
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTHOUR}}
+!! wikitext
+{{CURRENTHOUR}}
+!! html
+<p>00
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTWEEK}} (@bug 4594)
+!! wikitext
+{{CURRENTWEEK}}
+!! html
+<p>1
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTYEAR}}
+!! wikitext
+{{CURRENTYEAR}}
+!! html
+<p>1970
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTTIMESTAMP}}
+!! wikitext
+{{CURRENTTIMESTAMP}}
+!! html
+<p>19700101000203
+</p>
+!! end
+
+!! test
+Magic Words LOCAL (UTC)
+!! wikitext
+* {{LOCALMONTH}}
+* {{LOCALMONTH1}}
+* {{LOCALMONTHNAME}}
+* {{LOCALMONTHNAMEGEN}}
+* {{LOCALMONTHABBREV}}
+* {{LOCALDAY}}
+* {{LOCALDAY2}}
+* {{LOCALDAYNAME}}
+* {{LOCALYEAR}}
+* {{LOCALTIME}}
+* {{LOCALHOUR}}
+* {{LOCALWEEK}}
+* {{LOCALDOW}}
+* {{LOCALTIMESTAMP}}
+!! html
+<ul><li> 01</li>
+<li> 1</li>
+<li> January</li>
+<li> January</li>
+<li> Jan</li>
+<li> 1</li>
+<li> 01</li>
+<li> Thursday</li>
+<li> 1970</li>
+<li> 00:02</li>
+<li> 00</li>
+<li> 1</li>
+<li> 4</li>
+<li> 19700101000203</li></ul>
+
+!! end
+
+!! test
+Magic Word: {{FULLPAGENAME}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{FULLPAGENAME}}
+!! html
+<p>User:Ævar Arnfjörð Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{FULLPAGENAMEE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{FULLPAGENAMEE}}
+!! html
+<p>User:%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{TALKSPACE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{TALKSPACE}}
+!! html
+<p>User talk
+</p>
+!! end
+
+!! test
+Magic Word: {{TALKSPACE}}, same namespace
+!! options
+title=[[User talk:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{TALKSPACE}}
+!! html
+<p>User talk
+</p>
+!! end
+
+!! test
+Magic Word: {{TALKSPACE}}, main namespace
+!! options
+title=[[Parser Test]]
+!! wikitext
+{{TALKSPACE}}
+!! html
+<p>Talk
+</p>
+!! end
+
+!! test
+Magic Word: {{TALKSPACEE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{TALKSPACEE}}
+!! html
+<p>User_talk
+</p>
+!! end
+
+!! test
+Magic Word: {{SUBJECTSPACE}}
+!! options
+title=[[User talk:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{SUBJECTSPACE}}
+!! html
+<p>User
+</p>
+!! end
+
+!! test
+Magic Word: {{SUBJECTSPACE}}, same namespace
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{SUBJECTSPACE}}
+!! html
+<p>User
+</p>
+!! end
+
+!! test
+Magic Word: {{SUBJECTSPACE}}, main namespace
+!! options
+title=[[Parser Test]]
+!! wikitext
+{{SUBJECTSPACE}}
+!! html
+
+!! end
+
+!! test
+Magic Word: {{SUBJECTSPACEE}}
+!! options
+title=[[User talk:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{SUBJECTSPACEE}}
+!! html
+<p>User
+</p>
+!! end
+
+!! test
+Magic Word: {{NAMESPACE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{NAMESPACE}}
+!! html
+<p>User
+</p>
+!! end
+
+!! test
+Magic Word: {{NAMESPACEE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{NAMESPACEE}}
+!! html
+<p>User
+</p>
+!! end
+
+!! test
+Magic Word: {{NAMESPACENUMBER}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{NAMESPACENUMBER}}
+!! html
+<p>2
+</p>
+!! end
+
+!! test
+Magic Word: {{SUBPAGENAME}}
+!! options
+title=[[Ævar Arnfjörð Bjarmason/sub ö]] subpage
+!! wikitext
+{{SUBPAGENAME}}
+!! html
+<p>sub ö
+</p>
+!! end
+
+!! test
+Magic Word: {{SUBPAGENAMEE}}
+!! options
+title=[[Ævar Arnfjörð Bjarmason/sub ö]] subpage
+!! wikitext
+{{SUBPAGENAMEE}}
+!! html
+<p>sub_%C3%B6
+</p>
+!! end
+
+!! test
+Magic Word: {{ROOTPAGENAME}}
+!! options
+title=[[Ævar Arnfjörð Bjarmason/sub/sub2]] subpage
+!! wikitext
+{{ROOTPAGENAME}}
+!! html
+<p>Ævar Arnfjörð Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{ROOTPAGENAMEE}}
+!! options
+title=[[Ævar Arnfjörð Bjarmason/sub/sub2]] subpage
+!! wikitext
+{{ROOTPAGENAMEE}}
+!! html
+<p>%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{BASEPAGENAME}}
+!! options
+title=[[Ævar Arnfjörð Bjarmason/sub]] subpage
+!! wikitext
+{{BASEPAGENAME}}
+!! html
+<p>Ævar Arnfjörð Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{BASEPAGENAMEE}}
+!! options
+title=[[Ævar Arnfjörð Bjarmason/sub]] subpage
+!! wikitext
+{{BASEPAGENAMEE}}
+!! html
+<p>%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{TALKPAGENAME}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{TALKPAGENAME}}
+!! html
+<p>User talk:Ævar Arnfjörð Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{TALKPAGENAMEE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{TALKPAGENAMEE}}
+!! html
+<p>User_talk:%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{SUBJECTPAGENAME}}
+!! options
+title=[[User talk:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{SUBJECTPAGENAME}}
+!! html
+<p>User:Ævar Arnfjörð Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{SUBJECTPAGENAMEE}}
+!! options
+title=[[User talk:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{SUBJECTPAGENAMEE}}
+!! html
+<p>User:%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{NUMBEROFFILES}}
+!! wikitext
+{{NUMBEROFFILES}}
+!! html
+<p>5
+</p>
+!! end
+
+!! test
+Magic Word: {{PAGENAME}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{PAGENAME}}
+!! html
+<p>Ævar Arnfjörð Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{PAGENAME}} with metacharacters
+!! options
+title=[['foo & bar = baz']]
+!! wikitext
+''{{PAGENAME}}''
+!! html/php
+<p><i>&#39;foo &#38; bar &#61; baz&#39;</i>
+</p>
+!! html+tidy
+<p><i>'foo &amp; bar = baz'</i></p>
+!! end
+
+!! test
+Magic Word: {{PAGENAME}} with metacharacters (bug 26781)
+!! options
+title=[[*RFC 1234 http://example.com/]]
+!! wikitext
+{{PAGENAME}}
+!! html/php
+<p>&#42;RFC&#32;1234 http&#58;//example.com/
+</p>
+!! html+tidy
+<p>*RFC 1234 http://example.com/</p>
+!! end
+
+!! test
+Magic Word: {{PAGENAMEE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! wikitext
+{{PAGENAMEE}}
+!! html
+<p>%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{PAGENAMEE}} with metacharacters (bug 26781)
+!! options
+title=[[*RFC 1234 http://example.com/]]
+!! wikitext
+{{PAGENAMEE}}
+!! html/php
+<p>&#42;RFC_1234_http&#58;//example.com/
+</p>
+!! html+tidy
+<p>*RFC_1234_http://example.com/</p>
+!! end
+
+!! test
+Magic Word: {{REVISIONID}}
+!! wikitext
+{{REVISIONID}}
+!! html
+<p>1337
+</p>
+!! end
+
+!! test
+Magic Word: {{SCRIPTPATH}}
+!! wikitext
+{{SCRIPTPATH}}
+!! html
+<p>/
+</p>
+!! end
+
+!! test
+Magic Word: {{STYLEPATH}}
+!! wikitext
+{{STYLEPATH}}
+!! html
+<p>/skins
+</p>
+!! end
+
+!! test
+Magic Word: {{SERVER}}
+!! wikitext
+{{SERVER}}
+!! html
+<p><a rel="nofollow" class="external free" href="http://example.org">http://example.org</a>
+</p>
+!! end
+
+!! test
+Magic Word: {{SERVERNAME}}
+!! wikitext
+{{SERVERNAME}}
+!! html
+<p>example.org
+</p>
+!! end
+
+!! test
+Magic Word: {{SITENAME}}
+!! wikitext
+{{SITENAME}}
+!! html
+<p>MediaWiki
+</p>
+!! end
+
+!! test
+Case-sensitive magic words, when cased differently, should just be template transclusions
+!! wikitext
+{{CurrentMonth}}
+{{currentday}}
+{{cURreNTweEK}}
+{{currentHour}}
+!! html
+<p><a href="/index.php?title=Template:CurrentMonth&amp;action=edit&amp;redlink=1" class="new" title="Template:CurrentMonth (page does not exist)">Template:CurrentMonth</a>
+<a href="/index.php?title=Template:Currentday&amp;action=edit&amp;redlink=1" class="new" title="Template:Currentday (page does not exist)">Template:Currentday</a>
+<a href="/index.php?title=Template:CURreNTweEK&amp;action=edit&amp;redlink=1" class="new" title="Template:CURreNTweEK (page does not exist)">Template:CURreNTweEK</a>
+<a href="/index.php?title=Template:CurrentHour&amp;action=edit&amp;redlink=1" class="new" title="Template:CurrentHour (page does not exist)">Template:CurrentHour</a>
+</p>
+!! end
+
+!! test
+Case-insensitive magic words should still work with weird casing.
+!! wikitext
+{{sErVeRNaMe}}
+{{LCFirst:AOEU}}
+{{ucFIRST:aoeu}}
+{{SERver}}
+!! html
+<p>example.org
+aOEU
+Aoeu
+<a rel="nofollow" class="external free" href="http://example.org">http://example.org</a>
+</p>
+!! end
+
+!! test
+Namespace 1 {{ns:1}}
+!! wikitext
+{{ns:1}}
+!! html
+<p>Talk
+</p>
+!! end
+
+!! test
+Namespace 1 {{ns:01}}
+!! wikitext
+{{ns:01}}
+!! html
+<p>Talk
+</p>
+!! end
+
+!! test
+Namespace 0 {{ns:0}} (bug 4783)
+!! wikitext
+{{ns:0}}
+!! html
+
+!! end
+
+!! test
+Namespace 0 {{ns:00}} (bug 4783)
+!! wikitext
+{{ns:00}}
+!! html
+
+!! end
+
+!! test
+Namespace -1 {{ns:-1}}
+!! wikitext
+{{ns:-1}}
+!! html
+<p>Special
+</p>
+!! end
+
+!! test
+Namespace User {{ns:User}}
+!! wikitext
+{{ns:User}}
+!! html
+<p>User
+</p>
+!! end
+
+!! test
+Namespace User talk {{ns:User_talk}}
+!! wikitext
+{{ns:User_talk}}
+!! html
+<p>User talk
+</p>
+!! end
+
+!! test
+Namespace User talk {{ns:uSeR tAlK}}
+!! wikitext
+{{ns:uSeR tAlK}}
+!! html
+<p>User talk
+</p>
+!! end
+
+!! test
+Namespace File {{ns:File}}
+!! wikitext
+{{ns:File}}
+!! html
+<p>File
+</p>
+!! end
+
+!! test
+Namespace File {{ns:Image}}
+!! wikitext
+{{ns:Image}}
+!! html
+<p>File
+</p>
+!! end
+
+!! test
+Namespace (lang=de) Benutzer {{ns:User}}
+!! options
+language=de
+!! wikitext
+{{ns:User}}
+!! html
+<p>Benutzer
+</p>
+!! end
+
+!! test
+Namespace (lang=de) Benutzer Diskussion {{ns:3}}
+!! options
+language=de
+!! wikitext
+{{ns:3}}
+!! html
+<p>Benutzer Diskussion
+</p>
+!! end
+
+
+!! test
+Urlencode
+!! wikitext
+{{urlencode:hi world?!}}
+{{urlencode:hi world?!|WIKI}}
+{{urlencode:hi world?!|PATH}}
+{{urlencode:hi world?!|QUERY}}
+!! html
+<p>hi+world%3F%21
+hi_world%3F!
+hi%20world%3F%21
+hi+world%3F%21
+</p>
+!! end
+
+!! test
+Magic Word: prioritize type info over data-parsoid
+!! options
+parsoid=html2wt
+!! wikitext
+__FORCETOC__
+!! html
+<meta property="mw:PageProp/forcetoc" data-parsoid='{"src":"__NOTOC__","magicSrc":"__NOTOC__"}'/>
+!! end
+
+!! test
+Magic Word: serialize on separate line (parsoid)
+!! options
+parsoid=wt2wt,html2wt
+!! wikitext
+foo
+__NOTOC__
+bar
+!! html
+foo<meta property="mw:PageProp/notoc"/>bar
+!! end
+
+!! test
+Magic Word: rt non-english wikis
+!! options
+parsoid=wt2wt
+language=de
+!! wikitext
+__NOEDITSECTION__
+!! html
+<meta property="mw:PageProp/noeditsection" data-parsoid='{"src":"__NOEDITSECTION__","magicSrc":"__NOEDITSECTION__"}'/>
+!! end
+
+###
+### Magic links
+###
+!! test
+Magic links: internal link to RFC (bug 479)
+!! wikitext
+[[RFC 123]]
+!! html
+<p><a href="/index.php?title=RFC_123&amp;action=edit&amp;redlink=1" class="new" title="RFC 123 (page does not exist)">RFC 123</a>
+</p>
+!! end
+
+!! test
+Magic links: RFC (bug 479)
+!! wikitext
+RFC 822
+!! html
+<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc822">RFC 822</a>
+</p>
+!! end
+
+!! test
+Magic links: ISBN (bug 1937)
+!! wikitext
+ISBN 0-306-40615-2
+!! html
+<p><a href="/wiki/Special:BookSources/0306406152" class="internal mw-magiclink-isbn">ISBN 0-306-40615-2</a>
+</p>
+!! end
+
+!! test
+Magic links: PMID incorrectly converts space to underscore
+!! wikitext
+PMID 1234
+!! html
+<p><a class="external mw-magiclink-pmid" rel="nofollow" href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract">PMID 1234</a>
+</p>
+!! end
+
+###
+### Templates
+####
+
+!! test
+Nonexistent template
+!! wikitext
+{{thistemplatedoesnotexist}}
+!! html
+<p><a href="/index.php?title=Template:Thistemplatedoesnotexist&amp;action=edit&amp;redlink=1" class="new" title="Template:Thistemplatedoesnotexist (page does not exist)">Template:Thistemplatedoesnotexist</a>
+</p>
+!! end
+
+!! test
+Template with invalid target containing tags
+!! wikitext
+{{a<b>b</b>|{{echo|foo}}|{{echo|a}}={{echo|b}}|a = b}}
+!! html
+<p>{{a<b>b</b>|foo|a=b|a = b}}
+</p>
+!! end
+
+!! test
+Template with invalid target containing unclosed tag
+!! wikitext
+{{a<b>|{{echo|foo}}|{{echo|a}}={{echo|b}}|a = b}}
+!! html
+<p>{{a<b>|foo|a=b|a = b}}</b>
+</p>
+!! end
+
+!! test
+Template with invalid target containing wikilink
+!! wikitext
+{{[[Main Page]]}}
+!! html/php
+<p>{{<a href="/wiki/Main_Page" title="Main Page">Main Page</a>}}
+</p>
+!! html/parsoid
+<p><span typeof="mw:Transclusion" about="#mwt1" data-mw='{"parts":[{"template":{"target":{"wt":"[[Main Page]]"},"params":{},"i":0}}]}'>{{</span><a rel="mw:WikiLink" href="./Main_Page" about="#mwt1">Main Page</a><span about="#mwt1">}}</span></p>
+!! end
+
+!! test
+Template with just whitespace in it, bug #68421
+!! wikitext
+{{echo|{{ }}}}
+!! html/parsoid
+<p><span typeof="mw:Transclusion mw:Nowiki" about="#mwt1" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{ }}"}},"i":0}}]}'>{{ }}</span></p>
+!! end
+
+!! article
+Template:test
+!! text
+This is a test template
+!! endarticle
+
+!! test
+Simple template
+!! wikitext
+{{test}}
+!! html
+<p>This is a test template
+</p>
+!! end
+
+!! test
+Template with explicit namespace
+!! wikitext
+{{Template:test}}
+!! html
+<p>This is a test template
+</p>
+!! end
+
+
+!! article
+Template:paramtest
+!! text
+This is a test template with parameter {{{param}}}
+!! endarticle
+
+!! test
+Template parameter
+!! wikitext
+{{paramtest|param=foo}}
+!! html
+<p>This is a test template with parameter foo
+</p>
+!! end
+
+!! article
+Template:paramtestnum
+!! text
+[[{{{1}}}|{{{2}}}]]
+!! endarticle
+
+!! test
+Template unnamed parameter
+!! wikitext
+{{paramtestnum|Main Page|the main page}}
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">the main page</a>
+</p>
+!! end
+
+!! article
+Template:templatesimple
+!! text
+(test)
+!! endarticle
+
+!! article
+Template:templateredirect
+!! text
+#redirect [[Template:templatesimple]]
+!! endarticle
+
+!! article
+Template:templateasargtestnum
+!! text
+{{{{{1}}}}}
+!! endarticle
+
+!! article
+Template:templateasargtest
+!! text
+{{template{{{templ}}}}}
+!! endarticle
+
+!! article
+Template:templateasargtest2
+!! text
+{{{{{templ}}}}}
+!! endarticle
+
+!! test
+Template with template name as unnamed argument
+!! wikitext
+{{templateasargtestnum|templatesimple}}
+!! html
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with template name as argument
+!! wikitext
+{{templateasargtest|templ=simple}}
+!! html
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with template name as argument (2)
+!! wikitext
+{{templateasargtest2|templ=templatesimple}}
+!! html
+<p>(test)
+</p>
+!! end
+
+!! article
+Template:templateasargtestdefault
+!! text
+{{{{{templ|templatesimple}}}}}
+!! endarticle
+
+!! article
+Template:templa
+!! text
+'''templ'''
+!! endarticle
+
+!! test
+Template with default value
+!! wikitext
+{{templateasargtestdefault}}
+!! html
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with default value (value set)
+!! wikitext
+{{templateasargtestdefault|templ=templa}}
+!! html
+<p><b>templ</b>
+</p>
+!! end
+
+!! test
+Template redirect
+!! wikitext
+{{templateredirect}}
+!! html
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with argument in separate line
+!! wikitext
+{{ templateasargtest |
+ templ = simple }}
+!! html
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with complex template as argument
+!! wikitext
+{{paramtest|
+ param ={{ templateasargtest |
+ templ = simple }}}}
+!! html
+<p>This is a test template with parameter (test)
+</p>
+!! end
+
+!! test
+Template with thumb image (with link in description)
+!! wikitext
+{{paramtest|
+ param =[[Image:noimage.png|thumb|[[no link|link]] [[no link|caption]]]]}}
+!! html/php
+This is a test template with parameter <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&amp;wpDestFile=Noimage.png" class="new" title="File:Noimage.png">File:Noimage.png</a> <div class="thumbcaption"><a href="/index.php?title=No_link&amp;action=edit&amp;redlink=1" class="new" title="No link (page does not exist)">link</a> <a href="/index.php?title=No_link&amp;action=edit&amp;redlink=1" class="new" title="No link (page does not exist)">caption</a></div></div></div>
+
+!! html+tidy
+<p>This is a test template with parameter</p>
+<div class="thumb tright">
+<div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&amp;wpDestFile=Noimage.png" class="new" title="File:Noimage.png">File:Noimage.png</a>
+<div class="thumbcaption"><a href="/index.php?title=No_link&amp;action=edit&amp;redlink=1" class="new" title="No link (page does not exist)">link</a> <a href="/index.php?title=No_link&amp;action=edit&amp;redlink=1" class="new" title="No link (page does not exist)">caption</a></div>
+</div>
+</div>
+!! end
+
+!! article
+Template:complextemplate
+!! text
+{{{1}}} {{paramtest|
+ param ={{{param}}}}}
+!! endarticle
+
+!! test
+Template with complex arguments
+!! wikitext
+{{complextemplate|
+ param ={{ templateasargtest |
+ templ = simple }}|[[Template:complextemplate|link]]}}
+!! html
+<p><a href="/wiki/Template:Complextemplate" title="Template:Complextemplate">link</a> This is a test template with parameter (test)
+</p>
+!! end
+
+!! test
+BUG 553: link with two variables in a piped link
+!! wikitext
+{|
+|[[{{{1}}}|{{{2}}}]]
+|}
+!! html
+<table>
+<tr>
+<td>[[{{{1}}}|{{{2}}}]]
+</td></tr></table>
+
+!! end
+
+!! test
+Magic variable as template parameter
+!! wikitext
+{{paramtest|param={{SITENAME}}}}
+!! html
+<p>This is a test template with parameter MediaWiki
+</p>
+!! end
+
+!! article
+Template:linktest
+!! text
+[[{{{param}}}|link]]
+!! endarticle
+
+!! test
+Template parameter as link source
+!! wikitext
+{{linktest|param=Main Page}}
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">link</a>
+</p>
+!! end
+
+!!test
+Template-generated attribute string (k='v')
+!! wikitext
+<span {{attr_str|id|v1}}>bar</span>
+!! html
+<p><span id="v1">bar</span>
+</p>
+!!end
+
+!!article
+Template:paramtest2
+!! text
+including another template, {{paramtest|param={{{arg}}}}}
+!! endarticle
+
+!! test
+Template passing argument to another template
+!! wikitext
+{{paramtest2|arg='hmm'}}
+!! html
+<p>including another template, This is a test template with parameter 'hmm'
+</p>
+!! end
+
+!! article
+Template:Linktest2
+!! text
+Main Page
+!! endarticle
+
+!! test
+Template as link source
+!! wikitext
+[[{{linktest2}}]]
+
+[[{{linktest2}}|Main Page]]
+
+[[{{linktest2}}]]Page
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">Main Page</a>
+</p><p><a href="/wiki/Main_Page" title="Main Page">Main Page</a>
+</p><p><a href="/wiki/Main_Page" title="Main Page">Main Page</a>Page
+</p>
+!! end
+
+
+!! article
+Template:loop1
+!! text
+{{loop2}}
+!! endarticle
+
+!! article
+Template:loop2
+!! text
+{{loop1}}
+!! endarticle
+
+!! test
+Template infinite loop
+!! wikitext
+{{loop1}}
+!! html
+<p><span class="error">Template loop detected: <a href="/wiki/Template:Loop1" title="Template:Loop1">Template:Loop1</a></span>
+</p>
+!! end
+
+!! test
+Template from main namespace
+!! wikitext
+{{:Main Page}}
+!! html
+<p>blah blah
+</p>
+!! end
+
+!! article
+Template:table
+!! text
+{|
+| 1 || 2
+|-
+| 3 || 4
+|}
+!! endarticle
+
+!! test
+BUG 529: Template with table, not included at beginning of line
+!! wikitext
+foo {{table}}
+!! html
+<p>foo
+</p>
+<table>
+<tr>
+<td> 1 </td>
+<td> 2
+</td></tr>
+<tr>
+<td> 3 </td>
+<td> 4
+</td></tr></table>
+
+!! end
+
+!! test
+BUG 523: Template shouldn't eat newline (or add an extra one before table)
+!! wikitext
+foo
+{{table}}
+!! html
+<p>foo
+</p>
+<table>
+<tr>
+<td> 1 </td>
+<td> 2
+</td></tr>
+<tr>
+<td> 3 </td>
+<td> 4
+</td></tr></table>
+
+!! end
+
+!! test
+BUG 41: Template parameters shown as broken links
+!! wikitext
+{{{parameter}}}
+!! html
+<p>{{{parameter}}}
+</p>
+!! end
+
+!! test
+Template with targets containing wikilinks
+!! wikitext
+{{[[foo]]}}
+
+{{[[{{echo|foo}}]]}}
+
+{{{{echo|[[foo}}]]}}
+!! html
+<p>{{<a href="/wiki/Foo" title="Foo">foo</a>}}
+</p><p>{{<a href="/wiki/Foo" title="Foo">foo</a>}}
+</p><p>{{[[foo}}]]
+</p>
+!! end
+
+!! article
+Template:MSGNW test
+!! text
+''None'' of '''this''' should be
+* interpreted
+ but rather passed unmodified
+{{test}}
+<gallery>
+File:Foobar.jpg
+</gallery>
+!! endarticle
+
+# hmm, fix this or just deprecate msgnw and document its behavior?
+!! test
+msgnw keyword
+!! wikitext
+{{msgnw:MSGNW test}}
+!! html
+<p>&#39;&#39;None&#39;&#39; of &#39;&#39;&#39;this&#39;&#39;&#39; should be
+&#42; interpreted
+&#32;but rather passed unmodified
+&#123;&#123;test&#125;&#125;
+&#60;gallery&#62;
+File:Foobar.jpg
+&#60;/gallery&#62;
+</p>
+!! end
+
+!! test
+int keyword
+!! wikitext
+{{int:youhavenewmessages|lots of money|not!}}
+!! html
+<p>You have lots of money (not!).
+</p>
+!! end
+
+!! article
+Template:Includes
+!! text
+Foo<noinclude>zar</noinclude><includeonly>bar</includeonly>
+!! endarticle
+
+!! test
+<includeonly> and <noinclude> being included
+!! wikitext
+{{Includes}}
+!! html
+<p>Foobar
+</p>
+!! end
+
+!! article
+Template:Includes2
+!! text
+<onlyinclude>Foo</onlyinclude>bar
+!! endarticle
+
+!! test
+<onlyinclude> being included
+!! wikitext
+{{Includes2}}
+!! html
+<p>Foo
+</p>
+!! end
+
+
+!! article
+Template:Includes3
+!! text
+<onlyinclude>Foo</onlyinclude>bar<includeonly>zar</includeonly>
+!! endarticle
+
+!! test
+<onlyinclude> and <includeonly> being included
+!! wikitext
+{{Includes3}}
+!! html
+<p>Foo
+</p>
+!! end
+
+!! test
+<includeonly> and <noinclude> on a page
+!! wikitext
+Foo<noinclude>zar</noinclude><includeonly>bar</includeonly>
+!! html
+<p>Foozar
+</p>
+!! end
+
+!! test
+Un-closed <noinclude>
+!! wikitext
+<noinclude>
+!! html
+!! end
+
+!! test
+<onlyinclude> on a page
+!! wikitext
+<onlyinclude>Foo</onlyinclude>bar
+!! html
+<p>Foobar
+</p>
+!! end
+
+!! test
+Un-closed <onlyinclude>
+!! wikitext
+<onlyinclude>
+!! html
+!! end
+
+!!test
+Self-closed noinclude, includeonly, onlyinclude tags
+!! wikitext
+<noinclude />
+<includeonly />
+<onlyinclude />
+!! html
+<p><br />
+</p>
+!!end
+
+!!test
+Unbalanced includeonly and noinclude tags
+!! wikitext
+{|
+|a</noinclude>
+|b</noinclude></noinclude>
+|c</noinclude></includeonly>
+|d</includeonly></includeonly>
+|}
+!! html
+<table>
+<tr>
+<td>a
+</td>
+<td>b
+</td>
+<td>c&lt;/includeonly&gt;
+</td>
+<td>d&lt;/includeonly&gt;&lt;/includeonly&gt;
+</td></tr></table>
+
+!!end
+
+!! article
+Template:Includeonly section
+!! text
+<includeonly>
+==Includeonly section==
+</includeonly>
+==Section T-1==
+!!endarticle
+
+!! test
+Bug 6563: Edit link generation for section shown by <includeonly>
+!! wikitext
+{{includeonly section}}
+!! html
+<h2><span class="mw-headline" id="Includeonly_section">Includeonly section</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Template:Includeonly_section&amp;action=edit&amp;section=T-1" title="Template:Includeonly section">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Section_T-1">Section T-1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Template:Includeonly_section&amp;action=edit&amp;section=T-2" title="Template:Includeonly section">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+# Uses same input as the contents of [[Template:Includeonly section]]
+!! test
+Bug 6563: Section extraction for section shown by <includeonly>
+!! options
+section=T-2
+!! wikitext
+<includeonly>
+==Includeonly section==
+</includeonly>
+==Section T-2==
+!! html
+==Section T-2==
+!! end
+
+!! test
+Bug 6563: Edit link generation for section suppressed by <includeonly>
+!! wikitext
+<includeonly>
+==Includeonly section==
+</includeonly>
+==Section 1==
+!! html
+<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+Bug 6563: Section extraction for section suppressed by <includeonly>
+!! options
+section=1
+!! wikitext
+<includeonly>
+==Includeonly section==
+</includeonly>
+==Section 1==
+!! html
+==Section 1==
+!! end
+
+!! test
+Un-closed <includeonly>
+!! wikitext
+<includeonly>
+!! html
+!! end
+
+!! test
+Includes and comments at SOL
+!! wikitext
+<!-- comment --><noinclude><!-- comment --></noinclude><!-- comment -->== hu ==
+
+<noinclude>
+some
+</noinclude>* stuff
+* here
+
+<includeonly>can have stuff</includeonly>=== here ===
+
+!! html/php
+<h2><span class="mw-headline" id="hu">hu</span></h2>
+<p>some
+</p>
+<ul><li> stuff</li>
+<li> here</li></ul>
+<h3><span class="mw-headline" id="here">here</span></h3>
+
+!! html/parsoid
+<!-- comment --><meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"&lt;noinclude>"}'/><!-- comment --><meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"&lt;/noinclude>"}'/><!-- comment -->
+<h2 data-parsoid='{}'> hu </h2>
+
+<meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"&lt;noinclude>"}'/>
+
+<p data-parsoid='{}'>some</p>
+<meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"&lt;/noinclude>"}'/>
+<ul data-parsoid='{}'>
+<li data-parsoid='{}'> stuff</li>
+
+<li data-parsoid='{}'> here</li></ul>
+
+<h3 data-parsoid='{}'> here </h3>
+!! end
+
+# TODO: test with DOM fragment reuse!
+!! test
+Parsoid: DOM fragment reuse
+!! options
+parsoid=wt2wt,wt2html
+!! wikitext
+a{{echo|b<table></table>c}}d
+
+a{{echo|b
+<table></table>
+c}}d
+
+{{echo|a
+
+<table></table>
+
+b}}
+!! html
+a<span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"b
+<table></table>c"}},"i":0}}]}'>b</span>
+<table about="#mwt1"></table><span about="#mwt1">c</span>d
+
+
+<p about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":["a",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"b\n<table></table>\nc"}},"i":0}},"d"]}'>ab</p><span about="#mwt2">
+</span>
+<table about="#mwt2"></table><span about="#mwt2">
+</span>
+<p about="#mwt2">cd</p>
+
+
+<p about="#mwt3" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a\n\n<table></table>\n\nb"}},"i":0}}]}'>a</p><span about="#mwt3">
+
+</span>
+<table about="#mwt3"></table><span about="#mwt3">
+
+</span>
+<p about="#mwt3">b</p>
+!! end
+
+!! test
+Parsoid: Merge double tds (bug 50603)
+!! options
+parsoid
+!! wikitext
+{|
+|{{echo|{{!}} foo}}
+|}
+!! html
+<table><tbody>
+<tr><td about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":["|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{!}} foo"}},"i":0}}]}'> foo</td></tr>
+</tbody></table>
+!! end
+
+!! test
+Parsoid: Merge double tds in nested transclusion content (bug 50603)
+!! options
+parsoid
+!! wikitext
+{{echo|<div>}}
+{|
+|{{echo|{{!}} foo}}
+|}
+{{echo|</div>}}
+!! html
+<div about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<div>"}},"i":0}},"\n{|\n|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{!}} foo"}},"i":1}},"\n|}\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"</div>"}},"i":2}}]}'>
+<table><tbody>
+<tr><td data-mw='{"parts":["|"]}'> foo</td></tr>
+</tbody></table>
+</div>
+!! end
+
+###
+### <includeonly> and <noinclude> in attributes
+###
+!!test
+0. includeonly around the entire attribute
+!! wikitext
+<span <includeonly>id="v1"</includeonly><noinclude>id="v2"</noinclude>>bar</span>
+!! html
+<p><span id="v2">bar</span>
+</p>
+!!end
+
+!!test
+1. includeonly in html attr key
+!! wikitext
+<span <noinclude>id</noinclude><includeonly>about</includeonly>="foo">bar</span>
+!! html
+<p><span id="foo">bar</span>
+</p>
+!!end
+
+!!test
+2. includeonly in html attr value
+!! wikitext
+<span id="<noinclude>v1</noinclude><includeonly>v2</includeonly>">bar</span>
+<span id=<noinclude>"v1"</noinclude><includeonly>"v2"</includeonly>>bar</span>
+!! html
+<p><span id="v1">bar</span>
+<span id="v1">bar</span>
+</p>
+!!end
+
+!!test
+3. includeonly in part of an attr value
+!! wikitext
+<span style="color:<noinclude>red</noinclude><includeonly>blue</includeonly>;">bar</span>
+!! html
+<p><span style="color:red;">bar</span>
+</p>
+!!end
+
+!!test
+4. includeonly in table attributes
+!! wikitext
+{|
+|- <noinclude>
+|-
+|a
+</noinclude>
+|- <includeonly>
+|-
+|b
+</includeonly>
+|}
+!! html
+<table>
+
+
+<tr>
+<td>a
+</td></tr>
+</table>
+
+!!end
+
+###
+### Token Stream Patcher tests
+###
+### These tests won't always pass wt2wt and other modes because
+### on serialization, the table will be output on a new line.
+### For now, we are blacklisting them, and using this to test selser.
+###
+
+!!test
+1. Table tag in SOL posn. should get reparsed correctly with valid TSR
+!!options
+parsoid=wt2html,wt2wt
+!!wikitext
+{{echo|}}{| width = '100%'
+|foo
+|}
+!!html/parsoid
+<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":""}},"i":0}}]}'></span>
+<table width="100%">
+<tbody>
+<tr>
+<td>foo</td></tr></tbody></table>
+!!end
+
+!!test
+2. Table tag in SOL posn. should get reparsed correctly with valid TSR
+!!options
+parsoid=wt2html,wt2wt
+!!wikitext
+<includeonly>a</includeonly>{| {{{b}}}
+|c
+|}
+!!html/parsoid
+<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"&lt;includeonly>a&lt;/includeonly>"'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":""}'/><table about="#mwt2" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"{{{b}}}","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Param\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[31,38,null,null],&amp;quot;src&amp;quot;:&amp;quot;{{{b}}}&amp;quot;}\">{{{b}}}&lt;/span>"},{"html":""}]]}' data-parsoid='{"a":{"{{{b}}}":null},"sa":{"{{{b}}}":""}}'>
+<tbody><tr><td>c</td></tr>
+</tbody></table>
+
+!!end
+
+###
+### Testing parsing of templates where a template arg
+### has the same name as the template itself.
+###
+
+!! article
+Template:quote
+!! text
+{{{quote|{{{1}}}}}}
+!! endarticle
+
+!!test
+Templates: Template Name/Arg clash: 1. Use of positional param
+!! wikitext
+{{quote|foo}}
+!! html
+<p>foo
+</p>
+!!end
+
+!!test
+Templates: Template Name/Arg clash: 2. Use of named param
+!! wikitext
+{{quote|quote=foo}}
+!! html
+<p>foo
+</p>
+!!end
+
+!!test
+Templates: Template Name/Arg clash: 3. Use of named param with empty input
+!! wikitext
+{{quote|quote}}
+!! html
+<p>quote
+</p>
+!!end
+
+###
+### Parsoid-centric tests to stress Parsoid's ability to RT them unchanged
+###
+
+!!test
+Templates: 1. Simple use
+!! wikitext
+{{echo|Foo}}
+!! html
+<p>Foo
+</p>
+!!end
+
+!!test
+Templates: 2. Inside a block tag
+!! wikitext
+<div>{{echo|Foo}}</div>
+<blockquote>{{echo|Foo}}</blockquote>
+!! html
+<div>Foo</div>
+<blockquote>Foo</blockquote>
+
+!! html+tidy
+<div>Foo</div>
+<blockquote>
+<p>Foo</p>
+</blockquote>
+!!end
+
+!!test
+Templates: P-wrapping: 1a. Templates on consecutive lines
+!! wikitext
+{{echo|Foo}}
+{{echo|bar}}
+!! html
+<p>Foo
+bar
+</p>
+!!end
+
+!!test
+Templates: P-wrapping: 1b. Templates on consecutive lines
+!! wikitext
+Foo
+
+{{echo|bar}}
+{{echo|baz}}
+!! html
+<p>Foo
+</p><p>bar
+baz
+</p>
+!!end
+
+!!test
+Templates: P-wrapping: 1c. Templates on consecutive lines
+!! wikitext
+{{echo|Foo}}
+{{echo|bar}} <div>baz</div>
+!! html
+<p>Foo
+</p>
+bar <div>baz</div>
+
+!! html+tidy
+<p>Foo</p>
+<p>bar</p>
+<div>baz</div>
+!! end
+
+!!test
+Templates: P-wrapping: 1d. Template preceded by comment-only line
+!!options
+parsoid
+!! wikitext
+<!-- foo -->
+{{echo|Bar}}
+!! html
+<!-- foo -->
+
+<p about="#mwt223" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"Bar"}},"i":0}}]}'>Bar</p>
+!!end
+
+!!test
+Templates: Inline Text: 1. Multiple template uses
+!! wikitext
+{{echo|Foo}}bar{{echo|baz}}
+!! html
+<p>Foobarbaz
+</p>
+!!end
+
+!!test
+Templates: Inline Text: 2. Back-to-back template uses
+!! wikitext
+{{echo|Foo}}{{echo|bar}}
+!! html
+<p>Foobar
+</p>
+!!end
+
+!!test
+Templates: Block Tags: 1. Multiple template uses
+!! wikitext
+{{echo|<div>Foo</div>}}<div>bar</div>{{echo|<div>baz</div>}}
+!! html
+<div>Foo</div><div>bar</div><div>baz</div>
+
+!!end
+
+!!test
+Templates: Block Tags: 2. Back-to-back template uses
+!! wikitext
+{{echo|<div>Foo</div>}}{{echo|<div>bar</div>}}
+!! html
+<div>Foo</div><div>bar</div>
+
+!!end
+
+# This is an edge case relating to paragraph wrapping.
+!!test
+Templates: Correctly encapsulate templates producing </p> tag without a corresponding <p> tag
+!! wikitext
+{{echo|a
+b</p>}}
+!! html/parsoid
+<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a\nb&lt;/p>"}},"i":0}}]}'>a
+b</p><p></p>
+!!end
+
+!!test
+Templates: Links: 1. Simple example
+!! wikitext
+{{echo|[[Foo|bar]]}}
+!! html
+<p><a href="/wiki/Foo" title="Foo">bar</a>
+</p>
+!!end
+
+!!test
+Templates: Links: 2. Generation of link href
+!! wikitext
+[[{{echo|Foo}}|bar]]
+!! html
+<p><a href="/wiki/Foo" title="Foo">bar</a>
+</p>
+!!end
+
+!!test
+Templates: Links: 3. Generation of part of a link href
+!! wikitext
+[[Fo{{echo|o}}|bar]]
+
+[[Foo{{echo|bar}}]]
+
+[[Foo{{echo|bar}}baz]]
+
+[[Foo{{echo|bar}}|bar]]
+
+[[:Foo{{echo|bar}}]]
+
+[[:Foo{{echo|bar}}|bar]]
+!! html
+<p><a href="/wiki/Foo" title="Foo">bar</a>
+</p><p><a href="/index.php?title=Foobar&amp;action=edit&amp;redlink=1" class="new" title="Foobar (page does not exist)">Foobar</a>
+</p><p><a href="/index.php?title=Foobarbaz&amp;action=edit&amp;redlink=1" class="new" title="Foobarbaz (page does not exist)">Foobarbaz</a>
+</p><p><a href="/index.php?title=Foobar&amp;action=edit&amp;redlink=1" class="new" title="Foobar (page does not exist)">bar</a>
+</p><p><a href="/index.php?title=Foobar&amp;action=edit&amp;redlink=1" class="new" title="Foobar (page does not exist)">Foobar</a>
+</p><p><a href="/index.php?title=Foobar&amp;action=edit&amp;redlink=1" class="new" title="Foobar (page does not exist)">bar</a>
+</p>
+!!end
+
+!!test
+Templates: Links: 4. Multiple templates generating link href
+!! wikitext
+[[{{echo|F}}{{echo|o}}ob{{echo|ar}}]]
+!! html
+<p><a href="/index.php?title=Foobar&amp;action=edit&amp;redlink=1" class="new" title="Foobar (page does not exist)">Foobar</a>
+</p>
+!!end
+
+!!test
+Templates: Links: 5. Generation of link text
+!! wikitext
+[[Foo|{{echo|bar}}]]
+!! html
+<p><a href="/wiki/Foo" title="Foo">bar</a>
+</p>
+!!end
+
+!!test
+Templates: Links: 5. Nested templates (only outermost template should be marked)
+!! wikitext
+{{echo|[[{{echo|Foo}}|bar]]}}
+!! html
+<p><a href="/wiki/Foo" title="Foo">bar</a>
+</p>
+!!end
+
+!!test
+Templates: HTML Tag: 1. Generation of HTML attr. key
+!! wikitext
+<div {{echo|style}}="color:red;">foo</div>
+!! html
+<div style="color:red;">foo</div>
+
+!!end
+
+!!test
+Templates: HTML Tag: 2. Generation of HTML attr. value
+!! wikitext
+<div style={{echo|'color:red;'}}>foo</div>
+!! html
+<div style="color:red;">foo</div>
+
+!!end
+
+!!test
+Templates: HTML Tag: 3. Generation of HTML attr key and value
+!! wikitext
+<div {{echo|style}}={{echo|'color:red;'}}>foo</div>
+!! html
+<div style="color:red;">foo</div>
+
+!!end
+
+!!test
+Templates: HTML Tag: 4. Generation of starting piece of HTML attr value
+!! wikitext
+<div title="{{echo|This is a long title}} with just one piece templated">foo</div>
+!! html
+<div title="This is a long title with just one piece templated">foo</div>
+
+!!end
+
+!!test
+Templates: HTML Tag: 5. Generation of middle piece of HTML attr value
+!! wikitext
+<div title="This is a long title with just {{echo|one piece}} templated">foo</div>
+!! html
+<div title="This is a long title with just one piece templated">foo</div>
+
+!!end
+
+!!test
+Templates: HTML Tag: 6. Generation of end piece of HTML attr value
+!! wikitext
+<div title="This is a long title with just one piece {{echo|templated}}">foo</div>
+!! html
+<div title="This is a long title with just one piece templated">foo</div>
+
+!!end
+
+!!test
+Templates: HTML Tag: 7. Generation of partial attribute key string
+!! wikitext
+<div st{{echo|yle}}="color:red;">foo</div>
+!! html
+<div style="color:red;">foo</div>
+
+!!end
+
+!!test
+Templates: HTML Tables: 1. Generating start of a HTML table
+!! wikitext
+{{echo|<table><tr><td>foo</td>}}</tr></table>
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 2a. Generating middle of a HTML table
+!! wikitext
+<table><tr>{{echo|<td>foo</td>}}</tr></table>
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 2b. Generating middle of a HTML table
+!! wikitext
+<table>{{echo|<tr><td>foo</td></tr>}}</table>
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 3. Generating end of a HTML table
+!! wikitext
+<table><tr>{{echo|<td>foo</td></tr></table>}}
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 4a. Generating a single tag of a HTML table
+!! wikitext
+{{echo|<table>}}<tr><td>foo</td></tr></table>
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 4b. Generating a single tag of a HTML table
+!! wikitext
+<table>{{echo|<tr>}}<td>foo</td></tr></table>
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 4c. Generating a single tag of a HTML table
+!! wikitext
+<table><tr>{{echo|<td>}}foo</td></tr></table>
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 4d. Generating a single tag of a HTML table
+!! wikitext
+<table><tr><td>foo{{echo|</td>}}</tr></table>
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 4e. Generating a single tag of a HTML table
+!! wikitext
+<table><tr><td>foo</td>{{echo|</tr>}}</table>
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 4f. Generating a single tag of a HTML table
+!! wikitext
+<table><tr><td>foo</td></tr>{{echo|</table>}}
+!! html
+<table><tr><td>foo</td></tr></table>
+
+!!end
+
+!!test
+Templates: HTML Tables: 5. Proper fostering of categories from inside
+!!options
+parsoid=wt2html,wt2wt
+!! wikitext
+<table>[[Category:foo1]]<tr><td>foo</td></tr></table>
+<!--Two categories (Bug 50330)-->
+<table>[[Category:bar1]][[Category:bar2]]<tr><td>foo</td></tr></table>
+!! html
+<link rel="mw:PageProp/Category" href="./Category:Foo1"><table><tbody><tr><td>foo</td></tr></tbody></table>
+<!--Two categories (Bug 50330)-->
+<link rel="mw:PageProp/Category" href="./Category:Bar1"><link rel="mw:PageProp/Category" href="./Category:Bar2"><table><tbody><tr><td>foo</td></tr></tbody></table>
+!!end
+
+!!test
+Templates: Wiki Tables: 1a. Fostering of entire template content
+!! wikitext
+{|
+{{echo|a}}
+|}
+!! html
+<table>
+a
+<tr><td></td></tr></table>
+
+!! html+tidy
+<p>a</p>
+<table>
+<tr>
+<td></td>
+</tr>
+</table>
+!! end
+
+!!test
+Templates: Wiki Tables: 1b. Fostering of entire template content
+!! wikitext
+{|
+{{echo|<div>}}
+foo
+{{echo|</div>}}
+|}
+!! html
+<table>
+<div>
+<p>foo
+</p>
+</div>
+<tr><td></td></tr></table>
+
+!! html+tidy
+<div>
+<p>foo</p>
+</div>
+<table>
+<tr>
+<td></td>
+</tr>
+</table>
+!! end
+
+!!test
+Templates: Wiki Tables: 2. Fostering of partial template content
+!! wikitext
+{|
+{{echo|a
+<div>b</div>}}
+|}
+!! html
+<table>
+a
+<div>b</div>
+<tr><td></td></tr></table>
+
+!! html+tidy
+<p>a</p>
+<div>b</div>
+<table>
+<tr>
+<td></td>
+</tr>
+</table>
+!! end
+
+!!test
+Templates: Wiki Tables: 3. td-content via multiple templates
+!! wikitext
+{|
+{{echo|{{pipe}}a}}{{echo|b}}
+|}
+!! html
+<table>
+<tr>
+<td>ab
+</td></tr></table>
+
+!!end
+
+!!test
+Templates: Wiki Tables: 4. Templated tags, no content
+!! wikitext
+{{tbl-start}}
+{{tbl-end}}
+!! html
+<table>
+<tr><td></td></tr></table>
+
+!!end
+
+!!test
+Templates: Wiki Tables: 5. Templated tags, regular td-tags
+!! wikitext
+{{tbl-start}}
+|foo
+{{tbl-end}}
+!! html
+<table>
+<tr>
+<td>foo
+</td></tr></table>
+
+!!end
+
+!!test
+Templates: Wiki Tables: 6. Templated tags, templated td-tags
+!! wikitext
+{{tbl-start}}
+{{!}}foo
+{{tbl-end}}
+!! html
+<table>
+<tr>
+<td>foo
+</td></tr></table>
+
+!!end
+
+!!test
+Templates: Lists: Multi-line list-items via templates
+!! wikitext
+*{{echo|a {{nonexistent|
+unused}}}}
+*{{echo|b {{nonexistent|
+unused}}}}
+!! html
+<ul><li>a <a href="/index.php?title=Template:Nonexistent&amp;action=edit&amp;redlink=1" class="new" title="Template:Nonexistent (page does not exist)">Template:Nonexistent</a></li>
+<li>b <a href="/index.php?title=Template:Nonexistent&amp;action=edit&amp;redlink=1" class="new" title="Template:Nonexistent (page does not exist)">Template:Nonexistent</a></li></ul>
+
+!!end
+
+!!test
+Templates: Ugly nesting: 1. Quotes opened/closed across templates (echo)
+!! wikitext
+{{echo|''a}}{{echo|b''c''d}}{{echo|''e}}
+!! html
+<p><i>ab</i>c<i>d</i>e
+</p>
+!!end
+
+!!test
+Templates: Ugly nesting: 2. Quotes opened/closed across templates (echo_with_span)
+(PHP parser generates misnested html)
+!! wikitext
+{{echo_with_span|''a}}{{echo_with_span|b''c''d}}{{echo_with_span|''e}}
+!! html/parsoid
+<p><span about="#mwt1" typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo_with_span&quot;,&quot;href&quot;:&quot;./Template:Echo_with_span&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;''a&quot;}},&quot;i&quot;:0}}]}"><i>a</i></span><i about="#mwt2" typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo_with_span&quot;,&quot;href&quot;:&quot;./Template:Echo_with_span&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;b''c''d&quot;}},&quot;i&quot;:0}},{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo_with_span&quot;,&quot;href&quot;:&quot;./Template:Echo_with_span&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;''e&quot;}},&quot;i&quot;:1}}]}"><span>b</span></i><span about="#mwt2">c</span><i about="#mwt2">d<span></span></i><span about="#mwt2">e</span></p>
+!!end
+
+!!test
+Templates: Ugly nesting: 3. Quotes opened/closed across templates (echo_with_div)
+(PHP parser generates misnested html; Parsoid html2wt mode adds newlines between {{echo}}s)
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+{{echo_with_div|''a}}{{echo_with_div|b''c''d}}{{echo_with_div|''e}}
+!! html
+<div about="#mwt1" typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo_with_div&quot;,&quot;href&quot;:&quot;./Template:Echo_with_div&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;''a&quot;}},&quot;i&quot;:0}}]}"><i>a</i></div>
+<div about="#mwt2" typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo_with_div&quot;,&quot;href&quot;:&quot;./Template:Echo_with_div&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;b''c''d&quot;}},&quot;i&quot;:0}}]}"><i>b</i>c<i>d</i></div>
+<div about="#mwt3" typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo_with_div&quot;,&quot;href&quot;:&quot;./Template:Echo_with_div&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;''e&quot;}},&quot;i&quot;:0}}]}">e</div>
+!!end
+
+!!test
+Templates: Ugly nesting: 4. Divs opened/closed across templates
+!! wikitext
+a<div>b{{echo|c</div>d}}e
+!! html
+a<div>bc</div>de
+
+!! html+tidy
+<p>a</p>
+<div>bc</div>
+<p>de</p>
+!! end
+
+!!test
+Templates: Ugly templates: 1. Navbox template parses badly leading to table misnesting
+(Parsoid-centric)
+!! options
+parsoid
+!! wikitext
+{|
+|{{echo|foo</table>}}
+|bar
+|}
+!! html
+<table about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":["{|\n|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo</table>"}},"i":0}},"\n|bar\n|}"]}'>
+
+<tbody>
+<tr>
+<td>foo</td></tr></tbody></table><span about="#mwt1">
+</span><span about="#mwt1">|bar</span><span about="#mwt1">
+|}</span>
+!!end
+
+!!test
+Templates: Ugly templates: 2. Navbox template parses badly leading to table misnesting
+(Parsoid-centric)
+!! options
+parsoid
+!! wikitext
+<table>
+ <tr>
+ <td>
+ <table>
+ <tr>
+ <td>1. {{echo|foo </table>}}</td>
+ <td> bar </td>
+ <td>2. {{echo|baz </table>}}</td>
+ </tr>
+ <tr>
+ <td>abc</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td>xyz</td>
+ </tr>
+</table>
+!! html
+<table about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":["<table>\n <tr>\n <td>\n <table>\n <tr>\n <td>1. ",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo </table>"}},"i":0}},"</td>\n <td> bar </td>\n <td>2. ",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"baz </table>"}},"i":1}},"</td>\n </tr>\n <tr>\n <td>abc</td>\n </tr>\n </table>\n </td>\n </tr>\n <tr>\n <td>xyz</td>\n </tr>\n</table>"]}'>
+ <tbody><tr>
+ <td>
+ <table>
+ <tbody><tr>
+ <td>1. foo </td></tr></tbody></table></td>
+ <td> bar </td>
+ <td>2. baz </td></tr></tbody></table><span about="#mwt2">
+ </span><span about="#mwt2">
+ </span><span about="#mwt2">
+ </span><span about="#mwt2">abc</span><span about="#mwt2">
+ </span><span about="#mwt2">
+ </span><span about="#mwt2">
+ </span><span about="#mwt2">
+ </span><span about="#mwt2">
+ </span><span about="#mwt2">
+ </span><span about="#mwt2">xyz</span><span about="#mwt2">
+ </span><span about="#mwt2">
+</span>
+!!end
+
+!! test
+Templates: Ugly templates: 3. newline-only template parameter
+!! wikitext
+foo {{echo|
+}}
+!! html
+<p>foo
+</p>
+!! end
+
+# This looks like a bug: a single newline triggers p/br for some reason.
+!! test
+Templates: Ugly templates: 4. newline-only template parameter inconsistency
+!! wikitext
+{{echo|
+}}
+!! html
+<p><br />
+</p>
+!! end
+
+# Bug 64017 -- ugly wikitext with fostered content generates two template ranges that
+# have a true overlap (T1-start - T2-start - T1-end - T2-end).
+!! test
+Templates: Ugly templates: 5. Template encapsulation test: Non-trivial overlap of template ranges is properly handled
+!! wikitext
+{{echo|<table>}}
+{{echo|<div>foo}}
+{{echo|</table>}}
+!! html/parsoid
+<div about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;table>"}},"i":0}},"\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;div>foo"}},"i":1}},"\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;/table>"}},"i":2}}]}' data-parsoid='{"stx":"html","autoInsertedEnd":true,"pi":[[{"k":"1","spc":["","","",""]}],[{"k":"1","spc":["","","",""]}],[{"k":"1","spc":["","","",""]}]]}'>foo
+</div><table about="#mwt1" data-parsoid='{"stx":"html"}'>
+</table>
+!! end
+
+# Bug 64017 -- ugly wikitext with fostered content generates two template ranges
+# that are "identical" and generate nesting cycles in the algorithm
+!! test
+Templates: Ugly templates: 6. Template encapsulation test: Cyclical nesting of template ranges is properly handled
+!! wikitext
+{{echo|<table><tr><td><table>}}
+{{echo|<div>}}
+{{echo|</div>}}
+!! html/parsoid
+<table about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;table>&lt;tr>&lt;td>&lt;table>"}},"i":0}},"\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;div>"}},"i":1}},"\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;/div>"}},"i":2}}]}' data-parsoid='{"stx":"html","autoInsertedEnd":true,"pi":[[{"k":"1","spc":["","","",""]}],[{"k":"1","spc":["","","",""]}],[{"k":"1","spc":["","","",""]}]]}'><tbody><tr data-parsoid='{"stx":"html"}'><td data-parsoid='{"stx":"html"}'><div data-parsoid='{"stx":"html"}'>
+</div><table about="#mwt1" data-parsoid='{"stx":"html"}'>
+</table></td></tr></tbody></table>
+!! end
+
+!!test
+Parser Functions: 1. Simple example
+!! wikitext
+{{uc:foo}}
+!! html
+<p>FOO
+</p>
+!!end
+
+!!test
+Parser Functions: 2. Nested use (only outermost should be marked up)
+!! wikitext
+{{uc:{{lc:FOO}}}}
+!! html
+<p>FOO
+</p>
+!!end
+
+###
+### Pre-save transform tests
+###
+!! test
+pre-save transform: subst:
+!! options
+PST
+!! wikitext
+{{subst:test}}
+!! html
+This is a test template
+!! end
+
+!! test
+pre-save transform: normal template
+!! options
+PST
+!! wikitext
+{{test}}
+!! html
+{{test}}
+!! end
+
+!! test
+pre-save transform: nonexistent template
+!! options
+PST
+!! wikitext
+{{thistemplatedoesnotexist}}
+!! html
+{{thistemplatedoesnotexist}}
+!! end
+
+
+!! test
+pre-save transform: subst magic variables
+!! options
+PST
+!! wikitext
+{{subst:SITENAME}}
+!! html
+MediaWiki
+!! end
+
+# This is bug 89, which I fixed. -- wtm
+!! test
+pre-save transform: subst: templates with parameters
+!! options
+pst
+!! wikitext
+{{subst:paramtest|param="something else"}}
+!! html
+This is a test template with parameter "something else"
+!! end
+
+!! article
+Template:nowikitest
+!! text
+<nowiki>'''not wiki'''</nowiki>
+!! endarticle
+
+!! test
+pre-save transform: nowiki in subst (bug 1188)
+!! options
+pst
+!! wikitext
+{{subst:nowikitest}}
+!! html
+<nowiki>'''not wiki'''</nowiki>
+!! end
+
+
+!! article
+Template:commenttest
+!! text
+This template has <!-- a comment --> in it.
+!! endarticle
+
+!! test
+pre-save transform: comment in subst (bug 1936)
+!! options
+pst
+!! wikitext
+{{subst:commenttest}}
+!! html
+This template has <!-- a comment --> in it.
+!! end
+
+!! test
+pre-save transform: unclosed tag
+!! options
+pst noxml
+!! wikitext
+<nowiki>'''not wiki'''
+!! html
+<nowiki>'''not wiki'''
+!! end
+
+!! test
+pre-save transform: mixed tag case
+!! options
+pst noxml
+!! wikitext
+<NOwiki>'''not wiki'''</noWIKI>
+!! html
+<NOwiki>'''not wiki'''</noWIKI>
+!! end
+
+!! test
+pre-save transform: unclosed comment in <nowiki>
+!! options
+pst noxml
+!! wikitext
+wiki<nowiki>nowiki<!--nowiki</nowiki>wiki
+!! html
+wiki<nowiki>nowiki<!--nowiki</nowiki>wiki
+!!end
+
+# Leading @ in this template definition works around a limitation
+# in parsoid's parserTests which otherwise strips the <span> from the
+# result (confusing it for a template wrapper)
+!! article
+Template:dangerous
+!!text
+@<span onmouseover="alert('crap')">Oh no</span>
+!!endarticle
+
+!!test
+(confirming safety of fix for subst bug 1936)
+!! wikitext
+{{Template:dangerous}}
+!! html
+<p>@<span>Oh no</span>
+</p>
+!! end
+
+!! test
+pre-save transform: comment containing gallery (bug 5024)
+!! options
+pst
+!! wikitext
+<!-- <gallery>data</gallery> -->
+!! html
+<!-- <gallery>data</gallery> -->
+!!end
+
+!! test
+pre-save transform: comment containing extension
+!! options
+pst
+!! wikitext
+<!-- <tag>data</tag> -->
+!! html
+<!-- <tag>data</tag> -->
+!!end
+
+!! test
+pre-save transform: comment containing nowiki
+!! options
+pst
+!! wikitext
+<!-- <nowiki>data</nowiki> -->
+!! html
+<!-- <nowiki>data</nowiki> -->
+!!end
+
+!! test
+pre-save transform: <noinclude> in subst (bug 3298)
+!! options
+pst
+!! wikitext
+{{subst:Includes}}
+!! html
+Foobar
+!! end
+
+!! test
+pre-save transform: <onlyinclude> in subst (bug 3298)
+!! options
+pst
+!! wikitext
+{{subst:Includes2}}
+!! html
+Foo
+!! end
+
+!! article
+Template:SubstTest
+!!text
+{{<includeonly>subst:</includeonly>Includes}}
+!! endarticle
+
+!! article
+Template:SafeSubstTest
+!! text
+{{<includeonly>safesubst:</includeonly>Includes}}
+!! endarticle
+
+!! test
+bug 22297: safesubst: works during PST
+!! options
+pst
+!! wikitext
+{{subst:SafeSubstTest}}{{safesubst:SubstTest}}
+!! html
+FoobarFoobar
+!! end
+
+!! test
+bug 22297: safesubst: works during normal parse
+!! wikitext
+{{SafeSubstTest}}
+!! html
+<p>Foobar
+</p>
+!! end
+
+!! test
+subst: does not work during normal parse
+!! wikitext
+{{SubstTest}}
+!! html
+<p>{{subst:Includes}}
+</p>
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick")
+!! options
+pst
+!! wikitext
+[[Article (context)|]]
+[[Bar:Article|]]
+[[:Bar:Article|]]
+[[Bar:Article (context)|]]
+[[:Bar:Article (context)|]]
+[[|Article]]
+[[|Article (context)]]
+[[Bar:X (Y) Z|]]
+[[:Bar:X (Y) Z|]]
+!! html
+[[Article (context)|Article]]
+[[Bar:Article|Article]]
+[[:Bar:Article|Article]]
+[[Bar:Article (context)|Article]]
+[[:Bar:Article (context)|Article]]
+[[Article]]
+[[Article (context)]]
+[[Bar:X (Y) Z|X (Y) Z]]
+[[:Bar:X (Y) Z|X (Y) Z]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with interwiki prefix
+!! options
+pst
+!! wikitext
+[[interwiki:Article|]]
+[[:interwiki:Article|]]
+[[interwiki:Bar:Article|]]
+[[:interwiki:Bar:Article|]]
+!! html
+[[interwiki:Article|Article]]
+[[:interwiki:Article|Article]]
+[[interwiki:Bar:Article|Bar:Article]]
+[[:interwiki:Bar:Article|Bar:Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with parens in title
+!! options
+pst title=[[Somearticle (context)]]
+!! wikitext
+[[|Article]]
+!! html
+[[Article (context)|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with comma in title
+!! options
+pst title=[[Someplace, Somewhere]]
+!! wikitext
+[[|Otherplace]]
+[[Otherplace, Elsewhere|]]
+[[Otherplace, Elsewhere, Anywhere|]]
+!! html
+[[Otherplace, Somewhere|Otherplace]]
+[[Otherplace, Elsewhere|Otherplace]]
+[[Otherplace, Elsewhere, Anywhere|Otherplace]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with parens and comma
+!! options
+pst title=[[Someplace (IGNORED), Somewhere]]
+!! wikitext
+[[|Otherplace]]
+[[Otherplace (place), Elsewhere|]]
+!! html
+[[Otherplace, Somewhere|Otherplace]]
+[[Otherplace (place), Elsewhere|Otherplace]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with comma and parens
+!! options
+pst title=[[Who, me? (context)]]
+!! wikitext
+[[|Yes, you.]]
+[[Me, Myself, and I (1937 song)|]]
+!! html
+[[Yes, you. (context)|Yes, you.]]
+[[Me, Myself, and I (1937 song)|Me, Myself, and I]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace
+!! options
+pst title=[[Ns:Somearticle]]
+!! wikitext
+[[|Article]]
+!! html
+[[Ns:Article|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace and parens
+!! options
+pst title=[[Ns:Somearticle (context)]]
+!! wikitext
+[[|Article]]
+!! html
+[[Ns:Article (context)|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace and comma
+!! options
+pst title=[[Ns:Somearticle, Context, Whatever]]
+!! wikitext
+[[|Article]]
+!! html
+[[Ns:Article, Context, Whatever|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace, comma and parens
+!! options
+pst title=[[Ns:Somearticle, Context (context)]]
+!! wikitext
+[[|Article]]
+!! html
+[[Ns:Article (context)|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace, parens and comma
+!! options
+pst title=[[Ns:Somearticle (IGNORED), Context]]
+!! wikitext
+[[|Article]]
+!! html
+[[Ns:Article, Context|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with full-width parens and no space (Japanese and Chinese style, bug 30149)
+!! options
+pst
+!! wikitext
+[[Article(context)|]]
+[[Bar:Article(context)|]]
+[[:Bar:Article(context)|]]
+[[|Article(context)]]
+[[Bar:X(Y)Z|]]
+[[:Bar:X(Y)Z|]]
+!! html
+[[Article(context)|Article]]
+[[Bar:Article(context)|Article]]
+[[:Bar:Article(context)|Article]]
+[[Article(context)]]
+[[Bar:X(Y)Z|X(Y)Z]]
+[[:Bar:X(Y)Z|X(Y)Z]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with full-width parens and space (Japanese and Chinese style, bug 30149)
+!! options
+pst
+!! wikitext
+[[Article (context)|]]
+[[Bar:Article (context)|]]
+[[:Bar:Article (context)|]]
+[[|Article (context)]]
+[[Bar:X (Y) Z|]]
+[[:Bar:X (Y) Z|]]
+!! html
+[[Article (context)|Article]]
+[[Bar:Article (context)|Article]]
+[[:Bar:Article (context)|Article]]
+[[Article (context)]]
+[[Bar:X (Y) Z|X (Y) Z]]
+[[:Bar:X (Y) Z|X (Y) Z]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with parens and no space (Korean style, bug 30149)
+!! options
+pst
+!! wikitext
+[[Article(context)|]]
+[[Bar:Article(context)|]]
+[[:Bar:Article(context)|]]
+[[|Article(context)]]
+[[Bar:X(Y)Z|]]
+[[:Bar:X(Y)Z|]]
+!! html
+[[Article(context)|Article]]
+[[Bar:Article(context)|Article]]
+[[:Bar:Article(context)|Article]]
+[[Article(context)]]
+[[Bar:X(Y)Z|X(Y)Z]]
+[[:Bar:X(Y)Z|X(Y)Z]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with commas (bug 21660)
+!! options
+pst
+!! wikitext
+[[Article (context), context|]]
+[[Article (context),context|]]
+[[Bar:Article (context), context|]]
+[[Bar:Article (context),context|]]
+[[:Bar:Article (context), context|]]
+[[:Bar:Article (context),context|]]
+!! html
+[[Article (context), context|Article]]
+[[Article (context),context|Article]]
+[[Bar:Article (context), context|Article]]
+[[Bar:Article (context),context|Article]]
+[[:Bar:Article (context), context|Article]]
+[[:Bar:Article (context),context|Article]]
+!! end
+
+!! test
+pre-save transform: trim trailing empty lines
+!! options
+pst
+!! wikitext
+Empty lines are trimmed
+
+
+
+
+!! html
+Empty lines are trimmed
+!! end
+
+!! test
+pre-save transform: Signature expansion
+!! options
+pst
+!! wikitext
+* ~~~
+* <noinclude>~~~</noinclude>
+* <includeonly>~~~</includeonly>
+* <onlyinclude>~~~</onlyinclude>
+!! html
+* [[Special:Contributions/127.0.0.1|127.0.0.1]]
+* <noinclude>[[Special:Contributions/127.0.0.1|127.0.0.1]]</noinclude>
+* <includeonly>[[Special:Contributions/127.0.0.1|127.0.0.1]]</includeonly>
+* <onlyinclude>[[Special:Contributions/127.0.0.1|127.0.0.1]]</onlyinclude>
+!! end
+
+
+!! test
+pre-save transform: Signature expansion in nowiki tags (bug 93)
+!! options
+pst disabled
+!! wikitext
+Shall not expand:
+
+<nowiki>~~~~</nowiki>
+
+<includeonly><nowiki>~~~~</nowiki></includeonly>
+
+<noinclude><nowiki>~~~~</nowiki></noinclude>
+
+<onlyinclude><nowiki>~~~~</nowiki></onlyinclude>
+
+{{subst:Foo}} shall be converted to FOO
+
+As well as inside noinclude/onlyinclude
+<noinclude>{{subst:Foo}}</noinclude>
+<onlyinclude>{{subst:Foo}}</onlyinclude>
+
+But not inside includeonly
+<includeonly>{{subst:Foo}}</includeonly>
+!! html
+Shall not expand:
+
+<nowiki>~~~~</nowiki>
+
+<includeonly><nowiki>~~~~</nowiki></includeonly>
+
+<noinclude><nowiki>~~~~</nowiki></noinclude>
+
+<onlyinclude><nowiki>~~~~</nowiki></onlyinclude>
+
+FOO shall be converted to FOO
+
+As well as inside noinclude/onlyinclude
+<noinclude>FOO</noinclude>
+<onlyinclude>FOO</onlyinclude>
+
+But not inside includeonly
+<includeonly>{{subst:Foo}}</includeonly>
+!! end
+
+!! test
+Parsoid: Recognize nowiki with trailing space in tags
+!! options
+parsoid=wt2html
+!! wikitext
+<nowiki ><div>[[foo]]</nowiki >
+
+a<nowiki / >b
+
+c<nowiki />d
+
+e<nowiki/ >f
+!! html
+<p><span typeof="mw:Nowiki">&lt;div&gt;[[foo]]</span></p>
+<p>ab</p>
+<p>cd</p>
+<p>ef</p>
+!! end
+
+!! test
+Parsoid: Recognize nowiki with odd capitalization
+!! options
+parsoid=wt2html
+!! wikitext
+<noWikI ><div>[[foo]]</Nowiki >
+!! html
+<p><span typeof="mw:Nowiki">&lt;div&gt;[[foo]]</span></p>
+!! end
+
+
+!! test
+Parsoid: Escape nowiki with trailing space in tags
+!! options
+parsoid=html2wt
+!! wikitext
+&lt;nowiki &gt; foo &lt;/nowiki &gt;
+
+a&lt;nowiki /&gt;b
+
+c&lt;nowiki/ &gt;d
+!! html
+<p>&lt;nowiki &gt; foo &lt/nowiki ></p>
+<p>a&lt;nowiki /&gt;b</p>
+<p>c&lt;nowiki/ &gt;d</p>
+!! end
+
+!! test
+Parsoid: Escape weird noWikI capitalizations
+!! options
+parsoid=html2wt
+!! wikitext
+&lt;noWikI &gt; foo &lt;/NoWikI &gt;
+!! html
+<p>&lt;noWikI &gt; foo &lt/NoWikI ></p>
+!! end
+
+###
+### Message transform tests
+###
+!! test
+message transform: magic variables
+!! options
+msg
+!! wikitext
+{{SITENAME}}
+!! html
+MediaWiki
+!! end
+
+!! test
+message transform: should not transform wiki markup
+!! options
+msg
+!! wikitext
+''test''
+!! html
+''test''
+!! end
+
+!! test
+message transform: <noinclude> in transcluded template (bug 4926)
+!! options
+msg
+!! wikitext
+{{Includes}}
+!! html
+Foobar
+!! end
+
+!! test
+message transform: <onlyinclude> in transcluded template (bug 4926)
+!! options
+msg
+!! wikitext
+{{Includes2}}
+!! html
+Foo
+!! end
+
+!! test
+{{#special:}} page name, known
+!! options
+msg
+!! wikitext
+{{#special:Recentchanges}}
+!! html
+Special:RecentChanges
+!! end
+
+!! test
+{{#special:}} page name with subpage, known
+!! options
+msg
+!! wikitext
+{{#special:Recentchanges/param}}
+!! html
+Special:RecentChanges/param
+!! end
+
+!! test
+{{#special:}} page name, unknown
+!! options
+msg
+!! wikitext
+{{#special:foobar nonexistent}}
+!! html
+Special:Foobar nonexistent
+!! end
+
+!! test
+{{#speciale:}} page name, known
+!! options
+msg
+!! wikitext
+{{#speciale:Recentchanges}}
+!! html
+Special:RecentChanges
+!! end
+
+!! test
+{{#speciale:}} page name with subpage, known
+!! options
+msg
+!! wikitext
+{{#speciale:Recentchanges/param}}
+!! html
+Special:RecentChanges/param
+!! end
+
+!! test
+{{#speciale:}} page name, unknown
+!! options
+msg
+!! wikitext
+{{#speciale:foobar nonexistent}}
+!! html
+Special:Foobar_nonexistent
+!! end
+
+###
+### Images
+###
+### For Parsoid-specific tests, see
+#### https://www.mediawiki.org/wiki/Parsoid/MediaWiki_DOM_spec#Images
+
+!! test
+Simple image
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[Image:foobar.jpg]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></span>
+</p>
+!! end
+
+!! test
+Simple image (using File: namespace, now canonical)
+!! wikitext
+[[File:Foobar.jpg]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></span>
+</p>
+!! end
+
+!! test
+Right-aligned image
+!! wikitext
+[[File:Foobar.jpg|right]]
+!! html/php
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></figure>
+!! end
+
+!! test
+Image with caption
+!! wikitext
+[[File:Foobar.jpg|right|Caption text]]
+!! html/php
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption text"><img alt="Caption text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a><figcaption>Caption text</figcaption></figure>
+!! end
+
+!! test
+Image with caption, bug 53312 #1
+!! wikitext
+[[File:Foobar.jpg|right|Caption page stuff]]
+!! html/php
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption page stuff"><img alt="Caption page stuff" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a><figcaption>Caption page stuff</figcaption></figure>
+!! end
+
+!! test
+Image with caption, bug 53312 #2
+!! wikitext
+[[File:Foobar.jpg|right|Caption page=]]
+!! html/php
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption page="><img alt="Caption page=" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a><figcaption>Caption page=</figcaption></figure>
+!! end
+
+!! test
+Image with caption, bug 53312 #3
+!! wikitext
+[[File:Foobar.jpg|right|Caption page=stuff]]
+!! html/php
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption page=stuff"><img alt="Caption page=stuff" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a><figcaption>Caption page=stuff</figcaption></figure>
+!! end
+
+!! test
+Allow empty links in image captions (Bug 60753)
+!! options
+thumbsize=220
+!! wikitext
+[[File:Foobar.jpg|thumb|Caption [[Link1]]
+[[]]
+[[Link2]]
+]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Caption <a href="/index.php?title=Link1&amp;action=edit&amp;redlink=1" class="new" title="Link1 (page does not exist)">Link1</a> [[]] <a href="/index.php?title=Link2&amp;action=edit&amp;redlink=1" class="new" title="Link2 (page does not exist)">Link2</a></div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"Caption [[Link1]]\n[[]]\n[[Link2]]\n"}],"dsr":[0,59,2,2]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"dsr":[2,null,null,null]}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption data-parsoid='{"dsr":[null,57,null,null]}'>Caption <a rel="mw:WikiLink" href="./Link1" title="Link1" data-parsoid='{"stx":"simple","a":{"href":"./Link1"},"sa":{"href":"Link1"},"dsr":[32,41,2,2]}'>Link1</a>
+[[]]
+<a rel="mw:WikiLink" href="./Link2" title="Link2" data-parsoid='{"stx":"simple","a":{"href":"./Link2"},"sa":{"href":"Link2"},"dsr":[47,56,2,2]}'>Link2</a>
+</figcaption></figure>
+!! end
+
+!! test
+Link with empty target
+!! wikitext
+[[]]
+!! html
+<p>[[]]
+</p>
+!! end
+
+!! test
+Image with empty attribute
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|right||Caption text]]
+!! html/php
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption text"><img alt="Caption text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a><figcaption>Caption text</figcaption></figure>
+!! end
+
+!! test
+1. Block image with individual attributes from templates
+!! wikitext
+[[File:Foobar.jpg|thumb|{{echo|137px}}|This is a caption]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:139px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" width="137" height="16" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/206px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/274px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is a caption</div></div></div>
+
+!! html/parsoid
+<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;137px&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;,&amp;quot;spc&amp;quot;:[&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;]}]],&amp;quot;dsr&amp;quot;:[24,38,null,null]}\">137px&lt;/span>"}]]}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="16" width="137"/></a><figcaption>This is a caption</figcaption></figure>
+!! end
+
+!! test
+2. Block Image with individual attributes from templates
+!! wikitext
+[[File:Foobar.jpg|{{echo|thumb}}|{{echo|137px}}|This is a caption]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:139px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg" width="137" height="16" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/206px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/274px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is a caption</div></div></div>
+
+!! html/parsoid
+<figure typeof="mw:Image/Thumb mw:ExpandedAttrs" data-mw='{"attribs":[["thumbnail",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;thumb&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;,&amp;quot;spc&amp;quot;:[&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;]}]],&amp;quot;dsr&amp;quot;:[18,32,null,null]}\">thumb&lt;/span>"}],["width",{"html":"&lt;span about=\"#mwt2\" typeof=\"mw:Transclusion\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;137px&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;,&amp;quot;spc&amp;quot;:[&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;]}]],&amp;quot;dsr&amp;quot;:[33,47,null,null]}\">137px&lt;/span>"}]]}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="16" width="137"/></a><figcaption>This is a caption</figcaption></figure>
+!! end
+
+!! test
+3. Inline image with individual attributes from templates
+!! wikitext
+[[File:Foobar.jpg|{{echo|50px}}]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" width="50" height="6" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/75px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/100px-Foobar.jpg 2x" /></a>
+</p>
+!! html/parsoid
+<p><span typeof="mw:Image mw:ExpandedAttrs" about="#mwt2" data-mw='{"attribs":[["width",{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;echo&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Echo&amp;quot;},&amp;quot;params&amp;quot;:{&amp;quot;1&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;50px&amp;quot;}},&amp;quot;i&amp;quot;:0}}]}\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[{&amp;quot;k&amp;quot;:&amp;quot;1&amp;quot;,&amp;quot;spc&amp;quot;:[&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;,&amp;quot;&amp;quot;]}]],&amp;quot;dsr&amp;quot;:[18,31,null,null]}\">50px&lt;/span>"}]]}' data-parsoid='{"optList":[{"ck":"width","ak":"{{echo|50px}}"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+!! end
+
+## Parsoid does not provide editing support for images where templates produce multiple image attributes.
+## To signal this, we add a 'mw:Placeholder' type to such images. This could change in the future.
+!! test
+Image with multiple attributes from the same template
+!! wikitext
+[[File:Foobar.jpg|{{image_attribs}}]]
+!! html/php
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption text"><img alt="Caption text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-right" typeof="mw:Image mw:Placeholder"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a><figcaption>Caption text</figcaption></figure>
+!! end
+
+# Parsoid's output here is broken (incorrect p-wrapping); see bug 64901.
+!! test
+Image with link tails
+!! options
+thumbsize=220
+!! wikitext
+123[[File:Foobar.jpg]]456
+123[[File:Foobar.jpg|right]]456
+123[[File:Foobar.jpg|thumb]]456
+!! html/php
+<p>123<a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>456
+</p>
+123<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>456
+123<div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div></div></div></div>456
+
+!! html/php+tidy
+<p>123<a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>456</p>
+<p>123</p>
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+<p>456 123</p>
+<div class="thumb tright">
+<div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a>
+<div class="thumbcaption">
+<div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>
+</div>
+</div>
+</div>
+<p>456</p>
+!! html/parsoid
+<p>123<span class="mw-default-size" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></span>456</p>
+123<figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></figure>456
+123<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" height="25" width="220"></a></figure>456
+!! end
+
+!! test
+Image with multiple captions -- only last one is accepted
+!! wikitext
+[[File:Foobar.jpg|right|Caption1 - ignored|[[Caption2]] - ignored|Caption3 - accepted]]
+!! html/php
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption3 - accepted"><img alt="Caption3 - accepted" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a><figcaption>Caption3 - accepted</figcaption></figure>
+!! end
+
+!! test
+Image with multiple widths -- use last
+!! wikitext
+[[File:Foobar.jpg|200px|300px|caption]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg" width="300" height="34" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/450px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/600px-Foobar.jpg 2x" /></a>
+</p>
+!! html/parsoid
+<p><span typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="34" width="300"/></a></span></p>
+!! end
+
+!! test
+Image with multiple alignments -- use first (bug 48664)
+!! options
+thumbsize=220
+!! wikitext
+[[File:Foobar.jpg|thumb|left|right|center|caption]]
+
+[[File:Foobar.jpg|middle|text-top|caption]]
+!! html/php
+<div class="thumb tleft"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>caption</div></div></div>
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" style="vertical-align: middle" /></a>
+</p>
+!! html/parsoid
+<figure class="mw-default-size mw-halign-left" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure>
+<p><span class="mw-default-size mw-valign-middle" typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+!! test
+Image with width attribute at different positions
+!! wikitext
+[[File:Foobar.jpg|200px|right|Caption]]
+[[File:Foobar.jpg|right|200px|Caption]]
+[[File:Foobar.jpg|right|Caption|200px]]
+!! html/php
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption"><img alt="Caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg 2x" /></a></div>
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption"><img alt="Caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg 2x" /></a></div>
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption"><img alt="Caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg 2x" /></a></div>
+
+!! html/parsoid
+<figure class="mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" height="23" width="200"></a><figcaption>Caption</figcaption></figure>
+<figure class="mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" height="23" width="200"></a><figcaption>Caption</figcaption></figure>
+<figure class="mw-halign-right" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" height="23" width="200"></a><figcaption>Caption</figcaption></figure>
+!! end
+
+# a sad bit of backward-compatibility
+!! test
+Image with size specified with pxpx (bug 13500, 51628)
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|20pxpx]]
+[[File:Foobar.jpg|200x20pxpx]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" width="20" height="2" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/30px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/40px-Foobar.jpg 2x" /></a>
+<a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/177px-Foobar.jpg" width="177" height="20" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/265px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/353px-Foobar.jpg 2x" /></a>
+</p>
+!! html/parsoid
+<p><span typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="2" width="20"/></a></span><span typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="20" width="177"/></a></span></p>
+!! end
+
+!! test
+Image with link parameter, wiki target
+!! wikitext
+[[File:Foobar.jpg|link=Main Page]]
+!! html/php
+<p><a href="/wiki/Main_Page" title="Main Page"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image"><a href="Main_Page"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></span></p>
+!! end
+
+# parsoid bug 49293 (part 1)
+!! test
+Image with link parameter, URL target
+!! wikitext
+[[File:Foobar.jpg|link=http://example.com/]]
+!! html/php
+<p><a href="http://example.com/" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image"><a href="http://example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></span></p>
+!! end
+
+# parsoid bug 49293 (part 2)
+!! test
+Image with link parameter, protocol-less URL target
+!! wikitext
+[[File:Foobar.jpg|link=//example.com/]]
+!! html/php
+<p><a href="//example.com/" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image"><a href="//example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></span></p>
+!! end
+
+!! test
+Image with link parameter, wgExternalLinkTarget
+!! wikitext
+[[Image:foobar.jpg|link=http://example.com/]]
+!! config
+wgExternalLinkTarget='foobar'
+!! html
+<p><a href="http://example.com/" target="foobar" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Image with link parameter, wgNoFollowLinks set to false
+!! wikitext
+[[Image:foobar.jpg|link=http://example.com/]]
+!! config
+wgNoFollowLinks=false
+!! html
+<p><a href="http://example.com/"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Image with link parameter, wgNoFollowDomainExceptions
+!! wikitext
+[[Image:foobar.jpg|link=http://example.com/]]
+!! config
+wgNoFollowDomainExceptions='example.com'
+!! html
+<p><a href="http://example.com/"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Image with link parameter, wgExternalLinkTarget, unnamed parameter
+!! wikitext
+[[Image:foobar.jpg|link=http://example.com/|Title]]
+!! config
+wgExternalLinkTarget='foobar'
+!! html
+<p><a href="http://example.com/" title="Title" target="foobar" rel="nofollow"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Image with empty link parameter
+!! wikitext
+[[File:Foobar.jpg|link=]]
+!! html/php
+<p><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" />
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image"><span><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></span></span></p>
+!! end
+
+!! test
+Image with link parameter (wiki target) and unnamed parameter
+!! wikitext
+[[File:Foobar.jpg|link=Main_Page|Title]]
+!! html/php
+<p><a href="/wiki/Main_Page" title="Title"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"Title"}'><a href="Main_Page"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></span></p>
+!! end
+
+!! test
+Image with link parameter (URL target) and unnamed parameter
+!! wikitext
+[[File:Foobar.jpg|link=http://example.com/|Title]]
+!! html/php
+<p><a href="http://example.com/" title="Title" rel="nofollow"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"Title"}'><a href="http://example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a></span></p>
+!! end
+
+!! test
+Thumbnail image with link parameter
+!! options
+thumbsize=220
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb|link=http://example.com/|Title]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="http://example.com/"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Title</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="http://example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>Title</figcaption></figure>
+!! end
+
+!! test
+Manually-specified thumbnail image
+!! options
+thumbsize=220
+!! wikitext
+[[File:Foobar.jpg|thumb=Thumb.png|Title]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:137px;"><a href="/wiki/File:Foobar.jpg"><img alt="" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Title</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb" data-mw='{"thumb":"Thumb.png"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/e/ea/Thumb.png" height="135" width="135"/></a><figcaption>Title</figcaption></figure>
+!! end
+
+!! test
+Manually-specified thumbnail image with explicit link to wiki page
+!! options
+thumbsize=220
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb=Thumb.png|link=Main_Page|Title]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:137px;"><a href="/wiki/Main_Page" title="Main Page"><img alt="" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Title</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb" data-mw='{"thumb":"Thumb.png"}'><a href="Main_Page"><img resource="./File:Foobar.jpg" src="//example.com/images/e/ea/Thumb.png" height="135" width="135"/></a><figcaption>Title</figcaption></figure>
+!! end
+
+!! test
+Manually-specified thumbnail image with explicit link to url
+!! options
+thumbsize=220
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb=Thumb.png|link=http://example.com|Title]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:137px;"><a href="http://example.com"><img alt="" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Title</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb" data-mw='{"thumb":"Thumb.png"}'><a href="http://example.com"><img resource="./File:Foobar.jpg" src="//example.com/images/e/ea/Thumb.png" height="135" width="135"/></a><figcaption>Title</figcaption></figure>
+!! end
+
+!! test
+Manually-specified thumbnail image with explicit no link
+!! options
+thumbsize=220
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb=Thumb.png|link=|Title]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:137px;"><img alt="" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" class="thumbimage" /> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Title</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb" data-mw='{"thumb":"Thumb.png"}'><span><img resource="./File:Foobar.jpg" src="//example.com/images/e/ea/Thumb.png" height="135" width="135"/></span><figcaption>Title</figcaption></figure>
+!! end
+
+!! test
+Manually-specified thumbnail image with explicit link and alt text
+!! options
+thumbsize=220
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb=Thumb.png|link=Main_Page|alt=alttext|Title]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:137px;"><a href="/wiki/Main_Page" title="Main Page"><img alt="alttext" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Title</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb" data-mw='{"thumb":"Thumb.png"}'><a href="Main_Page"><img alt="alttext" resource="./File:Foobar.jpg" src="//example.com/images/e/ea/Thumb.png" height="135" width="135"/></a><figcaption>Title</figcaption></figure>
+!! end
+
+!! test
+Image with frame and link
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|frame|left|This is a test image [[Main Page]]]]
+!! html/php
+<div class="thumb tleft"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption">This is a test image <a href="/wiki/Main_Page" title="Main Page">Main Page</a></div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-left" typeof="mw:Image/Frame"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a><figcaption>This is a test image <a rel="mw:WikiLink" href="Main_Page" title="Main Page">Main Page</a></figcaption></figure>
+!! end
+
+!! test
+Image with frame and link and explicit alt
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[Image:Foobar.jpg|frame|left|This is a test image [[Main Page]]|alt=Altitude]]
+!! html/php
+<div class="thumb tleft"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Altitude" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption">This is a test image <a href="/wiki/Main_Page" title="Main Page">Main Page</a></div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-left" typeof="mw:Image/Frame"><a href="File:Foobar.jpg"><img alt="Altitude" resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a><figcaption>This is a test image <a rel="mw:WikiLink" href="Main_Page" title="Main Page">Main Page</a></figcaption></figure>
+!! end
+
+!! test
+Image with wiki markup in implicit alt
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[Image:Foobar.jpg|testing '''bold''' in alt]]
+
+[[Image:Foobar.jpg|alt=testing '''bold''' in alt]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="testing bold in alt"><img alt="testing bold in alt" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p><p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="testing bold in alt" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw="{&quot;caption&quot;:&quot;testing '''bold''' in alt&quot;}"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image"><a href="File:Foobar.jpg"><img alt="testing bold in alt" resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+###################
+# Conflicting image format options.
+# First option specified should 'win'.
+# All three cases in each test should be identical.
+
+!! test
+Image with 'frameless' first.
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|frameless|caption]]
+
+[[File:Foobar.jpg|frameless|frame|caption]]
+
+[[File:Foobar.jpg|frameless|thumb|caption]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>
+</p><p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>
+</p><p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a></span></p>
+<p><span class="mw-default-size" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a></span></p>
+!! end
+
+!! test
+Image with 'frame' first.
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|frame|caption]]
+[[File:Foobar.jpg|frame|frameless|caption]]
+[[File:Foobar.jpg|frame|thumb|caption]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption">caption</div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption">caption</div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption">caption</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Frame"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a><figcaption>caption</figcaption></figure><figure class="mw-default-size" typeof="mw:Image/Frame"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a><figcaption>caption</figcaption></figure><figure class="mw-default-size" typeof="mw:Image/Frame"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a><figcaption>caption</figcaption></figure>
+!! end
+
+!! test
+Image with 'thumb' first.
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb|caption]]
+[[File:Foobar.jpg|thumb|frameless|caption]]
+[[File:Foobar.jpg|thumb|frame|caption]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>caption</div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>caption</div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>caption</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure><figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure><figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure>
+!! end
+
+###################
+# Image sizing.
+# See https://www.mediawiki.org/wiki/Help:Images#Size_and_frame
+# and https://bugzilla.wikimedia.org/show_bug.cgi?id=62258
+# Foobar has actual size of 1941x220
+# 1. Thumbs & frameless always reduce, can't be enlarged unless it's
+# a scalable format.
+# 2. Framed images always ignore size options; always render at default size.
+# 3. "Unspecified format" and border are the only types which can be
+# enlarged.
+
+!! test
+Image: "unspecified format" and border enlarge
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|2000px]]
+
+[[File:Foobar.jpg|border|2000px]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="2000" height="227" /></a>
+</p><p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="2000" height="227" class="thumbborder" /></a>
+</p>
+!! html/parsoid
+<p><span typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="227" width="2000"/></a></span></p>
+<p><span class="mw-image-border" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="227" width="2000"/></a></span></p>
+!! end
+
+!! test
+Image: "unspecified format" and border reduce
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|1000px]]
+
+[[File:Foobar.jpg|border|1000px]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg" width="1000" height="113" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg 1.5x, http://example.com/images/3/3a/Foobar.jpg 2x" /></a>
+</p><p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg" width="1000" height="113" class="thumbborder" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg 1.5x, http://example.com/images/3/3a/Foobar.jpg 2x" /></a>
+</p>
+!! html/parsoid
+<p><span typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="113" width="1000"/></a></span></p>
+<p><span class="mw-image-border" typeof="mw:Image"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="113" width="1000"/></a></span></p>
+!! end
+
+!! test
+Image: thumbs reduce
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb|50px]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:52px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" width="50" height="6" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/75px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/100px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div></div></div></div>
+
+!! html/parsoid
+<figure typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="6" width="50"/></a></figure>
+!! end
+
+!! test
+Image: bitmap thumbs can't be enlarged past original size, but vector can.
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb|2000px]]
+
+[[File:Foobar.svg|thumb|2000px]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div></div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:2002px;"><a href="/wiki/File:Foobar.svg" class="image"><img alt="Foobar.svg" src="http://example.com/images/thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png" width="2000" height="1500" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div></div></div></div>
+
+!! html/parsoid
+<figure typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></figure>
+<figure typeof="mw:Image/Thumb"><a href="File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" height="1500" width="2000"/></a></figure>
+!! end
+
+!! test
+Image: frameless can reduce in size
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|frameless|50px]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" width="50" height="6" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/75px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/100px-Foobar.jpg 2x" /></a>
+</p>
+!! html/parsoid
+<p><span typeof="mw:Image/Frameless"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="6" width="50"/></a></span></p>
+!! end
+
+!! test
+Image: bitmap frameless can't be enlarged past original size, but vector can
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|frameless|2000px]]
+
+[[File:Foobar.svg|frameless|2000px]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p><p><a href="/wiki/File:Foobar.svg" class="image"><img alt="Foobar.svg" src="http://example.com/images/thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png" width="2000" height="1500" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png 2x" /></a>
+</p>
+!! html/parsoid
+<p><span typeof="mw:Image/Frameless"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+<p><span typeof="mw:Image/Frameless"><a href="File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" height="1500" width="2000"/></a></span></p>
+!! end
+
+!! test
+Image: framed images are always unscaled.
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|frame]]
+
+[[File:Foobar.jpg|frame|50px]]
+
+[[File:Foobar.jpg|frame|50x50px]]
+
+[[File:Foobar.jpg|frame|2000px]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption"></div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption"></div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption"></div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption"></div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Frame"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></figure><figure typeof="mw:Image/Frame"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></figure><figure typeof="mw:Image/Frame"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></figure><figure typeof="mw:Image/Frame"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></figure>
+!! end
+
+###################
+
+!! test
+Link to image page- image page normally doesn't exists, hence edit link
+Add test with existing image page
+#<p><a href="/wiki/File:Test" title="Image:Test">Image:test</a>
+!! wikitext
+[[:Image:test]]
+!! html
+<p><a href="/index.php?title=File:Test&amp;action=edit&amp;redlink=1" class="new" title="File:Test (page does not exist)">Image:test</a>
+</p>
+!! end
+
+!! test
+bug 18784 Link to non-existent image page with caption should use caption as link text
+!! wikitext
+[[:Image:test|caption]]
+!! html
+<p><a href="/index.php?title=File:Test&amp;action=edit&amp;redlink=1" class="new" title="File:Test (page does not exist)">caption</a>
+</p>
+!! end
+
+!! test
+Frameless image caption with a free URL
+!! wikitext
+[[File:Foobar.jpg|http://example.com]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="http://example.com"><img alt="http://example.com" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"http://example.com"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+!! test
+Thumbnail image caption with a free URL
+!! options
+thumbsize=220
+!! wikitext
+[[File:Foobar.jpg|thumb|http://example.com]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" href="http://example.com">http://example.com</a></figcaption></figure>
+!! end
+
+!! test
+Thumbnail image caption with a free URL and explicit alt
+!! options
+thumbsize=220
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb|http://example.com|alt=Alteration]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Alteration" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img alt="Alteration" resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" href="http://example.com">http://example.com</a></figcaption></figure>
+!! end
+
+!! test
+SVG thumbnails with no language set
+!! options
+!! wikitext
+[[File:Foobar.svg|thumb|caption]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.svg" class="image"><img alt="" src="http://example.com/images/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png" width="180" height="135" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div>caption</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" height="165" width="220"/></a><figcaption>caption</figcaption></figure>
+!! end
+
+!! test
+SVG thumbnails with language de
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.svg|thumb|caption|lang=de]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/index.php?title=File:Foobar.svg&amp;lang=de" class="image"><img alt="" src="http://example.com/images/thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.png" width="180" height="135" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.png 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div>caption</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" lang="de" height="165" width="220"/></a><figcaption>caption</figcaption></figure>
+!! end
+
+!! test
+SVG thumbnails with invalid language code
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.svg|thumb|caption|lang=invalid.language.code]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.svg" class="image"><img alt="" src="http://example.com/images/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png" width="180" height="135" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div>lang=invalid.language.code</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" height="165" width="220"/></a><figcaption>lang=invalid.language.code</figcaption></figure>
+!! end
+
+!! test
+BUG 1887: A ISBN with a thumbnail
+!! wikitext
+[[File:Foobar.jpg|thumb|ISBN 1235467890]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a href="/wiki/Special:BookSources/1235467890" class="internal mw-magiclink-isbn">ISBN 1235467890</a></div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption><a href="Special:BookSources/1235467890" rel="mw:ExtLink">ISBN 1235467890</a></figcaption></figure>
+!! end
+
+!! test
+BUG 1887: A RFC with a thumbnail
+!! wikitext
+[[File:Foobar.jpg|thumb|This is RFC 12354]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc12354">RFC 12354</a></div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>This is <a href="//tools.ietf.org/html/rfc12354" rel="mw:ExtLink">RFC 12354</a></figcaption></figure>
+!! end
+
+!! test
+BUG 1887: A mailto link with a thumbnail
+!! wikitext
+[[File:Foobar.jpg|thumb|Please mailto:nobody@example.com]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Please <a rel="nofollow" class="external free" href="mailto:nobody@example.com">mailto:nobody@example.com</a></div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>Please <a rel="mw:ExtLink" href="mailto:nobody@example.com">mailto:nobody@example.com</a></figcaption></figure>
+!! end
+
+# Pending resolution to bug 368
+!! test
+BUG 648: Frameless image caption with a link
+!! wikitext
+[[File:Foobar.jpg|text with a [[link]] in it]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a [[link]] in it"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+!! test
+BUG 648: Frameless image caption with a link (suffix)
+!! wikitext
+[[File:Foobar.jpg|text with a [[link]]foo in it]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a linkfoo in it"><img alt="text with a linkfoo in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a [[link]]foo in it"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+!! test
+BUG 648: Frameless image caption with an interwiki link
+!! wikitext
+[[File:Foobar.jpg|text with a [[MeatBall:Link]] in it]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a MeatBall:Link in it"><img alt="text with a MeatBall:Link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a [[MeatBall:Link]] in it"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+!! test
+BUG 648: Frameless image caption with a piped interwiki link
+!! wikitext
+[[File:Foobar.jpg|text with a [[MeatBall:Link|link]] in it]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"text with a [[MeatBall:Link|link]] in it"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+!! test
+Escape HTML special chars in image alt text
+!! wikitext
+[[File:Foobar.jpg|& < > "]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="&amp; &lt; &gt; &quot;"><img alt="&amp; &lt; &gt; &quot;" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&amp; &lt; > \""}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+!! test
+BUG 499: Alt text should have &#1234;, not &amp;1234;
+!! wikitext
+[[File:Foobar.jpg|&#9792;]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="♀"><img alt="♀" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&amp;#9792;"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+!! test
+Broken image caption with link
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[Image:Foobar.jpg|thumb|This is a broken caption. But [[Main Page|this]] is just an ordinary link.
+!! html/php
+<p>[[Image:Foobar.jpg|thumb|This is a broken caption. But <a href="/wiki/Main_Page" title="Main Page">this</a> is just an ordinary link.
+</p>
+!! html/parsoid
+<p>[[Image:Foobar.jpg|thumb|This is a broken caption. But <a rel="mw:WikiLink" href="Main_Page" title="Main Page">this</a> is just an ordinary link.</p>
+!! end
+
+!! test
+Image caption containing another image
+!! wikitext
+[[File:Foobar.jpg|thumb|This is a caption with another [[File:Thumb.png|image]] inside it!]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is a caption with another <a href="/wiki/File:Thumb.png" class="image" title="image"><img alt="image" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" /></a> inside it!</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>This is a caption with another <span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"image"}'><a href="File:Thumb.png"><img resource="./File:Thumb.png" src="//example.com/images/e/ea/Thumb.png" height="135" width="135"/></a></span> inside it!</figcaption></figure>
+!! end
+
+!! test
+Image: caption containing a newline
+!! wikitext
+[[File:Foobar.jpg|This
+*is some text]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="This *is some text"><img alt="This *is some text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"This\n*is some text"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!!end
+
+!!test
+Image: caption containing leading space
+(The leading space should not trigger nowiki escaping in wt2wt mode)
+!! wikitext
+[[File:Foobar.jpg|thumb| bar]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>bar</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption> bar</figcaption></figure>
+!!end
+
+!! test
+Image: caption containing a table
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[Image:Foobar.jpg|thumb|200px|This is an example image thumbnail caption with a table
+{|
+! Foo !! Bar
+|-
+| Foo1 || Bar1
+|}
+and some more text.]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:202px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is an example image thumbnail caption with a table <table> <tr> <th> Foo </th> <th> Bar </th></tr> <tr> <td> Foo1 </td> <td> Bar1 </td></tr></table> and some more text.</div></div></div>
+
+!! html/parsoid
+<figure typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="23" width="200"/></a><figcaption>This is an example image thumbnail caption with a table
+<table>
+<tbody>
+<tr><th>Foo </th><th>Bar</th></tr>
+<tr>
+<td>Foo1 </td>
+<td>Bar1</td></tr></tbody></table>and some more text.</figcaption></figure>
+!! end
+
+!! test
+Bug 3090: External links other than http: in image captions
+!! wikitext
+[[File:Foobar.jpg|thumb|200x200px|This caption has [irc://example.net irc] and [https://example.com Secure] ext links in it.]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:202px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This caption has <a rel="nofollow" class="external text" href="irc://example.net">irc</a> and <a rel="nofollow" class="external text" href="https://example.com">Secure</a> ext links in it.</div></div></div>
+
+!! html/parsoid
+<figure typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="23" width="200"/></a><figcaption>This caption has <a rel="mw:ExtLink" href="irc://example.net">irc</a> and <a rel="mw:ExtLink" href="https://example.com">Secure</a> ext links in it.</figcaption></figure>
+!! end
+
+!! test
+Custom class
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[Image:foobar.jpg|a|class=b]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="a"><img alt="a" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="b" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size b" typeof="mw:Image" data-mw='{"caption":"a"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a></span></p>
+!! end
+
+!! test
+Localized image handling (1).
+!! options
+parsoid=wt2html,wt2wt,html2html
+language=es
+!! wikitext
+[[Archivo:Foobar.jpg|izquierda|enlace=foo|caption]]
+!! html/php
+<div class="floatleft"><a href="/wiki/Foo" title="caption"><img alt="caption" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-left" typeof="mw:Image"><a href="./Foo"><img resource="./Archivo:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"/></a><figcaption>caption</figcaption></figure>
+!! end
+
+!! test
+Localized image handling (2).
+!! options
+thumbsize=220
+parsoid=wt2html,wt2wt,html2html
+language=es
+!! wikitext
+[[Archivo:Foobar.jpg|miniatura|izquierda|enlace=foo|caption]]
+!! html/php
+<div class="thumb tleft"><div class="thumbinner" style="width:222px;"><a href="/wiki/Foo" title="Foo"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Archivo:Foobar.jpg" class="internal" title="Aumentar"></a></div>caption</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-left" typeof="mw:Image/Thumb"><a href="./Foo"><img resource="./Archivo:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure>
+!! end
+
+!! test
+"border", "frameless" and "class" attributes on an image.
+!! options
+thumbsize=220
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|frameless|border|class=extra|caption]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="extra thumbborder" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size mw-image-border extra" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a></span></p>
+!! end
+
+# Note that 'right' is the default alignment, despite the misspelled 'righ' below
+!! test
+Invalid image attributes (bug 62500)
+!! options
+thumbsize=220
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb|float|left|caption]]
+
+[[File:Foobar.jpg|thumb|righ|caption]]
+
+[[File:Foobar.jpg|bogus1|thumb|bogus2|left|bogus3|caption]]
+!! html/php
+<div class="thumb tleft"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>caption</div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>caption</div></div></div>
+<div class="thumb tleft"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>caption</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size mw-halign-left" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure><figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure><figure class="mw-default-size mw-halign-left" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure>
+!! end
+
+!! article
+File:Barfoo.jpg
+!! text
+#REDIRECT [[File:Barfoo.jpg]]
+!! endarticle
+
+!! test
+Redirected image
+!! wikitext
+[[Image:Barfoo.jpg]]
+!! html
+<p><a href="/wiki/File:Barfoo.jpg" title="File:Barfoo.jpg">File:Barfoo.jpg</a>
+</p>
+!! end
+
+!! test
+Missing image with uploads disabled
+!! options
+wgEnableUploads=0
+!! wikitext
+[[Image:Foobaz.jpg]]
+!! html
+<p><a href="/wiki/File:Foobaz.jpg" title="File:Foobaz.jpg">File:Foobaz.jpg</a>
+</p>
+!! end
+
+# Parsoid-specific testing for images
+# https://www.mediawiki.org/wiki/Parsoid/MediaWiki_DOM_spec#Images
+# Currently imperfect due to a flaw in the Parsoid testrunner
+# Work in progress
+# THESE TESTS SHOULD BE MOVED UP and merged with the php-specific
+# image tests.
+
+!! test
+Parsoid-specific image handling - simple image with size and middle alignment
+!! wikitext
+[[File:Foobar.jpg|middle|50px]]
+!! html/parsoid
+<p><span class="mw-valign-middle" typeof="mw:Image">
+<a href="File:Foobar.jpg">
+<img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" height="6" width="50">
+</a>
+</span>
+</p>
+!! end
+
+!! test
+Parsoid-specific image handling - simple image with size, middle alignment,
+non-standard namespace alias
+!! options
+parsoid=wt2wt,wt2html,html2html
+!! wikitext
+[[Image:Foobar.jpg|middle|50px]]
+!! html/parsoid
+<p><span class="mw-valign-middle" typeof="mw:Image">
+<a href="File:Foobar.jpg">
+<img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" height="6" width="50">
+</a>
+</span>
+</p>
+!! end
+
+!! test
+Parsoid-specific image handling - simple image with size and middle alignment
+(existing content)
+!! wikitext
+[[File:Foobar.jpg|50px|middle]]
+!! html/parsoid
+<p><span class="mw-valign-middle" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"50px"},{"ck":"middle","ak":"middle"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+!! end
+
+!! test
+Parsoid-specific image handling - simple image with size and middle alignment
+and non-standard namespace name
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[Image:Foobar.jpg|50px|middle]]
+!! html/parsoid
+<p><span class="mw-valign-middle" typeof="mw:Image">
+<a href="File:Foobar.jpg">
+<img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" height="6" width="50">
+</a>
+</span>
+</p>
+!! end
+
+!! test
+Parsoid-specific image handling - simple image with both sizes, a baseline alignment, and a caption
+!! wikitext
+[[File:Foobar.jpg|500x10px|baseline|caption]]
+!! html/parsoid
+<p><span class="mw-valign-baseline" typeof="mw:Image" data-mw='{"caption":"caption"}' data-parsoid='{"optList":[{"ck":"width","ak":"500x10px"},{"ck":"baseline","ak":"baseline"},{"ck":"caption","ak":"caption"}],"size":"500x10"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/89px-Foobar.jpg" height="10" width="89" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"10","width":"89"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+!! end
+
+!! test
+Parsoid-specific image handling - simple image with border and size spec
+!! wikitext
+[[File:Foobar.jpg|50px|border|caption]]
+!! html/parsoid
+<p><span class="mw-image-border" typeof="mw:Image" data-mw='{"caption":"caption"}' data-parsoid='{"optList":[{"ck":"width","ak":"50px"},{"ck":"border","ak":"border"},{"ck":"caption","ak":"caption"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+!! end
+
+!! test
+Parsoid-specific image handling - thumbnail with halign, valign, and caption
+!! wikitext
+[[File:Foobar.jpg|left|baseline|thumb|caption content]]
+!! html/parsoid
+<figure class="mw-default-size mw-halign-left mw-valign-baseline" typeof="mw:Image/Thumb">
+<a href="File:Foobar.jpg">
+<img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" height="25" width="220" />
+</a>
+<figcaption>caption content</figcaption>
+</figure>
+!! end
+
+!! test
+Parsoid-specific image handling - thumbnail with halign, valign, and caption
+(existing content)
+!! wikitext
+[[File:Foobar.jpg|thumb|left|baseline|caption content]]
+!! html/parsoid
+<figure class="mw-default-size mw-halign-left mw-valign-baseline" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"left","ak":"left"},{"ck":"baseline","ak":"baseline"},{"ck":"caption","ak":"caption content"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>caption content</figcaption></figure>
+!! end
+
+!! test
+Parsoid-specific image handling - thumbnail with specific size, halign, valign, and caption
+!! wikitext
+[[Image:Foobar.jpg|right|middle|thumb|50x50px|caption]]
+!! html/parsoid
+<figure class="mw-halign-right mw-valign-middle" typeof="mw:Image/Thumb">
+<a href="File:Foobar.jpg">
+<img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" height="6" width="50" />
+</a>
+<figcaption>caption</figcaption>
+</figure>
+!! end
+
+!! test
+Parsoid-specific image handling - thumbnail with specific size, halign,
+valign, and caption (existing content)
+!! wikitext
+[[File:Foobar.jpg|thumb|50x50px|right|middle|caption]]
+!! html/parsoid
+<figure class="mw-halign-right mw-valign-middle" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"width","ak":"50x50px"},{"ck":"right","ak":"right"},{"ck":"middle","ak":"middle"},{"ck":"caption","ak":"caption"}],"size":"50x50"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>caption</figcaption></figure>
+!! end
+
+!! test
+Parsoid-specific image handling - framed image with specific size and caption
+(size is ignored)
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|frame|500x50px|caption]]
+!! html/parsoid
+<figure typeof="mw:Image/Frame">
+<a href="File:Foobar.jpg">
+<img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941" />
+</a>
+<figcaption>caption</figcaption>
+</figure>
+!! end
+
+!! test
+Parsoid-specific image handling - framed image with specific size, halign, valign, and caption
+(size is ignored)
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[[File:Foobar.jpg|left|baseline|frame|500x50px|caption]]
+!! html/parsoid
+<figure class="mw-halign-left mw-valign-baseline" typeof="mw:Image/Frame">
+<a href="File:Foobar.jpg">
+<img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941" />
+</a>
+<figcaption>caption</figcaption>
+</figure>
+!! end
+
+!! test
+Parsoid-specific image handling - frameless image with specific size, border, and caption
+!! wikitext
+[[File:Foobar.jpg|frameless|442x50px|border|caption]]
+!! html/parsoid
+<p><span class="mw-image-border" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}' data-parsoid='{"optList":[{"ck":"frameless","ak":"frameless"},{"ck":"width","ak":"442x50px"},{"ck":"border","ak":"border"},{"ck":"caption","ak":"caption"}],"size":"442x50"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/442px-Foobar.jpg" height="50" width="442" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"50","width":"442"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+!! end
+
+!! test
+Parsoid-specific image handling - simple image with a formatted caption
+!! wikitext
+[[File:Foobar.jpg|<table><tr><td>a</td><td>b</td></tr><tr><td>c</td></tr></table>]]
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"&lt;table>&lt;tr>&lt;td>a&lt;/td>&lt;td>b&lt;/td>&lt;/tr>&lt;tr>&lt;td>c&lt;/td>&lt;/tr>&lt;/table>"}'>
+<a href="File:Foobar.jpg">
+<img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941">
+</a></span></p>
+!! end
+
+!! test
+Parsoid-specific image handling - caption with a template in it
+!! wikitext
+[[File:Foobar.jpg|thumb|200x23px|This caption has a {{echo|transclusion}} in it.]]
+!! html/parsoid
+<figure typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" height="23" width="200"></a><figcaption>This caption has a <span about="#mwt1" typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;transclusion&quot;}},&quot;i&quot;:0}}]}">transclusion</span> in it.</figcaption></figure>
+!! end
+
+!! test
+Parsoid-specific image handling - caption with unbalanced tags in it
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+foo
+[[File:Foobar.jpg|thumb|200x200px|This caption has a <center>unbalanced tag in it.]]
+bar
+!! html/parsoid
+<p>foo</p>
+<figure typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="23" width="200"></a><figcaption>This caption has a <center>unbalanced tag in it.</center></figcaption></figure>
+<p>bar</p>
+!! end
+
+!! test
+Parsoid-specific image handling - empty caption (1)
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+[[File:Foobar.jpg|thumb|]]
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption></figcaption></figure>
+!! end
+
+# empty captions don't get serialized unless we're in the "round trip" case
+!! test
+Parsoid-specific image handling - empty caption (2)
+!! options
+parsoid=html2wt
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb">
+ <a href="File:Foobar.jpg">
+ <img resource="./File:Foobar.jpg"
+ src="//example.com/images/3/3a/Foobar.jpg"
+ height="25" width="220"/>
+ </a>
+ <figcaption></figcaption>
+</figure>
+!! wikitext
+[[File:Foobar.jpg|thumb]]
+!! end
+
+!! test
+Parsoid-specific image handling - whitespace caption
+!! wikitext
+[[File:Foobar.jpg|thumb| ]]
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption> </figcaption></figure>
+!! end
+
+!! test
+Parsoid-specific image handling - lang option
+!! wikitext
+foo
+[[File:Foobar.svg|lang=de|caption]]
+bar
+!! html/parsoid
+<p>foo
+<span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" lang="de" height="180" width="240"/></a></span>
+bar</p>
+!! end
+
+
+###
+### Subpages
+###
+!! article
+Subpage test/subpage
+!! text
+foo
+!! endarticle
+
+!! test
+Subpage link
+!! options
+subpage title=[[Subpage test]]
+!! wikitext
+[[/subpage]]
+!! html
+<p><a href="/wiki/Subpage_test/subpage" title="Subpage test/subpage">/subpage</a>
+</p>
+!! end
+
+!! test
+Subpage noslash link
+!! options
+subpage title=[[Subpage test]]
+!! wikitext
+[[/subpage/]]
+!! html
+<p><a href="/wiki/Subpage_test/subpage" title="Subpage test/subpage">subpage</a>
+</p>
+!! end
+
+# TODO: make this PHP-parser compatible!
+!! test
+Relative subpage noslash link
+!! options
+parsoid=wt2wt,wt2html,html2html
+subpage title=[[Subpage test/1/2/3/4]]
+!! wikitext
+[[../../subpage/]]
+
+[[../../subpage]]
+!! html
+<p><a rel="mw:WikiLink" href="Subpage_test/1/2/subpage/" title="Subpage test/1/2/subpage/">subpage</a></p>
+<p><a rel="mw:WikiLink" href="Subpage_test/1/2/subpage" title="Subpage test/1/2/subpage">Subpage_test/1/2/subpage</a></p>
+!! end
+
+!! test
+Parsoid: dot-slash prefixed wikilinks
+!! wikitext
+[[./foo]]
+
+[[././bar]]
+
+[[././baz/]]
+!! html/php
+<p>[[./foo]]
+</p><p>[[././bar]]
+</p><p>[[././baz/]]
+</p>
+!! html/parsoid
+<p>[[./foo]]
+</p><p>[[././bar]]
+</p><p>[[././baz/]]
+</p>
+!! end
+
+!! test
+Render invalid page names as plain text (bug 51090)
+!! wikitext
+[[./../foo|bar]]
+[[foo�|bar]]
+[[foo/.|bar]]
+[[foo/..|bar]]
+[[foo~~~bar]]
+[[foo>bar]]
+[[foo[bar]]
+[[.]]
+[[..]]
+[[foo././bar]]
+
+[[{{echo|./../foo}}|bar]]
+[[{{echo|foo/.}}|bar]]
+[[{{echo|foo/..}}|bar]]
+[[{{echo|foo~~~~bar}}]]
+[[{{echo|foo>bar}}]]
+[[{{echo|foo././bar}}]]
+[[{{echo|foo{bar}}]]
+[[{{echo|foo}bar}}]]
+[[{{echo|foo[bar}}]]
+[[{{echo|foo]bar}}]]
+[[{{echo|foo<bar}}]]
+!!html/php
+<p>[[./../foo|bar]]
+[[foo�|bar]]
+[[foo/.|bar]]
+[[foo/..|bar]]
+[[foo~~~bar]]
+[[foo&gt;bar]]
+[[foo[bar]]
+[[.]]
+[[..]]
+[[foo././bar]]
+</p><p>[[./../foo|bar]]
+[[foo/.|bar]]
+[[foo/..|bar]]
+[[foo~~~~bar]]
+[[foo&gt;bar]]
+[[foo././bar]]
+[[foo{bar]]
+[[foo}bar]]
+[[foo[bar]]
+[[foo]bar]]
+[[foo&lt;bar]]
+</p>
+!!html/parsoid
+<p>[[./../foo|bar]][[foo�|bar]][[foo/.|bar]][[foo/..|bar]][[foo~~~bar]][[foo>bar]][[foo[bar]][[.]][[..]][[foo././bar]]</p>
+<p>[[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"./../foo"}},"i":0}}]}'>./../foo</span>|bar]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo/."}},"i":0}}]}'>foo/.</span>|bar]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo/.."}},"i":0}}]}'>foo/..</span>|bar]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo~~~~bar"}},"i":0}}]}'>foo~~~~bar</span>]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo>bar"}},"i":0}}]}'>foo>bar</span>]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo././bar"}},"i":0}}]}'>foo././bar</span>]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo{bar"}},"i":0}}]}'>foo{bar</span>]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo}bar"}},"i":0}}]}'>foo}bar</span>]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo[bar"}},"i":0}}]}'>foo[bar</span>]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo]bar"}},"i":0}}]}'>foo]bar</span>]][[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo&lt;bar"}},"i":0}}]}'>foo&lt;bar</span>]]</p>
+!!end
+
+!! test
+Disabled subpages
+!! wikitext
+[[/subpage]]
+!! html
+<p><a href="/index.php?title=/subpage&amp;action=edit&amp;redlink=1" class="new" title="/subpage (page does not exist)">/subpage</a>
+</p>
+!! end
+
+!! test
+BUG 561: {{/Subpage}}
+!! options
+subpage title=[[Page]]
+!! wikitext
+{{/Subpage}}
+!! html
+<p><a href="/index.php?title=Page/Subpage&amp;action=edit&amp;redlink=1" class="new" title="Page/Subpage (page does not exist)">Page/Subpage</a>
+</p>
+!! end
+
+###
+### Categories
+###
+!! article
+Category:MediaWiki User's Guide
+!! text
+blah
+!! endarticle
+
+!! test
+Link to category
+!! wikitext
+[[:Category:MediaWiki User's Guide]]
+!! html
+<p><a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">Category:MediaWiki User's Guide</a>
+</p>
+!! end
+
+!! test
+Simple category
+!! options
+cat
+!! wikitext
+[[Category:MediaWiki User's Guide]]
+!! html
+<a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">MediaWiki User's Guide</a>
+!! end
+
+!! test
+PAGESINCATEGORY invalid title fatal (r33546 fix)
+!! wikitext
+{{PAGESINCATEGORY:<bogus>}}
+!! html
+<p>0
+</p>
+!! end
+
+!! test
+Category with different sort key
+!! options
+cat
+!! wikitext
+[[Category:MediaWiki User's Guide|Foo]]
+!! html
+<a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">MediaWiki User's Guide</a>
+!! end
+
+!! test
+Category with identical sort key
+!! options
+cat
+!! wikitext
+[[Category:MediaWiki User's Guide|MediaWiki User's Guide]]
+!! html
+<a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">MediaWiki User's Guide</a>
+!! end
+
+!! test
+Category with empty sort key
+!! options
+cat
+pst
+!! wikitext
+[[Category:MediaWiki User's Guide|]]
+!! html
+[[Category:MediaWiki User's Guide|MediaWiki User's Guide]]
+!! end
+
+!! test
+Category with empty sort key and parentheses
+!! options
+cat
+pst
+!! wikitext
+[[Category:Foo (bar)|]]
+!! html
+[[Category:Foo (bar)|Foo]]
+!! end
+
+!! test
+Category with link tail
+!! options
+cat
+pst
+!! wikitext
+123[[Category:Foo]]456
+!! html
+123[[Category:Foo]]456
+!! end
+
+!! test
+Category with template
+!! options
+cat
+pst
+!! wikitext
+[[Category:{{echo|Foo}}]]
+!! html
+[[Category:{{echo|Foo}}]]
+!! end
+
+!! test
+Category with template in sort key
+!! options
+cat
+pst
+!! wikitext
+[[Category:Foo|{{echo|Bar}}]]
+!! html
+[[Category:Foo|{{echo|Bar}}]]
+!! end
+
+!! test
+Category with template in sort key and title
+!! options
+cat
+pst
+!! wikitext
+[[Category:{{echo|Foo}}|{{echo|Bar}}]]
+!! html
+[[Category:{{echo|Foo}}|{{echo|Bar}}]]
+!! end
+
+!! test
+Category / paragraph interactions
+!! wikitext
+Foo [[Category:Baz]] Bar
+
+Foo [[Category:Baz]]
+Bar
+
+Foo
+[[Category:Baz]]
+Bar
+
+Foo
+[[Category:Baz]] Bar
+
+Foo
+[[Category:Baz]]
+ [[Category:Baz]]
+[[Category:Baz]]
+Bar
+
+[[Category:Baz]]
+ [[Category:Baz]]
+[[Category:Baz]]
+
+[[Category:Baz]]
+ {{echo|[[Category:Baz]]}}
+[[Category:Baz]]
+!! html
+<p>Foo Bar
+</p><p>Foo
+Bar
+</p><p>Foo
+Bar
+</p><p>Foo Bar
+</p><p>Foo
+Bar
+</p>
+!! end
+
+!! test
+Parsoid: Serialize link to category page with colon escape
+!! options
+parsoid
+!! wikitext
+
+[[:Category:Foo]]
+[[:Category:Foo|Bar]]
+!! html
+<p>
+<a rel="mw:WikiLink" href="Category:Foo" title="Category:Foo">Category:Foo</a>
+<a rel="mw:WikiLink" href="Category:Foo" title="Category:Foo">Bar</a>
+</p>
+!! end
+
+!! test
+Parsoid: Link prefix/suffixes aren't applied to category links
+!! options
+parsoid=wt2html,wt2wt,html2html
+language=is
+!! wikitext
+x[[Category:Foo]]y
+!! html
+<p>x<link rel="mw:PageProp/Category" href="Category:Foo">y</p>
+!! end
+
+!! test
+Parsoid: Serialize link to file page with colon escape
+!! options
+parsoid
+!! wikitext
+
+[[:File:Foo.png]]
+[[:File:Foo.png|Bar]]
+!! html
+<p>
+<a rel="mw:WikiLink" href="File:Foo.png" title="File:Foo.png">File:Foo.png</a>
+<a rel="mw:WikiLink" href="File:Foo.png" title="File:Foo.png">Bar</a>
+</p>
+!! end
+
+!! test
+Parsoid: Serialize a genuine category link without colon escape
+!! options
+parsoid
+!! wikitext
+[[Category:Foo]]
+[[Category:Foo|Bar]]
+!! html
+<link rel="mw:PageProp/Category" href="Category:Foo">
+<link rel="mw:PageProp/Category" href="Category:Foo#Bar">
+!! end
+
+!! test
+Parsoid: Defaultsort
+!! options
+parsoid
+!! wikitext
+{{DEFAULTSORT:Foo}}
+!! html
+<meta property="mw:PageProp/categorydefaultsort" content="Foo"/>
+!! end
+
+###
+### Inter-language links
+###
+!! test
+Interlanguage links
+!! options
+ill
+!! wikitext
+[[es:Alimento]]
+[[fr:Nourriture]]
+[[zh:食品]]
+!! html/php
+es:Alimento fr:Nourriture zh:食品
+!! html/parsoid
+<p><link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Alimento"/>
+<link rel="mw:PageProp/Language" href="//fr.wikipedia.org/wiki/Nourriture"/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/食品"/></p>
+!! end
+
+!! test
+Duplicate interlanguage links (bug 24502)
+!! options
+ill
+!! wikitext
+[[es:1]]
+[[es:2]]
+[[fr:1]]
+[[fr:2]]
+!! html/php
+es:1 fr:1
+!! html/parsoid
+<p><link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/1"/>
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/2"/>
+<link rel="mw:PageProp/Language" href="//fr.wikipedia.org/wiki/1"/>
+<link rel="mw:PageProp/Language" href="//fr.wikipedia.org/wiki/2"/></p>
+!! end
+
+###
+### Sections
+###
+!! test
+Basic section headings
+!! wikitext
+== Headline 1 ==
+Some text
+
+==Headline 2==
+More
+===Smaller headline===
+Blah blah
+!! html
+<h2><span class="mw-headline" id="Headline_1">Headline 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Headline 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>Some text
+</p>
+<h2><span class="mw-headline" id="Headline_2">Headline 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Headline 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>More
+</p>
+<h3><span class="mw-headline" id="Smaller_headline">Smaller headline</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: Smaller headline">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<p>Blah blah
+</p>
+!! end
+
+!! test
+Section headings with TOC
+!! wikitext
+== Headline 1 ==
+=== Subheadline 1 ===
+===== Skipping a level =====
+====== Skipping a level ======
+
+== Headline 2 ==
+Some text
+===Another headline===
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Headline_1"><span class="tocnumber">1</span> <span class="toctext">Headline 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#Subheadline_1"><span class="tocnumber">1.1</span> <span class="toctext">Subheadline 1</span></a>
+<ul>
+<li class="toclevel-3 tocsection-3"><a href="#Skipping_a_level"><span class="tocnumber">1.1.1</span> <span class="toctext">Skipping a level</span></a>
+<ul>
+<li class="toclevel-4 tocsection-4"><a href="#Skipping_a_level_2"><span class="tocnumber">1.1.1.1</span> <span class="toctext">Skipping a level</span></a></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-5"><a href="#Headline_2"><span class="tocnumber">2</span> <span class="toctext">Headline 2</span></a>
+<ul>
+<li class="toclevel-2 tocsection-6"><a href="#Another_headline"><span class="tocnumber">2.1</span> <span class="toctext">Another headline</span></a></li>
+</ul>
+</li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="Headline_1">Headline 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Headline 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h3><span class="mw-headline" id="Subheadline_1">Subheadline 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Subheadline 1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h5><span class="mw-headline" id="Skipping_a_level">Skipping a level</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: Skipping a level">edit</a><span class="mw-editsection-bracket">]</span></span></h5>
+<h6><span class="mw-headline" id="Skipping_a_level_2">Skipping a level</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: Skipping a level">edit</a><span class="mw-editsection-bracket">]</span></span></h6>
+<h2><span class="mw-headline" id="Headline_2">Headline 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: Headline 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>Some text
+</p>
+<h3><span class="mw-headline" id="Another_headline">Another headline</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=6" title="Edit section: Another headline">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+
+!! end
+
+# perl -e 'print "="x$_," Level $_ heading","="x$_,"\n" for 1..10'
+!! test
+Handling of sections up to level 6 and beyond
+!! wikitext
+= Level 1 Heading=
+== Level 2 Heading==
+=== Level 3 Heading===
+==== Level 4 Heading====
+===== Level 5 Heading=====
+====== Level 6 Heading======
+======= Level 7 Heading=======
+======== Level 8 Heading========
+========= Level 9 Heading=========
+========== Level 10 Heading==========
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Level_1_Heading"><span class="tocnumber">1</span> <span class="toctext">Level 1 Heading</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#Level_2_Heading"><span class="tocnumber">1.1</span> <span class="toctext">Level 2 Heading</span></a>
+<ul>
+<li class="toclevel-3 tocsection-3"><a href="#Level_3_Heading"><span class="tocnumber">1.1.1</span> <span class="toctext">Level 3 Heading</span></a>
+<ul>
+<li class="toclevel-4 tocsection-4"><a href="#Level_4_Heading"><span class="tocnumber">1.1.1.1</span> <span class="toctext">Level 4 Heading</span></a>
+<ul>
+<li class="toclevel-5 tocsection-5"><a href="#Level_5_Heading"><span class="tocnumber">1.1.1.1.1</span> <span class="toctext">Level 5 Heading</span></a>
+<ul>
+<li class="toclevel-6 tocsection-6"><a href="#Level_6_Heading"><span class="tocnumber">1.1.1.1.1.1</span> <span class="toctext">Level 6 Heading</span></a></li>
+<li class="toclevel-6 tocsection-7"><a href="#.3D_Level_7_Heading.3D"><span class="tocnumber">1.1.1.1.1.2</span> <span class="toctext">= Level 7 Heading=</span></a></li>
+<li class="toclevel-6 tocsection-8"><a href="#.3D.3D_Level_8_Heading.3D.3D"><span class="tocnumber">1.1.1.1.1.3</span> <span class="toctext">== Level 8 Heading==</span></a></li>
+<li class="toclevel-6 tocsection-9"><a href="#.3D.3D.3D_Level_9_Heading.3D.3D.3D"><span class="tocnumber">1.1.1.1.1.4</span> <span class="toctext">=== Level 9 Heading===</span></a></li>
+<li class="toclevel-6 tocsection-10"><a href="#.3D.3D.3D.3D_Level_10_Heading.3D.3D.3D.3D"><span class="tocnumber">1.1.1.1.1.5</span> <span class="toctext">==== Level 10 Heading====</span></a></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</div>
+
+<h1><span class="mw-headline" id="Level_1_Heading">Level 1 Heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Level 1 Heading">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<h2><span class="mw-headline" id="Level_2_Heading">Level 2 Heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Level 2 Heading">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h3><span class="mw-headline" id="Level_3_Heading">Level 3 Heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: Level 3 Heading">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h4><span class="mw-headline" id="Level_4_Heading">Level 4 Heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: Level 4 Heading">edit</a><span class="mw-editsection-bracket">]</span></span></h4>
+<h5><span class="mw-headline" id="Level_5_Heading">Level 5 Heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: Level 5 Heading">edit</a><span class="mw-editsection-bracket">]</span></span></h5>
+<h6><span class="mw-headline" id="Level_6_Heading">Level 6 Heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=6" title="Edit section: Level 6 Heading">edit</a><span class="mw-editsection-bracket">]</span></span></h6>
+<h6><span class="mw-headline" id=".3D_Level_7_Heading.3D">= Level 7 Heading=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=7" title="Edit section: = Level 7 Heading=">edit</a><span class="mw-editsection-bracket">]</span></span></h6>
+<h6><span class="mw-headline" id=".3D.3D_Level_8_Heading.3D.3D">== Level 8 Heading==</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=8" title="Edit section: == Level 8 Heading==">edit</a><span class="mw-editsection-bracket">]</span></span></h6>
+<h6><span class="mw-headline" id=".3D.3D.3D_Level_9_Heading.3D.3D.3D">=== Level 9 Heading===</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=9" title="Edit section: === Level 9 Heading===">edit</a><span class="mw-editsection-bracket">]</span></span></h6>
+<h6><span class="mw-headline" id=".3D.3D.3D.3D_Level_10_Heading.3D.3D.3D.3D">==== Level 10 Heading====</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=10" title="Edit section: ==== Level 10 Heading====">edit</a><span class="mw-editsection-bracket">]</span></span></h6>
+
+!! end
+
+!! test
+TOC regression (bug 9764)
+!! wikitext
+== title 1 ==
+=== title 1.1 ===
+==== title 1.1.1 ====
+=== title 1.2 ===
+== title 2 ==
+=== title 2.1 ===
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#title_1"><span class="tocnumber">1</span> <span class="toctext">title 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#title_1.1"><span class="tocnumber">1.1</span> <span class="toctext">title 1.1</span></a>
+<ul>
+<li class="toclevel-3 tocsection-3"><a href="#title_1.1.1"><span class="tocnumber">1.1.1</span> <span class="toctext">title 1.1.1</span></a></li>
+</ul>
+</li>
+<li class="toclevel-2 tocsection-4"><a href="#title_1.2"><span class="tocnumber">1.2</span> <span class="toctext">title 1.2</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-5"><a href="#title_2"><span class="tocnumber">2</span> <span class="toctext">title 2</span></a>
+<ul>
+<li class="toclevel-2 tocsection-6"><a href="#title_2.1"><span class="tocnumber">2.1</span> <span class="toctext">title 2.1</span></a></li>
+</ul>
+</li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="title_1">title 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: title 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h3><span class="mw-headline" id="title_1.1">title 1.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: title 1.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h4><span class="mw-headline" id="title_1.1.1">title 1.1.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: title 1.1.1">edit</a><span class="mw-editsection-bracket">]</span></span></h4>
+<h3><span class="mw-headline" id="title_1.2">title 1.2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: title 1.2">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h2><span class="mw-headline" id="title_2">title 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: title 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h3><span class="mw-headline" id="title_2.1">title 2.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=6" title="Edit section: title 2.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+
+!! end
+
+!! test
+TOC with wgMaxTocLevel=3 (bug 6204)
+!! options
+wgMaxTocLevel=3
+!! wikitext
+== title 1 ==
+=== title 1.1 ===
+==== title 1.1.1 ====
+=== title 1.2 ===
+== title 2 ==
+=== title 2.1 ===
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#title_1"><span class="tocnumber">1</span> <span class="toctext">title 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#title_1.1"><span class="tocnumber">1.1</span> <span class="toctext">title 1.1</span></a></li>
+<li class="toclevel-2 tocsection-4"><a href="#title_1.2"><span class="tocnumber">1.2</span> <span class="toctext">title 1.2</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-5"><a href="#title_2"><span class="tocnumber">2</span> <span class="toctext">title 2</span></a>
+<ul>
+<li class="toclevel-2 tocsection-6"><a href="#title_2.1"><span class="tocnumber">2.1</span> <span class="toctext">title 2.1</span></a></li>
+</ul>
+</li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="title_1">title 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: title 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h3><span class="mw-headline" id="title_1.1">title 1.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: title 1.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h4><span class="mw-headline" id="title_1.1.1">title 1.1.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: title 1.1.1">edit</a><span class="mw-editsection-bracket">]</span></span></h4>
+<h3><span class="mw-headline" id="title_1.2">title 1.2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: title 1.2">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h2><span class="mw-headline" id="title_2">title 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: title 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h3><span class="mw-headline" id="title_2.1">title 2.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=6" title="Edit section: title 2.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+
+!! end
+
+!! test
+TOC with wgMaxTocLevel=3 and two level four headings (bug 6204)
+!! options
+wgMaxTocLevel=3
+!! wikitext
+==Section 1==
+===Section 1.1===
+====Section 1.1.1====
+====Section 1.1.1.1====
+==Section 2==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#Section_1.1"><span class="tocnumber">1.1</span> <span class="toctext">Section 1.1</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-5"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h3><span class="mw-headline" id="Section_1.1">Section 1.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Section 1.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h4><span class="mw-headline" id="Section_1.1.1">Section 1.1.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: Section 1.1.1">edit</a><span class="mw-editsection-bracket">]</span></span></h4>
+<h4><span class="mw-headline" id="Section_1.1.1.1">Section 1.1.1.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: Section 1.1.1.1">edit</a><span class="mw-editsection-bracket">]</span></span></h4>
+<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+
+!! test
+Resolving duplicate section names
+!! wikitext
+== Foo bar ==
+== Foo bar ==
+!! html
+<h2><span class="mw-headline" id="Foo_bar">Foo bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Foo bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Foo_bar_2">Foo bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Foo bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+Resolving duplicate section names with differing case (bug 10721)
+!! wikitext
+== Foo bar ==
+== Foo Bar ==
+!! html
+<h2><span class="mw-headline" id="Foo_bar">Foo bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Foo bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Foo_Bar_2">Foo Bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! article
+Template:sections
+!! text
+===Section 1===
+==Section 2==
+!! endarticle
+
+!! test
+Template with sections, __NOTOC__
+!! wikitext
+__NOTOC__
+==Section 0==
+{{sections}}
+==Section 4==
+!! html
+<h2><span class="mw-headline" id="Section_0">Section 0</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Section 0">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h3><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Template:Sections&amp;action=edit&amp;section=T-1" title="Template:Sections">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Template:Sections&amp;action=edit&amp;section=T-2" title="Template:Sections">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Section_4">Section 4</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Section 4">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+__NOEDITSECTION__ keyword
+!! wikitext
+__NOEDITSECTION__
+==Section 1==
+==Section 2==
+!! html
+<h2><span class="mw-headline" id="Section_1">Section 1</span></h2>
+<h2><span class="mw-headline" id="Section_2">Section 2</span></h2>
+
+!! end
+
+!! test
+Link inside a section heading
+!! wikitext
+==Section with a [[Main Page|link]] in it==
+!! html
+<h2><span class="mw-headline" id="Section_with_a_link_in_it">Section with a <a href="/wiki/Main_Page" title="Main Page">link</a> in it</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Section with a link in it">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+TOC regression (bug 12077)
+!! wikitext
+__TOC__
+== title 1 ==
+=== title 1.1 ===
+== title 2 ==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#title_1"><span class="tocnumber">1</span> <span class="toctext">title 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#title_1.1"><span class="tocnumber">1.1</span> <span class="toctext">title 1.1</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-3"><a href="#title_2"><span class="tocnumber">2</span> <span class="toctext">title 2</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="title_1">title 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: title 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h3><span class="mw-headline" id="title_1.1">title 1.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: title 1.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h2><span class="mw-headline" id="title_2">title 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: title 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+BUG 1219 URL next to image (good)
+!! wikitext
+http://example.com [[Image:foobar.jpg]]
+!! html
+<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> <a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!!end
+
+!! test
+Short headings with trailing space should match behavior of Parser::doHeadings (bug 19910)
+!! wikitext
+===
+The line above must have a trailing space!
+=== <!--
+--> <!-- -->
+But just in case it doesn't...
+!! html
+<h1><span class="mw-headline" id=".3D">=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: =">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<p>The line above must have a trailing space!
+</p>
+<h1><span class="mw-headline" id=".3D_2">=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: =">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<p>But just in case it doesn't...
+</p>
+!! end
+
+!! test
+Header with special characters (bug 25462)
+!! wikitext
+The tooltips shall not show entities to the user (ie. be double escaped)
+
+== text > text ==
+section 1
+
+== text < text ==
+section 2
+
+== text & text ==
+section 3
+
+== text ' text ==
+section 4
+
+== text " text ==
+section 5
+!! html
+<p>The tooltips shall not show entities to the user (ie. be double escaped)
+</p>
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#text_.3E_text"><span class="tocnumber">1</span> <span class="toctext">text &gt; text</span></a></li>
+<li class="toclevel-1 tocsection-2"><a href="#text_.3C_text"><span class="tocnumber">2</span> <span class="toctext">text &lt; text</span></a></li>
+<li class="toclevel-1 tocsection-3"><a href="#text_.26_text"><span class="tocnumber">3</span> <span class="toctext">text &amp; text</span></a></li>
+<li class="toclevel-1 tocsection-4"><a href="#text_.27_text"><span class="tocnumber">4</span> <span class="toctext">text ' text</span></a></li>
+<li class="toclevel-1 tocsection-5"><a href="#text_.22_text"><span class="tocnumber">5</span> <span class="toctext">text " text</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="text_.3E_text">text &gt; text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: text &gt; text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 1
+</p>
+<h2><span class="mw-headline" id="text_.3C_text">text &lt; text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: text &lt; text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 2
+</p>
+<h2><span class="mw-headline" id="text_.26_text">text &amp; text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: text &amp; text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 3
+</p>
+<h2><span class="mw-headline" id="text_.27_text">text ' text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: text ' text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 4
+</p>
+<h2><span class="mw-headline" id="text_.22_text">text " text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: text &quot; text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 5
+</p>
+!! end
+
+!! test
+Header with space, plus and underscore as entity
+!! wikitext
+Id should not contain + for spaces
+
+== Space between Text ==
+section 1
+
+== Space-Entity&#32;between&#32;Text ==
+section 2
+
+== Plus+between+Text ==
+section 3
+
+== Plus-Entity&#43;between&#43;Text ==
+section 4
+
+== Underscore_between_Text ==
+section 5
+
+== Underscore-Entity&#95;between&#95;Text ==
+section 6
+
+[[#Space between Text]]
+[[#Space-Entity&#32;between&#32;Text]]
+[[#Plus+between+Text]]
+[[#Plus-Entity&#43;between&#43;Text]]
+[[#Underscore_between_Text]]
+[[#Underscore-Entity&#95;between&#95;Text]]
+!! html
+<p>Id should not contain + for spaces
+</p>
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Space_between_Text"><span class="tocnumber">1</span> <span class="toctext">Space between Text</span></a></li>
+<li class="toclevel-1 tocsection-2"><a href="#Space-Entity_between_Text"><span class="tocnumber">2</span> <span class="toctext">Space-Entity&#32;between&#32;Text</span></a></li>
+<li class="toclevel-1 tocsection-3"><a href="#Plus.2Bbetween.2BText"><span class="tocnumber">3</span> <span class="toctext">Plus+between+Text</span></a></li>
+<li class="toclevel-1 tocsection-4"><a href="#Plus-Entity.2Bbetween.2BText"><span class="tocnumber">4</span> <span class="toctext">Plus-Entity&#43;between&#43;Text</span></a></li>
+<li class="toclevel-1 tocsection-5"><a href="#Underscore_between_Text"><span class="tocnumber">5</span> <span class="toctext">Underscore_between_Text</span></a></li>
+<li class="toclevel-1 tocsection-6"><a href="#Underscore-Entity_between_Text"><span class="tocnumber">6</span> <span class="toctext">Underscore-Entity&#95;between&#95;Text</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="Space_between_Text">Space between Text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Space between Text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 1
+</p>
+<h2><span class="mw-headline" id="Space-Entity_between_Text">Space-Entity&#32;between&#32;Text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Space-Entity between Text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 2
+</p>
+<h2><span class="mw-headline" id="Plus.2Bbetween.2BText">Plus+between+Text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: Plus+between+Text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 3
+</p>
+<h2><span class="mw-headline" id="Plus-Entity.2Bbetween.2BText">Plus-Entity&#43;between&#43;Text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: Plus-Entity+between+Text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 4
+</p>
+<h2><span class="mw-headline" id="Underscore_between_Text">Underscore_between_Text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: Underscore between Text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 5
+</p>
+<h2><span class="mw-headline" id="Underscore-Entity_between_Text">Underscore-Entity&#95;between&#95;Text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=6" title="Edit section: Underscore-Entity_between_Text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>section 6
+</p><p><a href="#Space_between_Text">#Space between Text</a>
+<a href="#Space-Entity_between_Text">#Space-Entity&#32;between&#32;Text</a>
+<a href="#Plus.2Bbetween.2BText">#Plus+between+Text</a>
+<a href="#Plus-Entity.2Bbetween.2BText">#Plus-Entity&#43;between&#43;Text</a>
+<a href="#Underscore_between_Text">#Underscore_between_Text</a>
+<a href="#Underscore-Entity_between_Text">#Underscore-Entity&#95;between&#95;Text</a>
+</p>
+!! end
+
+!! test
+Headers with excess '=' characters
+(Are similar tests necessary beyond the 1st level?)
+!! wikitext
+=foo==
+==foo=
+=''italic'' heading==
+==''italic'' heading=
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#foo.3D"><span class="tocnumber">1</span> <span class="toctext">foo=</span></a></li>
+<li class="toclevel-1 tocsection-2"><a href="#.3Dfoo"><span class="tocnumber">2</span> <span class="toctext">=foo</span></a></li>
+<li class="toclevel-1 tocsection-3"><a href="#italic_heading.3D"><span class="tocnumber">3</span> <span class="toctext"><i>italic</i> heading=</span></a></li>
+<li class="toclevel-1 tocsection-4"><a href="#.3Ditalic_heading"><span class="tocnumber">4</span> <span class="toctext">=<i>italic</i> heading</span></a></li>
+</ul>
+</div>
+
+<h1><span class="mw-headline" id="foo.3D">foo=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: foo=">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<h1><span class="mw-headline" id=".3Dfoo">=foo</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: =foo">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<h1><span class="mw-headline" id="italic_heading.3D"><i>italic</i> heading=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: italic heading=">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<h1><span class="mw-headline" id=".3Ditalic_heading">=<i>italic</i> heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: =italic heading">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+
+!! end
+
+!! test
+HTML headers vs TOC (bug 23393)
+(__NOEDITSECTION__ for clearer output, doesn't matter here)
+!! wikitext
+<h1>Header 1</h1>
+== Header 1.1 ==
+== Header 1.2 ==
+
+<h1>Header 2
+</h1>
+== Header 2.1 ==
+== Header 2.2 ==
+__NOEDITSECTION__
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1"><a href="#Header_1"><span class="tocnumber">1</span> <span class="toctext">Header 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-1"><a href="#Header_1.1"><span class="tocnumber">1.1</span> <span class="toctext">Header 1.1</span></a></li>
+<li class="toclevel-2 tocsection-2"><a href="#Header_1.2"><span class="tocnumber">1.2</span> <span class="toctext">Header 1.2</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1"><a href="#Header_2"><span class="tocnumber">2</span> <span class="toctext">Header 2</span></a>
+<ul>
+<li class="toclevel-2 tocsection-3"><a href="#Header_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Header 2.1</span></a></li>
+<li class="toclevel-2 tocsection-4"><a href="#Header_2.2"><span class="tocnumber">2.2</span> <span class="toctext">Header 2.2</span></a></li>
+</ul>
+</li>
+</ul>
+</div>
+
+<h1><span class="mw-headline" id="Header_1">Header 1</span></h1>
+<h2><span class="mw-headline" id="Header_1.1">Header 1.1</span></h2>
+<h2><span class="mw-headline" id="Header_1.2">Header 1.2</span></h2>
+<h1><span class="mw-headline" id="Header_2">Header 2</span></h1>
+<h2><span class="mw-headline" id="Header_2.1">Header 2.1</span></h2>
+<h2><span class="mw-headline" id="Header_2.2">Header 2.2</span></h2>
+
+!! end
+
+!! test
+Single-line or multiline-comments can follow headings
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+==foo==<!---->
+==bar==<!--c1-->
+==baz==<!--
+c2
+c3-->
+!! html
+<h2><span class="mw-headline" id="foo">foo</span></h2>
+<h2><span class="mw-headline" id="bar">bar</span></h2>
+<h2><span class="mw-headline" id="baz">baz</span></h2>
+
+!! end
+
+!! test
+BUG 1219 URL next to image (broken)
+!! wikitext
+http://example.com[[Image:foobar.jpg]]
+!! html
+<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!!end
+
+!! test
+Bug 1186 news: in the middle of text
+!! wikitext
+http://en.wikinews.org/wiki/Wikinews:Workplace
+!! html
+<p><a rel="nofollow" class="external free" href="http://en.wikinews.org/wiki/Wikinews:Workplace">http://en.wikinews.org/wiki/Wikinews:Workplace</a>
+</p>
+!!end
+
+
+!! test
+Namespaced link must have a title
+!! wikitext
+[[Project:]]
+!! html
+<p>[[Project:]]
+</p>
+!!end
+
+!! test
+Namespaced link must have a title (bad fragment version)
+!! wikitext
+[[Project:#fragment]]
+!! html
+<p>[[Project:#fragment]]
+</p>
+!!end
+
+
+###
+### HTML tags and HTML attributes
+###
+
+!! test
+div with no attributes
+!! wikitext
+<div>HTML rocks</div>
+!! html
+<div>HTML rocks</div>
+
+!! end
+
+!! test
+div with double-quoted attribute
+!! wikitext
+<div id="rock">HTML rocks</div>
+!! html
+<div id="rock">HTML rocks</div>
+
+!! end
+
+!! test
+div with single-quoted attribute
+!! wikitext
+<div id='rock'>HTML rocks</div>
+!! html
+<div id="rock">HTML rocks</div>
+
+!! end
+
+!! test
+div with unquoted attribute
+!! wikitext
+<div id=rock>HTML rocks</div>
+!! html
+<div id="rock">HTML rocks</div>
+
+!! end
+
+!! test
+div with illegal double attributes
+!! wikitext
+<div id="a" id="b">HTML rocks</div>
+!! html
+<div id="b">HTML rocks</div>
+
+!!end
+
+# FIXME: produce empty string instead of "class" in the PHP parser, following
+# the HTML5 spec.
+!! test
+div with empty attribute value, space before equals
+!! options
+parsoid
+!! wikitext
+<div class =>HTML rocks</div>
+!! html
+<div class="">HTML rocks</div>
+
+!! end
+
+!! test
+div with multiple empty attribute values
+!! options
+parsoid
+!! wikitext
+<div id= title=>HTML rocks</div>
+!! html
+<div id="" title="">HTML rocks</div>
+
+!! end
+
+!! test
+table with multiple empty attribute values
+!! options
+parsoid
+!! wikitext
+{| title= id=
+| hi
+|}
+!! html
+<table title="" id="">
+<tbody><tr><td> hi</td></tr>
+</tbody></table>
+!! end
+
+# The PHP parser escapes the opening brace to &#123; for some reason, so
+# disabled this test for it.
+!! test
+div with braces in attribute value
+!! options
+parsoid
+!! wikitext
+<div title="{}">Foo</div>
+!! html
+<div title="{}">Foo</div>
+!! end
+
+# This it very inconsistent in the PHP parser: it returns
+# class="class" if there is a space between the name and the equal sign (see
+# 'div with empty attribute value, space before equals'), but strips the
+# attribute completely if the space is missing. We hope that not much content
+# depends on this, so are implementing the behavior below in Parsoid for
+# consistencies' sake. Disabled for the PHP parser.
+# FIXME: fix this behavior in the PHP parser?
+!! test
+div with empty attribute value, no space before equals
+!! options
+parsoid
+!! wikitext
+<div class=>HTML rocks</div>
+!! html
+<div class="">HTML rocks</div>
+
+!! end
+
+!! test
+HTML multiple attributes correction
+!! wikitext
+<p class="error" class="awesome">Awesome!</p>
+!! html
+<p class="awesome">Awesome!</p>
+
+!!end
+
+!! test
+Table multiple attributes correction
+!! wikitext
+{|
+!+ class="error" class="awesome"| status
+|}
+!! html
+<table>
+<tr>
+<th class="awesome"> status
+</th></tr></table>
+
+!!end
+
+!! test
+DIV IN UPPERCASE
+!! wikitext
+<DIV ID="x">HTML ROCKS</DIV>
+!! html
+<div id="x">HTML ROCKS</div>
+
+!!end
+
+!! test
+Non-ASCII pseudo-tags are rendered as text
+!! wikitext
+<khyô>
+!! html
+<p>&lt;khyô&gt;
+</p>
+!! end
+
+!! test
+Pseudo-tag with URL 'name' renders as url link
+!! wikitext
+<http://example.com/>
+!! html
+<p>&lt;<a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a>&gt;
+</p>
+!! end
+
+!! test
+text with amp in the middle of nowhere
+!! wikitext
+Remember AT&T?
+!! html
+<p>Remember AT&amp;T?
+</p>
+!! end
+
+!! test
+text with character entity: eacute
+!! wikitext
+I always thought &eacute; was a cute letter.
+!! html
+<p>I always thought &#233; was a cute letter.
+</p>
+!! html+tidy
+<p>I always thought é was a cute letter.</p>
+!! end
+
+!! test
+text with entity-escaped character entity-like string: eacute
+!! wikitext
+I always thought &amp;eacute; was a cute letter.
+!! html
+<p>I always thought &amp;eacute; was a cute letter.
+</p>
+!! end
+
+!! test
+text with undefined character entity: xacute
+!! wikitext
+I always thought &xacute; was a cute letter.
+!! html
+<p>I always thought &amp;xacute; was a cute letter.
+</p>
+!! end
+
+# TODO: generalize to PHP parser?
+!! test
+HTML5 tags
+!! options
+parsoid
+!! wikitext
+<data value="5">five</data>
+<time datetime="2000-01-01T00:00Z">The new millenium started</time>
+<mark>This highlighted text</mark>
+!! html
+<p><data value="5">five</data>
+<time datetime="2000-01-01T00:00Z">The new millenium started</time>
+<mark>This highlighted text</mark></p>
+!! end
+
+!! test
+HTML tag with leading space is parsed as text
+!! wikitext
+< div>foo< /div>
+!! html
+<p>&lt; div&gt;foo&lt; /div&gt;
+</p>
+!! end
+
+###
+### Nesting tests (see bug 41545, 50604, 51081)
+###
+
+# This test case is fixed in Parsoid by domino 1.0.12. (bug 50604)
+# Note that html2wt is considerably more difficult if we use <b> in
+# the test case, instead of <big>
+!! test
+Ensure that HTML adoption agency algorithm is properly implemented.
+!! wikitext
+<big>X<big>Y</big>Z</big>
+!! html
+<p><big>X<big>Y</big>Z</big>
+</p>
+!! end
+
+# This was bug 41545 in the PHP parser.
+# Note that tidy doesn't handle this correctly.
+!! test
+Nesting of <kbd>
+!! wikitext
+<kbd>X<kbd>Y</kbd>Z</kbd>
+!! html
+<p><kbd>X<kbd>Y</kbd>Z</kbd>
+</p>
+!! end
+
+# The following cases were bug 51081 in the PHP parser.
+# Note that there are some other nestable tags (b, i, etc) which are
+# not covered; see bug 51081 for discussion.
+
+# Note that tidy doesn't handle this correctly.
+!! test
+Nesting of <em>
+!! wikitext
+<em>X<em>Y</em>Z</em>
+!! html
+<p><em>X<em>Y</em>Z</em>
+</p>
+!! end
+
+# Note that tidy doesn't handle this correctly.
+!! test
+Nesting of <strong>
+!! wikitext
+<strong>X<strong>Y</strong>Z</strong>
+!! html
+<p><strong>X<strong>Y</strong>Z</strong>
+</p>
+!! end
+
+!! test
+Nesting of <q>
+!! wikitext
+<q>X<q>Y</q>Z</q>
+!! html+tidy
+<p><q>X<q>Y</q>Z</q></p>
+!! end
+
+# Note that tidy doesn't handle this correctly.
+!! test
+Nesting of <ruby>
+!! wikitext
+<ruby>X<ruby>Y</ruby>Z</ruby>
+!! html
+<p><ruby>X<ruby>Y</ruby>Z</ruby>
+</p>
+!! end
+
+# Note that tidy doesn't handle this correctly.
+!! test
+Nesting of <bdo>
+!! wikitext
+<bdo>X<bdo>Y</bdo>Z</bdo>
+!! html
+<p><bdo>X<bdo>Y</bdo>Z</bdo>
+</p>
+!! end
+
+
+###
+### Media links
+###
+
+!! test
+Media link
+!! wikitext
+[[Media:Foobar.jpg]]
+!! html
+<p><a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">Media:Foobar.jpg</a>
+</p>
+!! end
+
+!! test
+Media link with text
+!! wikitext
+[[Media:Foobar.jpg|A neat file to look at]]
+!! html
+<p><a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">A neat file to look at</a>
+</p>
+!! end
+
+# FIXME: this is still bad HTML tag nesting
+!! test
+Media link with nasty text
+fixme: doBlockLevels won't wrap this in a paragraph because it contains a div
+!! wikitext
+[[Media:Foobar.jpg|Safe Link<div style=display:none>" onmouseover="alert(document.cookie)" onfoo="</div>]]
+!! html
+<a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">Safe Link&lt;div style="display:none"&gt;" onmouseover="alert(document.cookie)" onfoo="&lt;/div&gt;</a>
+
+!! html+tidy
+<p><a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">Safe Link&lt;div style="display:none"&gt;" onmouseover="alert(document.cookie)" onfoo="&lt;/div&gt;</a></p>
+!! end
+
+!! test
+Media link to nonexistent file (bug 1702)
+!! wikitext
+[[Media:No such.jpg]]
+!! html
+<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=No_such.jpg" class="new" title="No such.jpg">Media:No such.jpg</a>
+</p>
+!! end
+
+!! test
+Image link to nonexistent file (bug 1850 - good)
+!! wikitext
+[[Image:No such.jpg]]
+!! html
+<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=No_such.jpg" class="new" title="File:No such.jpg">File:No such.jpg</a>
+</p>
+!! end
+
+!! test
+:Image link to nonexistent file (bug 1850 - bad)
+!! wikitext
+[[:Image:No such.jpg]]
+!! html
+<p><a href="/index.php?title=File:No_such.jpg&amp;action=edit&amp;redlink=1" class="new" title="File:No such.jpg (page does not exist)">Image:No such.jpg</a>
+</p>
+!! end
+
+
+
+!! test
+Character reference normalization in link text (bug 1938)
+!! wikitext
+[[Main Page|this&that]]
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">this&amp;that</a>
+</p>
+!!end
+
+!! article
+אַ
+!! text
+Test for unicode normalization
+
+The page's name is U+05d0 U+05b7, with non-canonical form U+FB2E
+!! endarticle
+
+!! test
+(bug 19451) Links should refer to the normalized form.
+!! wikitext
+[[&#xFB2E;]]
+[[&#x5d0;&#x5b7;]]
+[[&#x5d0;ַ]]
+[[א&#x5b7;]]
+[[אַ]]
+!! html
+<p><a href="/wiki/%D7%90%D6%B7" title="אַ">&#xfb2e;</a>
+<a href="/wiki/%D7%90%D6%B7" title="אַ">&#x5d0;&#x5b7;</a>
+<a href="/wiki/%D7%90%D6%B7" title="אַ">&#x5d0;ַ</a>
+<a href="/wiki/%D7%90%D6%B7" title="אַ">א&#x5b7;</a>
+<a href="/wiki/%D7%90%D6%B7" title="אַ">אַ</a>
+</p>
+!! end
+
+!! test
+Empty attribute crash test (bug 2067)
+!! wikitext
+<font color="">foo</font>
+!! html
+<p><font color="">foo</font>
+</p>
+!! end
+
+!! test
+Empty attribute crash test single-quotes (bug 2067)
+!! wikitext
+<font color=''>foo</font>
+!! html
+<p><font color="">foo</font>
+</p>
+!! end
+
+!! test
+Attribute test: equals, then nothing
+!! wikitext
+<font color=>foo</font>
+!! html
+<p><font>foo</font>
+</p>
+!! end
+
+!! test
+Attribute test: unquoted value
+!! wikitext
+<font color=x>foo</font>
+!! html
+<p><font color="x">foo</font>
+</p>
+!! end
+
+!! test
+Attribute test: unquoted but illegal value (hash)
+!! wikitext
+<font color=#x>foo</font>
+!! html
+<p><font color="#x">foo</font>
+</p>
+!! end
+
+!! test
+Attribute test: no value
+!! wikitext
+<font color>foo</font>
+!! html
+<p><font color="color">foo</font>
+</p>
+!! end
+
+!! test
+Bug 2095: link with three closing brackets
+!! wikitext
+[[Main Page]]]
+!! html/php
+<p><a href="/wiki/Main_Page" title="Main Page">Main Page</a>]
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a>]</p>
+!! end
+
+!! test
+Bug 2095: link with pipe and three closing brackets
+!! wikitext
+[[Main Page|link]]]
+!! html/php
+<p><a href="/wiki/Main_Page" title="Main Page">link</a>]
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Main_Page" title="Main Page">link</a>]</p>
+!! end
+
+!! test
+Bug 2095: link with pipe and three closing brackets, version 2
+!! wikitext
+[[Main Page|[http://example.com/]]]
+!! html/php
+<p><a href="/wiki/Main_Page" title="Main Page">[http://example.com/]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Main_Page" title="Main Page">[http://example.com/]</a></p>
+!! end
+
+
+###
+### Safety
+###
+
+!! article
+Template:Dangerous attribute
+!! text
+" onmouseover="alert(document.cookie)
+!! endarticle
+
+!! article
+Template:Dangerous style attribute
+!! text
+border-size: expression(alert(document.cookie))
+!! endarticle
+
+!! article
+Template:Div style
+!! text
+<div style="float: right; {{{1}}}">Magic div</div>
+!! endarticle
+
+!! test
+Bug 2304: HTML attribute safety (safe template; regression bug 2309)
+!! wikitext
+<div title="{{test}}"></div>
+!! html
+<div title="This is a test template"></div>
+
+!! end
+
+# Parsoid has enough context to handle this case
+!! test
+Bug 2304: HTML attribute safety (dangerous template; 2309)
+!! wikitext
+<div title="{{dangerous attribute}}"></div>
+!! html/php
+<div title=""></div>
+
+!! html/parsoid
+<div title='" onmouseover="alert(document.cookie)' about="#mwt2" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"title"},{"html":"&lt;span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-mw=\"{&amp;quot;parts&amp;quot;:[{&amp;quot;template&amp;quot;:{&amp;quot;target&amp;quot;:{&amp;quot;wt&amp;quot;:&amp;quot;dangerous attribute&amp;quot;,&amp;quot;href&amp;quot;:&amp;quot;./Template:Dangerous_attribute&amp;quot;},&amp;quot;params&amp;quot;:{},&amp;quot;i&amp;quot;:0}}]}\" data-parsoid=\"{&amp;quot;pi&amp;quot;:[[]],&amp;quot;dsr&amp;quot;:[12,35,null,null]}\">\" onmouseover=\"alert(document.cookie)&lt;/span>"}]]}' data-parsoid='{"stx":"html","a":{"title":"\" onmouseover=\"alert(document.cookie)"},"sa":{"title":"{{dangerous attribute}}"}}'></div>
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (dangerous style template; 2309)
+!! wikitext
+<div style="{{dangerous style attribute}}"></div>
+!! html
+<div style="/* insecure input */"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (safe parameter; 2309)
+!! wikitext
+{{div style|width: 200px}}
+!! html
+<div style="float: right; width: 200px">Magic div</div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (unsafe parameter; 2309)
+!! wikitext
+{{div style|width: expression(alert(document.cookie))}}
+!! html
+<div style="/* insecure input */">Magic div</div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (unsafe breakout parameter; 2309)
+!! wikitext
+{{div style|"><script>alert(document.cookie)</script>}}
+!! html
+<div style="float: right;">&lt;script&gt;alert(document.cookie)&lt;/script&gt;"&gt;Magic div</div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (unsafe breakout parameter 2; 2309)
+!! wikitext
+{{div style|" ><script>alert(document.cookie)</script>}}
+!! html
+<div style="float: right;">&lt;script&gt;alert(document.cookie)&lt;/script&gt;"&gt;Magic div</div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (link)
+!! wikitext
+<div title="[[Main Page]]"></div>
+!! html
+<div title="&#91;&#91;Main Page]]"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (italics)
+!! wikitext
+<div title="''foobar''"></div>
+!! html
+<div title="&#39;&#39;foobar&#39;&#39;"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (bold)
+!! wikitext
+<div title="'''foobar'''"></div>
+!! html
+<div title="&#39;&#39;&#39;foobar&#39;&#39;&#39;"></div>
+
+!! end
+
+
+!! test
+Bug 2304: HTML attribute safety (ISBN)
+!! wikitext
+<div title="ISBN 1234567890"></div>
+!! html
+<div title="&#73;SBN 1234567890"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (RFC)
+!! wikitext
+<div title="RFC 1234"></div>
+!! html
+<div title="&#82;FC 1234"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (PMID)
+!! wikitext
+<div title="PMID 1234567890"></div>
+!! html
+<div title="&#80;MID 1234567890"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (web link)
+!! wikitext
+<div title="http://example.com/"></div>
+!! html
+<div title="http&#58;//example.com/"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (named web link)
+!! wikitext
+<div title="[http://example.com/ link]"></div>
+!! html
+<div title="&#91;http&#58;//example.com/ link]"></div>
+
+!! end
+
+!! test
+Bug 3244: HTML attribute safety (extension; safe)
+!! wikitext
+<div style="<nowiki>background:blue</nowiki>"></div>
+!! html
+<div style="background:blue"></div>
+
+!! end
+
+!! test
+Bug 3244: HTML attribute safety (extension; unsafe)
+!! wikitext
+<div style="<nowiki>border-left:expression(alert(document.cookie))</nowiki>"></div>
+!! html
+<div style="/* insecure input */"></div>
+
+!! end
+
+# More MSIE fun discovered by Tom Gilder
+
+!! test
+MSIE CSS safety test: spurious slash
+!! wikitext
+<div style="background-image:u\rl(javascript:alert('boo'))">evil</div>
+!! html
+<div style="/* insecure input */">evil</div>
+
+!! end
+
+!! test
+MSIE CSS safety test: hex code
+!! wikitext
+<div style="background-image:u\72l(javascript:alert('boo'))">evil</div>
+!! html
+<div style="/* insecure input */">evil</div>
+
+!! end
+
+!! test
+MSIE CSS safety test: comment in url
+!! wikitext
+<div style="background-image:u/**/rl(javascript:alert('boo'))">evil</div>
+!! html
+<div style="background-image:u rl(javascript:alert(&#39;boo&#39;))">evil</div>
+
+!! end
+
+!! test
+MSIE CSS safety test: comment in expression
+!! wikitext
+<div style="background-image:expres/**/sion(alert('boo4'))">evil4</div>
+!! html
+<div style="background-image:expres sion(alert(&#39;boo4&#39;))">evil4</div>
+
+!! end
+
+!! test
+CSS safety test (all browsers): vertical tab (bug 55332 / CVE-2013-4567)
+!! wikitext
+<p style="font-size: 100px; background-image:url\b(https://www.google.com/images/srpr/logo6w.png)">A</p>
+!! html
+<p style="/* invalid control char */">A</p>
+
+!! end
+
+!! test
+MSIE 6 CSS safety test: Fullwidth (bug 55332)
+!! wikitext
+<p style="font-size: 100px; color: expression((title='XSSed'),'red')">A</p>
+<div style="top:EXPRESSION(alert())">B</div>
+!! html
+<p style="/* insecure input */">A</p>
+<div style="/* insecure input */">B</div>
+
+!! end
+
+!! test
+MSIE 6 CSS safety test: IPA extensions (bug 55332)
+!! wikitext
+<div style="background-image:uʀʟ(javascript:alert())">A</div>
+<p style="font-size: 100px; color: expʀessɪoɴ((title='XSSed'),'red')">B</p>
+!! html
+<div style="/* insecure input */">A</div>
+<p style="/* insecure input */">B</p>
+
+!! end
+
+!! test
+MSIE 6 CSS safety test: sup/sub script (bug 55332)
+!! wikitext
+<div style="background-image:url⁽javascript:alert())">A</div>
+<div style="background-image:url₍javascript:alert())">B</div>
+<p style="font-size: 100px; color: expressioⁿ((title='XSSed'),'red')">C</p>
+!! html
+<div style="/* insecure input */">A</div>
+<div style="/* insecure input */">B</div>
+<p style="/* insecure input */">C</p>
+
+!! end
+
+!! test
+Opera -o-link CSS
+!! wikitext
+<div
+title="&#100;&#97;&#116;&#97;&#58;&#116;&#101;&#120;&#116;&#47;&#104;&#116;&#109;&#108;&#44;&#60;&#105;&#109;&#103;&#32;&#115;&#114;&#99;&#61;&#49;&#32;&#111;&#110;&#101;&#114;&#114;&#111;&#114;&#61;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;&#62;"
+style="-o-link:attr(title);-o-link-source:current">X</div>
+!! html
+<div title="data:text/html,&lt;img src=1 onerror=alert(1)&gt;" style="/* insecure input */">X</div>
+
+!! end
+
+!! test
+MSIE 6 CSS safety test: Repetition markers (bug 55332)
+!! wikitext
+<p style="font-size: 100px; color: expres〱ion((title='XSSed'),'red')">A</p>
+<p style="font-size: 100px; color: expresゝion((title='XSSed'),'red')">B</p>
+<p style="font-size: 100px; color: expresーion((title='XSSed'),'red')">C</p>
+<p style="font-size: 100px; color: expresヽion((title='XSSed'),'red')">D</p>
+<p style="font-size: 100px; color: expresﹽion((title='XSSed'),'red')">E</p>
+<p style="font-size: 100px; color: expresﹼion((title='XSSed'),'red')">F</p>
+<p style="font-size: 100px; color: expresーion((title='XSSed'),'red')">G</p>
+!! html
+<p style="/* insecure input */">A</p>
+<p style="/* insecure input */">B</p>
+<p style="/* insecure input */">C</p>
+<p style="/* insecure input */">D</p>
+<p style="/* insecure input */">E</p>
+<p style="/* insecure input */">F</p>
+<p style="/* insecure input */">G</p>
+
+!! end
+
+!! test
+Table attribute legitimate extension
+!! wikitext
+{|
+!+ style="<nowiki>color:blue</nowiki>"| status
+|}
+!! html
+<table>
+<tr>
+<th style="color:blue"> status
+</th></tr></table>
+
+!!end
+
+!! test
+Table attribute safety
+!! wikitext
+{|
+!+ style="<nowiki>border-width:expression(0+alert(document.cookie))</nowiki>"| status
+|}
+!! html
+<table>
+<tr>
+<th style="/* insecure input */"> status
+</th></tr></table>
+
+!! end
+
+!! test
+CSS line continuation 1
+!! wikitext
+<div style="background-image: u\&#10;rl(test.jpg);"></div>
+!! html
+<div style="/* insecure input */"></div>
+
+!! end
+
+!! test
+CSS line continuation 2
+!! wikitext
+<div style="background-image: u\&#13;rl(test.jpg); "></div>
+!! html
+<div style="/* insecure input */"></div>
+
+!! end
+
+!! article
+Template:Identity
+!! text
+{{{1}}}
+!! endarticle
+
+!! test
+Expansion of multi-line templates in attribute values (bug 6255)
+!! wikitext
+<div style="background: {{identity|#00FF00}}">-</div>
+!! html
+<div style="background: #00FF00">-</div>
+
+!! end
+
+
+!! test
+Expansion of multi-line templates in attribute values (bug 6255 sanity check)
+!! wikitext
+<div style="background:
+#00FF00">-</div>
+!! html
+<div style="background: #00FF00">-</div>
+
+!! end
+
+!! test
+Expansion of multi-line templates in attribute values (bug 6255 sanity check 2)
+!! wikitext
+<div style="background: &#10;#00FF00">-</div>
+!! html
+<div style="background: &#10;#00FF00">-</div>
+
+!! end
+
+!! test
+evil <math>-wiki-tags without Extension:Math enabled
+!! wikitext
+<math><img src="some evil external link"><script>some_evil_javascript();</script></math>
+!! html+tidy
+<p>&lt;math&gt;&lt;img src="some evil external link"&gt;&lt;script&gt;some_evil_javascript();&lt;/script&gt;&lt;/math&gt;</p>
+!! end
+
+###
+### Parser hooks (see tests/parser/parserTestsParserHook.php for the <tag> extension)
+###
+!! test
+Parser hook: empty input
+!! wikitext
+<tag></tag>
+!! html
+<pre>
+''
+array (
+)
+</pre>
+
+!! end
+
+!! test
+Parser hook: empty input using terminated empty elements
+!! wikitext
+<tag/>
+!! html
+<pre>
+NULL
+array (
+)
+</pre>
+
+!! end
+
+!! test
+Parser hook: empty input using terminated empty elements (space before)
+!! wikitext
+<tag />
+!! html
+<pre>
+NULL
+array (
+)
+</pre>
+
+!! end
+
+!! test
+Parser hook: basic input
+!! wikitext
+<tag>input</tag>
+!! html
+<pre>
+'input'
+array (
+)
+</pre>
+
+!! end
+
+
+!! test
+Parser hook: case insensitive
+!! wikitext
+<TAG>input</TAG>
+!! html
+<pre>
+'input'
+array (
+)
+</pre>
+
+!! end
+
+
+!! test
+Parser hook: case insensitive, redux
+!! wikitext
+<TaG>input</TAg>
+!! html
+<pre>
+'input'
+array (
+)
+</pre>
+
+!! end
+
+!! test
+Parser hook: nested tags
+!! options
+noxml
+!! wikitext
+<tag><tag></tag></tag>
+!! html
+<pre>
+'<tag>'
+array (
+)
+</pre>&lt;/tag&gt;
+
+!! end
+
+!! test
+Parser hook: basic arguments
+!! wikitext
+<tag width=200 height = "100" depth = '50' square></tag>
+!! html
+<pre>
+''
+array (
+ 'width' => '200',
+ 'height' => '100',
+ 'depth' => '50',
+ 'square' => 'square',
+)
+</pre>
+
+!! end
+
+!! test
+Parser hook: argument containing a forward slash (bug 5344)
+!! wikitext
+<tag filename='/tmp/bla'></tag>
+!! html
+<pre>
+''
+array (
+ 'filename' => '/tmp/bla',
+)
+</pre>
+
+!! end
+
+!! test
+Parser hook: empty input using terminated empty elements (bug 2374)
+!! wikitext
+<tag foo=bar/>text
+!! html
+<pre>
+NULL
+array (
+ 'foo' => 'bar',
+)
+</pre>text
+
+!! end
+
+# </tag> should be output literally since there is no matching tag that begins it
+!! test
+Parser hook: basic arguments using terminated empty elements (bug 2374)
+!! wikitext
+<tag width=200 height = "100" depth = '50' square/>
+other stuff
+</tag>
+!! html
+<pre>
+NULL
+array (
+ 'width' => '200',
+ 'height' => '100',
+ 'depth' => '50',
+ 'square' => 'square',
+)
+</pre>
+<p>other stuff
+&lt;/tag&gt;
+</p>
+!! end
+
+###
+### (see tests/parser/parserTestsParserHook.php for the <statictag> extension)
+###
+
+!! test
+Parser hook: static parser hook not inside a comment
+!! wikitext
+<statictag>hello, world</statictag>
+<statictag action=flush/>
+!! html
+<p>hello, world
+</p>
+!! end
+
+
+!! test
+Parser hook: static parser hook inside a comment
+!! wikitext
+<!-- <statictag>hello, world</statictag> -->
+<statictag action=flush/>
+!! html
+<p><br />
+</p>
+!! end
+
+# Nested template calls; this case was broken by Parser.php rev 1.506,
+# since reverted.
+
+!! article
+Template:One-parameter
+!! text
+(My parameter is: {{{1}}})
+!! endarticle
+
+!! article
+Template:Map-one-parameter
+!! text
+{{{{{1}}}|{{{2}}}}}
+!! endarticle
+
+!! test
+Nested template calls
+!! wikitext
+{{Map-one-parameter|One-parameter|param}}
+!! html
+<p>(My parameter is: param)
+</p>
+!! end
+
+
+###
+### Sanitizer
+###
+!! test
+Sanitizer: Closing of open tags
+!! wikitext
+<s></s><table></table>
+!! html
+<s></s><table></table>
+
+!! end
+
+!! test
+Sanitizer: Closing of open but not closed tags
+!! wikitext
+<s>foo
+!! html
+<p><s>foo</s>
+</p>
+!! end
+
+!! test
+Sanitizer: Closing of closed but not open tags
+!! wikitext
+</s>
+!! html
+<p>&lt;/s&gt;
+</p>
+!! end
+
+!! test
+Sanitizer: Closing of closed but not open table tags
+!! wikitext
+Table not started</td></tr></table>
+!! html
+<p>Table not started&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
+</p>
+!! end
+
+!! test
+Sanitizer: Escaping of spaces, multibyte characters, colons & other stuff in id=""
+!! wikitext
+<span id="æ: v">byte</span>[[#æ: v|backlink]]
+!! html
+<p><span id=".C3.A6:_v">byte</span><a href="#.C3.A6:_v">backlink</a>
+</p>
+!! end
+
+!! test
+Sanitizer: Validating the contents of the id attribute (bug 4515)
+!! options
+disabled
+!! wikitext
+<br id=9 />
+!! html
+Something, but definitely not <br id="9" />...
+!! end
+
+!! test
+Sanitizer: Validating id attribute uniqueness (bug 4515, bug 6301)
+!! options
+disabled
+!! wikitext
+<br id="foo" /><br id="foo" />
+!! html
+Something need to be done. foo-2 ?
+!! end
+
+!! test
+Sanitizer: Validating that <meta> and <link> work, but only for Microdata
+!! wikitext
+<div itemscope>
+ <meta itemprop="hello" content="world">
+ <meta http-equiv="refresh" content="5">
+ <meta itemprop="hello" http-equiv="refresh" content="5">
+ <link itemprop="hello" href="{{SERVER}}">
+ <link rel="stylesheet" href="{{SERVER}}">
+ <link rel="stylesheet" itemprop="hello" href="{{SERVER}}">
+</div>
+!! html
+<div itemscope="itemscope">
+<p> <meta itemprop="hello" content="world" />
+ &lt;meta http-equiv="refresh" content="5"&gt;
+ <meta itemprop="hello" content="5" />
+</p>
+ <link itemprop="hello" href="http&#58;//example.org" />
+ &lt;link rel="stylesheet" href="<a rel="nofollow" class="external free" href="http://example.org">http://example.org</a>"&gt;
+ <link itemprop="hello" href="http&#58;//example.org" />
+</div>
+
+!! end
+
+!! test
+Language converter: output gets cut off unexpectedly (bug 5757)
+!! options
+language=zh
+!! wikitext
+this bit is safe: }-
+
+but if we add a conversion instance: -{zh-cn:xxx;zh-tw:yyy}-
+
+then we get cut off here: }-
+
+all additional text is vanished
+!! html
+<p>this bit is safe: }-
+</p><p>but if we add a conversion instance: xxx
+</p><p>then we get cut off here: }-
+</p><p>all additional text is vanished
+</p>
+!! end
+
+!! test
+Self closed html pairs (bug 5487)
+!! options
+!! wikitext
+<center><font id="bug" />Centered text</center>
+<div><font id="bug2" />In div text</div>
+!! html
+<center>&lt;font id="bug" /&gt;Centered text</center>
+<div>&lt;font id="bug2" /&gt;In div text</div>
+
+!! end
+
+#
+#
+#
+
+!! test
+Punctuation: nbsp before exclamation
+!! wikitext
+C'est grave !
+!! html
+<p>C'est grave&#160;!
+</p>
+!! end
+
+!! test
+Punctuation: CSS !important (bug 11874)
+!! wikitext
+<div style="width:50% !important">important</div>
+!! html
+<div style="width:50% !important">important</div>
+
+!!end
+
+!! test
+Punctuation: CSS ! important (bug 11874; with space after)
+!! wikitext
+<div style="width:50% ! important">important</div>
+!! html
+<div style="width:50% ! important">important</div>
+
+!!end
+
+
+!! test
+HTML bullet list, closed tags (bug 5497)
+!! wikitext
+<ul>
+<li>One</li>
+<li>Two</li>
+</ul>
+!! html
+<ul>
+<li>One</li>
+<li>Two</li>
+</ul>
+
+!! end
+
+!! test
+HTML bullet list, unclosed tags (bug 5497)
+!! options
+disabled
+!! wikitext
+<ul>
+<li>One
+<li>Two
+</ul>
+!! html
+<ul>
+<li>One
+</li>
+<li>Two
+</li>
+</ul>
+
+!! end
+
+!! test
+HTML ordered list, closed tags (bug 5497)
+!! wikitext
+<ol>
+<li>One</li>
+<li>Two</li>
+</ol>
+!! html
+<ol>
+<li>One</li>
+<li>Two</li>
+</ol>
+
+!! end
+
+!! test
+HTML ordered list, unclosed tags (bug 5497)
+!! options
+disabled
+!! wikitext
+<ol>
+<li>One
+<li>Two
+</ol>
+!! html
+<ol>
+<li>One
+</li>
+<li>Two
+</li>
+</ol>
+
+!! end
+
+!! test
+HTML nested bullet list, closed tags (bug 5497)
+!! wikitext
+<ul>
+<li>One</li>
+<li>Two:
+<ul>
+<li>Sub-one</li>
+<li>Sub-two</li>
+</ul>
+</li>
+</ul>
+!! html
+<ul>
+<li>One</li>
+<li>Two:
+<ul>
+<li>Sub-one</li>
+<li>Sub-two</li>
+</ul>
+</li>
+</ul>
+
+!! end
+
+!! test
+HTML nested bullet list, open tags (bug 5497)
+!! options
+disabled
+!! wikitext
+<ul>
+<li>One
+<li>Two:
+<ul>
+<li>Sub-one
+<li>Sub-two
+</ul>
+</ul>
+!! html
+<ul>
+<li>One
+</li>
+<li>Two:
+<ul>
+<li>Sub-one
+</li>
+<li>Sub-two
+</li>
+</ul>
+</li>
+</ul>
+
+!! end
+
+!! test
+HTML nested ordered list, closed tags (bug 5497)
+!! wikitext
+<ol>
+<li>One</li>
+<li>Two:
+<ol>
+<li>Sub-one</li>
+<li>Sub-two</li>
+</ol>
+</li>
+</ol>
+!! html
+<ol>
+<li>One</li>
+<li>Two:
+<ol>
+<li>Sub-one</li>
+<li>Sub-two</li>
+</ol>
+</li>
+</ol>
+
+!! end
+
+!! test
+HTML nested ordered list, open tags (bug 5497)
+!! options
+disabled
+!! wikitext
+<ol>
+<li>One
+<li>Two:
+<ol>
+<li>Sub-one
+<li>Sub-two
+</ol>
+</ol>
+!! html
+<ol>
+<li>One
+</li>
+<li>Two:
+<ol>
+<li>Sub-one
+</li>
+<li>Sub-two
+</li>
+</ol>
+</li>
+</ol>
+
+!! end
+
+!! test
+HTML ordered list item with parameters oddity
+!! wikitext
+<ol><li id="fragment">One</li>
+</ol>
+!! html
+<ol><li id="fragment">One</li>
+</ol>
+
+!! end
+
+# parsoid doesn't explicitly mark autonumbered links, see bug 53505
+!!test
+bug 5918: autonumbering
+!! wikitext
+[http://first/] [http://second] [ftp://ftp]
+
+ftp://inlineftp
+
+[mailto:enclosed@mail.tld With target]
+
+[mailto:enclosed@mail.tld]
+
+mailto:inline@mail.tld
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://first/">[1]</a> <a rel="nofollow" class="external autonumber" href="http://second">[2]</a> <a rel="nofollow" class="external autonumber" href="ftp://ftp">[3]</a>
+</p><p><a rel="nofollow" class="external free" href="ftp://inlineftp">ftp://inlineftp</a>
+</p><p><a rel="nofollow" class="external text" href="mailto:enclosed@mail.tld">With target</a>
+</p><p><a rel="nofollow" class="external autonumber" href="mailto:enclosed@mail.tld">[4]</a>
+</p><p><a rel="nofollow" class="external free" href="mailto:inline@mail.tld">mailto:inline@mail.tld</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://first/"></a> <a rel="mw:ExtLink" href="http://second"></a> <a rel="mw:ExtLink" href="ftp://ftp"></a></p>
+<p><a rel="mw:ExtLink" href="ftp://inlineftp">ftp://inlineftp</a></p>
+<p><a rel="mw:ExtLink" href="mailto:enclosed@mail.tld">With target</a></p>
+<p><a rel="mw:ExtLink" href="mailto:enclosed@mail.tld"></a></p>
+<p><a rel="mw:ExtLink" href="mailto:inline@mail.tld">mailto:inline@mail.tld</a></p>
+!! end
+
+
+#
+# Security and HTML correctness
+# From Nick Jenkins' fuzz testing
+#
+
+!! test
+Fuzz testing: Parser13
+!! wikitext
+{|
+| http://a|
+!! html
+<table>
+<tr>
+<td>
+</td>
+</tr>
+</table>
+
+!! end
+
+!! test
+Fuzz testing: Parser14
+!! wikitext
+== onmouseover= ==
+http://__TOC__
+!! html
+<h2><span class="mw-headline" id="onmouseover.3D">onmouseover=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: onmouseover=">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+http://<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#onmouseover.3D"><span class="tocnumber">1</span> <span class="toctext">onmouseover=</span></a></li>
+</ul>
+</div>
+
+
+!! html+tidy
+<h2><span class="mw-headline" id="onmouseover.3D">onmouseover=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: onmouseover=">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p>http://</p>
+<div id="toc" class="toc">
+<div id="toctitle">
+<h2>Contents</h2>
+</div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#onmouseover.3D"><span class="tocnumber">1</span> <span class="toctext">onmouseover=</span></a></li>
+</ul>
+</div>
+!! end
+
+!! test
+Fuzz testing: Parser14-table
+!! wikitext
+==a==
+{| STYLE=__TOC__
+!! html
+<h2><span class="mw-headline" id="a">a</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: a">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<table style="&#95;_TOC&#95;_">
+<tr><td></td></tr>
+</table>
+
+!! html+tidy
+<h2><span class="mw-headline" id="a">a</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: a">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<table style="__TOC__">
+<tr>
+<td></td>
+</tr>
+</table>
+!! end
+
+# Known to produce bogus xml (extra </td>)
+!! test
+Fuzz testing: Parser16
+!! options
+noxml
+!! wikitext
+{|
+!https://||||||
+!! html
+<table>
+<tr>
+<th>https://</th>
+<th></th>
+<th></th>
+<th>
+</td>
+</tr>
+</table>
+
+!! html+tidy
+<table>
+<tr>
+<th>https://</th>
+<th></th>
+<th></th>
+<th></th>
+</tr>
+</table>
+!! end
+
+!! test
+Fuzz testing: Parser21
+!! wikitext
+{|
+! irc://{{ftp://a" onmouseover="alert('hello world');"
+|
+!! html
+<table>
+<tr>
+<th> <a rel="nofollow" class="external free" href="irc://{{ftp://a">irc://{{ftp://a</a>" onmouseover="alert('hello world');"
+</th>
+<td>
+</td>
+</tr>
+</table>
+
+!! end
+
+!! test
+Fuzz testing: Parser22
+!! wikitext
+http://===r:::https://b
+
+{|
+!! html
+<p><a rel="nofollow" class="external free" href="http://===r:::https://b">http://===r:::https://b</a>
+</p>
+<table>
+<tr><td></td></tr>
+</table>
+
+!! end
+
+# Known to produce bad XML for now
+!! test
+Fuzz testing: Parser24
+!! options
+noxml
+!! wikitext
+{|
+{{{|
+<u CLASS=
+| {{{{SSSll!!!!!!!VVVV)]]][[Special:*xxxxxxx--><noinclude>}}}} >
+<br style="onmouseover='alert(document.cookie);' " />
+
+MOVE YOUR MOUSE CURSOR OVER THIS TEXT
+|
+!! html
+<table>
+{{{|
+<u class="&#124;">}}}} &gt;
+<br style="onmouseover=&#39;alert(document.cookie);&#39;" />
+
+MOVE YOUR MOUSE CURSOR OVER THIS TEXT
+<tr>
+<td></u>
+</td>
+</tr>
+</table>
+
+!! end
+
+# Note: the current result listed for this is not what the original one was,
+# but the original bug was JavaScript injection, which is fixed in any case.
+# It's not clear that the original result listed was any more correct than the
+# current one. Original result:
+# <p>{{{|
+# </p>
+# <li class="&#124;&#124;">
+# }}}blah" onmouseover="alert('hello world');" align="left"<b>MOVE MOUSE CURSOR OVER HERE</b>
+!!test
+Fuzz testing: Parser25 (bug 6055)
+!! wikitext
+{{{
+|
+<LI CLASS=||
+ >
+}}}blah" onmouseover="alert('hello world');" align="left"'''MOVE MOUSE CURSOR OVER HERE
+!! html
+<p>&lt;LI CLASS=blah" onmouseover="alert('hello world');" align="left"<b>MOVE MOUSE CURSOR OVER HERE</b>
+</p>
+!! end
+
+!!test
+Fuzz testing: URL adjacent extension (with space, clean)
+!! wikitext
+http://example.com <nowiki>junk</nowiki>
+!! html
+<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> junk
+</p>
+!!end
+
+!!test
+Fuzz testing: URL adjacent extension (no space, dirty; nowiki)
+!! wikitext
+http://example.com<nowiki>junk</nowiki>
+!! html
+<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>junk
+</p>
+!!end
+
+!!test
+Fuzz testing: URL adjacent extension (no space, dirty; pre)
+!! wikitext
+http://example.com<pre>junk</pre>
+!! html
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a><pre>junk</pre>
+
+!! html+tidy
+<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></p>
+<pre>
+junk
+</pre>
+!!end
+
+!!test
+Fuzz testing: image with bogus manual thumbnail
+!! wikitext
+[[Image:foobar.jpg|thumbnail= ]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;">Error creating thumbnail: <div class="thumbcaption"></div></div></div>
+
+!! html/parsoid
+<meta typeof="mw:Placeholder" data-parsoid='{"src":"[[Image:foobar.jpg|thumbnail= ]]","optList":[{"ck":"manualthumb","ak":"thumbnail= "}],"dsr":[0,32,null,null]}'/>
+!!end
+
+!! test
+Fuzz testing: encoded newline in generated HTML replacements (bug 6577)
+!! wikitext
+<pre dir="&#10;"></pre>
+!! html
+<pre dir="&#10;"></pre>
+
+!! end
+
+!! test
+Parsing optional HTML elements (Bug 6171)
+!! options
+!! wikitext
+<table>
+ <tr>
+ <td> Some tabular data</td>
+ <td> More tabular data ...
+ <td> And yet som tabular data</td>
+ </tr>
+</table>
+!! html
+<table>
+ <tr>
+ <td> Some tabular data</td>
+ <td> More tabular data ...
+ </td><td> And yet som tabular data</td>
+ </tr>
+</table>
+
+!! end
+
+!! test
+Correct handling of <td>, <tr> (Bug 6171)
+!! options
+!! wikitext
+<table>
+ <tr>
+ <td> Some tabular data</td>
+ <td> More tabular data ...</td>
+ <td> And yet som tabular data</td>
+ </tr>
+</table>
+!! html
+<table>
+ <tr>
+ <td> Some tabular data</td>
+ <td> More tabular data ...</td>
+ <td> And yet som tabular data</td>
+ </tr>
+</table>
+
+!! end
+
+
+!! test
+Parsing crashing regression (fr:JavaScript)
+!! wikitext
+</body></x>
+!! html
+<p>&lt;/body&gt;&lt;/x&gt;
+</p>
+!! end
+
+!! test
+Inline wiki vs wiki block nesting
+!! wikitext
+'''Bold paragraph
+
+New wiki paragraph
+!! html
+<p><b>Bold paragraph</b>
+</p><p>New wiki paragraph
+</p>
+!! end
+
+!! test
+Inline HTML vs wiki block nesting
+!! options
+disabled
+!! wikitext
+<b>Bold paragraph
+
+New wiki paragraph
+!! html
+<p><b>Bold paragraph</b>
+</p><p>New wiki paragraph
+</p>
+!! end
+
+# Original result was this:
+# <p><b>bold</b><b>bold<i>bolditalics</i></b>
+# </p>
+# While that might be marginally more intuitive, maybe, the six-apostrophe
+# construct is clearly pathological and the result stated here (which is what
+# the parser actually does) is about as reasonable as anything.
+!!test
+Mixing markup for italics and bold
+!! options
+!! wikitext
+'''bold''''''bold''bolditalics'''''
+!! html
+<p>'<i>bold'</i><b>bold<i>bolditalics</i></b>
+</p>
+!! end
+
+
+!! article
+Xyzzyx
+!! text
+Article for special page transclusion test
+!! endarticle
+
+!! test
+Special page transclusion
+!! options
+!! wikitext
+{{Special:Prefixindex/Xyzzyx}}
+!! html
+<table class="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx" title="Xyzzyx">Xyzzyx</a></td></tr></table>
+
+!! end
+
+!! test
+Special page transclusion twice (bug 5021)
+!! options
+!! wikitext
+{{Special:Prefixindex/Xyzzyx}}
+{{Special:Prefixindex/Xyzzyx}}
+!! html
+<table class="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx" title="Xyzzyx">Xyzzyx</a></td></tr></table>
+<table class="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx" title="Xyzzyx">Xyzzyx</a></td></tr></table>
+
+!! end
+
+!! test
+Transclusion of default MediaWiki message
+!! wikitext
+{{MediaWiki:Mainpage}}
+!! html
+<p>Main Page
+</p>
+!! end
+
+!! test
+Transclusion of nonexistent MediaWiki message
+!! wikitext
+{{MediaWiki:Mainpagexxx}}
+!! html
+<p><a href="/index.php?title=MediaWiki:Mainpagexxx&amp;action=edit&amp;redlink=1" class="new" title="MediaWiki:Mainpagexxx (page does not exist)">MediaWiki:Mainpagexxx</a>
+</p>
+!! end
+
+!! test
+Transclusion of MediaWiki message with underscore
+!! wikitext
+{{MediaWiki:history_short}}
+!! html
+<p>History
+</p>
+!! end
+
+!! test
+Transclusion of MediaWiki message with space
+!! wikitext
+{{MediaWiki:history short}}
+!! html
+<p>History
+</p>
+!! end
+
+!! test
+Invalid header with following text
+!! wikitext
+= x = y
+!! html
+<p>= x = y
+</p>
+!! end
+
+
+!! test
+Section extraction test (section 0)
+!! options
+section=0
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+!! end
+
+!! test
+Section extraction test (section 1)
+!! options
+section=1
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+==a==
+===aa===
+====aaa====
+!! end
+
+!! test
+Section extraction test (section 2)
+!! options
+section=2
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+===aa===
+====aaa====
+!! end
+
+!! test
+Section extraction test (section 3)
+!! options
+section=3
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+====aaa====
+!! end
+
+!! test
+Section extraction test (section 4)
+!! options
+section=4
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+!! end
+
+!! test
+Section extraction test (section 5)
+!! options
+section=5
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+===ba===
+!! end
+
+!! test
+Section extraction test (section 6)
+!! options
+section=6
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+===bb===
+====bba====
+!! end
+
+!! test
+Section extraction test (section 7)
+!! options
+section=7
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+====bba====
+!! end
+
+!! test
+Section extraction test (section 8)
+!! options
+section=8
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+===bc===
+!! end
+
+!! test
+Section extraction test (section 9)
+!! options
+section=9
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+==c==
+===ca===
+!! end
+
+!! test
+Section extraction test (section 10)
+!! options
+section=10
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+===ca===
+!! end
+
+!! test
+Section extraction test (nonexistent section 11)
+!! options
+section=11
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+!! end
+
+!! test
+Section extraction test with bogus heading (section 1)
+!! options
+section=1
+!! wikitext
+==a==
+==bogus== not a legal section
+==b==
+!! html
+==a==
+==bogus== not a legal section
+!! end
+
+!! test
+Section extraction test with bogus heading (section 2)
+!! options
+section=2
+!! wikitext
+==a==
+==bogus== not a legal section
+==b==
+!! html
+==b==
+!! end
+
+!! test
+Section extraction test with comment after heading (section 1)
+!! options
+section=1
+!! wikitext
+==a==
+==b== <!-- -->
+==c==
+!! html
+==a==
+!! end
+
+!! test
+Section extraction test with comment after heading (section 2)
+!! options
+section=2
+!! wikitext
+==a==
+==b== <!-- -->
+==c==
+!! html
+==b== <!-- -->
+!! end
+
+!! test
+Section extraction test with bogus <nowiki> heading (section 1)
+!! options
+section=1
+!! wikitext
+==a==
+==bogus== <nowiki>not a legal section</nowiki>
+==b==
+!! html
+==a==
+==bogus== <nowiki>not a legal section</nowiki>
+!! end
+
+!! test
+Section extraction test with bogus <nowiki> heading (section 2)
+!! options
+section=2
+!! wikitext
+==a==
+==bogus== <nowiki>not a legal section</nowiki>
+==b==
+!! html
+==b==
+!! end
+
+
+# Formerly testing for bug 2587, now resolved by the use of unmarked sections
+# instead of respecting commented sections
+!! test
+Section extraction prefixed by comment (section 1)
+!! options
+section=1
+!! wikitext
+<!-- -->==sec1==
+==sec2==
+!! html
+==sec2==
+!!end
+
+!! test
+Section extraction prefixed by comment (section 2)
+!! options
+section=2
+!! wikitext
+<!-- -->==sec1==
+==sec2==
+!! html
+
+!!end
+
+
+# Formerly testing for bug 2607, now resolved by the use of unmarked sections
+# instead of respecting HTML-style headings
+!! test
+Section extraction, mixed wiki and html (section 1)
+!! options
+section=1
+!! wikitext
+<h2>unmarked</h2>
+unmarked
+==1==
+one
+==2==
+two
+!! html
+==1==
+one
+!! end
+
+!! test
+Section extraction, mixed wiki and html (section 2)
+!! options
+section=2
+!! wikitext
+<h2>unmarked</h2>
+unmarked
+==1==
+one
+==2==
+two
+!! html
+==2==
+two
+!! end
+
+
+# Formerly testing for bug 3342
+!! test
+Section extraction, heading surrounded by <noinclude>
+!! options
+section=1
+!! wikitext
+<noinclude>==unmarked==</noinclude>
+==marked==
+!! html
+==marked==
+!!end
+
+# Test behavior of bug 19910
+!! test
+Sectiion with all-equals
+!! options
+section=2
+!! wikitext
+===
+The line above must have a trailing space
+=== <!--
+--> <!-- -->
+But just in case it doesn't...
+!! html
+=== <!--
+--> <!-- -->
+But just in case it doesn't...
+!! end
+
+!! test
+Section replacement test (section 0)
+!! options
+replace=0,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+xxx
+
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 1)
+!! options
+replace=1,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+xxx
+
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 2)
+!! options
+replace=2,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+==a==
+xxx
+
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 3)
+!! options
+replace=3,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+==a==
+===aa===
+xxx
+
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 4)
+!! options
+replace=4,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+==a==
+===aa===
+====aaa====
+xxx
+
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 5)
+!! options
+replace=5,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+==a==
+===aa===
+====aaa====
+==b==
+xxx
+
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 6)
+!! options
+replace=6,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+xxx
+
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 7)
+!! options
+replace=7,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+xxx
+
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 8)
+!! options
+replace=8,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+xxx
+
+==c==
+===ca===
+!!end
+
+!! test
+Section replacement test (section 9)
+!! options
+replace=9,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+xxx
+!! end
+
+!! test
+Section replacement test (section 10)
+!! options
+replace=10,"xxx"
+!! wikitext
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! html
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+xxx
+!! end
+
+!! test
+Section replacement test with initial whitespace (bug 13728)
+!! options
+replace=2,"xxx"
+!! wikitext
+ Preformatted initial line
+==a==
+===a===
+!! html
+ Preformatted initial line
+==a==
+xxx
+!! end
+
+
+!! test
+Section extraction, heading followed by pre with 20 spaces (bug 6398)
+!! options
+section=1
+!! wikitext
+==a==
+ a
+!! html
+==a==
+ a
+!! end
+
+!! test
+Section extraction, heading followed by pre with 19 spaces (bug 6398 sanity check)
+!! options
+section=1
+!! wikitext
+==a==
+ a
+!! html
+==a==
+ a
+!! end
+
+
+!! test
+Section extraction, <pre> around bogus header (bug 10309)
+!! options
+noxml section=2
+!! wikitext
+== Section One ==
+<pre>
+=======
+</pre>
+
+== Section Two ==
+stuff
+!! html
+== Section Two ==
+stuff
+!! end
+
+!! test
+Section replacement, <pre> around bogus header (bug 10309)
+!! options
+noxml replace=2,"xxx"
+!! wikitext
+== Section One ==
+<pre>
+=======
+</pre>
+
+== Section Two ==
+stuff
+!! html
+== Section One ==
+<pre>
+=======
+</pre>
+
+xxx
+!! end
+
+
+
+!! test
+Handling of &#x0A; in URLs
+!! wikitext
+** irc://&#x0A;a
+!! html/php
+<ul><li><ul><li> <a rel="nofollow" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul></li></ul>
+
+!! html/parsoid
+<ul><li><ul><li> <a rel="mw:ExtLink" href="irc://
+a">irc://
+a</a></li></ul></li></ul>
+!! end
+
+!! test
+Handling of %0A in URLs
+!! wikitext
+** irc://%0Aa
+!! html/php
+<ul><li><ul><li> <a rel="nofollow" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul></li></ul>
+
+!! html/parsoid
+<ul><li><ul><li> <a rel="mw:ExtLink" href="irc://%0Aa">irc://%0Aa</a></li></ul></li></ul>
+!! end
+
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+5 quotes, code coverage +1 line
+!! options
+parsoid=wt2html
+!! wikitext
+'''''
+!! html/php
+!! html/parsoid
+<p><b><i></i></b></p>
+!! end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+# note that wt2html and html2html will put the <i> before the <b>
+!! test
+5 quotes, code coverage +1 line w/ nowiki (1)
+!! options
+parsoid=wt2wt,html2wt
+!! wikitext
+'''''<nowiki/>'''''
+!! html/php
+<p><i></i>
+</p>
+!! html/parsoid
+<p><b><i></i></b></p>
+!! end
+
+# same as previous, just swapping the <i> and <b>
+!! test
+5 quotes, code coverage +1 line w/ nowiki (2)
+!! wikitext
+'''''<nowiki/>'''''
+!! html/php
+<p><i></i>
+</p>
+!! html/parsoid
+<p><i><b></b></i></p>
+!! end
+
+!! test
+Special:Search page linking.
+!! wikitext
+{{Special:search}}
+!! html
+<p><a href="/wiki/Special:Search" title="Special:Search">Special:Search</a>
+</p>
+!! end
+
+!! test
+Say the magic word
+!! options
+title=[[Parser test]]
+!! wikitext
+* {{PAGENAME}}
+* {{PAGENAMEE}}
+* {{FULLPAGENAME}}
+* {{FULLPAGENAMEE}}
+* {{BASEPAGENAME}}
+* {{BASEPAGENAMEE}}
+* {{SUBPAGENAME}}
+* {{SUBPAGENAMEE}}
+* {{ROOTPAGENAME}}
+* {{ROOTPAGENAMEE}}
+* {{TALKPAGENAME}}
+* {{TALKPAGENAMEE}}
+* {{SUBJECTPAGENAME}}
+* {{SUBJECTPAGENAMEE}}
+* {{NAMESPACEE}}
+* {{NAMESPACE}}
+* {{NAMESPACENUMBER}}
+* {{TALKSPACE}}
+* {{TALKSPACEE}}
+* {{SUBJECTSPACE}}
+* {{SUBJECTSPACEE}}
+* {{Dynamic|{{NUMBEROFUSERS}}|{{NUMBEROFPAGES}}|{{CURRENTVERSION}}|{{CONTENTLANGUAGE}}|{{DIRECTIONMARK}}|{{CURRENTTIMESTAMP}}|{{NUMBEROFARTICLES}}}}
+!! html
+<ul><li> Parser test</li>
+<li> Parser_test</li>
+<li> Parser test</li>
+<li> Parser_test</li>
+<li> Parser test</li>
+<li> Parser_test</li>
+<li> Parser test</li>
+<li> Parser_test</li>
+<li> Parser test</li>
+<li> Parser_test</li>
+<li> Talk:Parser test</li>
+<li> Talk:Parser_test</li>
+<li> Parser test</li>
+<li> Parser_test</li>
+<li> </li>
+<li> </li>
+<li> 0</li>
+<li> Talk</li>
+<li> Talk</li>
+<li> </li>
+<li> </li>
+<li> <a href="/index.php?title=Template:Dynamic&amp;action=edit&amp;redlink=1" class="new" title="Template:Dynamic (page does not exist)">Template:Dynamic</a></li></ul>
+
+!! end
+### Note: Above tests excludes the "{{NUMBEROFADMINS}}" magic word because it generates a MySQL error when included.
+
+!! test
+Gallery
+!! wikitext
+<gallery>
+image1.png |
+image2.gif|||||
+
+image3|
+image4 |300px| centre
+ image5.svg| http://///////
+[[x|xx]]]]
+* image6
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">Image1.png</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">Image2.gif</div>
+ <div class="gallerytext">
+<p>||||
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">Image3</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">Image4</div>
+ <div class="gallerytext">
+<p>300px| centre
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">Image5.svg</div>
+ <div class="gallerytext">
+<p><a rel="nofollow" class="external free" href="http://///////">http://///////</a>
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">* image6</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!! test
+Gallery (with options)
+!! wikitext
+<gallery widths='70px' heights='40px' perrow='2' caption='Foo [[Main Page]]' >
+File:Nonexistant.jpg|caption
+File:Nonexistant.jpg
+image:foobar.jpg|some '''caption''' [[Main Page]]
+image:foobar.jpg
+image:foobar.jpg|Blabla|alt=This is a foo-bar.|blabla.
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional" style="max-width: 226px;_width: 226px;">
+ <li class='gallerycaption'>Foo <a href="/wiki/Main_Page" title="Main Page">Main Page</a></li>
+ <li class="gallerybox" style="width: 105px"><div style="width: 105px">
+ <div class="thumb" style="height: 70px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 105px"><div style="width: 105px">
+ <div class="thumb" style="height: 70px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 105px"><div style="width: 105px">
+ <div class="thumb" style="width: 100px;"><div style="margin:31px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" width="70" height="8" /></a></div></div>
+ <div class="gallerytext">
+<p>some <b>caption</b> <a href="/wiki/Main_Page" title="Main Page">Main Page</a>
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 105px"><div style="width: 105px">
+ <div class="thumb" style="width: 100px;"><div style="margin:31px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" width="70" height="8" /></a></div></div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 105px"><div style="width: 105px">
+ <div class="thumb" style="width: 100px;"><div style="margin:31px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="This is a foo-bar." src="http://example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" width="70" height="8" /></a></div></div>
+ <div class="gallerytext">
+<p>Blabla|blabla.
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!! test
+Gallery with link that has fragment
+!! wikitext
+<gallery>
+image:foobar.jpg|link=Main_Page
+image:foobar.jpg|link=Main_Page#section
+image:foobar.jpg|link=Main Page#section|caption
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/Main_Page"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/Main_Page#section"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/Main_Page#section"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!! test
+Gallery with wikitext inside caption
+!! wikitext
+<gallery>
+File:foobar.jpg|[[File:foobar.jpg|20px|desc|alt=inneralt]]|alt=galleryalt
+File:foobar.jpg|{{Test|unamedParam|alt=param}}|alt=galleryalt
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="desc"><img alt="inneralt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" width="20" height="2" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/30px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/40px-Foobar.jpg 2x" /></a>
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p>This is a test template
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!! test
+gallery (with showfilename option)
+!! wikitext
+<gallery showfilename>
+File:Nonexistant.jpg|caption
+File:Nonexistant.jpg
+image:foobar.jpg|some '''caption''' [[Main Page]]
+File:Foobar.jpg
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Nonexistant.jpg" title="File:Nonexistant.jpg">Nonexistant.jpg</a><br />
+caption
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Nonexistant.jpg" title="File:Nonexistant.jpg">Nonexistant.jpg</a><br />
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Foobar.jpg" title="File:Foobar.jpg">Foobar.jpg</a><br />
+some <b>caption</b> <a href="/wiki/Main_Page" title="Main Page">Main Page</a>
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Foobar.jpg" title="File:Foobar.jpg">Foobar.jpg</a><br />
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!! test
+Gallery (with namespace-less filenames)
+!! wikitext
+<gallery>
+File:Nonexistant.jpg
+Nonexistant.jpg
+image:foobar.jpg
+foobar.jpg
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="height: 150px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!! test
+HTML Hex character encoding (spells the word "JavaScript")
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+&#x4A;&#x061;&#x0076;&#x00061;&#x000053;&#x0000063;&#114;&#x0000069;&#00000112;&#x0000000074;
+!! html/php
+<p>&#x4a;&#x61;&#x76;&#x61;&#x53;&#x63;&#114;&#x69;&#112;&#x74;
+</p>
+!! html/php+tidy
+<p>JavaScript</p>
+!! html/parsoid
+<p><span typeof="mw:Entity">J</span><span typeof="mw:Entity">a</span><span typeof="mw:Entity">v</span><span typeof="mw:Entity">a</span><span typeof="mw:Entity">S</span><span typeof="mw:Entity">c</span><span typeof="mw:Entity">r</span><span typeof="mw:Entity">i</span><span typeof="mw:Entity">p</span><span typeof="mw:Entity">t</span></p>
+!! end
+
+!! test
+HTML Hex character encoding bogus encoding (bug 26437 regression check)
+!! wikitext
+&#xsee;&#XSEE;
+!! html/php
+<p>&amp;#xsee;&amp;#XSEE;
+</p>
+!! html/parsoid
+<p>&amp;#xsee;&amp;#XSEE;</p>
+!! end
+
+!! test
+HTML Hex character encoding mixed case
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+&#xEE;&#Xee;
+!! html/php
+<p>&#xee;&#xee;
+</p>
+!! html/php+tidy
+<p>îî</p>
+!! html/parsoid
+<p><span typeof="mw:Entity">î</span><span typeof="mw:Entity">î</span></p>
+!! end
+
+!! test
+__FORCETOC__ override
+!! wikitext
+__NEWSECTIONLINK__
+__FORCETOC__
+!! html
+<p><br />
+</p>
+!! end
+
+!! test
+ISBN code coverage
+!! wikitext
+ISBN 978-0-1234-56&#x20;789
+!! html
+<p><a href="/wiki/Special:BookSources/9780123456" class="internal mw-magiclink-isbn">ISBN 978-0-1234-56</a>&#x20;789
+</p>
+!! html+tidy
+<p><a href="/wiki/Special:BookSources/9780123456" class="internal mw-magiclink-isbn">ISBN 978-0-1234-56</a> 789</p>
+!! end
+
+!! test
+ISBN followed by 5 spaces
+!! wikitext
+ISBN
+!! html
+<p>ISBN
+</p>
+!! end
+
+!! test
+Double ISBN
+!! wikitext
+ISBN ISBN 1234567890
+!! html
+<p>ISBN <a href="/wiki/Special:BookSources/1234567890" class="internal mw-magiclink-isbn">ISBN 1234567890</a>
+</p>
+!! end
+
+!! test
+ISBN with an X
+!! wikitext
+ISBN 3-462-04561-X
+!! html
+<p><a href="/wiki/Special:BookSources/346204561X" class="internal mw-magiclink-isbn">ISBN 3-462-04561-X</a>
+</p>
+!! end
+
+!! test
+ISBN with empty prefix (parsoid test)
+!! wikitext
+ISBN 1234567890
+!! html/parsoid
+<p><a href="Special:BookSources/1234567890" rel="mw:ExtLink">ISBN 1234567890</a></p>
+!! end
+
+!! test
+Bug 22905: <abbr> followed by ISBN followed by </a>
+!! wikitext
+<abbr>(fr)</abbr> ISBN 2753300917 [http://www.example.com example.com]
+!! html
+<p><abbr>(fr)</abbr> <a href="/wiki/Special:BookSources/2753300917" class="internal mw-magiclink-isbn">ISBN 2753300917</a> <a rel="nofollow" class="external text" href="http://www.example.com">example.com</a>
+</p>
+!! end
+
+!! test
+Double RFC
+!! wikitext
+RFC RFC 1234
+!! html
+<p>RFC <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc1234">RFC 1234</a>
+</p>
+!! end
+
+!! test
+Double RFC with a wiki link
+!! wikitext
+RFC [[RFC 1234]]
+!! html
+<p>RFC <a href="/index.php?title=RFC_1234&amp;action=edit&amp;redlink=1" class="new" title="RFC 1234 (page does not exist)">RFC 1234</a>
+</p>
+!! end
+
+!! test
+RFC code coverage
+!! wikitext
+RFC 983&#x20;987
+!! html
+<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc983">RFC 983</a>&#x20;987
+</p>
+!! html+tidy
+<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc983">RFC 983</a> 987</p>
+!! end
+
+!! test
+Centre-aligned image
+!! wikitext
+[[Image:foobar.jpg|centre]]
+!! html
+<div class="center"><div class="floatnone"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div></div>
+
+!!end
+
+!! test
+None-aligned image
+!! wikitext
+[[Image:foobar.jpg|none]]
+!! html
+<div class="floatnone"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!!end
+
+!! test
+Width + Height sized image (using px) (height is ignored)
+!! wikitext
+[[Image:foobar.jpg|640x480px]]
+!! html
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg 2x" /></a>
+</p>
+!!end
+
+!! test
+Width-sized image (using px, no following whitespace)
+!! wikitext
+[[Image:foobar.jpg|640px]]
+!! html
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg 2x" /></a>
+</p>
+!!end
+
+!! test
+Width-sized image (using px, with following whitespace - test regression from r39467)
+!! wikitext
+[[Image:foobar.jpg|640px ]]
+!! html
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg 2x" /></a>
+</p>
+!!end
+
+!! test
+Width-sized image (using px, with preceding whitespace - test regression from r39467)
+!! wikitext
+[[Image:foobar.jpg| 640px]]
+!! html
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg 2x" /></a>
+</p>
+!!end
+
+!! test
+Image with page parameter
+!! options
+djvu
+!! wikitext
+[[File:LoremIpsum.djvu|page=2]]
+!! html
+<p><a href="/index.php?title=File:LoremIpsum.djvu&amp;page=2" class="image"><img alt="LoremIpsum.djvu" src="http://example.com/images/thumb/5/5f/LoremIpsum.djvu/page2-2480px-LoremIpsum.djvu.jpg" width="2480" height="3508" srcset="http://example.com/images/thumb/5/5f/LoremIpsum.djvu/page2-3720px-LoremIpsum.djvu.jpg 1.5x, http://example.com/images/thumb/5/5f/LoremIpsum.djvu/page2-4960px-LoremIpsum.djvu.jpg 2x" /></a>
+</p>
+!! end
+
+!! test
+Another italics / bold test
+!! wikitext
+ ''' ''x'
+!! html
+<pre>'<i> </i>x'
+</pre>
+!!end
+
+# Note the results may be incorrect, as parserTest output included this:
+# XML error: Mismatched tag at byte 6120:
+# ...<dd> </dt></dl> </dd...
+!! test
+dt/dd/dl test
+!! options
+disabled
+!! wikitext
+:;;;::
+!! html
+<dl>
+<dd><dl>
+<dt><dl>
+<dt><dl>
+<dt><dl>
+<dd><dl>
+<dd>
+</dd>
+</dl>
+</dd>
+</dl>
+</dt>
+</dl>
+</dt>
+</dl>
+</dt>
+</dl>
+</dd>
+</dl>
+
+!!end
+
+
+# Images with the "|" character in external URLs in comment tags; Eats half the comment, leaves unmatched "</a>" tag.
+!! test
+Images with the "|" character in the comment
+!! wikitext
+[[File:Foobar.jpg|thumb|An [http://test/?param1=|left|&param2=|x external] URL]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>An <a rel="nofollow" class="external text" href="http://test/?param1=%7Cleft%7C&amp;param2=%7Cx">external</a> URL</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>An <a rel="mw:ExtLink" href="http://test/?param1=|left|&amp;param2=|x">external</a> URL</figcaption></figure>
+!! end
+
+!! test
+[Before] HTML without raw HTML enabled ($wgRawHtml==false)
+!! wikitext
+<html><script>alert(1);</script></html>
+!! html
+<p>&lt;html&gt;&lt;script&gt;alert(1);&lt;/script&gt;&lt;/html&gt;
+</p>
+!! end
+
+!! test
+HTML with raw HTML ($wgRawHtml==true)
+!! options
+wgRawHtml=1
+!! wikitext
+<html><script>alert(1);</script></html>
+!! html
+<p><script>alert(1);</script>
+</p>
+!! end
+
+!! test
+Parents of subpages, one level up
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! wikitext
+[[../|L2]]
+!! html
+<p><a href="/index.php?title=Subpage_test/L1/L2&amp;action=edit&amp;redlink=1" class="new" title="Subpage test/L1/L2 (page does not exist)">L2</a>
+</p>
+!! end
+
+
+!! test
+Parents of subpages, one level up, not named
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! wikitext
+[[../]]
+!! html
+<p><a href="/index.php?title=Subpage_test/L1/L2&amp;action=edit&amp;redlink=1" class="new" title="Subpage test/L1/L2 (page does not exist)">Subpage test/L1/L2</a>
+</p>
+!! end
+
+
+
+!! test
+Parents of subpages, two levels up
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! wikitext
+[[../../|L1]]2
+
+[[../../|L1]]l
+!! html
+<p><a href="/index.php?title=Subpage_test/L1&amp;action=edit&amp;redlink=1" class="new" title="Subpage test/L1 (page does not exist)">L1</a>2
+</p><p><a href="/index.php?title=Subpage_test/L1&amp;action=edit&amp;redlink=1" class="new" title="Subpage test/L1 (page does not exist)">L1l</a>
+</p>
+!! end
+
+!! test
+Parents of subpages, two levels up, without trailing slash or name.
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! wikitext
+[[../..]]
+!! html
+<p>[[../..]]
+</p>
+!! end
+
+!! test
+Parents of subpages, two levels up, with lots of extra trailing slashes.
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! wikitext
+[[../../////]]
+!! html
+<p><a href="/index.php?title=Subpage_test/L1////&amp;action=edit&amp;redlink=1" class="new" title="Subpage test/L1//// (page does not exist)">///</a>
+</p>
+!! end
+
+!! article
+Subpage test/L1/L2/L3Sibling
+!! text
+Sibling article
+!! endarticle
+
+!! test
+Transclusion of a sibling page (one level up)
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! wikitext
+{{../L3Sibling}}
+!! html
+<p>Sibling article
+</p>
+!! end
+
+!! test
+Transclusion of a child page
+!! options
+subpage title=[[Subpage test/L1/L2]]
+!! wikitext
+{{/L3Sibling}}
+!! html
+<p>Sibling article
+</p>
+!! end
+
+!! test
+Non-transclusion because of too many up levels
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! wikitext
+{{../../../../More than parent}}
+!! html
+<p>{{../../../../More than parent}}
+</p>
+!! end
+
+!! test
+Definition list code coverage
+!! wikitext
+; title : def
+; title : def
+;title: def
+!! html
+<dl><dt> title &#160;</dt>
+<dd> def</dd>
+<dt> title&#160;</dt>
+<dd> def</dd>
+<dt>title</dt>
+<dd> def</dd></dl>
+
+!! end
+
+!! test
+Don't fall for the self-closing div
+!! wikitext
+<div>hello world</div/>
+!! html
+<div>hello world</div>
+
+!! end
+
+!! test
+MSGNW magic word
+!! wikitext
+{{MSGNW:msg}}
+!! html
+<p>&#91;&#91;:Template:Msg&#93;&#93;
+</p>
+!! end
+
+!! test
+RAW magic word
+!! wikitext
+{{RAW:QUERTY}}
+!! html
+<p><a href="/index.php?title=Template:QUERTY&amp;action=edit&amp;redlink=1" class="new" title="Template:QUERTY (page does not exist)">Template:QUERTY</a>
+</p>
+!! end
+
+# This isn't needed for XHTML conformance, but would be handy as a fallback security measure
+!! test
+Always escape literal '>' in output, not just after '<'
+!! wikitext
+><>
+!! html
+<p>&gt;&lt;&gt;
+</p>
+!! end
+
+!! test
+Template caching
+!! wikitext
+{{Test}}
+{{Test}}
+!! html
+<p>This is a test template
+This is a test template
+</p>
+!! end
+
+
+!! article
+MediaWiki:Fake
+!! text
+==header==
+!! endarticle
+
+!! test
+Inclusion of !userCanEdit() content
+!! wikitext
+{{MediaWiki:Fake}}
+!! html
+<h2><span class="mw-headline" id="header">header</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=MediaWiki:Fake&amp;action=edit&amp;section=T-1" title="MediaWiki:Fake">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+
+!! test
+Out-of-order TOC heading levels
+!! wikitext
+==2==
+======6======
+===3===
+=1=
+=====5=====
+==2==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#2"><span class="tocnumber">1</span> <span class="toctext">2</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#6"><span class="tocnumber">1.1</span> <span class="toctext">6</span></a></li>
+<li class="toclevel-2 tocsection-3"><a href="#3"><span class="tocnumber">1.2</span> <span class="toctext">3</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-4"><a href="#1"><span class="tocnumber">2</span> <span class="toctext">1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-5"><a href="#5"><span class="tocnumber">2.1</span> <span class="toctext">5</span></a></li>
+<li class="toclevel-2 tocsection-6"><a href="#2_2"><span class="tocnumber">2.2</span> <span class="toctext">2</span></a></li>
+</ul>
+</li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="2">2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h6><span class="mw-headline" id="6">6</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: 6">edit</a><span class="mw-editsection-bracket">]</span></span></h6>
+<h3><span class="mw-headline" id="3">3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: 3">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
+<h1><span class="mw-headline" id="1">1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: 1">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<h5><span class="mw-headline" id="5">5</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: 5">edit</a><span class="mw-editsection-bracket">]</span></span></h5>
+<h2><span class="mw-headline" id="2_2">2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=6" title="Edit section: 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+
+!! test
+ISBN with a dummy number
+!! wikitext
+ISBN ---
+!! html
+<p>ISBN ---
+</p>
+!! end
+
+
+!! test
+ISBN with space-delimited number
+!! wikitext
+ISBN 92 9017 032 8
+!! html
+<p><a href="/wiki/Special:BookSources/9290170328" class="internal mw-magiclink-isbn">ISBN 92 9017 032 8</a>
+</p>
+!! end
+
+
+!! test
+ISBN with multiple spaces, no number
+!! wikitext
+ISBN foo
+!! html
+<p>ISBN foo
+</p>
+!! end
+
+
+!! test
+ISBN length
+!! wikitext
+ISBN 123456789
+
+ISBN 1234567890
+
+ISBN 12345678901
+!! html
+<p>ISBN 123456789
+</p><p><a href="/wiki/Special:BookSources/1234567890" class="internal mw-magiclink-isbn">ISBN 1234567890</a>
+</p><p>ISBN 12345678901
+</p>
+!! end
+
+
+!! test
+ISBN with trailing year (bug 8110)
+!! wikitext
+ISBN 1-234-56789-0 - 2006
+
+ISBN 1 234 56789 0 - 2006
+!! html
+<p><a href="/wiki/Special:BookSources/1234567890" class="internal mw-magiclink-isbn">ISBN 1-234-56789-0</a> - 2006
+</p><p><a href="/wiki/Special:BookSources/1234567890" class="internal mw-magiclink-isbn">ISBN 1 234 56789 0</a> - 2006
+</p>
+!! end
+
+
+!! test
+anchorencode
+!! wikitext
+{{anchorencode:foo bar©#%n}}
+!! html
+<p>foo_bar.C2.A9.23.25n
+</p>
+!! end
+
+!! test
+anchorencode trims spaces
+!! wikitext
+{{anchorencode: __pretty__please__}}
+!! html
+<p>pretty_please
+</p>
+!! end
+
+!! test
+anchorencode deals with links
+!! wikitext
+{{anchorencode: [[hello|world]] [[hi]]}}
+!! html
+<p>world_hi
+</p>
+!! end
+
+!! test
+anchorencode deals with templates
+!! wikitext
+{{anchorencode: {{Foo}} }}
+!! html
+<p>FOO
+</p>
+!! end
+
+!! test
+anchorencode encodes like the TOC generator: (bug 18431)
+!! wikitext
+=== _ +:.3A%3A&&amp;]] ===
+{{anchorencode: _ +:.3A%3A&&amp;]] }}
+__NOEDITSECTION__
+!! html
+<h3><span class="mw-headline" id=".2B:.3A.253A.26.26.5D.5D">_ +:.3A%3A&amp;&amp;]]</span></h3>
+<p>.2B:.3A.253A.26.26.5D.5D
+</p>
+!! end
+
+!! test
+Bug 6200: blockquotes and paragraph formatting
+!! wikitext
+<blockquote>
+foo
+</blockquote>
+
+bar
+
+ baz
+!! html
+<blockquote>
+<p>foo
+</p>
+</blockquote>
+<p>bar
+</p>
+<pre>baz
+</pre>
+!! end
+
+!! test
+Bug 8293: Use of center tag ruins paragraph formatting
+!! wikitext
+<center>
+foo
+</center>
+
+bar
+
+ baz
+!! html
+<center>
+<p>foo
+</p>
+</center>
+<p>bar
+</p>
+<pre>baz
+</pre>
+!! end
+
+!!test
+Parsing of overlapping (improperly nested) inline html tags
+!! wikitext
+<span><s>x</span></s>
+!! html/php
+<p><span><s>x&lt;/span&gt;</s></span>
+</p>
+!! html/parsoid
+<p><span><s>x</s></span>
+</p>
+!!end
+
+###
+### Language variants related tests
+###
+!! test
+Self-link in language variants
+!! options
+title=[[Dunav]] language=sr
+!! wikitext
+Both [[Dunav]] and [[Дунав]] are names for this river.
+!! html
+<p>Both <strong class="selflink">Dunav</strong> and <strong class="selflink">Дунав</strong> are names for this river.
+</p>
+!!end
+
+!! article
+Дуна
+!! text
+content
+!! endarticle
+
+!! test
+Link to another existing title shouldn't be parsed as self-link even if it's a variant of this title
+!! options
+title=[[Duna]] language=sr
+!! wikitext
+[[Дуна]] is not a self-link while [[Duna]] and [[Dуна]] are still self-links.
+!! html
+<p><a href="/wiki/%D0%94%D1%83%D0%BD%D0%B0" title="Дуна">Дуна</a> is not a self-link while <strong class="selflink">Duna</strong> and <strong class="selflink">Dуна</strong> are still self-links.
+</p>
+!! end
+
+!! test
+Link to a section of a variant of this title shouldn't be parsed as self-link
+!! options
+title=[[Duna]] language=sr
+!! wikitext
+[[Dуна]] is a self-link while [[Dunа#Foo]] and [[Dуна#Foo]] are not self-links.
+!! html
+<p><strong class="selflink">Dуна</strong> is a self-link while <a href="/wiki/%D0%94%D1%83%D0%BD%D0%B0" title="Дуна">Dunа#Foo</a> and <a href="/wiki/%D0%94%D1%83%D0%BD%D0%B0" title="Дуна">Dуна#Foo</a> are not self-links.
+</p>
+!! end
+
+!! test
+Link to pages in language variants
+!! options
+language=sr
+!! wikitext
+Main Page can be written as [[Маин Паге]]
+!! html
+<p>Main Page can be written as <a href="/wiki/Main_Page" title="Main Page">Маин Паге</a>
+</p>
+!!end
+
+
+!! test
+Multiple links to pages in language variants
+!! options
+language=sr
+!! wikitext
+[[Main Page]] can be written as [[Маин Паге]] same as [[Маин Паге]].
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">Main Page</a> can be written as <a href="/wiki/Main_Page" title="Main Page">Маин Паге</a> same as <a href="/wiki/Main_Page" title="Main Page">Маин Паге</a>.
+</p>
+!!end
+
+
+!! test
+Simple template in language variants
+!! options
+language=sr
+!! wikitext
+{{тест}}
+!! html
+<p>This is a test template
+</p>
+!! end
+
+
+!! test
+Template with explicit namespace in language variants
+!! options
+language=sr
+!! wikitext
+{{Template:тест}}
+!! html
+<p>This is a test template
+</p>
+!! end
+
+
+!! test
+Basic test for template parameter in language variants
+!! options
+language=sr
+!! wikitext
+{{парамтест|param=foo}}
+!! html
+<p>This is a test template with parameter foo
+</p>
+!! end
+
+
+!! test
+Simple category in language variants
+!! options
+language=sr cat
+!! wikitext
+[[Category:МедиаWики Усер'с Гуиде]]
+!! html
+<a href="/wiki/%D0%9A%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%98%D0%B0:MediaWiki_User%27s_Guide" title="Категорија:MediaWiki User's Guide">MediaWiki User's Guide</a>
+!! end
+
+
+!! article
+Category:分类
+!! text
+blah
+!! endarticle
+
+!! article
+Category:分類
+!! text
+blah
+!! endarticle
+
+!! test
+Don't convert blue categorylinks to another variant (bug 33210)
+!! options
+language=zh cat
+!! wikitext
+[[A]][[Category:分类]]
+!! html
+<a href="/wiki/Category:%E5%88%86%E7%B1%BB" title="Category:分类">分类</a>
+!! end
+
+
+!! test
+Stripping -{}- tags (language variants)
+!! options
+language=sr
+!! wikitext
+Latin proverb: -{Ne nuntium necare}-
+!! html
+<p>Latin proverb: Ne nuntium necare
+</p>
+!! end
+
+
+!! test
+Prevent conversion with -{}- tags (language variants)
+!! options
+language=sr variant=sr-ec
+!! wikitext
+Latinski: -{Ne nuntium necare}-
+!! html
+<p>Латински: Ne nuntium necare
+</p>
+!! end
+
+
+!! test
+Prevent conversion of text with -{}- tags (language variants)
+!! options
+language=sr variant=sr-ec
+!! wikitext
+Latinski: -{Ne nuntium necare}-
+!! html
+<p>Латински: Ne nuntium necare
+</p>
+!! end
+
+
+!! test
+Prevent conversion of links with -{}- tags (language variants)
+!! options
+language=sr variant=sr-ec
+!! wikitext
+-{[[Main Page]]}-
+!! html
+<p><a href="/wiki/Main_Page" title="Main Page">Main Page</a>
+</p>
+!! end
+
+
+!! test
+-{}- tags within headlines (within html for parserConvert())
+!! options
+language=sr variant=sr-ec
+!! wikitext
+== -{Naslov}- ==
+!! html
+<h2><span class="mw-headline" id="-.7BNaslov.7D-">Naslov</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Уредите одељак „Naslov“">уреди</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+
+!! test
+Explicit definition of language variant alternatives
+!! options
+language=zh variant=zh-tw
+!! wikitext
+-{zh:China;zh-tw:Taiwan}-, not China
+!! html
+<p>Taiwan, not China
+</p>
+!! end
+
+
+!! test
+Conversion around HTML tags
+!! options
+language=sr variant=sr-ec
+!! wikitext
+-{H|span=>sr-ec:script;title=>sr-ec:src;}-
+<span title="La-{sr-el:L;sr-ec:C;}-tin">ski</span>
+!! html
+<p>
+<span title="ЛаCтин">ски</span>
+</p>
+!! end
+
+
+!! test
+Explicit session-wise language variant mapping (A flag and - flag)
+!! options
+language=zh variant=zh-tw
+!! wikitext
+Taiwan is not China.
+But -{A|zh:China;zh-tw:Taiwan}- is China,
+(This-{-|zh:China;zh-tw:Taiwan}- should be stripped!)
+and -{China}- is China.
+!! html
+<p>Taiwan is not China.
+But Taiwan is Taiwan,
+(This should be stripped!)
+and China is China.
+</p>
+!! end
+
+!! test
+Explicit session-wise language variant mapping (H flag for hide)
+!! options
+language=zh variant=zh-tw
+!! wikitext
+(This-{H|zh:China;zh-tw:Taiwan}- should be stripped!)
+Taiwan is China.
+!! html
+<p>(This should be stripped!)
+Taiwan is Taiwan.
+</p>
+!! end
+
+!! test
+Adding explicit conversion rule for title (T flag)
+!! options
+language=zh variant=zh-tw showtitle
+!! wikitext
+Should be stripped-{T|zh:China;zh-tw:Taiwan}-!
+!! html
+Taiwan
+<p>Should be stripped!
+</p>
+!! end
+
+!! test
+Testing that changing the language variant here in the tests actually works
+!! options
+language=zh variant=zh showtitle
+!! wikitext
+Should be stripped-{T|zh:China;zh-tw:Taiwan}-!
+!! html
+China
+<p>Should be stripped!
+</p>
+!! end
+
+!! test
+Recursive conversion of alt and title attrs shouldn't clear converter state
+!! options
+language=zh variant=zh-cn showtitle
+!! wikitext
+-{H|zh-cn:Exclamation;zh-tw:exclamation;}-
+Should be stripped-{T|zh-cn:China;zh-tw:Taiwan}-<span title="exclamation">!</span>
+!! html
+China
+<p>
+Should be stripped<span title="Exclamation">!</span>
+</p>
+!! end
+
+!! test
+Bug 24072: more test on conversion rule for title
+!! options
+language=zh variant=zh-tw showtitle
+!! wikitext
+This should be stripped-{T|zh:China;zh-tw:Taiwan}-!
+This won't take interferes with the title rule-{H|zh:Beijing;zh-tw:Taipei}-.
+!! html
+Taiwan
+<p>This should be stripped!
+This won't take interferes with the title rule.
+</p>
+!! end
+
+!! test
+Partly disable title conversion if variant == main language code
+!! options
+language=zh variant=zh title=[[ZH]] showtitle
+!! wikitext
+-{T|zh-cn:CN;zh-tw:TW}-
+!! html
+ZH
+<p>
+</p>
+!! end
+
+!! test
+Partly disable title conversion if variant == main language code, more
+!! options
+language=zh variant=zh title=[[ZH]] showtitle
+!! wikitext
+-{T|TW}-
+!! html
+ZH
+<p>
+</p>
+!! end
+
+!! test
+Raw output of variant escape tags (R flag)
+!! options
+language=zh variant=zh-tw
+!! wikitext
+Raw: -{R|zh:China;zh-tw:Taiwan}-
+!! html
+<p>Raw: zh:China;zh-tw:Taiwan
+</p>
+!! end
+
+!! test
+Nested using of manual convert syntax
+!! options
+language=zh variant=zh-hk
+!! wikitext
+Nested: -{zh-hans:Hi -{zh-cn:China;zh-sg:Singapore;}-;zh-hant:Hello -{zh-tw:Taiwan;zh-hk:H-{ong}- K-{}-ong;}-;}-!
+!! html
+<p>Nested: Hello Hong Kong!
+</p>
+!! end
+
+!! test
+Proper conversion of text in external links
+!! options
+language=sr variant=sr-ec
+!! wikitext
+http://www.google.com
+gopher://www.google.com
+[http://www.google.com http://www.google.com]
+[gopher://www.google.com gopher://www.google.com]
+[https://www.google.com irc://www.google.com]
+[ftp://www.google.com www.google.com/ftp://dir]
+[//www.google.com www.google.com]
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.google.com">http://www.google.com</a>
+<a rel="nofollow" class="external free" href="gopher://www.google.com">gopher://www.google.com</a>
+<a rel="nofollow" class="external free" href="http://www.google.com">http://www.google.com</a>
+<a rel="nofollow" class="external free" href="gopher://www.google.com">gopher://www.google.com</a>
+<a rel="nofollow" class="external text" href="https://www.google.com">irc://www.google.com</a>
+<a rel="nofollow" class="external text" href="ftp://www.google.com">www.гоогле.цом/фтп://дир</a>
+<a rel="nofollow" class="external text" href="//www.google.com">www.гоогле.цом</a>
+</p>
+!! end
+
+!! test
+Do not convert roman numbers to language variants
+!! options
+language=sr variant=sr-ec
+!! wikitext
+Fridrih IV je car.
+!! html
+<p>Фридрих IV је цар.
+</p>
+!! end
+
+!! test
+Unclosed language converter markup "-{"
+!! options
+language=sr
+!! wikitext
+-{T|hello
+!! html
+<p>-{T|hello
+</p>
+!! end
+
+!! test
+Don't convert raw rule "-{R|=&gt;}-" to "=>"
+!! options
+language=sr
+!! wikitext
+-{R|=&gt;}-
+!! html
+<p>=&gt;
+</p>
+!!end
+
+!! test
+Don't break link parsing if language converter markup is in the caption.
+!! options
+language=sr variant=sr-ec
+!! wikitext
+[[Main Page|-{R|main page}-]]
+!! html
+<p><a href="/wiki/Main_Page" title="Маин Паге">main page</a>
+</p>
+!! end
+
+# This test is currently broken in the PHP parser (bug 52661)
+!! test
+Don't break image parsing if language converter markup is in the caption.
+!! options
+language=sr
+disabled
+!! wikitext
+[[File:Foobar.jpg|-{R|caption}-]]
+!! html
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+# This test is currently broken in the PHP parser (bug 52661)
+!! test
+Don't break list handling if language converter markup is in the item.
+!! options
+language=zh variant=zh-cn
+disabled
+!! wikitext
+;-{zh-cn:AAA;zh-tw:BBB}-
+!! html
+<dl><dt>AAA
+</dt></dl>
+
+!! end
+
+# This test is currently broken in the PHP parser (bug 52661)
+!! test
+Don't break table handling if language converter markup is in the cell.
+!! options
+language=sr variant=sr-ec
+disabled
+!! wikitext
+{|
+|-
+| -{R|B}-
+|}
+!! html
+<table>
+
+<tr>
+<td> B
+</td></tr></table>
+
+!! end
+
+!! test
+Bug 529: Uncovered bullet
+!! wikitext
+* Foo {{bullet}}
+!! html
+<ul><li> Foo </li>
+<li> Bar</li></ul>
+
+!! end
+
+# Plain MediaWiki does not remove empty lists, but tidy actually does.
+# Templates in Wikipedia rely on this behavior, as tidy has always been
+# enabled there. These tests are normally run *without* tidy, so specify the
+# full output here.
+# To test realistic parsing behavior, apply a tidy-like transformation to both
+# the expected output and your parser's output.
+!! test
+Bug 529: Uncovered bullet leaving empty list, normally removed by tidy
+!! wikitext
+******* Foo {{bullet}}
+!! html
+<ul><li><ul><li><ul><li><ul><li><ul><li><ul><li><ul><li> Foo </li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li>
+<li> Bar</li></ul>
+
+!! end
+
+!! test
+Bug 529: Uncovered table already at line-start
+!! wikitext
+x
+
+{{table}}
+y
+!! html
+<p>x
+</p>
+<table>
+<tr>
+<td> 1 </td>
+<td> 2
+</td></tr>
+<tr>
+<td> 3 </td>
+<td> 4
+</td></tr></table>
+<p>y
+</p>
+!! end
+
+!! test
+Bug 529: Uncovered bullet in parser function result
+!! wikitext
+* Foo {{lc:{{bullet}} }}
+!! html
+<ul><li> Foo </li>
+<li> bar</li></ul>
+
+!! end
+
+!! test
+Bug 5678: Double-parsed template argument
+!! wikitext
+{{lc:{{{1}}}|hello}}
+!! html
+<p>{{{1}}}
+</p>
+!! end
+
+!! test
+Bug 5678: Double-parsed template invocation
+!! wikitext
+{{lc:{{paramtest {{!}} param = hello }} }}
+!! html
+<p>{{paramtest | param = hello }}
+</p>
+!! end
+
+!! test
+Case insensitivity of parser functions for non-ASCII characters (bug 8143)
+!! options
+language=cs
+title=[[Main Page]]
+!! wikitext
+{{PRVNÍVELKÉ:ěščř}}
+{{prvnívelké:ěščř}}
+{{PRVNÍMALÉ:ěščř}}
+{{prvnímalé:ěščř}}
+{{MALÁ:ěščř}}
+{{malá:ěščř}}
+{{VELKÁ:ěščř}}
+{{velká:ěščř}}
+!! html
+<p>Ěščř
+Ěščř
+ěščř
+ěščř
+ěščř
+ěščř
+ĚŠČŘ
+ĚŠČŘ
+</p>
+!! end
+
+!! test
+Morwen/13: Unclosed link followed by heading
+!! wikitext
+[[link
+==heading==
+!! html
+<p>[[link
+</p>
+<h2><span class="mw-headline" id="heading">heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: heading">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+HHP2.1: Heuristics for headings in preprocessor parenthetical structures
+!! wikitext
+{{foo|
+=heading=
+!! html
+<p>{{foo|
+</p>
+<h1><span class="mw-headline" id="heading">heading</span></h1>
+
+!! end
+
+!! test
+HHP2.2: Heuristics for headings in preprocessor parenthetical structures
+!! wikitext
+{{foo|
+==heading==
+!! html
+<p>{{foo|
+</p>
+<h2><span class="mw-headline" id="heading">heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: heading">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+Tildes in comments
+!! options
+pst
+!! wikitext
+<!-- ~~~~ -->
+!! html
+<!-- ~~~~ -->
+!! end
+
+!! test
+Paragraphs inside divs (no extra line breaks)
+!! wikitext
+<div>Line one
+
+Line two</div>
+!! html
+<div>Line one
+Line two</div>
+
+!! end
+
+!! test
+Paragraphs inside divs (extra line break on open)
+!! wikitext
+<div>
+Line one
+
+Line two</div>
+!! html
+<div>
+<p>Line one
+</p>
+Line two</div>
+
+!! end
+
+!! test
+Paragraphs inside divs (extra line break on close)
+!! wikitext
+<div>Line one
+
+Line two
+</div>
+!! html
+<div>Line one
+<p>Line two
+</p>
+</div>
+
+!! end
+
+!! test
+Paragraphs inside divs (extra line break on open and close)
+!! wikitext
+<div>
+Line one
+
+Line two
+</div>
+!! html
+<div>
+<p>Line one
+</p><p>Line two
+</p>
+</div>
+
+!! end
+
+!! test
+Nesting tags, paragraphs on lines which begin with <div>
+!! options
+disabled
+!! wikitext
+<div></div><strong>A
+B</strong>
+!! html
+<div></div>
+<p><strong>A
+B</strong>
+</p>
+!! end
+
+# Bug 6200: <blockquote> should behave like <div> with respect to line breaks
+!! test
+Bug 6200: paragraphs inside blockquotes (no extra line breaks)
+!! wikitext
+<blockquote>Line one
+
+Line two</blockquote>
+!! html
+<blockquote>Line one
+Line two</blockquote>
+
+!! html+tidy
+<blockquote>
+<p>Line one Line two</p>
+</blockquote>
+!! end
+
+!! test
+Bug 6200: paragraphs inside blockquotes (extra line break on open)
+!! wikitext
+<blockquote>
+Line one
+
+Line two</blockquote>
+!! html
+<blockquote>
+<p>Line one
+</p>
+Line two</blockquote>
+
+!! html+tidy
+<blockquote>
+<p>Line one</p>
+Line two</blockquote>
+!! end
+
+!! test
+Bug 6200: paragraphs inside blockquotes (extra line break on close)
+!! wikitext
+<blockquote>Line one
+
+Line two
+</blockquote>
+!! html
+<blockquote>Line one
+<p>Line two
+</p>
+</blockquote>
+
+!! html+tidy
+<blockquote>
+<p>Line one</p>
+<p>Line two</p>
+</blockquote>
+!! end
+
+!! test
+Bug 6200: paragraphs inside blockquotes (extra line break on open and close)
+!! wikitext
+<blockquote>
+Line one
+
+Line two
+</blockquote>
+!! html
+<blockquote>
+<p>Line one
+</p><p>Line two
+</p>
+</blockquote>
+
+!! html+tidy
+<blockquote>
+<p>Line one</p>
+<p>Line two</p>
+</blockquote>
+!! end
+
+!! test
+Paragraphs inside blockquotes/divs (no extra line breaks)
+!! wikitext
+<blockquote><div>Line one
+
+Line two</div></blockquote>
+!! html
+<blockquote><div>Line one
+Line two</div></blockquote>
+
+!! end
+
+!! test
+Paragraphs inside blockquotes/divs (extra line break on open)
+!! wikitext
+<blockquote><div>
+Line one
+
+Line two</div></blockquote>
+!! html
+<blockquote><div>
+<p>Line one
+</p>
+Line two</div></blockquote>
+
+!! end
+
+!! test
+Paragraphs inside blockquotes/divs (extra line break on close)
+!! wikitext
+<blockquote><div>Line one
+
+Line two
+</div></blockquote>
+!! html
+<blockquote><div>Line one
+<p>Line two
+</p>
+</div></blockquote>
+
+!! end
+
+!! test
+Paragraphs inside blockquotes/divs (extra line break on open and close)
+!! wikitext
+<blockquote><div>
+Line one
+
+Line two
+</div></blockquote>
+!! html
+<blockquote><div>
+<p>Line one
+</p><p>Line two
+</p>
+</div></blockquote>
+
+!! end
+
+!! test
+Interwiki links trounced by replaceExternalLinks after early LinkHolderArray expansion
+!! options
+wgLinkHolderBatchSize=0
+!! wikitext
+[[meatball:1]]
+[[meatball:2]]
+[[meatball:3]]
+!! html
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?1" class="extiw" title="meatball:1">meatball:1</a>
+<a href="http://www.usemod.com/cgi-bin/mb.pl?2" class="extiw" title="meatball:2">meatball:2</a>
+<a href="http://www.usemod.com/cgi-bin/mb.pl?3" class="extiw" title="meatball:3">meatball:3</a>
+</p>
+!! end
+
+!! test
+Free external link invading image caption
+!! wikitext
+[[Image:Foobar.jpg|thumb|http://x|hello]]
+!! html
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>hello</div></div></div>
+
+!! end
+
+!! test
+Bug 15196: localised external link numbers
+!! options
+language=fa
+!! wikitext
+[http://en.wikipedia.org/]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://en.wikipedia.org/">[۱]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/"></a></p>
+!! end
+
+!! test
+Multibyte character in padleft
+!! wikitext
+{{padleft:-Hello|7|Æ}}
+!! html
+<p>Æ-Hello
+</p>
+!! end
+
+!! test
+Multibyte character in padright
+!! wikitext
+{{padright:Hello-|7|Æ}}
+!! html
+<p>Hello-Æ
+</p>
+!! end
+
+!!test
+formatdate parser function
+!! wikitext
+{{#formatdate:2009-03-24}}
+!! html
+<p><span class="mw-formatted-date" title="2009-03-24">2009-03-24</span>
+</p>
+!! end
+
+!!test
+formatdate parser function, with default format
+!! wikitext
+{{#formatdate:2009-03-24|mdy}}
+!! html
+<p><span class="mw-formatted-date" title="2009-03-24">March 24, 2009</span>
+</p>
+!! end
+
+!! test
+Spacing of numbers in formatted dates
+!! wikitext
+{{#formatdate:January 15}}
+!! html
+<p><span class="mw-formatted-date" title="01-15">January 15</span>
+</p>
+!! end
+
+!! test
+formatdate parser function, with default format and on a page of which the content language is always English and different from the wiki content language
+!! options
+language=nl title=[[MediaWiki:Common.css]]
+!! wikitext
+{{#formatdate:2009-03-24|dmy}}
+!! html
+<p><span class="mw-formatted-date" title="2009-03-24">24 March 2009</span>
+</p>
+!! end
+
+#
+#
+#
+
+#
+# Edit comments
+#
+
+!! test
+Edit comment with link
+!! options
+comment
+!! wikitext
+I like the [[Main Page]] a lot
+!! html
+I like the <a href="/wiki/Main_Page" title="Main Page">Main Page</a> a lot
+!!end
+
+!! test
+Edit comment with link and link text
+!! options
+comment
+!! wikitext
+I like the [[Main Page|best pages]] a lot
+!! html
+I like the <a href="/wiki/Main_Page" title="Main Page">best pages</a> a lot
+!!end
+
+!! test
+Edit comment with link and link text with suffix
+!! options
+comment
+!! wikitext
+I like the [[Main Page|best page]]s a lot
+!! html
+I like the <a href="/wiki/Main_Page" title="Main Page">best pages</a> a lot
+!!end
+
+!! test
+Edit comment with section link (non-local, eg in history list)
+!! options
+comment title=[[Main Page]]
+!! wikitext
+/* External links */ removed bogus entries
+!! html
+<a href="/wiki/Main_Page#External_links" title="Main Page">→</a>‎<span dir="auto"><span class="autocomment">External links: </span> removed bogus entries</span>
+!!end
+
+!! test
+Edit comment with section link and text before it (non-local, eg in history list)
+!! options
+comment title=[[Main Page]]
+!! wikitext
+pre-comment text /* External links */ removed bogus entries
+!! html
+pre-comment text <a href="/wiki/Main_Page#External_links" title="Main Page">→</a>‎<span dir="auto"><span class="autocomment">External links: </span> removed bogus entries</span>
+!!end
+
+!! test
+Edit comment with section link (local, eg in diff view)
+!! options
+comment local title=[[Main Page]]
+!! wikitext
+/* External links */ removed bogus entries
+!! html
+<a href="#External_links">→</a>‎<span dir="auto"><span class="autocomment">External links: </span> removed bogus entries</span>
+!!end
+
+!! test
+Edit comment with subpage link (bug 14080)
+!! options
+comment
+subpage
+title=[[Subpage test]]
+!! wikitext
+Poked at a [[/subpage]] here...
+!! html
+Poked at a <a href="/wiki/Subpage_test/subpage" title="Subpage test/subpage">/subpage</a> here...
+!!end
+
+!! test
+Edit comment with subpage link and link text (bug 14080)
+!! options
+comment
+subpage
+title=[[Subpage test]]
+!! wikitext
+Poked at a [[/subpage|neat little page]] here...
+!! html
+Poked at a <a href="/wiki/Subpage_test/subpage" title="Subpage test/subpage">neat little page</a> here...
+!!end
+
+!! test
+Edit comment with bogus subpage link in non-subpage NS (bug 14080)
+!! options
+comment
+title=[[Subpage test]]
+!! wikitext
+Poked at a [[/subpage]] here...
+!! html
+Poked at a <a href="/index.php?title=/subpage&amp;action=edit&amp;redlink=1" class="new" title="/subpage (page does not exist)">/subpage</a> here...
+!!end
+
+!! test
+Edit comment with bare anchor link (local, as on diff)
+!! options
+comment
+local
+title=[[Main Page]]
+!! wikitext
+[[#section]]
+!! html
+<a href="#section">#section</a>
+!! end
+
+!! test
+Edit comment with bare anchor link (non-local, as on history)
+!! options
+comment
+title=[[Main Page]]
+!! wikitext
+[[#section]]
+!! html
+<a href="/wiki/Main_Page#section" title="Main Page">#section</a>
+!! end
+
+!! test
+Anchor starting with underscore
+!! wikitext
+[[#_ref|One]]
+!! html
+<p><a href="#_ref">One</a>
+</p>
+!! end
+
+!! test
+Id starting with underscore
+!! wikitext
+<div id="_ref"></div>
+!! html
+<div id="_ref"></div>
+
+!! end
+
+!! test
+Space normalisation on autocomment (bug 22784)
+!! options
+comment
+title=[[Main Page]]
+!! wikitext
+/* __hello__world__ */
+!! html
+<a href="/wiki/Main_Page#hello_world" title="Main Page">→</a>‎<span dir="auto"><span class="autocomment">__hello__world__</span></span>
+!! end
+
+!! test
+percent-encoding and + signs in comments (Bug 26410)
+!! options
+comment
+!! wikitext
+[[ABC%33D% ++]] [[ABC%33D% ++|+%20]]
+!! html
+<a href="/index.php?title=ABC3D%25_%2B%2B&amp;action=edit&amp;redlink=1" class="new" title="ABC3D% ++ (page does not exist)">ABC3D% ++</a> <a href="/index.php?title=ABC3D%25_%2B%2B&amp;action=edit&amp;redlink=1" class="new" title="ABC3D% ++ (page does not exist)">+%20</a>
+!! end
+
+!! test
+Bad images - basic functionality
+!! options
+disabled
+!! wikitext
+[[File:Bad.jpg]]
+!! html
+!! end
+
+!! test
+Bad images - bug 16039: text after bad image disappears
+!! options
+disabled
+!! wikitext
+Foo bar
+[[File:Bad.jpg]]
+Bar foo
+!! html
+<p>Foo bar
+</p><p>Bar foo
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) no displaytitle
+!! options
+showtitle
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=false
+!! wikitext
+this is not the the title
+!! html
+Parser test
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) RestrictDisplayTitle=false
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=false
+!! wikitext
+this is not the the title
+{{DISPLAYTITLE:whatever}}
+!! html
+whatever
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) RestrictDisplayTitle=true mismatch
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=true
+!! wikitext
+this is not the the title
+{{DISPLAYTITLE:whatever}}
+!! html
+Screen
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) RestrictDisplayTitle=true matching
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=true
+!! wikitext
+this is not the the title
+{{DISPLAYTITLE:screen}}
+!! html
+screen
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) AllowDisplayTitle=false
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=false
+!! wikitext
+this is not the the title
+{{DISPLAYTITLE:screen}}
+!! html
+Screen
+<p>this is not the the title
+<a href="/index.php?title=Template:DISPLAYTITLE:screen&amp;action=edit&amp;redlink=1" class="new" title="Template:DISPLAYTITLE:screen (page does not exist)">Template:DISPLAYTITLE:screen</a>
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) AllowDisplayTitle=false no DISPLAYTITLE
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=false
+!! wikitext
+this is not the the title
+!! html
+Screen
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle handles inline CSS styles (bug 26547) - rejected value
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=true
+!! wikitext
+this is not the the title
+{{DISPLAYTITLE:<span style="display: none;">s</span>creen}}
+!! html
+<span style="/* attempt to bypass $wgRestrictDisplayTitle */">s</span>creen
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle handles inline CSS styles (bug 26547) - accepted value
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=true
+!! wikitext
+this is not the the title
+{{DISPLAYTITLE:<span style="color: red;">s</span>creen}}
+!! html
+<span style="color: red;">s</span>creen
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+preload: check <noinclude> and <includeonly>
+!! options
+preload
+!! wikitext
+Hello <noinclude>cruel</noinclude><includeonly>kind</includeonly> world.
+!! html
+Hello kind world.
+!! end
+
+!! test
+preload: check <onlyinclude>
+!! options
+preload
+!! wikitext
+Goodbye <onlyinclude>Hello world</onlyinclude>
+!! html
+Hello world
+!! end
+
+!! test
+preload: can pass tags through if we want to
+!! options
+preload
+!! wikitext
+<includeonly><</includeonly>includeonly>Hello world<includeonly><</includeonly>/includeonly>
+!! html
+<includeonly>Hello world</includeonly>
+!! end
+
+!! test
+preload: check that it doesn't try to do tricks
+!! options
+preload
+!! wikitext
+* <!-- Hello --> ''{{world}}'' {{<includeonly>subst:</includeonly>How are you}}{{ {{{|safesubst:}}} #if:1|2|3}}
+!! html
+* <!-- Hello --> ''{{world}}'' {{subst:How are you}}{{ {{{|safesubst:}}} #if:1|2|3}}
+!! end
+
+!! test
+Play a bit with r67090 and bug 3158
+!! options
+disabled
+!! wikitext
+<div style="width:50% !important">&nbsp;</div>
+<div style="width:50%&nbsp;!important">&nbsp;</div>
+<div style="width:50%&#160;!important">&nbsp;</div>
+<div style="border : solid;">&nbsp;</div>
+!! html
+<div style="width:50% !important">&nbsp;</div>
+<div style="width:50% !important">&nbsp;</div>
+<div style="width:50% !important">&nbsp;</div>
+<div style="border&#160;: solid;">&nbsp;</div>
+
+!! end
+
+!! test
+HTML5 data attributes
+!! wikitext
+<span data-foo="bar">Baz</span>
+<p data-abc-def_hij="">Quuz</p>
+!! html
+<p><span data-foo="bar">Baz</span>
+</p>
+<p data-abc-def_hij="">Quuz</p>
+
+!! end
+
+!! test
+percent-encoding and + signs in internal links (Bug 26410)
+!! wikitext
+[[User:+%]] [[Page+title%]]
+[[%+]] [[%+|%20]] [[%+ ]] [[%+r]]
+[[%]] [[+]] [[image:%+abc%39|foo|[[bar]]]]
+[[%33%45]] [[%33%45+]]
+!! html
+<p><a href="/index.php?title=User:%2B%25&amp;action=edit&amp;redlink=1" class="new" title="User:+% (page does not exist)">User:+%</a> <a href="/index.php?title=Page%2Btitle%25&amp;action=edit&amp;redlink=1" class="new" title="Page+title% (page does not exist)">Page+title%</a>
+<a href="/index.php?title=%25%2B&amp;action=edit&amp;redlink=1" class="new" title="%+ (page does not exist)">%+</a> <a href="/index.php?title=%25%2B&amp;action=edit&amp;redlink=1" class="new" title="%+ (page does not exist)">%20</a> <a href="/index.php?title=%25%2B&amp;action=edit&amp;redlink=1" class="new" title="%+ (page does not exist)">%+ </a> <a href="/index.php?title=%25%2Br&amp;action=edit&amp;redlink=1" class="new" title="%+r (page does not exist)">%+r</a>
+<a href="/index.php?title=%25&amp;action=edit&amp;redlink=1" class="new" title="% (page does not exist)">%</a> <a href="/index.php?title=%2B&amp;action=edit&amp;redlink=1" class="new" title="+ (page does not exist)">+</a> <a href="/index.php?title=Special:Upload&amp;wpDestFile=%25%2Babc9" class="new" title="File:%+abc9">bar</a>
+<a href="/index.php?title=3E&amp;action=edit&amp;redlink=1" class="new" title="3E (page does not exist)">3E</a> <a href="/index.php?title=3E%2B&amp;action=edit&amp;redlink=1" class="new" title="3E+ (page does not exist)">3E+</a>
+</p>
+!! end
+
+!! test
+Special characters in embedded file links (bug 27679)
+!! wikitext
+[[File:Contains & ampersand.jpg]]
+[[File:Does not exist.jpg|Title with & ampersand]]
+!! html
+<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=Contains_%26_ampersand.jpg" class="new" title="File:Contains &amp; ampersand.jpg">File:Contains &amp; ampersand.jpg</a>
+<a href="/index.php?title=Special:Upload&amp;wpDestFile=Does_not_exist.jpg" class="new" title="File:Does not exist.jpg">Title with &amp; ampersand</a>
+</p>
+!! end
+
+
+!! test
+Confirm that 'apos' named character reference doesn't make it to output (not legal in HTML 4)
+!! wikitext
+Text&apos;s been normalized?
+!! html
+<p>Text&#39;s been normalized?
+</p>
+!! end
+
+!! test
+Bug 19052 U+3000 IDEOGRAPHIC SPACE should terminate free external links
+!! wikitext
+http://www.example.org/ <-- U+3000 (vim: ^Vu3000)
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.example.org/">http://www.example.org/</a> &lt;-- U+3000 (vim: ^Vu3000)
+</p>
+!! end
+
+!! test
+Bug 19052 U+3000 IDEOGRAPHIC SPACE should terminate bracketed external links
+!! wikitext
+[http://www.example.org/ ideograms]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.org/">ideograms</a>
+</p>
+!! end
+
+!! test
+Bug 19052 U+3000 IDEOGRAPHIC SPACE should terminate external images links
+!! wikitext
+http://www.example.org/pic.png <-- U+3000 (vim: ^Vu3000)
+!! html
+<p><img src="http://www.example.org/pic.png" alt="pic.png" /> &lt;-- U+3000 (vim: ^Vu3000)
+</p>
+!! end
+
+!! article
+Mediawiki:loop1
+!! text
+{{Identical|A}}
+!! endarticle
+
+!! article
+Mediawiki:loop2
+!! text
+{{Identical|B}}
+!! endarticle
+
+!! article
+Template:Identical
+!! text
+{{int:loop1}}
+{{int:loop2}}
+!! endarticle
+
+!! test
+Bug 31098 Template which includes system messages which includes the template
+!! wikitext
+{{Identical}}
+!! html
+<p><span class="error">Template loop detected: <a href="/wiki/Template:Identical" title="Template:Identical">Template:Identical</a></span>
+<span class="error">Template loop detected: <a href="/wiki/Template:Identical" title="Template:Identical">Template:Identical</a></span>
+</p>
+!! end
+
+!! test
+Bug31490 Turkish: ucfirst 'blah'
+!! options
+language=tr
+!! wikitext
+{{ucfirst:blah}}
+!! html
+<p>Blah
+</p>
+!! end
+
+!! test
+Bug31490 Turkish: ucfirst 'ix'
+!! options
+language=tr
+!! wikitext
+{{ucfirst:ix}}
+!! html
+<p>İx
+</p>
+!! end
+
+!! test
+Bug31490 Turkish: lcfirst 'BLAH'
+!! options
+language=tr
+!! wikitext
+{{lcfirst:BLAH}}
+!! html
+<p>bLAH
+</p>
+!! end
+
+!! test
+Bug31490 Turkish: ucfırst (with a dotless i)
+!! options
+language=tr
+!! wikitext
+{{ucfırst:blah}}
+!! html
+<p><a href="/index.php?title=%C5%9Eablon:Ucf%C4%B1rst:blah&amp;action=edit&amp;redlink=1" class="new" title="Şablon:Ucfırst:blah (sayfa mevcut değil)">Şablon:Ucfırst:blah</a>
+</p>
+!! end
+
+!! test
+Bug31490 ucfırst (with a dotless i) with English language
+!! options
+language=en
+!! wikitext
+{{ucfırst:blah}}
+!! html
+<p><a href="/index.php?title=Template:Ucf%C4%B1rst:blah&amp;action=edit&amp;redlink=1" class="new" title="Template:Ucfırst:blah (page does not exist)">Template:Ucfırst:blah</a>
+</p>
+!! end
+
+!! test
+Bug 26375: TOC with italics
+!! options
+title=[[Main Page]]
+!! wikitext
+__TOC__
+== ''Lost'' episodes ==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Lost_episodes"><span class="tocnumber">1</span> <span class="toctext"><i>Lost</i> episodes</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="Lost_episodes"><i>Lost</i> episodes</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&amp;action=edit&amp;section=1" title="Edit section: Lost episodes">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+Bug 26375: TOC with bold
+!! options
+title=[[Main Page]]
+!! wikitext
+__TOC__
+== '''should be bold''' then normal text ==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#should_be_bold_then_normal_text"><span class="tocnumber">1</span> <span class="toctext"><b>should be bold</b> then normal text</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="should_be_bold_then_normal_text"><b>should be bold</b> then normal text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&amp;action=edit&amp;section=1" title="Edit section: should be bold then normal text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+Bug 33845: Headings become cursive in TOC when they contain an image
+!! options
+title=[[Main Page]]
+!! wikitext
+__TOC__
+== Image [[Image:foobar.jpg]] ==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Image"><span class="tocnumber">1</span> <span class="toctext">Image</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="Image">Image <a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&amp;action=edit&amp;section=1" title="Edit section: Image">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+Bug 33845 (2): Headings become bold in TOC when they contain a blockquote
+!! options
+title=[[Main Page]]
+!! wikitext
+__TOC__
+== <blockquote>Quote</blockquote> ==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Quote"><span class="tocnumber">1</span> <span class="toctext">Quote</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="Quote"><blockquote>Quote</blockquote></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&amp;action=edit&amp;section=1" title="Edit section: Quote">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! html+tidy
+<div id="toc" class="toc">
+<div id="toctitle">
+<h2>Contents</h2>
+</div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Quote"><span class="tocnumber">1</span> <span class="toctext">Quote</span></a></li>
+</ul>
+</div>
+<h2><span class="mw-headline" id="Quote"></span></h2>
+<blockquote>
+<p><span class="mw-headline" id="Quote">Quote</span></p>
+</blockquote>
+<p><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&amp;action=edit&amp;section=1" title="Edit section: Quote">edit</a><span class="mw-editsection-bracket">]</span></span></p>
+!! end
+
+!! test
+Unclosed tags in TOC
+!! options
+title=[[Main Page]]
+!! wikitext
+__TOC__
+== Proof: 2 < 3 ==
+<small>Hanc marginis exiguitas non caperet.</small>
+QED
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Proof:_2_.3C_3"><span class="tocnumber">1</span> <span class="toctext">Proof: 2 &lt; 3</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="Proof:_2_.3C_3">Proof: 2 &lt; 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&amp;action=edit&amp;section=1" title="Edit section: Proof: 2 &lt; 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<p><small>Hanc marginis exiguitas non caperet.</small>
+QED
+</p>
+!! end
+
+!! test
+Multiple tags in TOC
+!! wikitext
+__TOC__
+== <i>Foo</i> <b>Bar</b> ==
+
+== <i>Foo</i> <blockquote>Bar</blockquote> ==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Foo_Bar"><span class="tocnumber">1</span> <span class="toctext"><i>Foo</i> <b>Bar</b></span></a></li>
+<li class="toclevel-1 tocsection-2"><a href="#Foo_Bar_2"><span class="tocnumber">2</span> <span class="toctext"><i>Foo</i> Bar</span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="Foo_Bar"><i>Foo</i> <b>Bar</b></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Foo_Bar_2"><i>Foo</i> <blockquote>Bar</blockquote></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! html+tidy
+<div id="toc" class="toc">
+<div id="toctitle">
+<h2>Contents</h2>
+</div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Foo_Bar"><span class="tocnumber">1</span> <span class="toctext"><i>Foo</i> <b>Bar</b></span></a></li>
+<li class="toclevel-1 tocsection-2"><a href="#Foo_Bar_2"><span class="tocnumber">2</span> <span class="toctext"><i>Foo</i> Bar</span></a></li>
+</ul>
+</div>
+<h2><span class="mw-headline" id="Foo_Bar"><i>Foo</i> <b>Bar</b></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Foo_Bar_2"><i>Foo</i></span></h2>
+<blockquote>
+<p><span class="mw-headline" id="Foo_Bar_2">Bar</span></p>
+</blockquote>
+<p><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></p>
+!! end
+
+!! test
+Tags with parameters in TOC
+!! wikitext
+__TOC__
+== <sup class="in-h2">Hello</sup> ==
+
+== <sup class="a > b">Evilbye</sup> ==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Hello"><span class="tocnumber">1</span> <span class="toctext"><sup>Hello</sup></span></a></li>
+<li class="toclevel-1 tocsection-2"><a href="#b.22.3EEvilbye"><span class="tocnumber">2</span> <span class="toctext"><sup> b"&gt;Evilbye</sup></span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="Hello"><sup class="in-h2">Hello</sup></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Hello">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="b.22.3EEvilbye"><sup> b"&gt;Evilbye</sup></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: b&quot;&gt;Evilbye">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+span tags with directionality in TOC
+!! wikitext
+__TOC__
+== <span dir="ltr">C++</span> ==
+
+== <span dir="rtl">זבנג!</span> ==
+
+== <span style="font-style: italic">The attributes on these span tags must be deleted from the TOC</span> ==
+
+== <span style="font-style: italic" dir="ltr">All attributes on these span tags must be deleted from the TOC</span> ==
+
+== <span dir="ltr" style="font-style: italic">Attributes after dir on these span tags must be deleted from the TOC</span> ==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#C.2B.2B"><span class="tocnumber">1</span> <span class="toctext"><span dir="ltr">C++</span></span></a></li>
+<li class="toclevel-1 tocsection-2"><a href="#.D7.96.D7.91.D7.A0.D7.92.21"><span class="tocnumber">2</span> <span class="toctext"><span dir="rtl">זבנג!</span></span></a></li>
+<li class="toclevel-1 tocsection-3"><a href="#The_attributes_on_these_span_tags_must_be_deleted_from_the_TOC"><span class="tocnumber">3</span> <span class="toctext"><span>The attributes on these span tags must be deleted from the TOC</span></span></a></li>
+<li class="toclevel-1 tocsection-4"><a href="#All_attributes_on_these_span_tags_must_be_deleted_from_the_TOC"><span class="tocnumber">4</span> <span class="toctext"><span>All attributes on these span tags must be deleted from the TOC</span></span></a></li>
+<li class="toclevel-1 tocsection-5"><a href="#Attributes_after_dir_on_these_span_tags_must_be_deleted_from_the_TOC"><span class="tocnumber">5</span> <span class="toctext"><span dir="ltr">Attributes after dir on these span tags must be deleted from the TOC</span></span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="C.2B.2B"><span dir="ltr">C++</span></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: C++">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id=".D7.96.D7.91.D7.A0.D7.92.21"><span dir="rtl">זבנג!</span></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: זבנג!">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="The_attributes_on_these_span_tags_must_be_deleted_from_the_TOC"><span style="font-style: italic">The attributes on these span tags must be deleted from the TOC</span></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: The attributes on these span tags must be deleted from the TOC">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="All_attributes_on_these_span_tags_must_be_deleted_from_the_TOC"><span style="font-style: italic" dir="ltr">All attributes on these span tags must be deleted from the TOC</span></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: All attributes on these span tags must be deleted from the TOC">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Attributes_after_dir_on_these_span_tags_must_be_deleted_from_the_TOC"><span dir="ltr" style="font-style: italic">Attributes after dir on these span tags must be deleted from the TOC</span></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: Attributes after dir on these span tags must be deleted from the TOC">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! article
+MediaWiki:Bug32057
+!! text
+== {{int:headline_sample}} ==
+!! endarticle
+
+!! test
+Bug 32057: Title needed when expanding <h> nodes.
+!! options
+title=[[Main Page]]
+!! wikitext
+{{int:Bug32057}}
+!! html
+<h2><span class="mw-headline" id="Headline_text">Headline text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&amp;action=edit&amp;section=1" title="Edit section: Headline text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+Strip marker in urlencode
+!! wikitext
+{{urlencode:x<nowiki/>y}}
+{{urlencode:x<nowiki/>y|wiki}}
+{{urlencode:x<nowiki/>y|path}}
+!! html
+<p>xy
+xy
+xy
+</p>
+!! end
+
+!! test
+Strip marker in lc
+!! wikitext
+{{lc:x<nowiki/>y}}
+!! html
+<p>xy
+</p>
+!! end
+
+!! test
+Strip marker in uc
+!! wikitext
+{{uc:x<nowiki/>y}}
+!! html
+<p>XY
+</p>
+!! end
+
+!! test
+Strip marker in formatNum
+!! wikitext
+{{formatnum:1<nowiki/>2}}
+{{formatnum:1<nowiki/>2|R}}
+!! html
+<p>12
+12
+</p>
+!! end
+
+!! test
+Check noCommafy in formatNum
+!! options
+language=be-tarask
+!! wikitext
+{{formatnum:123456.78}}
+{{formatnum:123456.78|NOSEP}}
+!! html
+<p>123 456,78
+123456.78
+</p>
+!! end
+
+!! test
+Wrong option for formatNum (bug 56199)
+!! wikitext
+{{formatnum:1,234.56|Random}}
+{{formatnum:1,234.56|EVERYTHING}}
+{{formatnum:1234.56|any argument that has the string 'NOSEP'}}
+!! html
+<p>1,234.56
+1,234.56
+1,234.56
+</p>
+!! end
+
+!! test
+Strip marker in grammar
+!! options
+language=fi
+!! wikitext
+{{grammar:elative|foo<nowiki/>bar}}
+!! html
+<p>foobarista
+</p>
+!! end
+
+!! test
+Strip marker in padleft
+!! wikitext
+{{padleft:|2|x<nowiki/>y}}
+!! html
+<p>xy
+</p>
+!! end
+
+!! test
+Strip marker in padright
+!! wikitext
+{{padright:|2|x<nowiki/>y}}
+!! html
+<p>xy
+</p>
+!! end
+
+!! test
+Strip marker in anchorencode
+!! wikitext
+{{anchorencode:x<nowiki/>y}}
+!! html
+<p>xy
+</p>
+!! end
+
+!! test
+nowiki inside link inside heading (bug 18295)
+!! wikitext
+==[[foo|x<nowiki>y</nowiki>z]]==
+!! html
+<h2><span class="mw-headline" id="xyz"><a href="/wiki/Foo" title="Foo">xyz</a></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: xyz">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+!! test
+new support for bdi element (bug 31817)
+!! wikitext
+<p dir="rtl" lang="he">ולדימיר לנין (ברוסית: <bdi lang="ru">Владимир Ленин</bdi>, 24 באפריל 1870–22 בינואר 1924) הוא מנהיג פוליטי קומוניסטי רוסי.</p>
+!! html
+<p dir="rtl" lang="he">ולדימיר לנין (ברוסית: <bdi lang="ru">Владимир Ленин</bdi>, 24 באפריל 1870–22 בינואר 1924) הוא מנהיג פוליטי קומוניסטי רוסי.</p>
+
+!!end
+
+!! test
+Ignore pipe between table row attributes
+!! wikitext
+{|
+| quux
+|- id=foo | style='color: red'
+| bar
+|}
+!! html
+<table>
+<tr>
+<td> quux
+</td></tr>
+<tr id="foo" style="color: red">
+<td> bar
+</td></tr></table>
+
+!! end
+
+!!test
+Gallery override link with WikiLink (bug 34852)
+!! wikitext
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=InterWikiLink
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/InterWikiLink"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!!test
+Gallery override link with absolute external link (bug 34852)
+!! wikitext
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=http://www.example.org
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="http://www.example.org"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!!test
+Gallery override link with malicious javascript (bug 34852)
+!! wikitext
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=" onclick="alert('malicious javascript code!');
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/%22_onclick%3D%22alert(%27malicious_javascript_code!%27);"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!!test
+Gallery with invalid title as link (bug 43964)
+!! wikitext
+<gallery>
+File:foobar.jpg|link=<
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!!test
+Language parser function
+!! wikitext
+{{#language:ar}}
+!! html
+<p>العربية
+</p>
+!! end
+
+!!test
+Padleft and padright as substr
+!! wikitext
+{{padleft:|3|abcde}}
+{{padright:|3|abcde}}
+!! html
+<p>abc
+abc
+</p>
+!! end
+
+!!test
+Special parser function
+!! wikitext
+{{#special:RandomPage}}
+{{#special:BaDtItLe}}
+{{#special:Foobar}}
+!! html
+<p>Special:Random
+Special:Badtitle
+Special:Foobar
+</p>
+!! end
+
+!!test
+Bug 34939 - Case insensitive link parsing ([HttP://])
+!! wikitext
+[HttP://MediaWiki.Org/]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="HttP://MediaWiki.Org/">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="HttP://MediaWiki.Org/"></a></p>
+!! end
+
+!!test
+Bug 34939 - Case insensitive link parsing ([HttP:// title])
+!! wikitext
+[HttP://MediaWiki.Org/ MediaWiki]
+!! html
+<p><a rel="nofollow" class="external text" href="HttP://MediaWiki.Org/">MediaWiki</a>
+</p>
+!! end
+
+!!test
+Bug 34939 - Case insensitive link parsing (HttP://)
+!! wikitext
+HttP://MediaWiki.Org/
+!! html/php
+<p><a rel="nofollow" class="external free" href="HttP://MediaWiki.Org/">HttP://MediaWiki.Org/</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="HttP://MediaWiki.Org/">HttP://MediaWiki.Org/</a></p>
+!! end
+
+!!test
+Disable TOC
+!! options
+notoc
+!! wikitext
+Lead
+== Section 1 ==
+== Section 2 ==
+== Section 3 ==
+== Section 4 ==
+== Section 5 ==
+!! html
+<p>Lead
+</p>
+
+<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=3" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Section_4">Section 4</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=4" title="Edit section: Section 4">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="Section_5">Section 5</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=5" title="Edit section: Section 5">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
+
+###
+### Parsoid-specific tests
+### Parsoid-PHP parser incompatibilities
+###
+!!test
+1. SOL-sensitive wikitext tokens as template-args
+!!options
+parsoid=wt2html,wt2wt
+!! wikitext
+{{echo|*a}}
+{{echo|#a}}
+{{echo|:a}}
+!! html
+<span about="#mwt1" typeof="mw:Transclusion">
+</span><ul about="#mwt1"><li>a</li>
+</ul>
+<span about="#mwt2" typeof="mw:Transclusion">
+</span><ol about="#mwt2"><li>a</li>
+</ol>
+<span about="#mwt3" typeof="mw:Transclusion">
+</span><dl about="#mwt3"><dd>a</dd>
+</dl>
+!!end
+
+#### -----------------------------------------------------------------
+#### Parsoid-specific functionality tests
+#### -----------------------------------------------------------------
+
+# Bug 63642: Formatting elt fixup is cleaned up.
+# We know wt2wt will fail, but we expect selser to pass.
+# Due to the nature of our testing, wt2wt and selser tests will enter the
+# blacklist and we'll catch selser regressions based on changes to the
+# blacklist entries for selser tests.
+!! test
+Bad treebuilder fixup of formatting elt is cleaned up
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+{|
+|
+<small>
+[[Image:Foobar.jpg|right|Test]]
+</small>
+|}
+!! html/parsoid
+<table>
+<tbody><tr><td>
+<p><small></small></p>
+<figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="220" width="1941"></a><figcaption><small>Test</small></figcaption></figure>
+<p></p></td></tr>
+</tbody></table>
+!! end
+
+#### ----------------------------------------------------------------
+#### Parsoid-only testing of Parsoid's impl of <ref> and <references>
+#### tags. Parsoid's output for these tags differs from that of the
+#### PHP parser.
+#### ----------------------------------------------------------------
+
+!!test
+Ref: 1. ref-location should be replaced with an index span
+!!options
+parsoid
+!! wikitext
+A <ref>foo</ref>
+B <ref name="x">foo</ref>
+C <ref name="y" />
+!! html
+<p>A <span about="#mwt1" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span>
+B <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{"name":"x"}}' id="cite_ref-x-2-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-x-2">[2]</a></span>
+C <span about="#mwt3" class="reference" data-mw='{"name":"ref","attrs":{"name":"y"}}' id="cite_ref-y-3-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-y-3">[3]</a></span></p>
+!!end
+
+!!test
+Ref: 2. ref-tags with identical names should all get the same index
+!!options
+parsoid
+!! wikitext
+A <ref name="x">foo</ref>
+B <ref name="x" />
+!! html
+<p>A <span about="#mwt1" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{"name":"x"}}' id="cite_ref-x-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-x-1">[1]</a></span>
+B <span about="#mwt2" class="reference" data-mw='{"name":"ref","attrs":{"name":"x"}}' id="cite_ref-x-1-1" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-x-1">[1]</a></span></p>
+!!end
+
+!!test
+Ref: 3. spaces in ref-names should be ignored
+!!options
+parsoid
+!! wikitext
+A <ref name="x">foo</ref>
+B <ref name=" x " />
+C <ref name= x />
+!! html
+<p>A <span about="#mwt1" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{"name":"x"}}' id="cite_ref-x-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-x-1">[1]</a></span>
+B <span about="#mwt2" class="reference" data-mw='{"name":"ref","attrs":{"name":"x"}}' id="cite_ref-x-1-1" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-x-1">[1]</a></span>
+C <span about="#mwt3" class="reference" data-mw='{"name":"ref","attrs":{"name":"x"}}' id="cite_ref-x-1-2" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-x-1">[1]</a></span></p>
+!!end
+
+!!test
+Ref: 4. 'constructor' should be accepted as a valid ref-name
+(NOTE: constructor is a predefined property in JS and constructor as a ref-name can clash with it if not handled properly)
+!!options
+parsoid
+!! wikitext
+A <ref name="constructor">foo</ref>
+!! html
+<p>A <span about="#mwt1" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{"name":"constructor"}}' id="cite_ref-constructor-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-constructor-1">[1]</a></span></p>
+!!end
+
+!!test
+Ref: 5. body should accept generic wikitext
+!!options
+parsoid
+!! wikitext
+A <ref>
+ This is a '''[[bolded link]]''' and this is a {{echo|transclusion}}
+</ref>
+
+<references />
+!! html
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"This is a &lt;b data-parsoid=&#39;{\"dsr\":[19,40,3,3]}&#39;>&lt;a rel=\"mw:WikiLink\" href=\"./Bolded_link\" title=\"Bolded link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Bolded_link\"},\"sa\":{\"href\":\"bolded link\"},\"dsr\":[22,37,2,2]}&#39;>bolded link&lt;/a>&lt;/b> and this is a &lt;span about=\"#mwt3\" typeof=\"mw:Transclusion\" data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"transclusion\"}},\"i\":0}}]}&#39; data-parsoid=&#39;{\"pi\":[[{\"k\":\"1\",\"spc\":[\"\",\"\",\"\",\"\"]}]],\"dsr\":[55,76,null,null]}&#39;>transclusion&lt;/span>\n"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span></p>
+
+<ol class="references" typeof="mw:Extension/references" about="#mwt4" data-mw='{"name":"references","attrs":{}}'>
+<li about="#cite_note-1" id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> This is a <b><a rel="mw:WikiLink" href="./Bolded_link" title="Bolded link">bolded link</a></b> and this is a <span about="#mwt3" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"transclusion"}},"i":0}}]}'>transclusion</span>
+</li>
+</ol>
+!!end
+
+!!test
+Ref: 6. indent-pres should not be output in ref-body
+!!options
+parsoid
+!! wikitext
+A <ref>
+ foo
+ bar
+ baz
+</ref>
+
+<references />
+!! html
+<p>A <span about="#mwt1" class="reference" data-mw='{"name":"ref","body":{"html":"foo\n bar\n baz\n"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span></p>
+
+<ol class="references" typeof="mw:Extension/references" about="#mwt3" data-mw='{"name":"references","attrs":{}}'>
+<li about="#cite_note-1" id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> foo
+ bar
+ baz
+</li>
+</ol>
+!!end
+
+!!test
+Ref: 7. No p-wrapping in ref-body
+!!options
+parsoid
+!! wikitext
+A <ref>
+foo
+
+bar
+
+
+baz
+
+
+
+booz
+</ref>
+
+<references />
+!! html
+<p>A <span about="#mwt1" class="reference" data-mw='{"name":"ref","body":{"html":"foo\n\nbar\n\n\nbaz\n\n\n\nbooz\n"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span></p>
+
+<ol about="#mwt2" class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{}}'>
+<li about="#cite_note-1" id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> foo
+
+bar
+
+
+baz
+
+
+
+booz
+</li>
+</ol>
+!!end
+
+!!test
+Ref: 8. transclusion wikitext has lower precedence
+!!options
+parsoid
+!! wikitext
+A <ref> foo {{echo|</ref> B C}}
+
+<references />
+!! html
+<p>A <span class="reference" data-mw="{&quot;name&quot;:&quot;ref&quot;,&quot;body&quot;:{&quot;html&quot;:&quot;foo <span typeof=\&quot;mw:Nowiki\&quot; data-parsoid='{\&quot;src\&quot;:\&quot;{{\&quot;,\&quot;dsr\&quot;:[12,14,0,0]}'>{{</span>echo|&quot;},&quot;attrs&quot;:{}}" id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span> B C<span typeof="mw:Nowiki">}}</span></p>
+<ol class="references" typeof="mw:Extension/references" data-mw="{&quot;name&quot;:&quot;references&quot;,&quot;attrs&quot;:{}}">
+<li id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> foo <span typeof="mw:Nowiki">{{</span>echo|</li>
+</ol>
+!!end
+
+!!test
+Ref: 9. unclosed comments should not leak out of ref-body
+!!options
+parsoid
+!! wikitext
+A <ref> foo <!--</ref> B C
+<references />
+!! html
+<p>A <span class="reference" data-mw='{"name":"ref","body":{"html":"foo &lt;!---->"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span> B C</p>
+<ol class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{}}'>
+<li id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> foo </li>
+</ol>
+!!end
+
+!!test
+Ref: 10. Unclosed HTML tags should not leak out of ref-body
+!!options
+parsoid
+!! wikitext
+A <ref> <b> foo </ref> B C
+
+<references />
+!! html
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"&lt;b data-parsoid=&#39;{\"stx\":\"html\",\"autoInsertedEnd\":true,\"dsr\":[8,16,3,0]}&#39;> foo &lt;/b>"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref> &lt;b> foo &lt;/ref>"}'><a href="#cite_note-1">[1]</a></span> B C</p>
+
+
+<ol class="references" typeof="mw:Extension/references" about="#mwt4" data-parsoid='{"src":"&lt;references />"}' data-mw='{"name":"references","attrs":{}}'>
+<li about="#cite_note-1" id="cite_note-1" data-parsoid="{}"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> <b data-parsoid='{"stx":"html","autoInsertedEnd":true}'> foo </b></li>
+</ol>
+!!end
+
+!!test
+Ref: 11. ref-tags acts like an inline element wrt P-wrapping
+!!options
+parsoid
+!! wikitext
+A <ref>foo</ref> B
+C <ref>bar</ref> D
+!! html
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref>foo&lt;/ref>"}'><a href="#cite_note-1">[1]</a></span> B
+C <span about="#mwt4" class="reference" data-mw='{"name":"ref","body":{"html":"bar"},"attrs":{}}' id="cite_ref-2-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref>bar&lt;/ref>"}'><a href="#cite_note-2">[2]</a></span> D</p>
+!!end
+
+!!test
+Ref: 12. ref-tags act as trailing newline migration barrier
+!!options
+parsoid
+!! wikitext
+<!--the newline at the end of this line moves out of the p-tag-->a
+
+b<!--the newline at the end of this line stays inside the p-tag--> <ref />
+<ref />
+
+c
+!! html
+<p><!--the newline at the end of this line moves out of the p-tag-->a</p>
+
+
+<p>b<!--the newline at the end of this line stays inside the p-tag--> <span about="#mwt1" class="reference" data-mw='{"name":"ref","attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span>
+<span about="#mwt2" class="reference" data-mw='{"name":"ref","attrs":{}}' id="cite_ref-2-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-2">[2]</a></span></p>
+
+
+<p>c</p>
+!!end
+
+!!test
+Ref: 13. ref-tags are not SOL-transparent and block indent-pres
+!!options
+parsoid
+!! wikitext
+<ref>foo</ref> A
+<ref>bar
+</ref> B
+!! html
+<p><span about="#mwt1" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span> A
+<span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"bar\n"},"attrs":{}}' id="cite_ref-2-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-2">[2]</a></span> B</p>
+!!end
+
+!!test
+Ref: 14. A nested ref-tag should be emitted as plain text
+!!options
+parsoid
+!! wikitext
+<ref>foo <ref>bar</ref> baz</ref>
+
+<references />
+!! html
+<p><span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo &amp;lt;ref>bar&amp;lt;/ref> baz"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref>foo &lt;ref>bar&lt;/ref> baz&lt;/ref>"}'><a href="#cite_note-1">[1]</a></span></p>
+
+<ol class="references" typeof="mw:Extension/references" about="#mwt5" data-parsoid='{"src":"&lt;references />"}' data-mw='{"name":"references","attrs":{}}'>
+<li about="#cite_note-1" id="cite_note-1" data-parsoid="{}"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> foo &lt;ref>bar&lt;/ref> baz</li>
+</ol>
+!!end
+
+!!test
+Ref: 15. ref-tags with identical names should get identical indexes
+!!options
+parsoid
+!! wikitext
+A1 <ref name="a">foo</ref> A2 <ref name="a" />
+B1 <ref name="b" /> B2 <ref name="b">bar</ref>
+
+<references />
+!! html
+<p>A1 <span about="#mwt3" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{"name":"a"}}' id="cite_ref-a-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-a-1">[1]</a></span> A2 <span about="#mwt4" class="reference" data-mw='{"name":"ref","attrs":{"name":"a"}}' id="cite_ref-a-1-1" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-a-1">[1]</a></span>
+B1 <span about="#mwt7" class="reference" data-mw='{"name":"ref","attrs":{"name":"b"}}' id="cite_ref-b-2-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-b-2">[2]</a></span> B2 <span about="#mwt8" class="reference" data-mw='{"name":"ref","body":{"html":"bar"},"attrs":{"name":"b"}}' id="cite_ref-b-2-1" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-b-2">[2]</a></span></p>
+
+<ol about="#mwt10" class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-a-1" id="cite_note-a-1"><span rel="mw:referencedBy">↑ <a href="#cite_ref-a-1-0">1.0</a> <a href="#cite_ref-a-1-1">1.1</a></span> foo</li><li about="#cite_note-b-2" id="cite_note-b-2"><span rel="mw:referencedBy">↑ <a href="#cite_ref-b-2-0">2.0</a> <a href="#cite_ref-b-2-1">2.1</a></span> bar</li>
+</ol>
+!!end
+
+## We don't bother wt2wt-ing non-standard whitespace
+!!test
+Ref: 16. Tokenizer should accept non-standard whitespace in <ref> and </ref> tags
+!!options
+parsoid=wt2html
+!! wikitext
+A <ref >foo</ref >
+
+<references />
+!! html
+<p>A <span class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span></p>
+
+<ol class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{}}'>
+<li id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> foo</li></ol>
+!!end
+
+!!test
+References: 1. references tag without any refs should be handled properly
+!!options
+parsoid
+!! wikitext
+<references />
+!! html
+<ol about="#mwt2" class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{}}'></ol>
+!!end
+
+!!test
+References: 2. references tag with group only outputs references from that group
+!!options
+parsoid
+!! wikitext
+A <ref group="a">foo</ref>
+B <ref group="b">bar</ref>
+
+<references group="a" />
+!! html
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{"group":"a"}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[a 1]</a></span>
+B <span about="#mwt4" class="reference" data-mw='{"name":"ref","body":{"html":"bar"},"attrs":{"group":"b"}}' id="cite_ref-2-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-2">[b 1]</a></span></p>
+
+<ol about="#mwt6" class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{"group":"a"}}'><li about="#cite_note-1" id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> foo</li>
+</ol>
+!!end
+
+!!test
+References: 3. ref list should be cleared after processing references
+!!options
+parsoid
+!! wikitext
+A <ref>foo</ref>
+
+<references />
+
+B <ref>bar</ref>
+
+<references />
+!! html
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span></p>
+
+<ol about="#mwt4" class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> foo</li>
+</ol>
+
+<p>B <span about="#mwt6" class="reference" data-mw='{"name":"ref","body":{"html":"bar"},"attrs":{}}' id="cite_ref-2-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-2">[1]</a></span></p>
+
+<ol about="#mwt8" class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-2" id="cite_note-2"><span rel="mw:referencedBy"><a href="#cite_ref-2-0">↑</a></span> bar</li>
+</ol>
+!!end
+
+!!test
+References: 4. only referenced group should be cleared after processing references
+!!options
+parsoid
+!! wikitext
+A <ref group="a">afoo</ref>
+B <ref>bfoo</ref>
+
+<references group="a" />
+
+C <ref>cfoo</ref>
+
+<references />
+!! html
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"afoo"},"attrs":{"group":"a"}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[a 1]</a></span>
+B <span about="#mwt4" class="reference" data-mw='{"name":"ref","body":{"html":"bfoo"},"attrs":{}}' id="cite_ref-2-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"<ref>bfoo</ref>"}'><a href="#cite_note-2">[1]</a></span></p>
+
+<ol about="#mwt6" class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{"group":"a"}}'><li about="#cite_note-1" id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> afoo</li>
+</ol>
+
+<p>C <span about="#mwt8" class="reference" data-mw='{"name":"ref","body":{"html":"cfoo"},"attrs":{}}' id="cite_ref-3-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-3">[2]</a></span></p>
+
+<ol about="#mwt10" class="references" typeof="mw:Extension/references" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-2" id="cite_note-2"><span rel="mw:referencedBy"><a href="#cite_ref-2-0">↑</a></span> bfoo</li><li about="#cite_note-3" id="cite_note-3"><span rel="mw:referencedBy"><a href="#cite_ref-3-0">↑</a></span> cfoo</li>
+</ol>
+!!end
+
+!!test
+References: 5. ref tags in references should be processed while ignoring all other content
+!!options
+parsoid
+!! wikitext
+A <ref name="a" />
+B <ref name="b">bar</ref>
+
+<references>
+<ref name="a">foo</ref>
+This should just get lost.
+</references>
+!! html
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","attrs":{"name":"a"}}' id="cite_ref-a-1-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref name=\"a\" />"}'><a href="#cite_note-a-1">[1]</a></span>
+B <span about="#mwt4" class="reference" data-mw='{"name":"ref","body":{"html":"bar"},"attrs":{"name":"b"}}' id="cite_ref-b-2-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref name=\"b\">bar&lt;/ref>"}'><a href="#cite_note-b-2">[2]</a></span></p>
+
+
+<ol class="references" typeof="mw:Extension/references" about="#mwt6" data-parsoid='{"src":"&lt;references>\n&lt;ref name=\"a\">foo&lt;/ref>\nThis should just get lost.\n&lt;/references>"}' data-mw='{"name":"references","body":{"extsrc":"&lt;ref name=\"a\">foo&lt;/ref>\nThis should just get lost.","html":"\n&lt;span about=\"#mwt8\" class=\"reference\" data-mw=&#39;{\"name\":\"ref\",\"body\":{\"html\":\"foo\"},\"attrs\":{\"name\":\"a\"}}&#39; rel=\"dc:references\" typeof=\"mw:Extension/ref\">&lt;a href=\"#cite_note-a-1\">[1]&lt;/a>&lt;/span>\n"},"attrs":{}}'>
+<li about="#cite_note-a-1" id="cite_note-a-1" data-parsoid="{}"><span rel="mw:referencedBy"><a href="#cite_ref-a-1-0">↑</a></span> foo</li>
+<li about="#cite_note-b-2" id="cite_note-b-2" data-parsoid="{}"><span rel="mw:referencedBy"><a href="#cite_ref-b-2-0">↑</a></span> bar</li>
+</ol>
+!!end
+
+!!test
+References: 6. <references /> from a transclusion
+!!options
+parsoid
+!! wikitext
+<ref>Foo</ref> {{echo|<references />}}
+!! html
+<span about="#mwt3" class="reference" data-mw='{"name":"ref","body":{"html":"Foo"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span> <ol class="references" typeof="mw:Extension/references mw:Transclusion" about="#mwt4" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;references />"}},"i":0}}]}'><li about="#cite_note-1" id="cite_note-1"><span rel="mw:referencedBy"><a href="#cite_ref-1-0">↑</a></span> Foo</li></ol>
+!!end
+
+!! test
+References: 7. Multiple references tags (one without and one with nested refs) should be correctly handled
+!! options
+parsoid
+!! wikitext
+A <ref>foo bar for a</ref>
+B <ref group="X" name="b" />
+
+<references />
+
+<references group="X">
+<ref name="b">foo</ref>
+</references>
+!! html
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo bar for a"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref>foo bar for a&lt;/ref>"}'><a href="#cite_note-1" data-parsoid="{}">[1]</a></span>
+B <span about="#mwt4" class="reference" data-mw='{"name":"ref","attrs":{"group":"X","name":"b"}}' id="cite_ref-b-2-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref group=\"X\" name=\"b\" />"}'><a href="#cite_note-b-2" data-parsoid="{}">[X 1]</a></span></p>
+
+<ol class="references" typeof="mw:Extension/references" about="#mwt6" data-parsoid='{"src":"&lt;references />"}' data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-1-0" data-parsoid="{}">↑</a></span> foo bar for a</li></ol>
+
+<ol class="references" typeof="mw:Extension/references" about="#mwt8" data-parsoid='{"src":"&lt;references group=\"X\">\n&lt;ref name=\"b\">foo&lt;/ref>\n&lt;/references>","group":"X"}' data-mw='{"name":"references","body":{"extsrc":"&lt;ref name=\"b\">foo&lt;/ref>","html":"\n&lt;span about=\"#mwt10\" class=\"reference\" data-mw=&#39;{\"name\":\"ref\",\"body\":{\"html\":\"foo\"},\"attrs\":{\"name\":\"b\"}}&#39; rel=\"dc:references\" typeof=\"mw:Extension/ref\">&lt;a href=\"#cite_note-b-2\">[X 1]&lt;/a>&lt;/span>\n"},"attrs":{"group":"X"}}'><li about="#cite_note-b-2" id="cite_note-b-2" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-b-2-0" data-parsoid="{}">↑</a></span> foo</li></ol>
+!! end
+
+!! test
+Entities in ref name
+!! options
+parsoid
+!! wikitext
+<ref name="test &amp; me">hi</ref>
+!! html
+<p data-parsoid='{}'><span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"hi"},"attrs":{"name":"test &amp; me"}}' id="cite_ref-test &amp; me-1-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref name=\"test &amp;amp; me\">hi&lt;/ref>"}'><a href="#cite_note-test &amp; me-1" data-parsoid="{}">[1]</a></span></p>
+!! end
+
+# This test is wt2html only because we're permitting the serializer to produce
+# dirty diffs, normalizing the unclosed references to the self-closed version.
+!! test
+Generate references for unclosed references tag
+!! options
+parsoid=wt2html
+!! wikitext
+a<ref>foo</ref>
+
+<references>
+!! html
+<p data-parsoid='{}'>a<span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref>foo&lt;/ref>"}'><a href="#cite_note-1" data-parsoid="{}">[1]</a></span></p>
+
+
+<ol class="references" typeof="mw:Extension/references" about="#mwt4" data-parsoid='{"src":"&lt;references>"}' data-mw='{"name":"references","attrs":{}}'>
+<li about="#cite_note-1" id="cite_note-1" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-1-0" data-parsoid="{}">↑</a></span> foo</li></ol>
+!! end
+
+!! test
+New reference serializes on its own line
+!! options
+parsoid=wt2wt,html2wt
+!! wikitext
+foo
+<references />
+!! html
+foo<ol class="references" typeof="mw:Extension/references" about="#mwt2" data-mw='{"name":"references","attrs":{}}'></ol>
+!! end
+
+#### ----------------------------------------------------------------
+#### The following section of tests are primarily to test
+#### wikitext escaping capabilities of Parsoid. Given that
+#### escaping can be done any number of ways, the wikitext (input)
+#### is always adjusted to reflect how Parsoid adds nowiki
+#### escape tags.
+####
+#### We are marking several tests as parsoid-only since the
+#### HTML in the result section is different from what the
+#### PHP parser generates for it.
+#### ----------------------------------------------------------------
+
+
+#### --------------- Headings ---------------
+#### 0. Unnested
+#### 1. Nested inside html <h1>=foo=</h1>
+#### 2. Outside heading nest on a single line <h1>foo</h1>*bar
+#### 3. Nested inside html with wikitext split by html tags
+#### 4. No escape needed
+#### 5. Empty headings <h1></h1>
+#### 6. Heading chars in SOL context
+#### ----------------------------------------
+!! test
+Headings: 0. Unnested
+!! options
+parsoid
+!! wikitext
+<nowiki>=foo=</nowiki>
+
+<nowiki> =foo= </nowiki>
+<!--cmt-->
+<nowiki>=foo=</nowiki>
+
+=foo''a''<nowiki>=</nowiki>
+!! html
+<p><span typeof="mw:Nowiki">=foo=</span></p>
+
+<p><span typeof="mw:Nowiki"> =foo= </span>
+<!--cmt-->
+<span typeof="mw:Nowiki">=foo=</span></p>
+
+<p>=foo<i>a</i><span typeof="mw:Nowiki">=</span></p>
+!!end
+
+!! test
+Headings: 1. Nested inside html
+(New headings and existing headings are handled differently)
+!! options
+parsoid=html2wt
+!! wikitext
+= =foo= =
+
+== =foo= ==
+
+=== =foo= ===
+
+=<nowiki>=foo=</nowiki>=
+==<nowiki>=foo=</nowiki>==
+===<nowiki>=foo=</nowiki>===
+====<nowiki>=foo=</nowiki>====
+=====<nowiki>=foo=</nowiki>=====
+======<nowiki>=foo=</nowiki>======
+
+!! html
+<h1>=foo=</h1>
+<h2>=foo=</h2>
+<h3>=foo=</h3>
+
+<h1 data-parsoid='{}'>=foo=</h1>
+<h2 data-parsoid='{}'>=foo=</h2>
+<h3 data-parsoid='{}'>=foo=</h3>
+<h4 data-parsoid='{}'>=foo=</h4>
+<h5 data-parsoid='{}'>=foo=</h5>
+<h6 data-parsoid='{}'>=foo=</h6>
+!!end
+
+!! test
+Headings: 2. Outside heading nest on a single line <h1>foo</h1>*bar
+!! options
+parsoid=html2wt
+!! wikitext
+= foo =
+<nowiki>*</nowiki>bar
+
+= foo =
+=bar
+
+= foo =
+<nowiki>=bar=</nowiki>
+!! html
+<h1>foo</h1>*bar
+<h1>foo</h1>=bar
+<h1>foo</h1>=bar=
+!!end
+
+!! test
+Headings: 3. Nested inside html with wikitext split by html tags
+!! options
+parsoid=html2wt
+!! wikitext
+= ='''bold'''<nowiki>foo=</nowiki> =
+!! html
+<h1>=<b>bold</b><span typeof="mw:Nowiki">foo=</span></h1>
+!!end
+
+!! test
+Headings: 4a. No escaping needed (testing just h1 and h2)
+!! options
+parsoid=html2wt
+!! wikitext
+= =foo =
+
+= foo= =
+
+= =foo= =
+
+= =foo= bar =
+
+== =foo ==
+
+== foo= ==
+
+= ''=''foo= =
+
+= <nowiki>=</nowiki> =
+!! html
+<h1>=foo</h1>
+<h1>foo=</h1>
+<h1> =foo= </h1>
+<h1>=foo= bar</h1>
+<h2>=foo</h2>
+<h2>foo=</h2>
+<h1><i>=</i>foo=</h1>
+<h1><span typeof="mw:Nowiki">=</span></h1>
+!!end
+
+!! test
+Headings: 4b. No escaping needed (inside p-tags)
+!! options
+parsoid=html2wt
+!! wikitext
+===
+=foo= x
+=foo= <s></s>
+!! html
+<p>===
+=foo= x
+=foo= <s></s>
+</p>
+!!end
+
+!! test
+Headings: 5. Empty headings
+!! options
+parsoid
+!! wikitext
+=<nowiki/>=
+
+==<nowiki/>==
+
+===<nowiki/>===
+
+====<nowiki/>====
+
+=====<nowiki/>=====
+
+======<nowiki/>======
+!! html
+<h1></h1>
+<h2></h2>
+<h3></h3>
+<h4></h4>
+<h5></h5>
+<h6></h6>
+!!end
+
+!! test
+Headings: 6a. Heading chars in SOL context (with trailing spaces)
+!! options
+parsoid
+!! wikitext
+<nowiki>=a=</nowiki>
+
+<nowiki>=a=</nowiki>
+
+<nowiki>=a=</nowiki>
+
+<nowiki>=a=</nowiki>
+!! html
+<p>=a=</p>
+<p>=a= </p>
+<p>=a= </p>
+<p>=a= </p>
+!!end
+
+!! test
+Headings: 6b. Heading chars in SOL context (with trailing newlines)
+!! options
+parsoid
+!! wikitext
+<nowiki>=a=
+b</nowiki>
+
+<nowiki>=a=
+b</nowiki>
+
+<nowiki>=a=
+b</nowiki>
+
+<nowiki>=a=
+b</nowiki>
+!! html
+<p>=a=
+b</p>
+<p>=a=
+b</p>
+<p>=a=
+b</p>
+<p>=a=
+b</p>
+</p>
+!!end
+
+!! test
+Headings: 6c. Heading chars in SOL context (leading newline break)
+!! options
+parsoid
+!! wikitext
+a
+<nowiki>=b=</nowiki>
+!! html
+<p>a
+=b=</p>
+!!end
+
+!! test
+Headings: 6d. Heading chars in SOL context (with interspersed comments)
+!! options
+parsoid
+!! wikitext
+<!--c0--><nowiki>=a=</nowiki>
+
+<!--c1--><nowiki>=a=</nowiki> <!--c2--> <!--c3-->
+!! html
+<p><!--c0-->=a=</p>
+<p><!--c1-->=a= <!--c2--> <!--c3--></p>
+!!end
+
+!! test
+Headings: 6d. Heading chars in SOL context (No escaping needed)
+!! options
+parsoid=html2wt
+!! wikitext
+=a=<div>b</div>
+!! html
+=a=<div>b</div>
+!!end
+
+#### --------------- Lists ---------------
+#### 0. Outside nests (*foo, etc.)
+#### 1. Nested inside html <ul><li>*foo</li></ul>
+#### 2. Inside definition lists
+#### 3. Only bullets at start should be escaped
+#### 4. No escapes needed
+#### 5. No unnecessary escapes
+#### 6. Escape bullets in SOL position
+#### 7. Escape bullets in a multi-line context
+#### ----------------------------------------
+
+!! test
+Lists: 0. Outside nests
+!! wikitext
+<nowiki>*</nowiki>foo
+
+<nowiki>#</nowiki>foo
+
+<nowiki>;Foo:</nowiki>bar
+!! html
+<p>*foo
+</p><p>#foo
+</p><p>;Foo:bar
+</p>
+!!end
+
+!! test
+Lists: 1. Nested inside html
+!! wikitext
+*<nowiki>*foo</nowiki>
+
+*<nowiki>#foo</nowiki>
+
+*<nowiki>:foo</nowiki>
+
+*<nowiki>;foo</nowiki>
+
+#<nowiki>*foo</nowiki>
+
+#<nowiki>#foo</nowiki>
+
+#<nowiki>:foo</nowiki>
+
+#<nowiki>;foo</nowiki>
+!! html
+<ul><li>*foo</li></ul>
+<ul><li>#foo</li></ul>
+<ul><li>:foo</li></ul>
+<ul><li>;foo</li></ul>
+<ol><li>*foo</li></ol>
+<ol><li>#foo</li></ol>
+<ol><li>:foo</li></ol>
+<ol><li>;foo</li></ol>
+
+!!end
+
+!! test
+Lists: 2. Inside definition lists
+!! wikitext
+;<nowiki>;foo</nowiki>
+
+;<nowiki>:foo</nowiki>
+
+;<nowiki>:foo</nowiki>
+:bar
+
+:<nowiki>:foo</nowiki>
+!! html
+<dl><dt>;foo</dt></dl>
+<dl><dt>:foo</dt></dl>
+<dl><dt>:foo</dt>
+<dd>bar</dd></dl>
+<dl><dd>:foo</dd></dl>
+
+!!end
+
+!! test
+Lists: 3. Only bullets at start of text should be escaped
+!! wikitext
+*<nowiki>*foo*bar</nowiki>
+
+*<nowiki>*foo</nowiki>''it''*bar
+!! html
+<ul><li>*foo*bar</li></ul>
+<ul><li>*foo<i>it</i>*bar</li></ul>
+
+!!end
+
+!! test
+Lists: 4. No escapes needed
+!! options
+parsoid
+!! wikitext
+*foo*bar
+
+*''foo''*bar
+
+*[[Foo]]: bar
+
+*[[Foo]]*bar
+!! html
+<ul>
+<li>foo*bar
+</li>
+</ul>
+<ul>
+<li><i>foo</i>*bar
+</li>
+</ul>
+<ul>
+<li><a rel="mw:WikiLink" href="Foo" title="Foo">Foo</a>: bar
+</li>
+</ul>
+<ul>
+<li><a rel="mw:WikiLink" href="Foo" title="Foo">Foo</a>*bar
+</li>
+</ul>
+!!end
+
+!! test
+Lists: 5. No unnecessary escapes
+!! wikitext
+* bar <span><nowiki>[[foo]]</nowiki></span>
+
+*=bar <span><nowiki>[[foo]]</nowiki></span>
+
+*[[bar <span><nowiki>[[foo]]</nowiki></span>
+
+*]]bar <span><nowiki>[[foo]]</nowiki></span>
+
+*=bar <span>foo]]</span>=
+
+* <s></s>: a
+!! html
+<ul><li> bar <span>[[foo]]</span></li></ul>
+<ul><li>=bar <span>[[foo]]</span></li></ul>
+<ul><li>[[bar <span>[[foo]]</span></li></ul>
+<ul><li>]]bar <span>[[foo]]</span></li></ul>
+<ul><li>=bar <span>foo]]</span>=</li></ul>
+<ul><li> <s></s>: a</li></ul>
+
+!!end
+
+!! test
+Lists: 6. Escape bullets in SOL position
+!! options
+parsoid
+!! wikitext
+<!--cmt--><nowiki>*foo</nowiki>
+!! html
+<p><!--cmt--><span typeof="mw:Nowiki">*foo</span></p>
+!!end
+
+!! test
+Lists: 7. Escape bullets in a multi-line context
+!! wikitext
+a
+<nowiki>*</nowiki>b
+!! html
+<p>a
+*b
+</p>
+!!end
+
+#### --------------- HRs ---------------
+#### 1. Single line
+#### -----------------------------------
+
+!! test
+HRs: 1. Single line
+!! options
+parsoid
+!! wikitext
+----<nowiki>----</nowiki>
+----=foo=
+----*foo
+!! html
+<hr><span typeof="mw:Nowiki">----</span>
+<hr>=foo=
+<hr>*foo
+!! end
+
+#### --------------- Tables ---------------
+#### 1a. Simple example
+#### 1b. No escaping needed (!foo)
+#### 1c. No escaping needed (|foo)
+#### 1d. No escaping needed (|}foo)
+####
+#### 2a. Nested in td (<td>foo|bar</td>)
+#### 2b. Nested in td (<td>foo||bar</td>)
+#### 2c. Nested in td -- no escaping needed(<td>foo!!bar</td>)
+####
+#### 3a. Nested in th (<th>foo!bar</th>)
+#### 3b. Nested in th (<th>foo!!bar</th>)
+#### 3c. Nested in th -- no escaping needed(<th>foo||bar</th>)
+####
+#### 4a. Escape -
+#### 4b. Escape +
+#### 4c. No escaping needed
+#### --------------------------------------
+
+!! test
+Tables: 1a. Simple example
+!! wikitext
+<nowiki>{|
+|}</nowiki>
+!! html
+<p>{|
+|}
+</p>
+!! end
+
+!! test
+Tables: 1b. No escaping needed
+!! wikitext
+!foo
+!! html
+<p>!foo
+</p>
+!! end
+
+!! test
+Tables: 1c. No escaping needed
+!! wikitext
+|foo
+!! html
+<p>|foo
+</p>
+!! end
+
+!! test
+Tables: 1d. No escaping needed
+!! wikitext
+|}foo
+!! html
+<p>|}foo
+</p>
+!! end
+
+!! test
+Tables: 2a. Nested in td
+!! options
+parsoid=html2wt
+!! wikitext
+{|
+|<nowiki>foo|bar</nowiki>
+|-
+|x<div><nowiki>a|b</nowiki></div>
+|}
+!! html
+<table><tbody><tr>
+<td>foo|bar</td></tr>
+<tr><td>x<div>a|b</div></td>
+</tbody></table>
+!! end
+
+!! test
+Tables: 2b. Nested in td
+!! options
+parsoid
+!! wikitext
+{|
+|<nowiki>foo||bar</nowiki>
+|''it''<nowiki>foo||bar</nowiki>
+|}
+!! html
+<table><tbody><tr>
+<td><span typeof="mw:Nowiki">foo||bar</span></td>
+<td><i>it</i><span typeof="mw:Nowiki">foo||bar</span></td></tr></tbody></table>
+!! end
+
+!! test
+Tables: 2c. Nested in td -- no escaping needed
+!! options
+parsoid
+!! wikitext
+{|
+|foo!!bar
+|}
+!! html
+<table><tbody><tr><td>foo!!bar
+</td></tr></tbody></table>
+
+!! end
+
+!! test
+Tables: 3a. Nested in th
+!! options
+parsoid
+!! wikitext
+{|
+!foo!bar
+|}
+!! html
+<table><tbody><tr><th>foo!bar
+</th></tr></tbody></table>
+
+!! end
+
+!! test
+Tables: 3b. Nested in th
+!! options
+parsoid
+!! wikitext
+{|
+!<nowiki>foo!!bar</nowiki>
+|}
+!! html
+<table>
+<tbody><tr><th><span typeof="mw:Nowiki">foo!!bar</span></th></tr>
+</tbody></table>
+!! end
+
+!! test
+Tables: 3c. Nested in th -- no escaping needed
+!! options
+parsoid
+!! wikitext
+{|
+!<nowiki>foo||bar</nowiki>
+|}
+!! html
+<table><tbody><tr>
+<th><span typeof="mw:Nowiki">foo||bar</span></th></tr></tbody></table>
+!! end
+
+!! test
+Tables: 4a. Escape -
+!! options
+parsoid
+!! wikitext
+{|
+!-bar
+|-
+|<nowiki>-bar</nowiki>
+|}
+!! html
+<table><tbody>
+<tr><th>-bar</th></tr>
+<tr>
+<td><span typeof="mw:Nowiki">-bar</span></td></tr></tbody></table>
+!! end
+
+!! test
+Tables: 4b. Escape +
+!! options
+parsoid
+!! wikitext
+{|
+!+bar
+|-
+|<nowiki>+bar</nowiki>
+|}
+!! html
+<table><tbody>
+<tr><th>+bar</th></tr>
+<tr>
+<td><span typeof="mw:Nowiki">+bar</span></td></tr></tbody></table>
+!! end
+
+!! test
+Tables: 4c. No escaping needed
+!! options
+parsoid
+!! wikitext
+{|
+|foo-bar
+|foo+bar
+|-
+|''foo''-bar
+|''foo''+bar
+|-
+|foo
+bar|baz
++bar
+-bar
+|-
+|x
+<div>a|b</div>
+|}
+!! html
+<table><tbody>
+<tr><td>foo-bar</td><td>foo+bar</td></tr>
+<tr><td><i>foo</i>-bar</td><td><i>foo</i>+bar</td></tr>
+<tr><td>foo
+<p>bar|baz
++bar
+-bar</p></td></tr>
+<tr><td>x
+<div>a|b</div></td>
+</tbody></table>
+!! end
+
+!! test
+Tables: 4d. No escaping needed
+!! options
+parsoid
+!! wikitext
+{|
+|[[Foo]]-bar
+||+1
+||-2
+|}
+!! html
+<table>
+<tbody><tr><td><a rel="mw:WikiLink" href="./Foo" title="Foo">Foo</a>-bar</td>
+<td data-parsoid='{"startTagSrc":"|","attrSepSrc":"|"}'>+1</td>
+<td data-parsoid='{"startTagSrc":"|","attrSepSrc":"|"}'>-2</td></tr>
+</tbody></table>
+!! end
+
+!! test
+Tables: Digest broken attributes on table and tr tag
+!! options
+parsoid=wt2html
+!! wikitext
+{| || |} ++
+|- || || ++ --
+|- > [
+|}
+!! html
+<table>
+<tbody>
+<tr></tr>
+<tr></tr>
+</tbody></table>
+!! end
+
+#### --------------- Links ----------------
+#### 1. Quote marks in link text
+#### 2. Wikilinks: Escapes needed
+#### 3. Wikilinks: No escapes needed
+#### 4. Extlinks: Escapes needed
+#### 5. Extlinks: No escapes needed
+#### --------------------------------------
+!! test
+Links 1. Quote marks in link text
+!! options
+parsoid
+!! wikitext
+[[Foo|Foo<nowiki>''boo''</nowiki>]]
+!! html
+<a rel="mw:WikiLink" href="Foo">Foo''boo''</a>
+!! end
+
+!! test
+Links 2. WikiLinks: Escapes needed
+!! options
+parsoid
+!! wikitext
+[[Foo|[Foobar]]]
+[[Foo|<nowiki>Foobar]</nowiki>]]
+[[Foo|x [Foobar] x]]
+[[Foo|x <nowiki>[http://google.com g]</nowiki> x]]
+[[Foo|<nowiki>[[Bar]]</nowiki>]]
+[[Foo|<nowiki>x [[Bar]] x</nowiki>]]
+[[Foo|<nowiki>|Bar</nowiki>]]
+[[Foo|<nowiki>]]bar</nowiki>]]
+[[Foo|<nowiki>[[bar</nowiki>]]
+[[Foo|<nowiki>x [[ y</nowiki>]]
+[[Foo|<nowiki>x ]] y</nowiki>]]
+[[Foo|<nowiki>x ]] y [[ z</nowiki>]]
+!! html
+<a href="Foo" rel="mw:WikiLink">[Foobar]</a>
+<a href="Foo" rel="mw:WikiLink">Foobar]</a>
+<a href="Foo" rel="mw:WikiLink">x [Foobar] x</a>
+<a href="Foo" rel="mw:WikiLink">x [http://google.com g] x</a>
+<a href="Foo" rel="mw:WikiLink">[[Bar]]</a>
+<a href="Foo" rel="mw:WikiLink">x [[Bar]] x</a>
+<a href="Foo" rel="mw:WikiLink">|Bar</a>
+<a href="Foo" rel="mw:WikiLink">]]bar</a>
+<a href="Foo" rel="mw:WikiLink">[[bar</a>
+<a href="Foo" rel="mw:WikiLink">x [[ y</a>
+<a href="Foo" rel="mw:WikiLink">x ]] y</a>
+<a href="Foo" rel="mw:WikiLink">x ]] y [[ z</a>
+!! end
+
+!! test
+Links 3. WikiLinks: No escapes needed
+!! options
+parsoid
+!! wikitext
+[[Foo|[Foobar]]
+[[Foo|foo|bar]]
+!! html
+<a href="Foo" rel="mw:WikiLink">[Foobar</a>
+<a href="Foo" rel="mw:WikiLink">foo|bar</a>
+!! end
+
+!! test
+Links 4. ExtLinks: Escapes needed
+!! options
+parsoid
+!! wikitext
+[http://google.com <nowiki>[google]</nowiki>]
+[http://google.com <nowiki>google]</nowiki>]
+
+<nowiki>[http://google.com]</nowiki>
+
+<nowiki>[http://google.com google]</nowiki>
+
+!! html
+<p><a href="http://google.com" rel="mw:ExtLink">[google]</a>
+<a href="http://google.com" rel="mw:ExtLink">google]</a></p>
+<p>[http://google.com]</p>
+<p>[http://google.com google]</p>
+!! end
+
+!! test
+Links 5. ExtLinks: No escapes needed
+!! options
+parsoid
+!! wikitext
+[http://google.com [google]
+!! html
+<a href="http://google.com" rel="mw:ExtLink">[google</a>
+!! end
+
+!! test
+Links 6. Add <nowiki/>s between text-nodes and url-links when required (bug 64300)
+!! html/parsoid
+<p>x<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>y
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>?x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>&amp;x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>'x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>,x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>.x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>;x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>:x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>;x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>!x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>=x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>(x)
+<a rel="mw:ExtLink" href="http://example.com(x" data-parsoid='{"stx":"url"}'>http://example.com(x</a>)
+</p>
+!! wikitext
+x<nowiki/>http://example.com<nowiki/>y
+http://example.com<nowiki/>?x
+http://example.com<nowiki/>&x
+http://example.com<nowiki/>'x
+http://example.com<nowiki/>,x
+http://example.com<nowiki/>.x
+http://example.com<nowiki/>;x
+http://example.com<nowiki/>:x
+http://example.com<nowiki/>;x
+http://example.com<nowiki/>!x
+http://example.com<nowiki/>=x
+http://example.com<nowiki/>(x)
+http://example.com(x<nowiki/>)
+!! end
+
+!! test
+Links 7a. Don't add spurious <nowiki/>s between text-nodes and url-links (bug 64300)
+!! html/parsoid
+<p>x
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>
+y
+"<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>"
+(<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>)
+(<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>) foo
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>,
+<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>, foo
+</p>
+!! wikitext
+x
+http://example.com
+y
+"http://example.com"
+(http://example.com)
+(http://example.com) foo
+http://example.com,
+http://example.com, foo
+!! end
+
+## Parsoid currently fails wt2html on this one!
+!! test
+Links 7b. Don't add spurious <nowiki/>s between text-nodes and url-links (bug 64300)
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a>.,;:!?</p>
+!! wikitext
+http://example.com.,;:!?
+!! end
+
+!! test
+Links 8. Add <nowiki/>s between text-nodes and RFC-links when required (bug 64300)
+!! html/parsoid
+<p><a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>4</p>
+!! wikitext
+RFC 123<nowiki/>4
+!! end
+
+!! test
+Links 9. Don't add spurious <nowiki/>s between text-nodes and RFC-links (bug 64300)
+!! html/parsoid
+<p>x<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y
+X<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y
+<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>?foo
+<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>&amp;foo
+</p>
+!! wikitext
+xRFC 123y
+XRFC 123y
+RFC 123?foo
+RFC 123&foo
+!! end
+
+!! test
+Links 10. Add <nowiki/>s between text-nodes and PMID-links when required (bug 64300)
+!! html/parsoid
+<p><a href="//www.ncbi.nlm.nih.gov/pubmed/123?dopt=Abstract" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>PMID 123</a>4
+!! wikitext
+PMID 123<nowiki/>4
+!! end
+
+!! test
+Links 11. Don't add spurious <nowiki/>s between text-nodes and PMID-links (bug 64300)
+!! html/parsoid
+<p>x<a href="//www.ncbi.nlm.nih.gov/pubmed/123?dopt=Abstract" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>PMID 123</a>y
+X<a href="//www.ncbi.nlm.nih.gov/pubmed/123?dopt=Abstract" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>PMID 123</a>y
+<a href="//www.ncbi.nlm.nih.gov/pubmed/123?dopt=Abstract" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>PMID 123</a>?foo
+<a href="//www.ncbi.nlm.nih.gov/pubmed/123?dopt=Abstract" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>PMID 123</a>&foo
+</p>
+!! wikitext
+xPMID 123y
+XPMID 123y
+PMID 123?foo
+PMID 123&foo
+!! end
+
+!! test
+Links 12. Add <nowiki/>s between text-nodes and ISBN-links when required (bug 64300)
+!! html/parsoid
+<p><a href="./Special:BookSources/1234567890" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>ISBN 1234567890</a>1
+<a href="./Special:BookSources/1234567890" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>ISBN 1234567890</a>x
+<a href="./Special:BookSources/1234567890" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>ISBN 1234567890</a>b
+</p>
+!! wikitext
+ISBN 1234567890<nowiki/>1
+ISBN 1234567890<nowiki/>x
+ISBN 1234567890<nowiki/>b
+!! end
+
+!! test
+Links 12. Don't add spurious <nowiki/>s between text-nodes and ISBN-links (bug 64300)
+!! html/parsoid
+<p><a href="./Special:BookSources/1234567890" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>ISBN 1234567890</a>'s
+!! wikitext
+ISBN 1234567890's
+!! end
+
+#### --------------- Quotes ---------------
+#### 1. Quotes inside <b> and <i>
+#### 2. Link fragments separated by <i> and <b> tags
+#### 3. Link fragments inside <i> and <b>
+#### 4. No escaping needed
+#### --------------------------------------
+!! test
+1. Quotes inside <b> and <i>
+!! options
+parsoid=html2wt,wt2wt
+!! wikitext
+''<nowiki>'foo'</nowiki>''
+''<nowiki>''foo''</nowiki>''
+''<nowiki>'''foo'''</nowiki>''
+''foo''<nowiki/>'s
+'''<nowiki>'foo'</nowiki>'''
+'''<nowiki>''foo''</nowiki>'''
+'''<nowiki>'''foo'''</nowiki>'''
+'''<nowiki>foo'</nowiki>''<nowiki>bar'</nowiki>''baz'''
+'''foo'''<nowiki/>'s
+'''foo''
+''foo''<nowiki/>'
+'<nowiki/>''foo''<nowiki/>'
+''''foo'''
+'''foo'''<nowiki/>'
+'<nowiki/>'''foo'''<nowiki/>'
+''fools'<span> errand</span>''
+''<span>fool</span>'s errand''
+!! html
+<p><i>'foo'</i>
+<i>''foo''</i>
+<i>'''foo'''</i>
+<i>foo</i>'s
+<b>'foo'</b>
+<b>''foo''</b>
+<b>'''foo'''</b>
+<b>foo'<i>bar'</i>baz</b>
+<b>foo</b>'s
+'<i>foo</i>
+<i>foo</i>'
+'<i>foo</i>'
+'<b>foo</b>
+<b>foo</b>'
+'<b>foo</b>'</p>
+<i>fools'<span> errand</span></i>
+<i><span>fool</span>'s errand</i>
+!! end
+
+!! test
+2. Link fragments separated by <i> and <b> tags
+!! wikitext
+[[''foo''<nowiki>hello]]</nowiki>
+
+[['''foo'''<nowiki>hello]]</nowiki>
+!! html
+<p>[[<i>foo</i>hello]]
+</p><p>[[<b>foo</b>hello]]
+</p>
+!! end
+
+!! test
+3. Link fragments inside <i> and <b>
+(FIXME: Escaping one or both of [[ and ]] is also acceptable --
+ this is one of the shortcomings of this format)
+!! wikitext
+''[[foo''<nowiki>]]</nowiki>
+
+'''[[foo'''<nowiki>]]</nowiki>
+!! html
+<p><i>[[foo</i>]]
+</p><p><b>[[foo</b>]]
+</p>
+!! end
+
+!! test
+4. No escaping needed
+!! wikitext
+'<span>''bar''</span>'
+'<span>'''bar'''</span>'
+!! html
+<p>'<span><i>bar</i></span>'
+'<span><b>bar</b></span>'
+</p>
+!! end
+
+#### ----------- Paragraphs ---------------
+#### 1. No unnecessary escapes
+#### --------------------------------------
+
+!! test
+1. No unnecessary escapes
+!! wikitext
+bar <span><nowiki>[[foo]]</nowiki></span>
+
+=bar <span><nowiki>[[foo]]</nowiki></span>
+
+[[bar <span><nowiki>[[foo]]</nowiki></span>
+
+]]bar <span><nowiki>[[foo]]</nowiki></span>
+
+=bar <span>foo]]</span><nowiki>=</nowiki>
+!! html
+<p>bar <span>[[foo]]</span>
+</p><p>=bar <span>[[foo]]</span>
+</p><p>[[bar <span>[[foo]]</span>
+</p><p>]]bar <span>[[foo]]</span>
+</p><p>=bar <span>foo]]</span>=
+</p>
+!!end
+
+#### ----------------------- PRE --------------------------
+#### 1. Leading whitespace in SOL context should be escaped
+#### ------------------------------------------------------
+!! test
+1. Leading whitespace in SOL context should be escaped
+!! options
+parsoid
+!! wikitext
+<nowiki> </nowiki>a
+
+<nowiki> </nowiki> a
+
+<nowiki> </nowiki>a(tab)
+
+<nowiki> </nowiki> a
+<!--cmt-->
+<nowiki> </nowiki> a
+
+a
+<nowiki> </nowiki>b
+
+a
+<nowiki> </nowiki>b
+
+a
+<nowiki> </nowiki> b
+!! html
+<p> a</p>
+<p> a</p>
+<p> a(tab)</p>
+<p> a</p>
+<p><!--cmt--> a</p>
+<p>a
+ b</p>
+<p>a
+ b</p>
+<p>a
+ b</p>
+!! end
+
+!! test
+2. Leading whitespace in non-indent-pre contexts should not be escaped
+!! options
+parsoid
+!! wikitext
+foo <ref>''a''
+ b</ref>
+!! html
+<p>foo <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"&lt;i data-parsoid=&#39;{\"dsr\":[9,14,2,2]}&#39;>a&lt;/i>\n b"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref"><a href="#cite_note-1">[1]</a></span></p>
+!! end
+
+!! test
+3. Leading whitespace in indent-pre suppressing contexts should not be escaped
+!! options
+parsoid
+!! wikitext
+<blockquote>
+ a
+ <span>b</span>
+ c
+</blockquote>
+!! html
+<blockquote>
+<p>
+ a
+ <span>b</span>
+ c</p>
+</blockquote>
+!! end
+
+!! test
+4. Leading whitespace in indent-pre suppressing contexts should not be escaped
+!! options
+parsoid
+!! wikitext
+ [[File:Foobar.jpg|thumb|caption]]
+!! html
+!! html/parsoid
+ <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure>
+!! end
+
+!! test
+5. Nowiki escaping should account for indent-pres
+!! options
+parsoid=html2wt
+!! html
+<pre>==foo==</pre>
+!! wikitext
+ ==foo==
+!! end
+
+#### --------------- Behavior Switches --------------------
+!! test
+1. Valid behavior switches should be escaped
+!! options
+parsoid=html2wt
+!! wikitext
+<nowiki>__TOC__</nowiki>
+''<nowiki>__TOC__</nowiki>''
+!! html
+__TOC__
+<i>__TOC__</i>
+!! end
+
+!! test
+2. Invalid behavior switches should not be escaped
+!! options
+parsoid=html2wt
+!! wikitext
+__TOO__
+__|__
+!! html
+__TOO__
+__|__
+!! end
+
+#### --------------- HTML tags ---------------
+#### 1. a tags
+#### 2. other tags
+#### 3. multi-line html tag
+#### 4. extension tags
+#### -----------------------------------------
+!! test
+1. a tags
+!! options
+parsoid
+!! wikitext
+<a href="http://google.com">google</a>
+!! html
+&lt;a href=&quot;http://google.com&quot;&gt;google&lt;/a&gt;
+!! end
+
+!! test
+2. other tags
+!! wikitext
+<nowiki><div>foo</div>
+<div style="color:red">foo</div></nowiki>
+!! html
+<p>&lt;div&gt;foo&lt;/div&gt;
+&lt;div style=&quot;color:red&quot;&gt;foo&lt;/div&gt;
+</p>
+!! end
+
+!! test
+3. multi-line html tag
+!! wikitext
+<nowiki><div
+>foo</div
+></nowiki>
+!! html
+<p>&lt;div
+&gt;foo&lt;/div
+&gt;
+</p>
+!! end
+
+!! test
+4. extension tags
+!! wikitext
+<nowiki><ref>foo</ref></nowiki>
+
+<nowiki><ref>bar</nowiki>
+
+baz<nowiki></ref></nowiki>
+!! html
+<p>&lt;ref&gt;foo&lt;/ref&gt;
+</p><p>&lt;ref&gt;bar
+</p><p>baz&lt;/ref&gt;
+</p>
+!! end
+
+#### --------------- Others ---------------
+!! test
+Escaping nowikis
+!! wikitext
+&lt;nowiki&gt;foo&lt;/nowiki&gt;
+!! html
+<p>&lt;nowiki&gt;foo&lt;/nowiki&gt;
+</p>
+!! end
+
+## The quote-char in the input is necessary for triggering the bug
+!! test
+(Bug 52035) Nowiki-escaping should not get tripped by " :" in text
+!! options
+parsoid=wt2wt,html2wt
+!! wikitext
+foo's bar :
+!! html
+<p>foo's bar :</p>
+!! end
+
+!! test
+
+Tag-like HTML structures are passed through as text
+!! wikitext
+<x y>
+
+<x.y>
+
+<x-y>
+
+1>2
+
+x<y
+
+a>b
+
+1<d e>f
+!! html
+<p>&lt;x y&gt;
+</p><p>&lt;x.y&gt;
+</p><p>&lt;x-y&gt;
+</p><p>1&gt;2
+</p><p>x&lt;y
+</p><p>a&gt;b
+</p><p>1&lt;d e&gt;f
+</p>
+!! end
+
+
+# This was a bug in the PHP parser (see bug 17663 and its dups,
+# https://bugzilla.wikimedia.org/show_bug.cgi?id=17663)
+!! test
+Tag names followed by punctuation should not be recognized as tags
+!! wikitext
+<s.ome> text
+!! html
+<p>&lt;s.ome&gt; text
+</p>
+!! end
+
+!! test
+HTML tag with necessary entities in attributes
+!! wikitext
+<span title="&amp;amp;">foo</span>
+!! html
+<p><span title="&amp;amp;">foo</span>
+</p>
+!! end
+
+!! test
+HTML tag with 'unnecessary' entity encoding in attributes
+!! wikitext
+<span title="&amp;">foo</span>
+!! html
+<p><span title="&amp;">foo</span>
+</p>
+!! end
+
+!! test
+HTML tag with broken attribute value quoting
+!! wikitext
+<span title="Hello world>Foo</span>
+!! html/php
+<p><span>Foo</span>
+</p>
+!! html/parsoid
+<p><span title="Hello world">Foo</span>
+</p>
+!! end
+
+!! test
+Parsoid-only: HTML tag with broken attribute value quoting
+!! options
+parsoid
+!! wikitext
+<span title="Hello world>Foo</span>
+!! html
+<p><span title="Hello world">Foo</span>
+</p>
+!! end
+
+!! test
+Table with broken attribute value quoting
+!! wikitext
+{|
+| title="Hello world|Foo
+|}
+!! html/php
+<table>
+<tr>
+<td>Foo
+</td></tr></table>
+
+!! html/parsoid
+<table>
+<tr>
+<td title="Hello world">Foo
+</td></tr></table>
+
+!! end
+
+!! test
+Table with broken attribute value quoting on consecutive lines
+!! wikitext
+{|
+| title="Hello world|Foo
+| style="color:red|Bar
+|}
+!! html
+<table>
+<tr>
+<td>Foo
+</td>
+<td>Bar
+</td></tr></table>
+
+!! end
+
+!! test
+Parsoid-only: Table with broken attribute value quoting on consecutive lines
+!! options
+parsoid
+!! wikitext
+{|
+| title="Hello world|Foo
+| style="color:red|Bar
+|}
+!! html
+<table><tbody>
+<tr>
+<td title="Hello world">Foo
+</td><td style="color: red">Bar
+</td></tr></tbody></table>
+
+!! end
+
+!! test
+Parsoid-only: Don't wrap broken template tags in <nowiki> on wt2wt (Bug 42353)
+!! options
+parsoid
+!! wikitext
+{{}}
+!! html
+{{}}
+!! end
+
+!! test
+Parsoid-only: Don't wrap broken template tags in <nowiki> on wt2wt (Bug 42353)
+!! options
+parsoid
+!! wikitext
+}}{{
+!! html
+}}{{
+!! end
+
+!!test
+Accept empty td cell attribute
+!! wikitext
+{|
+| align="center" | foo || |
+|}
+!! html
+<table>
+<tr>
+<td align="center"> foo </td>
+<td>
+</td></tr></table>
+
+!!end
+
+!!test
+Non-empty attributes in th-cells
+!! wikitext
+{|
+! Foo !! style="color: red" | Bar
+|}
+!! html
+<table>
+<tr>
+<th> Foo </th>
+<th style="color: red"> Bar
+</th></tr></table>
+
+!!end
+
+!!test
+Accept empty attributes in th-cells
+!! wikitext
+{|
+!| foo !!| bar
+|}
+!! html
+<table>
+<tr>
+<th> foo </th>
+<th> bar
+</th></tr></table>
+
+!!end
+
+!!test
+Empty table rows go away
+!! wikitext
+{|
+| Hello
+| there
+|- class="foo"
+|-
+|}
+!! html
+<table>
+<tr>
+<td> Hello
+</td>
+<td> there
+</td></tr>
+
+</table>
+
+!! end
+
+###
+### Parsoid-centric tests for testing RTing of inter-element separators
+### Edge cases not tested by existing parser tests and specific to
+### Parsoid-specific serialization strategies.
+###
+
+!!test
+RT-ed inter-element separators should be valid separators
+!! wikitext
+{|
+|- [[foo]]
+|}
+!! html
+<table>
+
+</table>
+
+!!end
+
+!!test
+Trailing newlines in a deep dom-subtree that ends a wikitext line should be migrated out
+(Parsoid-only since PHP parser relies on Tidy for correct output)
+!!options
+parsoid
+!! wikitext
+{|
+|<small>foo
+bar
+|}
+
+{|
+|<small>foo<small>
+|}
+!! html
+!!end
+
+!!test
+Empty TD followed by TD with tpl-generated attribute
+!! wikitext
+{|
+|-
+|
+|{{echo|style='color:red'}}|foo
+|}
+!! html
+<table>
+
+<tr>
+<td>
+</td>
+<td>foo
+</td></tr></table>
+
+!!end
+
+!!test
+Indented table with an empty td
+!! wikitext
+ {|
+ |-
+ |
+ |foo
+ |}
+!! html
+<table>
+
+<tr>
+<td>
+</td>
+<td>foo
+</td></tr></table>
+
+!!end
+
+!!test
+Indented block & table
+!! wikitext
+ <div>foo</div>
+ {|
+ |foo
+ |}
+!! html/php
+ <div>foo</div>
+<table>
+<tr>
+<td>foo
+</td></tr></table>
+
+!! html/parsoid
+ <div data-parsoid='{"stx":"html"}'>foo</div>
+ <table><tbody>
+ <tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'>foo</td></tr>
+ </tbody></table>
+!!end
+
+!! test
+Indent and comment before table row
+!! wikitext
+{|
+ <!--hi-->|-
+ | there
+|}
+!! html/php
+<table>
+
+<tr>
+<td> there
+</td></tr></table>
+
+!! html/parsoid
+<table data-parsoid='{}'>
+ <!--hi--><tbody data-parsoid='{}'><tr data-parsoid='{"startTagSrc":"|-","autoInsertedEnd":true}'>
+ <td data-parsoid='{"autoInsertedEnd":true}'> there</td></tr>
+</tbody></table>
+!! end
+
+!!test
+Empty TR followed by a template-generated TR
+(Parsoid-specific since PHP parser doesn't handle this mixed tbl-wikitext)
+!!options
+parsoid
+!! wikitext
+{|
+|-
+{{echo|<tr><td>foo</td></tr>}}
+|}
+!! html
+<table>
+<tbody>
+<tr></tr>
+<tr about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<tr><td>foo</td></tr>"}},"i":0}}]}'>
+<td>foo</td></tr>
+</tbody></table>
+!!end
+
+## PHP and parsoid output differ for this, and since this is primarily
+## for testing Parsoid's serializer, marking this Parsoid only
+!!test
+Empty TR followed by mixed-ws-comment line should RT correctly
+!!options
+parsoid
+!! wikitext
+{|
+|-
+ <!--c-->
+|-
+<!--c--> <!--d-->
+|}
+!! html
+<table>
+<tbody>
+<tr></tr>
+ <!--c-->
+<tr>
+<!--c--> </tr><!--d-->
+</tbody></table>
+
+!!end
+
+!!test
+Multi-line image caption generated by templates with/without trailing newlines
+!!options
+parsoid
+!! wikitext
+[[File:foo.jpg|thumb|300px|foo\n{{echo|A}}\n{{echo|B}}\n{{echo|C}}]]
+[[File:foo.jpg|thumb|300px|foo\n{{echo|A}}\n{{echo|B}}\n{{echo|C}}\n\n]]
+!! html
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&amp;wpDestFile=Foo.jpg" class="new" title="File:Foo.jpg">File:Foo.jpg</a> <div class="thumbcaption">foo\nA\nB\nC</div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&amp;wpDestFile=Foo.jpg" class="new" title="File:Foo.jpg">File:Foo.jpg</a> <div class="thumbcaption">foo\nA\nB\nC\n\n</div></div></div>
+
+!!end
+
+!! test
+New element inserted (without intervening newlines) after an old sol-transparent node should serialize correctly
+!! options
+parsoid=html2wt
+!! wikitext
+<includeonly>foo</includeonly>
+new para
+
+[[./Category:Foo]]
+
+= new heading =
+!! html
+<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"&lt;includeonly>foo&lt;/includeonly>"}'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":""}'/><p>new para</p>
+
+<link rel="mw:PageProp/Category" href="./Category:Foo" data-parsoid=''/><h1>new heading</h1>
+!! end
+
+## PHP emits broken html for this, and since this is primarily
+## a Parsoid serializer test, marking this Parsoid only
+!!test
+Improperly nested inline or quotes tags with whitespace in between
+!!options
+parsoid
+!! wikitext
+<span> <s>x</span> </s>
+''' ''x''' ''
+!! html
+<p><span> <s>x</s></span><s> </s>
+<b> <i>x</i></b><i> </i>
+</p>
+!!end
+
+!!test
+Encapsulate protected attributes from wt
+!!options
+parsoid
+!! wikitext
+<div typeof="mw:placeholder stuff" data-parsoid="weird" data-parsoid-other="no" about="time" rel="mw:true">foo</div>
+!! html
+<body><div data-x-typeof="mw:placeholder stuff" data-x-data-parsoid="weird" data-x-data-parsoid-other="no" data-x-about="time" data-x-rel="mw:true">foo</div>
+</body>
+!!end
+
+## Currently the p-wrapper is fragile in how it adds / removes transformations.
+## Having nested or stray pre tags results in the attempt to add duplicates,
+## causing an assertion fail. This test tries to prevent that situation.
+!!test
+Ensure ParagraphWrapper can deal with stray closing pre tags
+!!options
+parsoid=wt2html
+!! wikitext
+plain text</pre>
+!! html
+plain text
+!!end
+
+!!test
+1. Ensure fostered text content is wrapped in spans
+!!options
+parsoid=wt2html
+!! wikitext
+<table>hi</table><table>ho</table>
+!! html
+<span>hi</span>
+<table></table>
+<span>ho</span>
+<table></table>
+!!end
+
+!!test
+2. Ensure fostered text content is wrapped in spans (traps regressions around fostered marker on the span getting lost)
+!!options
+parsoid=wt2html,wt2wt
+!! wikitext
+<table>
+<tr> || ||
+<td> a
+</table>
+!! html
+<span> || ||</span>
+<table>
+<tbody>
+<tr>
+<td> a</td></tr>
+</tbody></table>
+!!end
+
+!!test
+Encapsulation properly handles null DSR information from foster box
+!!options
+parsoid=wt2html,wt2wt
+!! wikitext
+{{echo|<table>foo<tr><td>bar</td></tr></table>}}
+!! html
+<span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;
+<table>foo
+<tr>
+<td>bar</td></tr></table>&quot;}},&quot;i&quot;:0}}]}">foo</span>
+<table>
+<tbody>
+<tr>
+<td>bar</td></tr></tbody></table>
+!!end
+
+!!test
+1. Encapsulate foster-parented transclusion content
+!!options
+parsoid=wt2wt,wt2html
+!! wikitext
+<table>{{echo|foo<tr><td>bar</td></tr>}}</table>
+!! html
+<span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[&quot;
+<table>&quot;,{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;foo
+<tr>
+<td>bar</td></tr>&quot;}},&quot;i&quot;:0}},&quot;</table>&quot;]}">foo</span>
+<table>
+<tbody>
+<tr>
+<td>bar</td></tr></tbody></table>
+!!end
+
+!!test
+2. Encapsulate foster-parented transclusion content
+!!options
+parsoid=wt2wt,wt2html
+!! wikitext
+<table><div>{{echo|foo}}</div><tr><td>bar</td></tr></table>
+!! html
+<div typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[&quot;
+<table>
+<div>&quot;,{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;foo&quot;}},&quot;i&quot;:0}},&quot;</div>
+<tr>
+<td>bar</td></tr></table>&quot;]}">foo</div>
+<table>
+<tbody>
+<tr>
+<td>bar</td></tr></tbody></table>
+!!end
+
+!!test
+3. Encapsulate foster-parented transclusion content
+!!options
+parsoid=wt2wt,wt2html
+!! wikitext
+<table><div><p>{{echo|foo</p></div><tr><td>}}bar</td></tr></table>
+!! html
+<div typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[&quot;
+<table>
+<div>
+<p>&quot;,{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;foo</p></div>
+<tr>
+<td>&quot;}},&quot;i&quot;:0}},&quot;bar</td></tr></table>&quot;]}">
+<p>foo</p></div>
+<table>
+<tbody>
+<tr>
+<td>bar</td></tr></tbody></table>
+!!end
+
+!!test
+4. Encapsulate foster-parented transclusion content
+!!options
+parsoid=wt2wt,wt2html
+!! wikitext
+<table><div><p>{{echo|foo</p></div><tr><td>}}bar</td></tr></table>
+!! html
+<div typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[&quot;
+<table>
+<div>
+<p>&quot;,{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;foo</p></div>
+<tr>
+<td>&quot;}},&quot;i&quot;:0}},&quot;bar</td></tr></table>&quot;]}">
+<p>foo</p></div>
+<table>
+<tbody>
+<tr>
+<td>bar</td></tr></tbody></table>
+!!end
+
+!!test
+5. Encapsulate foster-parented transclusion content
+!!options
+parsoid=wt2wt,wt2html
+!! wikitext
+<table><tr><td><div><p>{{echo|foo</p></div></td>foo}}</tr></table>
+!! html
+<span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[&quot;
+<table>
+<tr>
+<td>
+<div>
+<p>&quot;,{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;foo</p></div></td>foo&quot;}},&quot;i&quot;:0}},&quot;</tr></table>&quot;]}">foo</span>
+<table>
+<tbody>
+<tr>
+<td>
+<div>
+<p>foo</p></div></td></tr></tbody></table>
+!!end
+
+!!test
+6. Encapsulate foster-parented transclusion content
+!!options
+parsoid=wt2wt,wt2html
+!! wikitext
+<table><tr><td><div><p>{{echo|foo</p></div></td>foo</tr></table>}}<p>ok</p>
+!! html
+<span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[&quot;
+<table>
+<tr>
+<td>
+<div>
+<p>&quot;,{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;foo</p></div></td>foo</tr></table>&quot;}},&quot;i&quot;:0}}]}">foo</span>
+<table>
+<tbody>
+<tr>
+<td>
+<div>
+<p>foo</p></div></td></tr></tbody></table>
+<p>ok</p>
+!!end
+
+!!test
+7. Encapsulate foster-parented transclusion content
+!!options
+parsoid=wt2wt,wt2html
+!! wikitext
+<table>{{echo|<p>foo</p>}}<td>bar</td></table>
+!! html
+<p typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[&quot;
+<table>&quot;,{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;
+<p>foo</p>&quot;}},&quot;i&quot;:0}},&quot;
+<td>bar</td></table>&quot;]}">foo</p>
+<table>
+<tbody>
+<tr>
+<td>bar</td></tr></tbody></table>
+!!end
+
+!!test
+8. Encapsulate foster-parented transclusion content
+!!options
+parsoid=wt2wt,wt2html
+!! wikitext
+{{echo|a
+}}{|{{echo|style='color:red'}}
+|-
+|b
+|}
+!! html
+<p typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;a\n&quot;}},&quot;i&quot;:0}}]}">a</p><span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[&quot;{|&quot;,{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;style&quot;:{&quot;wt&quot;:&quot;'color:red'&quot;}},&quot;i&quot;:0}},&quot;\n|-\n|b\n|}&quot;]}">{{{1}}}</span>
+<table>
+<tbody>
+<tr>
+<td>b</td></tr></tbody></table>
+!!end
+
+!!test
+9. Encapsulate foster-parented transclusion content
+!!options
+parsoid=wt2wt,wt2html
+!! wikitext
+<table>{{echo|hi</table>hello}}
+!! html
+<span about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":["&lt;table>",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi&lt;/table>hello"}},"i":0}}]}' data-parsoid='{"fostered":true,"autoInsertedEnd":true,"autoInsertedStart":true,"pi":[[{"k":"1","spc":["","","",""]}]]}'>hi</span>
+<table about="#mwt2" data-parsoid='{"stx":"html"}'></table><span about="#mwt2" data-parsoid="{}">hello</span>
+!!end
+
+!!test
+Table in fosterable position
+!!options
+parsoid=wt2html,wt2wt
+!! wikitext
+{{OpenTable}}
+<div>
+{|
+|}
+</div>
+|}
+!! html
+<div about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"OpenTable","href":"./Template:OpenTable"},"params":{},"i":0}},"\n&lt;div>"]}' data-parsoid='{"stx":"html","autoInsertedEnd":true,"pi":[[]]}'></div><span about="#mwt1" data-parsoid="{}">
+</span>
+<table about="#mwt1" data-parsoid='{"autoInsertedEnd":true}'></table>
+
+<table>
+</table>
+!!end
+
+# Parsoid only for bug 64747
+!! test
+Properly encapsulate empty-content transclusions in fosterable positions
+!! wikitext
+<table>
+{{#if:|
+<td>foo</td>
+}}
+</table>
+!! html/parsoid
+<table about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":["&lt;table>\n",{"template":{"target":{"wt":"#if:","function":"#if"},"params":{"1":{"wt":"\n&lt;td>foo&lt;/td>\n"}},"i":0}},"\n&lt;/table>"]}' data-parsoid='{"stx":"html","pi":[[{"k":"1","spc":["","","",""]}]],"src":"&lt;table>\n{{#if:|\n&lt;td>foo&lt;/td>\n}}\n&lt;/table>"}'>
+
+</table>
+!! end
+
+!!test
+Support <object> element with .data attribute
+!!options
+parsoid=html2wt
+!! wikitext
+<object data="test.swf"></object>
+!! html
+<object data="test.swf"></object>
+!!end
+
+# -----------------------------------------------------------------
+# The following section of tests are primarily to spec requirements
+# around serialization of new/edited content.
+#
+# All these tests are marked Parsoid html2wt and html2html only
+# ----------------------------------------------------------------
+
+!! test
+Serialize interwiki links pointing to the current wiki as plain wiki links (bug 65869)
+!! options
+parsoid=html2wt
+language=es
+!! wikitext
+[[Foo]]
+!! html
+<p><a rel="mw:ExtLink" href="http://es.wikipedia.org/wiki/Foo">Foo</a></p>
+!! end
+
+!! test
+Image: Modifying size of an image (1)
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ ["img[height]", "attr", "height", "22"],
+ ["img[width]", "attr", "width", "200"]
+ ]
+}
+!! wikitext
+[[Image:Foobar.jpg|230x230px]]
+!! wikitext/edited
+[[Image:Foobar.jpg|200x200px]]
+!!end
+
+!! test
+Image: Modifying size of an image (2)
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ ["img[height]", "attr", "height", "100"],
+ ["img[width]", "attr", "width", "500"]
+ ]
+}
+!! wikitext
+[[Image:Foobar.jpg|230x230px]]
+!! wikitext/edited
+[[Image:Foobar.jpg|500x500px]]
+!!end
+
+# Change in size is ignored so long as class='mw-default-size'
+!! test
+Image: Modifying size of an image (3)
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ ["figure[class]", "removeClass", "mw-default-size"],
+ ["figure img", "attr", "height", "19"],
+ ["figure img", "attr", "width", "170"]
+ ]
+}
+!! wikitext
+[[Image:Foobar.jpg|thumb]]
+!! wikitext/edited
+[[Image:Foobar.jpg|thumb|170x170px]]
+!!end
+
+!! test
+Image: Modifying alignment of an image (bug 48665)
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ ["figure[class]", "removeClass", "mw-halign-right"],
+ ["figure[class]", "addClass", "mw-halign-left"]
+ ]
+}
+!! wikitext
+[[Image:Foobar.jpg|thumb|caption|right]]
+!! wikitext/edited
+[[Image:Foobar.jpg|thumb|caption|left]]
+!! end
+
+!! test
+Image: Modifying mw-default-size of an frameless image (bug 62805)
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ ["figure.mw-default-size", "removeClass", "mw-default-size"]
+ ]
+}
+!! wikitext
+[[Image:Foobar.jpg|frameless|right]]
+!! wikitext/edited
+[[Image:Foobar.jpg|frameless|right|220x220px]]
+!! end
+
+!! test
+Image: Modifying valign of an image (bug 49221)
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ ["*[typeof=\"mw:Image\"]", "removeClass", "mw-valign-middle"],
+ ["*[typeof=\"mw:Image\"]", "addClass", "mw-valign-text-top"]
+ ]
+}
+!! wikitext
+[[File:Foobar.jpg|20px|middle]]
+!! wikitext/edited
+[[File:Foobar.jpg|20px|text-top]]
+!! end
+
+!! test
+Image: Modifying alt attribute of an image (bug 56400)
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ ["img[alt]", "attr", "alt", "some alternate edited text"]
+ ]
+}
+!! wikitext
+[[File:Foobar.jpg|thumb|some caption|alt=some alternate text]]
+!! wikitext/edited
+[[File:Foobar.jpg|thumb|some caption|alt=some alternate edited text]]
+!!end
+
+!! test
+Image: Modifying caption of an image
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ ["figcaption", "text", "new caption"]
+ ]
+}
+!! wikitext
+[[Image:Foobar.jpg|thumb|original caption]]
+!! wikitext/edited
+[[Image:Foobar.jpg|thumb|new caption]]
+!!end
+
+!! test
+Image: empty alt attribute (bug 48924)
+!! options
+parsoid
+!! wikitext
+[[File:Foobar.jpg|thumb|alt=|bar]]
+!! html
+<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"alt","ak":"alt="},{"ck":"caption","ak":"bar"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img alt="" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" height="25" width="220" data-parsoid='{"a":{"alt":"","resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"alt":"alt=","resource":"File:Foobar.jpg"}}'/></a><figcaption>bar</figcaption></figure>
+!! end
+
+#!! test
+#Image: new attributes should be serialized in wiki's language for RTL languages (bug 51852)
+#!! options
+#parsoid=html2wt
+#language=ar
+#!! input
+#[[Imagen:Foobar.jpg|derecha|miniaturadeimagen]]
+#!! result
+#<figure class="mw-default-size mw-halign-right" typeof="mw:Image/Thumb"><a href="Imagen:Foobar.jpg"><img resource="./Imagen:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="20" width="180"/></a></figure>
+#!! end
+
+!! test
+Image: Block level image should have \n before and after
+!! options
+parsoid
+!! wikitext
+123
+[[File:Foobar.jpg|right|thumb|150x150px]]
+456
+!! html
+<p>123</p><figure typeof="mw:Image/Thumb" class="mw-halign-right"><a href="./File:Foobar.png"><img src="http://192.168.142.128/mw/images/thumb/b/bc/Foobar.png/131px-Foobar.png" width="131" height="150" resource="./File:Foobar.png" data-parsoid='{"a":{"resource":"./File:Foobar.png","width":"131"},"sa":{"resource":"File:Foobar.png","width":"150"}}'></a></figure><p>456</p>
+!!end
+
+!! test
+Image: New block level image should have \n before and after (existing
+content)
+!! options
+parsoid
+!! wikitext
+123
+[[File:Foobar.jpg|right|thumb|150x150px]]
+456
+!! html
+<p data-parsoid='{"dsr":[0,3,0,0]}'>123</p>
+<figure class="mw-halign-right" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"right","ak":"right"},{"ck":"thumbnail","ak":"thumb"},{"ck":"width","ak":"150x150px"}],"dsr":[4,45,2,2]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"dsr":[6,43,null,null]}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/150px-Foobar.jpg" height="17" width="150" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"17","width":"150"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure>
+<p data-parsoid='{"dsr":[46,49,0,0]}'>456</p>
+!!end
+
+!! test
+Image: upright option (parsoid)
+!! options
+parsoid
+!! wikitext
+[[File:Foobar.jpg|thumb|upright|caption]]
+[[File:Foobar.jpg|thumb|upright=0.5|caption]]
+[[File:Foobar.jpg|thumb|500x500px|upright=0.5|caption]]
+!! html
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="19" width="170"/></a><figcaption>caption</figcaption></figure><figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="12" width="110"/></a><figcaption>caption</figcaption></figure><figure typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="57" width="500"/></a><figcaption>caption</figcaption></figure>
+!!end
+
+!! test
+Image: upright option is ignored on inline and frame images (parsoid)
+!! options
+parsoid
+!! wikitext
+[[File:Foobar.jpg|500x500px|upright=0.5|caption]]
+!! html
+<p><span typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="57" width="500"/></a></span></p>
+!!end
+
+!! test
+Image: from basic HTML (1)
+!! options
+parsoid=html2wt
+!! html/parsoid
+<span typeof="mw:Image">
+ <img src="File:Foobar.jpg" width=100 height=100 alt="Alt">
+</span>
+!! wikitext
+[[File:Foobar.jpg|link=|alt=Alt|100x100px]]
+!! end
+
+!! test
+Image: from basic HTML (2)
+!! options
+parsoid=html2wt
+!! html/parsoid
+<img src="File:Foobar.jpg" width=100 height=100 alt="Alt">
+!! wikitext
+[[File:Foobar.jpg|link=|alt=Alt|100x100px]]
+!! end
+
+!! test
+Image: from basic HTML (3)
+!! options
+parsoid=html2wt
+!! html/parsoid
+<a href="Main"><img src="File:Foobar.jpg" width=100 height=100 alt="Alt"></a>
+!! wikitext
+[[File:Foobar.jpg|link=Main|alt=Alt|100x100px]]
+!! end
+
+!! test
+Image: from basic HTML (4)
+!! options
+parsoid=html2wt
+!! html/parsoid
+<img src="File:Foobar.jpg">
+!! wikitext
+[[File:Foobar.jpg|link=]]
+!! end
+
+!! test
+Lists: Serialize correctly even when list content is wrapped in p-tags (like VE does)
+!! options
+parsoid=html2wt
+!! wikitext
+* foo
+!! html
+<ul>
+<li><p>foo</p></li>
+</ul>
+!! end
+
+!! test
+Lists: Serialize correctly even when list tags has unneeded whitespace between tags
+!! options
+parsoid=html2wt
+!! wikitext
+* foo
+!! html
+<ul> <li>foo</li></ul>
+!! end
+
+!! test
+Don't strip leading whitespace when handling indent-pre suppressing tags
+!! options
+parsoid=html2wt
+!! wikitext
+{|
+ | indented row
+|}
+<blockquote>
+ '''This is very bold of you!'''
+
+{|
+|
+ indented cell (no pre-wrapping!)
+|}
+</blockquote>
+foo
+ <div>bar</div>
+!! html
+<table>
+ <tr><td> indented row</td></tr>
+</table>
+<blockquote><p>
+ <b>This is very bold of you!</b>
+</p>
+<table><tr><td>
+ indented cell (no pre-wrapping!)
+</td></tr></table>
+</blockquote>
+<p>foo</p>
+ <div>bar</div>
+!! end
+
+!! test
+Nowiki-wrap leading whitespace when handling indent-pre inducing tags
+!! options
+parsoid=html2wt
+!! wikitext
+foo
+<nowiki> </nowiki><span>bar</span>
+
+<span>foo2
+<nowiki> </nowiki></span>bar2
+
+<div>foo</div>
+<nowiki> </nowiki><span>bar</span>
+
+<div>
+<nowiki> </nowiki><span>foo</span>
+</div>
+!! html
+<p>foo</p>
+ <span>bar</span>
+
+<span>foo2
+ </span>bar2
+
+<div>foo</div>
+ <span>bar</span>
+
+<div>
+ <span>foo</span>
+</div>
+!! end
+
+!! test
+Lists: Add space after bullets
+!! options
+parsoid=html2wt
+!! wikitext
+* foo
+* bar
+* <span> baz</span>
+!! html
+<ul>
+<li>foo</li>
+<li> bar</li>
+<li><span> baz</span></li>
+</ul>
+!! end
+
+!! test
+Lists: Dont insert newlines in a serialized list item.
+!! options
+parsoid=html2wt
+!! wikitext
+* a<br>b
+* c
+!! html
+<ul><li>a<br>b</li><li>c</li></ul>
+!! end
+
+!! test
+Headings: Add space before/after == (Bug 51744)
+!! options
+parsoid=html2wt
+!! wikitext
+== foo ==
+
+== bar ==
+
+== baz ==
+
+== <span> baz</span> ==
+!! html
+<h2>foo</h2>
+<h2> bar</h2>
+<h2>baz </h2>
+<h2><span> baz</span></h2>
+!! end
+
+!! test
+Parsoid: Serialize positional parameters with = in them as named parameter
+!! options
+parsoid=html2wt
+!! wikitext
+{{echo|1 = f=oo}}
+
+{{echo|1 = f=oo|2 = bar}}
+
+<!--Orig params with data-parsoid has heuristics for handling = chars-->
+<!--FIXME: But maybe the heuristic needs fixing to apply to new params as well-->
+{{echo|<nowiki>f=oo</nowiki>|bar}}
+!! html
+<p about="#mwt1" typeof="mw:Transclusion"
+data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"f=oo"}},"i":0}}]}'>foo</p>
+
+<p about="#mwt1" typeof="mw:Transclusion"
+data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"f=oo"}, "2":{"wt":"bar"}},"i":0}}]}'>foo</p>
+
+<!--Orig params with data-parsoid has heuristics for handling = chars-->
+<!--FIXME: But maybe the heuristic needs fixing to apply to new params as well-->
+<p data-parsoid='{"pi":[[{"k":"1","spc":["","","",""]},{"k":"2","spc":["","","",""]}]]}' about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"f=oo"},"2":{"wt":"bar"}},"i":0}}]}'>foo</p>
+!! end
+
+!! test
+Parsoid: Correctly serialize block-node children when they are a combination of text and p-nodes
+!! options
+parsoid=html2wt
+!! wikitext
+<div>a
+b
+</div>
+<div>a
+b
+</div>
+<div>
+a
+
+b
+</div>
+!! html
+<div>a<p>b</p></div>
+<div>a
+<p>b</p></div>
+<div>
+a
+<p>b</p></div>
+!! end
+
+!! test
+Substrings resembling wikitext in hrefs should not get nowiki escapes
+!! options
+parsoid=html2wt
+!! wikitext
+[[Foo''bar''baz]]
+!! html
+<a rel="mw:WikiLink" href="./Foo''bar''baz">Foo''bar''baz</a>
+!! end
+
+#-----------------------------
+# I/B quote minimization tests
+#-----------------------------
+
+!! test
+1. I/B quote minimization: wikitext-only tags should be combined
+!! options
+parsoid=html2wt
+!! wikitext
+''AB''
+
+'''AB'''
+
+''A'''B'''''
+
+'''A''B'''''
+
+'''A''BC''D'''
+
+'''''AB'''''
+
+'''''AB'''''
+
+'''''AB'''''
+!! html
+<p><i>A</i><i>B</i></p>
+<p><b>A</b><b>B</b></p>
+<p><i>A</i><b><i>B</i></b></p>
+<p><b>A</b><i><b>B</b></i></p>
+<p><b>A</b><i><b>B</b><b>C</b></i><b>D</b></p>
+<p><i><b>A</b></i><i><b>B</b></i></p>
+<p><i><b>A</b></i><b><i>B</i></b></p>
+<p><b><i>A</i></b><i><b>B</b></i></p>
+!! end
+
+!! test
+2. I/B quote minimization: wikitext and html tags should not be combined
+!! options
+parsoid=html2wt
+!! wikitext
+''A''<i>B</i>
+
+''A'''''<i>B</i>'''
+!! html
+<p><i>A</i><i data-parsoid='{"stx":"html"}'>B</i></p>
+<p><i>A</i><b><i data-parsoid='{"stx":"html"}'>B</i></b></p>
+!! end
+
+!! test
+3. I/B quote minimization: templated content stops minimization
+!! options
+parsoid=html2wt
+!! wikitext
+''A''{{echo|''B''}}
+
+''A''{{echo|'''''B'''''}}
+!! html
+<p><i>A</i><i about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&#39;&#39;B&#39;&#39;"}},"i":0}}]}'>B</i>
+<p><i>A</i><b about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&#39;&#39;&#39;&#39;&#39;B&#39;&#39;&#39;&#39;&#39;"}},"i":0}}]}'><i>B</i></b>
+!! end
+
+!! test
+4. I/B quote minimization: new content should be mimimized with adjacent old content
+!! options
+parsoid=html2wt
+!! wikitext
+''AB''
+
+'''AB'''
+
+''A'''B'''''
+!! html
+<p><i>A</i><i data-parsoid='{}'>B</i></p>
+<p><b data-parsoid='{}'>A</b><b>B</b></p>
+<p><i>A</i><b data-parsoid='{}'><i data-parsoid='{}'>B</i></b></p>
+!! end
+
+#------------------------------------
+# End of I/B quote minimization tests
+#------------------------------------
+
+!!test
+Bug 54262: New entities
+!! options
+parsoid=html2wt
+!! wikitext
+&nbsp;
+!! html
+<span typeof="mw:Entity">&nbsp;</span>
+!! end
+
+## Note that there is no wikitext output for 'unknownproperty' ##
+## Unknown magic words are silently dropped ##
+
+!! test
+Magic words
+!! options
+parsoid=html2wt
+!! wikitext
+__TOC__
+__NOTOC__
+__FORCETOC__
+__INDEX__
+__NOINDEX__
+__NOGALLERY__
+__NOEDITSECTION__
+__NOTITLECONVERT__
+__NOCONTENTCONVERT__
+!! html
+<meta property='mw:PageProp/toc' />
+<meta property='mw:PageProp/notoc' />
+<meta property='mw:PageProp/forcetoc' />
+<meta property='mw:PageProp/index' />
+<meta property='mw:PageProp/noindex' />
+<meta property='mw:PageProp/nogallery' />
+<meta property='mw:PageProp/noeditsection' />
+<meta property='mw:PageProp/notitleconvert' />
+<meta property='mw:PageProp/nocontentconvert' />
+<meta property='mw:PageProp/unknownproperty' />
+!! end
+
+!! test
+Consecutive <pre>s should not get merged
+!! options
+parsoid=html2wt,html2html
+!! wikitext
+ a
+
+ b
+
+ c
+
+ d
+
+ e
+
+
+
+ f
+!! html
+<pre>a</pre><pre>b</pre>
+
+<pre>c
+</pre><pre>
+d</pre>
+
+<pre>e
+
+</pre><pre>
+
+f</pre>
+!! end
+
+!! test
+Edited ISBN links not serializable as ISBN links should serialize as wikilinks
+!! options
+parsoid=html2wt
+!! wikitext
+[[Special:BookSources/1234567890|ISBN 1234567895]]
+!! html
+<a rel="mw:ExtLink" href="./Special:BookSources/1234567890">ISBN 1234567895</a>
+!! end
+
+!! test
+Edited RFC links not serializable as RFC links should serialize as extlinks
+!! options
+parsoid=html2wt
+!! wikitext
+[//tools.ietf.org/html/rfc123 New RFC]
+!! html
+<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink">New RFC</a>
+!! end
+
+!! test
+Edited PMID links not serializable as PMID links should serialize as extlinks
+!! options
+parsoid=html2wt
+!! wikitext
+[//www.ncbi.nlm.nih.gov/pubmed/123?dopt=Abstract New PMID]
+!! html
+<a href="//www.ncbi.nlm.nih.gov/pubmed/123?dopt=Abstract" rel="mw:ExtLink">New PMID</a>
+!! end
+
+!! test
+Edited Redirect link should emit a non-piped wikitext link
+!! options
+parsoid=html2wt
+!! wikitext
+#REDIRECT [[Bar]]
+!! html
+<link rel="mw:PageProp/redirect" href="Bar" data-parsoid='{"src":"#REDIRECT ","a":{"href":"./Foo"},"sa":{"href":"Foo"}}'>
+!! end
+
+# -----------------------------------------------------------------
+# End of section for Parsoid-only html2wt tests for serialization
+# of new content
+# -----------------------------------------------------------------
+
+TODO:
+more images
+more tables
+character entities
+and much more
+Try for 100% code coverage
diff --git a/tests/parser/parserTestsParserHook.php b/tests/parser/parserTestsParserHook.php
new file mode 100644
index 00000000..c8b3e897
--- /dev/null
+++ b/tests/parser/parserTestsParserHook.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * A basic extension that's used by the parser tests to test whether input and
+ * arguments are passed to extensions properly.
+ *
+ * Copyright © 2005, 2006 Ævar Arnfjörð Bjarmason
+ *
+ * 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
+ * @ingroup Testing
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ */
+
+class ParserTestParserHook {
+
+ static function setup( &$parser ) {
+ $parser->setHook( 'tag', array( __CLASS__, 'dumpHook' ) );
+ $parser->setHook( 'statictag', array( __CLASS__, 'staticTagHook' ) );
+ return true;
+ }
+
+ static function dumpHook( $in, $argv ) {
+ return "<pre>\n" .
+ var_export( $in, true ) . "\n" .
+ var_export( $argv, true ) . "\n" .
+ "</pre>";
+ }
+
+ static function staticTagHook( $in, $argv, $parser ) {
+ if ( !count( $argv ) ) {
+ $parser->static_tag_buf = $in;
+ return '';
+ } elseif ( count( $argv ) === 1 && isset( $argv['action'] )
+ && $argv['action'] === 'flush' && $in === null
+ ) {
+ // Clear the buffer, we probably don't need to
+ if ( isset( $parser->static_tag_buf ) ) {
+ $tmp = $parser->static_tag_buf;
+ } else {
+ $tmp = '';
+ }
+ $parser->static_tag_buf = null;
+ return $tmp;
+ } else { // wtf?
+ return
+ "\nCall this extension as <statictag>string</statictag> or as" .
+ " <statictag action=flush/>, not in any other way.\n" .
+ "text: " . var_export( $in, true ) . "\n" .
+ "argv: " . var_export( $argv, true ) . "\n";
+ }
+ }
+}
diff --git a/tests/parser/preprocess/All_system_messages.expected b/tests/parser/preprocess/All_system_messages.expected
new file mode 100644
index 00000000..078d8f0d
--- /dev/null
+++ b/tests/parser/preprocess/All_system_messages.expected
@@ -0,0 +1,5625 @@
+<root><template><title>int:allmessagestext</title></template>
+
+&lt;table border=1 width=100%&gt;&lt;tr&gt;&lt;td&gt;
+'''Name'''
+&lt;/td&gt;&lt;td&gt;
+'''Default text'''
+&lt;/td&gt;&lt;td&gt;
+'''Current text'''
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:1movedto2&amp;action=edit 1movedto2]&lt;br&gt;
+[[MediaWiki_talk:1movedto2|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 moved to $2
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:1movedto2</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Monobook.css&amp;action=edit Monobook.css]&lt;br&gt;
+[[MediaWiki_talk:Monobook.css|Talk]]
+&lt;/td&gt;&lt;td&gt;
+/* edit this file to customize the monobook skin for the entire site */
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Monobook.css</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:About&amp;action=edit about]&lt;br&gt;
+[[MediaWiki_talk:About|Talk]]
+&lt;/td&gt;&lt;td&gt;
+About
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:About</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Aboutpage&amp;action=edit aboutpage]&lt;br&gt;
+[[MediaWiki_talk:Aboutpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary:About
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Aboutpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Aboutwikipedia&amp;action=edit aboutwikipedia]&lt;br&gt;
+[[MediaWiki_talk:Aboutwikipedia|Talk]]
+&lt;/td&gt;&lt;td&gt;
+About Wiktionary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Aboutwikipedia</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-addsection&amp;action=edit accesskey-addsection]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-addsection|Talk]]
+&lt;/td&gt;&lt;td&gt;
++
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-addsection</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-anontalk&amp;action=edit accesskey-anontalk]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-anontalk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+n
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-anontalk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-anonuserpage&amp;action=edit accesskey-anonuserpage]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-anonuserpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-anonuserpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-article&amp;action=edit accesskey-article]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-article|Talk]]
+&lt;/td&gt;&lt;td&gt;
+a
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-article</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-compareselectedversions&amp;action=edit accesskey-compareselectedversions]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-compareselectedversions|Talk]]
+&lt;/td&gt;&lt;td&gt;
+v
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-compareselectedversions</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-contributions&amp;action=edit accesskey-contributions]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-contributions|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;amp;lt;accesskey-contributions&amp;amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-contributions</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-currentevents&amp;action=edit accesskey-currentevents]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-currentevents|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;amp;lt;accesskey-currentevents&amp;amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-currentevents</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-delete&amp;action=edit accesskey-delete]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-delete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+d
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-delete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-edit&amp;action=edit accesskey-edit]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-edit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+e
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-edit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-emailuser&amp;action=edit accesskey-emailuser]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-emailuser|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;amp;lt;accesskey-emailuser&amp;amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-emailuser</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-help&amp;action=edit accesskey-help]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-help|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;amp;lt;accesskey-help&amp;amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-help</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-history&amp;action=edit accesskey-history]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-history|Talk]]
+&lt;/td&gt;&lt;td&gt;
+h
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-history</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-login&amp;action=edit accesskey-login]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-login|Talk]]
+&lt;/td&gt;&lt;td&gt;
+o
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-login</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-logout&amp;action=edit accesskey-logout]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-logout|Talk]]
+&lt;/td&gt;&lt;td&gt;
+o
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-logout</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-mainpage&amp;action=edit accesskey-mainpage]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-mainpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+z
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-mainpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-minoredit&amp;action=edit accesskey-minoredit]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-minoredit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+i
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-minoredit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-move&amp;action=edit accesskey-move]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-move|Talk]]
+&lt;/td&gt;&lt;td&gt;
+m
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-move</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-mycontris&amp;action=edit accesskey-mycontris]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-mycontris|Talk]]
+&lt;/td&gt;&lt;td&gt;
+y
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-mycontris</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-mytalk&amp;action=edit accesskey-mytalk]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-mytalk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+n
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-mytalk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-portal&amp;action=edit accesskey-portal]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-portal|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;amp;lt;accesskey-portal&amp;amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-portal</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-preferences&amp;action=edit accesskey-preferences]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-preferences|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;amp;lt;accesskey-preferences&amp;amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-preferences</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-preview&amp;action=edit accesskey-preview]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-preview|Talk]]
+&lt;/td&gt;&lt;td&gt;
+p
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-preview</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-protect&amp;action=edit accesskey-protect]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-protect|Talk]]
+&lt;/td&gt;&lt;td&gt;
+=
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-protect</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-randompage&amp;action=edit accesskey-randompage]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-randompage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+x
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-randompage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-recentchanges&amp;action=edit accesskey-recentchanges]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-recentchanges|Talk]]
+&lt;/td&gt;&lt;td&gt;
+r
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-recentchanges</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-recentchangeslinked&amp;action=edit accesskey-recentchangeslinked]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-recentchangeslinked|Talk]]
+&lt;/td&gt;&lt;td&gt;
+c
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-recentchangeslinked</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-save&amp;action=edit accesskey-save]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-save|Talk]]
+&lt;/td&gt;&lt;td&gt;
+s
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-save</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-search&amp;action=edit accesskey-search]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-search|Talk]]
+&lt;/td&gt;&lt;td&gt;
+f
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-search</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-sitesupport&amp;action=edit accesskey-sitesupport]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-sitesupport|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;amp;lt;accesskey-sitesupport&amp;amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-sitesupport</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-specialpage&amp;action=edit accesskey-specialpage]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-specialpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;amp;lt;accesskey-specialpage&amp;amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-specialpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-specialpages&amp;action=edit accesskey-specialpages]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-specialpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+q
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-specialpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-talk&amp;action=edit accesskey-talk]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-talk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+t
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-talk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-undelete&amp;action=edit accesskey-undelete]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-undelete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+d
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-undelete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-unwatch&amp;action=edit accesskey-unwatch]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-unwatch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+w
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-unwatch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-upload&amp;action=edit accesskey-upload]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-upload|Talk]]
+&lt;/td&gt;&lt;td&gt;
+u
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-upload</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-userpage&amp;action=edit accesskey-userpage]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-userpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-userpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-viewsource&amp;action=edit accesskey-viewsource]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-viewsource|Talk]]
+&lt;/td&gt;&lt;td&gt;
+e
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-viewsource</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-watch&amp;action=edit accesskey-watch]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-watch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+w
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-watch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-watchlist&amp;action=edit accesskey-watchlist]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-watchlist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+l
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-watchlist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-whatlinkshere&amp;action=edit accesskey-whatlinkshere]&lt;br&gt;
+[[MediaWiki_talk:Accesskey-whatlinkshere|Talk]]
+&lt;/td&gt;&lt;td&gt;
+b
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accesskey-whatlinkshere</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accmailtext&amp;action=edit accmailtext]&lt;br&gt;
+[[MediaWiki_talk:Accmailtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The Password for &amp;#39;$1&amp;#39; has been sent to $2.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accmailtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accmailtitle&amp;action=edit accmailtitle]&lt;br&gt;
+[[MediaWiki_talk:Accmailtitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Password sent.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Accmailtitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Actioncomplete&amp;action=edit actioncomplete]&lt;br&gt;
+[[MediaWiki_talk:Actioncomplete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Action complete
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Actioncomplete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Addedwatch&amp;action=edit addedwatch]&lt;br&gt;
+[[MediaWiki_talk:Addedwatch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Added to watchlist
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Addedwatch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Addedwatchtext&amp;action=edit addedwatchtext]&lt;br&gt;
+[[MediaWiki_talk:Addedwatchtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The page &amp;quot;$1&amp;quot; has been added to your &amp;#91;&amp;#91;Special:Watchlist&amp;#124;watchlist]].
+Future changes to this page and its associated Talk page will be listed there,
+and the page will appear &amp;#39;&amp;#39;&amp;#39;bolded&amp;#39;&amp;#39;&amp;#39; in the &amp;#91;&amp;#91;Special:Recentchanges&amp;#124;list of recent changes]] to
+make it easier to pick out.
+
+&amp;lt;p&amp;gt;If you want to remove the page from your watchlist later, click &amp;quot;Stop watching&amp;quot; in the sidebar.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Addedwatchtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Addsection&amp;action=edit addsection]&lt;br&gt;
+[[MediaWiki_talk:Addsection|Talk]]
+&lt;/td&gt;&lt;td&gt;
++
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Addsection</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Administrators&amp;action=edit administrators]&lt;br&gt;
+[[MediaWiki_talk:Administrators|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary:Administrators
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Administrators</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Affirmation&amp;action=edit affirmation]&lt;br&gt;
+[[MediaWiki_talk:Affirmation|Talk]]
+&lt;/td&gt;&lt;td&gt;
+I affirm that the copyright holder of this file
+agrees to license it under the terms of the $1.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Affirmation</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:All&amp;action=edit all]&lt;br&gt;
+[[MediaWiki_talk:All|Talk]]
+&lt;/td&gt;&lt;td&gt;
+all
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:All</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Allmessages&amp;action=edit allmessages]&lt;br&gt;
+[[MediaWiki_talk:Allmessages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+All system messages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Allmessages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Allmessagestext&amp;action=edit allmessagestext]&lt;br&gt;
+[[MediaWiki_talk:Allmessagestext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This is a list of all system messages available in the MediaWiki: namespace.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Allmessagestext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Allpages&amp;action=edit allpages]&lt;br&gt;
+[[MediaWiki_talk:Allpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+All pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Allpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Alphaindexline&amp;action=edit alphaindexline]&lt;br&gt;
+[[MediaWiki_talk:Alphaindexline|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 to $2
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Alphaindexline</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Alreadyloggedin&amp;action=edit alreadyloggedin]&lt;br&gt;
+[[MediaWiki_talk:Alreadyloggedin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;User $1, you are already logged in!&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;&amp;lt;br /&amp;gt;
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Alreadyloggedin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Alreadyrolled&amp;action=edit alreadyrolled]&lt;br&gt;
+[[MediaWiki_talk:Alreadyrolled|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Cannot rollback last edit of &amp;#91;&amp;#91;$1]]
+by &amp;#91;&amp;#91;User:$2&amp;#124;$2]] (&amp;#91;&amp;#91;User talk:$2&amp;#124;Talk]]); someone else has edited or rolled back the page already.
+
+Last edit was by &amp;#91;&amp;#91;User:$3&amp;#124;$3]] (&amp;#91;&amp;#91;User talk:$3&amp;#124;Talk]]).
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Alreadyrolled</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ancientpages&amp;action=edit ancientpages]&lt;br&gt;
+[[MediaWiki_talk:Ancientpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Oldest pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ancientpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:And&amp;action=edit and]&lt;br&gt;
+[[MediaWiki_talk:And|Talk]]
+&lt;/td&gt;&lt;td&gt;
+and
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:And</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Anontalk&amp;action=edit anontalk]&lt;br&gt;
+[[MediaWiki_talk:Anontalk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Talk for this IP
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Anontalk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Anontalkpagetext&amp;action=edit anontalkpagetext]&lt;br&gt;
+[[MediaWiki_talk:Anontalkpagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+----&amp;#39;&amp;#39;This is the discussion page for an anonymous user who has not created an account yet or who does not use it. We therefore have to use the numerical &amp;#91;&amp;#91;IP address]] to identify him/her. Such an IP address can be shared by several users. If you are an anonymous user and feel that irrelevant comments have been directed at you, please &amp;#91;&amp;#91;Special:Userlogin&amp;#124;create an account or log in]] to avoid future confusion with other anonymous users.&amp;#39;&amp;#39;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Anontalkpagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Anonymous&amp;action=edit anonymous]&lt;br&gt;
+[[MediaWiki_talk:Anonymous|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Anonymous user(s) of Wiktionary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Anonymous</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Article&amp;action=edit article]&lt;br&gt;
+[[MediaWiki_talk:Article|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Content page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Article</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Articleexists&amp;action=edit articleexists]&lt;br&gt;
+[[MediaWiki_talk:Articleexists|Talk]]
+&lt;/td&gt;&lt;td&gt;
+A page of that name already exists, or the
+name you have chosen is not valid.
+Please choose another name.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Articleexists</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Articlepage&amp;action=edit articlepage]&lt;br&gt;
+[[MediaWiki_talk:Articlepage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View content page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Articlepage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Asksql&amp;action=edit asksql]&lt;br&gt;
+[[MediaWiki_talk:Asksql|Talk]]
+&lt;/td&gt;&lt;td&gt;
+SQL query
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Asksql</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Asksqltext&amp;action=edit asksqltext]&lt;br&gt;
+[[MediaWiki_talk:Asksqltext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Use the form below to make a direct query of the
+database.
+Use single quotes (&amp;#39;like this&amp;#39;) to delimit string literals.
+This can often add considerable load to the server, so please use
+this function sparingly.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Asksqltext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Autoblocker&amp;action=edit autoblocker]&lt;br&gt;
+[[MediaWiki_talk:Autoblocker|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Autoblocked because you share an IP address with &amp;quot;$1&amp;quot;. Reason &amp;quot;$2&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Autoblocker</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badarticleerror&amp;action=edit badarticleerror]&lt;br&gt;
+[[MediaWiki_talk:Badarticleerror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This action cannot be performed on this page.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Badarticleerror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badfilename&amp;action=edit badfilename]&lt;br&gt;
+[[MediaWiki_talk:Badfilename|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Image name has been changed to &amp;quot;$1&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Badfilename</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badfiletype&amp;action=edit badfiletype]&lt;br&gt;
+[[MediaWiki_talk:Badfiletype|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;quot;.$1&amp;quot; is not a recommended image file format.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Badfiletype</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badipaddress&amp;action=edit badipaddress]&lt;br&gt;
+[[MediaWiki_talk:Badipaddress|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Invalid IP address
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Badipaddress</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badquery&amp;action=edit badquery]&lt;br&gt;
+[[MediaWiki_talk:Badquery|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Badly formed search query
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Badquery</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badquerytext&amp;action=edit badquerytext]&lt;br&gt;
+[[MediaWiki_talk:Badquerytext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+We could not process your query.
+This is probably because you have attempted to search for a
+word fewer than three letters long, which is not yet supported.
+It could also be that you have mistyped the expression, for
+example &amp;quot;fish and and scales&amp;quot;.
+Please try another query.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Badquerytext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badretype&amp;action=edit badretype]&lt;br&gt;
+[[MediaWiki_talk:Badretype|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The passwords you entered do not match.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Badretype</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badtitle&amp;action=edit badtitle]&lt;br&gt;
+[[MediaWiki_talk:Badtitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Bad title
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Badtitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badtitletext&amp;action=edit badtitletext]&lt;br&gt;
+[[MediaWiki_talk:Badtitletext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The requested page title was invalid, empty, or
+an incorrectly linked inter-language or inter-wiki title.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Badtitletext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blanknamespace&amp;action=edit blanknamespace]&lt;br&gt;
+[[MediaWiki_talk:Blanknamespace|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(Main)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blanknamespace</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockedtext&amp;action=edit blockedtext]&lt;br&gt;
+[[MediaWiki_talk:Blockedtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your user name or IP address has been blocked by $1.
+The reason given is this:&amp;lt;br /&amp;gt;&amp;#39;&amp;#39;$2&amp;#39;&amp;#39;&amp;lt;p&amp;gt;You may contact $1 or one of the other
+&amp;#91;&amp;#91;Wiktionary:Administrators&amp;#124;administrators]] to discuss the block.
+
+Note that you may not use the &amp;quot;email this user&amp;quot; feature unless you have a valid email address registered in your &amp;#91;&amp;#91;Special:Preferences&amp;#124;user preferences]].
+
+Your IP address is $3. Please include this address in any queries you make.
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blockedtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockedtitle&amp;action=edit blockedtitle]&lt;br&gt;
+[[MediaWiki_talk:Blockedtitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+User is blocked
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blockedtitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockip&amp;action=edit blockip]&lt;br&gt;
+[[MediaWiki_talk:Blockip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Block user
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blockip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockipsuccesssub&amp;action=edit blockipsuccesssub]&lt;br&gt;
+[[MediaWiki_talk:Blockipsuccesssub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Block succeeded
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blockipsuccesssub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockipsuccesstext&amp;action=edit blockipsuccesstext]&lt;br&gt;
+[[MediaWiki_talk:Blockipsuccesstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;quot;$1&amp;quot; has been blocked.
+&amp;lt;br /&amp;gt;See &amp;#91;&amp;#91;Special:Ipblocklist&amp;#124;IP block list]] to review blocks.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blockipsuccesstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockiptext&amp;action=edit blockiptext]&lt;br&gt;
+[[MediaWiki_talk:Blockiptext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Use the form below to block write access
+from a specific IP address or username.
+This should be done only only to prevent vandalism, and in
+accordance with &amp;#91;&amp;#91;Wiktionary:Policy&amp;#124;policy]].
+Fill in a specific reason below (for example, citing particular
+pages that were vandalized).
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blockiptext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklink&amp;action=edit blocklink]&lt;br&gt;
+[[MediaWiki_talk:Blocklink|Talk]]
+&lt;/td&gt;&lt;td&gt;
+block
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blocklink</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklistline&amp;action=edit blocklistline]&lt;br&gt;
+[[MediaWiki_talk:Blocklistline|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1, $2 blocked $3 (expires $4)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blocklistline</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklogentry&amp;action=edit blocklogentry]&lt;br&gt;
+[[MediaWiki_talk:Blocklogentry|Talk]]
+&lt;/td&gt;&lt;td&gt;
+blocked &amp;quot;$1&amp;quot; with an expiry time of $2
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blocklogentry</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklogpage&amp;action=edit blocklogpage]&lt;br&gt;
+[[MediaWiki_talk:Blocklogpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Block_log
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blocklogpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklogtext&amp;action=edit blocklogtext]&lt;br&gt;
+[[MediaWiki_talk:Blocklogtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This is a log of user blocking and unblocking actions. Automatically
+blocked IP addresses are not be listed. See the &amp;#91;&amp;#91;Special:Ipblocklist&amp;#124;IP block list]] for
+the list of currently operational bans and blocks.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Blocklogtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bold_sample&amp;action=edit bold_sample]&lt;br&gt;
+[[MediaWiki_talk:Bold_sample|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Bold text
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bold_sample</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bold_tip&amp;action=edit bold_tip]&lt;br&gt;
+[[MediaWiki_talk:Bold_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Bold text
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bold_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Booksources&amp;action=edit booksources]&lt;br&gt;
+[[MediaWiki_talk:Booksources|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Book sources
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Booksources</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Booksourcetext&amp;action=edit booksourcetext]&lt;br&gt;
+[[MediaWiki_talk:Booksourcetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Below is a list of links to other sites that
+sell new and used books, and may also have further information
+about books you are looking for.Wiktionary is not affiliated with any of these businesses, and
+this list should not be construed as an endorsement.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Booksourcetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Brokenredirects&amp;action=edit brokenredirects]&lt;br&gt;
+[[MediaWiki_talk:Brokenredirects|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Broken Redirects
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Brokenredirects</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Brokenredirectstext&amp;action=edit brokenredirectstext]&lt;br&gt;
+[[MediaWiki_talk:Brokenredirectstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The following redirects link to a non-existing pages.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Brokenredirectstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bugreports&amp;action=edit bugreports]&lt;br&gt;
+[[MediaWiki_talk:Bugreports|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Bug reports
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bugreports</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bugreportspage&amp;action=edit bugreportspage]&lt;br&gt;
+[[MediaWiki_talk:Bugreportspage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary:Bug_reports
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bugreportspage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bureaucratlog&amp;action=edit bureaucratlog]&lt;br&gt;
+[[MediaWiki_talk:Bureaucratlog|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Bureaucrat_log
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bureaucratlog</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bureaucratlogentry&amp;action=edit bureaucratlogentry]&lt;br&gt;
+[[MediaWiki_talk:Bureaucratlogentry|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Rights for user &amp;quot;$1&amp;quot; set &amp;quot;$2&amp;quot;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bureaucratlogentry</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bureaucrattext&amp;action=edit bureaucrattext]&lt;br&gt;
+[[MediaWiki_talk:Bureaucrattext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The action you have requested can only be
+performed by sysops with &amp;quot;bureaucrat&amp;quot; status.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bureaucrattext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bureaucrattitle&amp;action=edit bureaucrattitle]&lt;br&gt;
+[[MediaWiki_talk:Bureaucrattitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Bureaucrat access required
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bureaucrattitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bydate&amp;action=edit bydate]&lt;br&gt;
+[[MediaWiki_talk:Bydate|Talk]]
+&lt;/td&gt;&lt;td&gt;
+by date
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bydate</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Byname&amp;action=edit byname]&lt;br&gt;
+[[MediaWiki_talk:Byname|Talk]]
+&lt;/td&gt;&lt;td&gt;
+by name
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Byname</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bysize&amp;action=edit bysize]&lt;br&gt;
+[[MediaWiki_talk:Bysize|Talk]]
+&lt;/td&gt;&lt;td&gt;
+by size
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Bysize</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cachederror&amp;action=edit cachederror]&lt;br&gt;
+[[MediaWiki_talk:Cachederror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The following is a cached copy of the requested page, and may not be up to date.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Cachederror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cancel&amp;action=edit cancel]&lt;br&gt;
+[[MediaWiki_talk:Cancel|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Cancel
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Cancel</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cannotdelete&amp;action=edit cannotdelete]&lt;br&gt;
+[[MediaWiki_talk:Cannotdelete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Could not delete the page or image specified. (It may have already been deleted by someone else.)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Cannotdelete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cantrollback&amp;action=edit cantrollback]&lt;br&gt;
+[[MediaWiki_talk:Cantrollback|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Cannot revert edit; last contributor is only author of this page.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Cantrollback</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Categories&amp;action=edit categories]&lt;br&gt;
+[[MediaWiki_talk:Categories|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Categories
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Categories</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Category&amp;action=edit category]&lt;br&gt;
+[[MediaWiki_talk:Category|Talk]]
+&lt;/td&gt;&lt;td&gt;
+category
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Category</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Category_header&amp;action=edit category_header]&lt;br&gt;
+[[MediaWiki_talk:Category_header|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Articles in category &amp;quot;$1&amp;quot;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Category_header</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Changepassword&amp;action=edit changepassword]&lt;br&gt;
+[[MediaWiki_talk:Changepassword|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Change password
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Changepassword</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Changes&amp;action=edit changes]&lt;br&gt;
+[[MediaWiki_talk:Changes|Talk]]
+&lt;/td&gt;&lt;td&gt;
+changes
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Changes</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Columns&amp;action=edit columns]&lt;br&gt;
+[[MediaWiki_talk:Columns|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Columns
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Columns</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Commentedit&amp;action=edit commentedit]&lt;br&gt;
+[[MediaWiki_talk:Commentedit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+ (comment)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Commentedit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Compareselectedversions&amp;action=edit compareselectedversions]&lt;br&gt;
+[[MediaWiki_talk:Compareselectedversions|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Compare selected versions
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Compareselectedversions</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirm&amp;action=edit confirm]&lt;br&gt;
+[[MediaWiki_talk:Confirm|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Confirm
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Confirm</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmcheck&amp;action=edit confirmcheck]&lt;br&gt;
+[[MediaWiki_talk:Confirmcheck|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Yes, I really want to delete this.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Confirmcheck</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmdelete&amp;action=edit confirmdelete]&lt;br&gt;
+[[MediaWiki_talk:Confirmdelete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Confirm delete
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Confirmdelete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmdeletetext&amp;action=edit confirmdeletetext]&lt;br&gt;
+[[MediaWiki_talk:Confirmdeletetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You are about to permanently delete a page
+or image along with all of its history from the database.
+Please confirm that you intend to do this, that you understand the
+consequences, and that you are doing this in accordance with
+&amp;#91;&amp;#91;Wiktionary:Policy]].
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Confirmdeletetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmprotect&amp;action=edit confirmprotect]&lt;br&gt;
+[[MediaWiki_talk:Confirmprotect|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Confirm protection
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Confirmprotect</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmprotecttext&amp;action=edit confirmprotecttext]&lt;br&gt;
+[[MediaWiki_talk:Confirmprotecttext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Do you really want to protect this page?
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Confirmprotecttext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmunprotect&amp;action=edit confirmunprotect]&lt;br&gt;
+[[MediaWiki_talk:Confirmunprotect|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Confirm unprotection
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Confirmunprotect</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmunprotecttext&amp;action=edit confirmunprotecttext]&lt;br&gt;
+[[MediaWiki_talk:Confirmunprotecttext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Do you really want to unprotect this page?
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Confirmunprotecttext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contextchars&amp;action=edit contextchars]&lt;br&gt;
+[[MediaWiki_talk:Contextchars|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Characters of context per line
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Contextchars</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contextlines&amp;action=edit contextlines]&lt;br&gt;
+[[MediaWiki_talk:Contextlines|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Lines to show per hit
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Contextlines</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contribslink&amp;action=edit contribslink]&lt;br&gt;
+[[MediaWiki_talk:Contribslink|Talk]]
+&lt;/td&gt;&lt;td&gt;
+contribs
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Contribslink</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contribsub&amp;action=edit contribsub]&lt;br&gt;
+[[MediaWiki_talk:Contribsub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+For $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Contribsub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contributions&amp;action=edit contributions]&lt;br&gt;
+[[MediaWiki_talk:Contributions|Talk]]
+&lt;/td&gt;&lt;td&gt;
+User contributions
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Contributions</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Copyright&amp;action=edit copyright]&lt;br&gt;
+[[MediaWiki_talk:Copyright|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Content is available under $1.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Copyright</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Copyrightpage&amp;action=edit copyrightpage]&lt;br&gt;
+[[MediaWiki_talk:Copyrightpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary:Copyrights
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Copyrightpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Copyrightpagename&amp;action=edit copyrightpagename]&lt;br&gt;
+[[MediaWiki_talk:Copyrightpagename|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary copyright
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Copyrightpagename</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Copyrightwarning&amp;action=edit copyrightwarning]&lt;br&gt;
+[[MediaWiki_talk:Copyrightwarning|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Please note that all contributions to Wiktionary are
+considered to be released under the GNU Free Documentation License
+(see $1 for details).
+If you don&amp;#39;t want your writing to be edited mercilessly and redistributed
+at will, then don&amp;#39;t submit it here.&amp;lt;br /&amp;gt;
+You are also promising us that you wrote this yourself, or copied it from a
+public domain or similar free resource.
+&amp;lt;strong&amp;gt;DO NOT SUBMIT COPYRIGHTED WORK WITHOUT PERMISSION!&amp;lt;/strong&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Copyrightwarning</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Couldntremove&amp;action=edit couldntremove]&lt;br&gt;
+[[MediaWiki_talk:Couldntremove|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Couldn&amp;#39;t remove item &amp;#39;$1&amp;#39;...
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Couldntremove</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Createaccount&amp;action=edit createaccount]&lt;br&gt;
+[[MediaWiki_talk:Createaccount|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Create new account
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Createaccount</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Createaccountmail&amp;action=edit createaccountmail]&lt;br&gt;
+[[MediaWiki_talk:Createaccountmail|Talk]]
+&lt;/td&gt;&lt;td&gt;
+by email
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Createaccountmail</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cur&amp;action=edit cur]&lt;br&gt;
+[[MediaWiki_talk:Cur|Talk]]
+&lt;/td&gt;&lt;td&gt;
+cur
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Cur</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Currentevents&amp;action=edit currentevents]&lt;br&gt;
+[[MediaWiki_talk:Currentevents|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Current events
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Currentevents</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Currentrev&amp;action=edit currentrev]&lt;br&gt;
+[[MediaWiki_talk:Currentrev|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Current revision
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Currentrev</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Databaseerror&amp;action=edit databaseerror]&lt;br&gt;
+[[MediaWiki_talk:Databaseerror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Database error
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Databaseerror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dateformat&amp;action=edit dateformat]&lt;br&gt;
+[[MediaWiki_talk:Dateformat|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Date format
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Dateformat</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dberrortext&amp;action=edit dberrortext]&lt;br&gt;
+[[MediaWiki_talk:Dberrortext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+A database query syntax error has occurred.
+This could be because of an illegal search query (see $5),
+or it may indicate a bug in the software.
+The last attempted database query was:
+&amp;lt;blockquote&amp;gt;&amp;lt;tt&amp;gt;$1&amp;lt;/tt&amp;gt;&amp;lt;/blockquote&amp;gt;
+from within function &amp;quot;&amp;lt;tt&amp;gt;$2&amp;lt;/tt&amp;gt;&amp;quot;.
+MySQL returned error &amp;quot;&amp;lt;tt&amp;gt;$3: $4&amp;lt;/tt&amp;gt;&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Dberrortext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dberrortextcl&amp;action=edit dberrortextcl]&lt;br&gt;
+[[MediaWiki_talk:Dberrortextcl|Talk]]
+&lt;/td&gt;&lt;td&gt;
+A database query syntax error has occurred.
+The last attempted database query was:
+&amp;quot;$1&amp;quot;
+from within function &amp;quot;$2&amp;quot;.
+MySQL returned error &amp;quot;$3: $4&amp;quot;.
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Dberrortextcl</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deadendpages&amp;action=edit deadendpages]&lt;br&gt;
+[[MediaWiki_talk:Deadendpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Dead-end pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Deadendpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Debug&amp;action=edit debug]&lt;br&gt;
+[[MediaWiki_talk:Debug|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Debug
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Debug</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Defaultns&amp;action=edit defaultns]&lt;br&gt;
+[[MediaWiki_talk:Defaultns|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Search in these namespaces by default:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Defaultns</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Defemailsubject&amp;action=edit defemailsubject]&lt;br&gt;
+[[MediaWiki_talk:Defemailsubject|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary e-mail
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Defemailsubject</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Delete&amp;action=edit delete]&lt;br&gt;
+[[MediaWiki_talk:Delete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Delete
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Delete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletecomment&amp;action=edit deletecomment]&lt;br&gt;
+[[MediaWiki_talk:Deletecomment|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Reason for deletion
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Deletecomment</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletedarticle&amp;action=edit deletedarticle]&lt;br&gt;
+[[MediaWiki_talk:Deletedarticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+deleted &amp;quot;$1&amp;quot;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Deletedarticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletedtext&amp;action=edit deletedtext]&lt;br&gt;
+[[MediaWiki_talk:Deletedtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;quot;$1&amp;quot; has been deleted.
+See $2 for a record of recent deletions.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Deletedtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deleteimg&amp;action=edit deleteimg]&lt;br&gt;
+[[MediaWiki_talk:Deleteimg|Talk]]
+&lt;/td&gt;&lt;td&gt;
+del
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Deleteimg</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletepage&amp;action=edit deletepage]&lt;br&gt;
+[[MediaWiki_talk:Deletepage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Delete page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Deletepage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletesub&amp;action=edit deletesub]&lt;br&gt;
+[[MediaWiki_talk:Deletesub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(Deleting &amp;quot;$1&amp;quot;)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Deletesub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletethispage&amp;action=edit deletethispage]&lt;br&gt;
+[[MediaWiki_talk:Deletethispage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Delete this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Deletethispage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletionlog&amp;action=edit deletionlog]&lt;br&gt;
+[[MediaWiki_talk:Deletionlog|Talk]]
+&lt;/td&gt;&lt;td&gt;
+deletion log
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Deletionlog</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dellogpage&amp;action=edit dellogpage]&lt;br&gt;
+[[MediaWiki_talk:Dellogpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Deletion_log
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Dellogpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dellogpagetext&amp;action=edit dellogpagetext]&lt;br&gt;
+[[MediaWiki_talk:Dellogpagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Below is a list of the most recent deletions.
+All times shown are server time (UTC).
+&amp;lt;ul&amp;gt;
+&amp;lt;/ul&amp;gt;
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Dellogpagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Developerspheading&amp;action=edit developerspheading]&lt;br&gt;
+[[MediaWiki_talk:Developerspheading|Talk]]
+&lt;/td&gt;&lt;td&gt;
+For developer use only
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Developerspheading</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Developertext&amp;action=edit developertext]&lt;br&gt;
+[[MediaWiki_talk:Developertext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The action you have requested can only be
+performed by users with &amp;quot;developer&amp;quot; status.
+See $1.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Developertext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Developertitle&amp;action=edit developertitle]&lt;br&gt;
+[[MediaWiki_talk:Developertitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Developer access required
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Developertitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Diff&amp;action=edit diff]&lt;br&gt;
+[[MediaWiki_talk:Diff|Talk]]
+&lt;/td&gt;&lt;td&gt;
+diff
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Diff</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Difference&amp;action=edit difference]&lt;br&gt;
+[[MediaWiki_talk:Difference|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(Difference between revisions)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Difference</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Disclaimerpage&amp;action=edit disclaimerpage]&lt;br&gt;
+[[MediaWiki_talk:Disclaimerpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary:General_disclaimer
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Disclaimerpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Disclaimers&amp;action=edit disclaimers]&lt;br&gt;
+[[MediaWiki_talk:Disclaimers|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Disclaimers
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Disclaimers</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Doubleredirects&amp;action=edit doubleredirects]&lt;br&gt;
+[[MediaWiki_talk:Doubleredirects|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Double Redirects
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Doubleredirects</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Doubleredirectstext&amp;action=edit doubleredirectstext]&lt;br&gt;
+[[MediaWiki_talk:Doubleredirectstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;b&amp;gt;Attention:&amp;lt;/b&amp;gt; This list may contain false positives. That usually means there is additional text with links below the first #REDIRECT.&amp;lt;br /&amp;gt;
+Each row contains links to the first and second redirect, as well as the first line of the second redirect text, usually giving the &amp;quot;real&amp;quot; target page, which the first redirect should point to.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Doubleredirectstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Edit&amp;action=edit edit]&lt;br&gt;
+[[MediaWiki_talk:Edit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Edit
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Edit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editcomment&amp;action=edit editcomment]&lt;br&gt;
+[[MediaWiki_talk:Editcomment|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The edit comment was: &amp;quot;&amp;lt;i&amp;gt;$1&amp;lt;/i&amp;gt;&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Editcomment</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editconflict&amp;action=edit editconflict]&lt;br&gt;
+[[MediaWiki_talk:Editconflict|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Edit conflict: $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Editconflict</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editcurrent&amp;action=edit editcurrent]&lt;br&gt;
+[[MediaWiki_talk:Editcurrent|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Edit the current version of this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Editcurrent</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Edithelp&amp;action=edit edithelp]&lt;br&gt;
+[[MediaWiki_talk:Edithelp|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Editing help
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Edithelp</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Edithelppage&amp;action=edit edithelppage]&lt;br&gt;
+[[MediaWiki_talk:Edithelppage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Help:Editing
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Edithelppage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editing&amp;action=edit editing]&lt;br&gt;
+[[MediaWiki_talk:Editing|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Editing $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Editing</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editingold&amp;action=edit editingold]&lt;br&gt;
+[[MediaWiki_talk:Editingold|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;strong&amp;gt;WARNING: You are editing an out-of-date
+revision of this page.
+If you save it, any changes made since this revision will be lost.&amp;lt;/strong&amp;gt;
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Editingold</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editsection&amp;action=edit editsection]&lt;br&gt;
+[[MediaWiki_talk:Editsection|Talk]]
+&lt;/td&gt;&lt;td&gt;
+edit
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Editsection</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editthispage&amp;action=edit editthispage]&lt;br&gt;
+[[MediaWiki_talk:Editthispage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Edit this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Editthispage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailflag&amp;action=edit emailflag]&lt;br&gt;
+[[MediaWiki_talk:Emailflag|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Disable e-mail from other users
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailflag</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailforlost&amp;action=edit emailforlost]&lt;br&gt;
+[[MediaWiki_talk:Emailforlost|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Fields marked with a star (*) are optional. Storing an email address enables people to contact you through the website without you having to reveal your
+email address to them, and it can be used to send you a new password if you forget it.&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;Your real name, if you choose to provide it, will be used for giving you attribution for your work.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailforlost</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailfrom&amp;action=edit emailfrom]&lt;br&gt;
+[[MediaWiki_talk:Emailfrom|Talk]]
+&lt;/td&gt;&lt;td&gt;
+From
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailfrom</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailmessage&amp;action=edit emailmessage]&lt;br&gt;
+[[MediaWiki_talk:Emailmessage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Message
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailmessage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailpage&amp;action=edit emailpage]&lt;br&gt;
+[[MediaWiki_talk:Emailpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+E-mail user
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailpagetext&amp;action=edit emailpagetext]&lt;br&gt;
+[[MediaWiki_talk:Emailpagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+If this user has entered a valid e-mail address in
+his or her user preferences, the form below will send a single message.
+The e-mail address you entered in your user preferences will appear
+as the &amp;quot;From&amp;quot; address of the mail, so the recipient will be able
+to reply.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailpagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailsend&amp;action=edit emailsend]&lt;br&gt;
+[[MediaWiki_talk:Emailsend|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Send
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailsend</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailsent&amp;action=edit emailsent]&lt;br&gt;
+[[MediaWiki_talk:Emailsent|Talk]]
+&lt;/td&gt;&lt;td&gt;
+E-mail sent
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailsent</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailsenttext&amp;action=edit emailsenttext]&lt;br&gt;
+[[MediaWiki_talk:Emailsenttext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your e-mail message has been sent.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailsenttext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailsubject&amp;action=edit emailsubject]&lt;br&gt;
+[[MediaWiki_talk:Emailsubject|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Subject
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailsubject</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailto&amp;action=edit emailto]&lt;br&gt;
+[[MediaWiki_talk:Emailto|Talk]]
+&lt;/td&gt;&lt;td&gt;
+To
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailto</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailuser&amp;action=edit emailuser]&lt;br&gt;
+[[MediaWiki_talk:Emailuser|Talk]]
+&lt;/td&gt;&lt;td&gt;
+E-mail this user
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Emailuser</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Enterlockreason&amp;action=edit enterlockreason]&lt;br&gt;
+[[MediaWiki_talk:Enterlockreason|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Enter a reason for the lock, including an estimate
+of when the lock will be released
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Enterlockreason</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Error&amp;action=edit error]&lt;br&gt;
+[[MediaWiki_talk:Error|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Error
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Error</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Errorpagetitle&amp;action=edit errorpagetitle]&lt;br&gt;
+[[MediaWiki_talk:Errorpagetitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Error
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Errorpagetitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Exbeforeblank&amp;action=edit exbeforeblank]&lt;br&gt;
+[[MediaWiki_talk:Exbeforeblank|Talk]]
+&lt;/td&gt;&lt;td&gt;
+content before blanking was:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Exbeforeblank</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Exblank&amp;action=edit exblank]&lt;br&gt;
+[[MediaWiki_talk:Exblank|Talk]]
+&lt;/td&gt;&lt;td&gt;
+page was empty
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Exblank</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Excontent&amp;action=edit excontent]&lt;br&gt;
+[[MediaWiki_talk:Excontent|Talk]]
+&lt;/td&gt;&lt;td&gt;
+content was:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Excontent</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Explainconflict&amp;action=edit explainconflict]&lt;br&gt;
+[[MediaWiki_talk:Explainconflict|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Someone else has changed this page since you
+started editing it.
+The upper text area contains the page text as it currently exists.
+Your changes are shown in the lower text area.
+You will have to merge your changes into the existing text.
+&amp;lt;b&amp;gt;Only&amp;lt;/b&amp;gt; the text in the upper text area will be saved when you
+press &amp;quot;Save page&amp;quot;.
+&amp;lt;p&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Explainconflict</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Export&amp;action=edit export]&lt;br&gt;
+[[MediaWiki_talk:Export|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Export pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Export</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Exportcuronly&amp;action=edit exportcuronly]&lt;br&gt;
+[[MediaWiki_talk:Exportcuronly|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Include only the current revision, not the full history
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Exportcuronly</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Exporttext&amp;action=edit exporttext]&lt;br&gt;
+[[MediaWiki_talk:Exporttext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You can export the text and editing history of a particular
+page or set of pages wrapped in some XML; this can then be imported into another
+wiki running MediaWiki software, transformed, or just kept for your private
+amusement.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Exporttext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Extlink_sample&amp;action=edit extlink_sample]&lt;br&gt;
+[[MediaWiki_talk:Extlink_sample|Talk]]
+&lt;/td&gt;&lt;td&gt;
+http&amp;#58;//www.example.com link title
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Extlink_sample</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Extlink_tip&amp;action=edit extlink_tip]&lt;br&gt;
+[[MediaWiki_talk:Extlink_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+External link (remember http&amp;#58;// prefix)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Extlink_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Faq&amp;action=edit faq]&lt;br&gt;
+[[MediaWiki_talk:Faq|Talk]]
+&lt;/td&gt;&lt;td&gt;
+FAQ
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Faq</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Faqpage&amp;action=edit faqpage]&lt;br&gt;
+[[MediaWiki_talk:Faqpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary:FAQ
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Faqpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Feedlinks&amp;action=edit feedlinks]&lt;br&gt;
+[[MediaWiki_talk:Feedlinks|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Feed:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Feedlinks</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filecopyerror&amp;action=edit filecopyerror]&lt;br&gt;
+[[MediaWiki_talk:Filecopyerror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Could not copy file &amp;quot;$1&amp;quot; to &amp;quot;$2&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Filecopyerror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filedeleteerror&amp;action=edit filedeleteerror]&lt;br&gt;
+[[MediaWiki_talk:Filedeleteerror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Could not delete file &amp;quot;$1&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Filedeleteerror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filedesc&amp;action=edit filedesc]&lt;br&gt;
+[[MediaWiki_talk:Filedesc|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Summary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Filedesc</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filename&amp;action=edit filename]&lt;br&gt;
+[[MediaWiki_talk:Filename|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Filename
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Filename</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filenotfound&amp;action=edit filenotfound]&lt;br&gt;
+[[MediaWiki_talk:Filenotfound|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Could not find file &amp;quot;$1&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Filenotfound</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filerenameerror&amp;action=edit filerenameerror]&lt;br&gt;
+[[MediaWiki_talk:Filerenameerror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Could not rename file &amp;quot;$1&amp;quot; to &amp;quot;$2&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Filerenameerror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filesource&amp;action=edit filesource]&lt;br&gt;
+[[MediaWiki_talk:Filesource|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Source
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Filesource</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filestatus&amp;action=edit filestatus]&lt;br&gt;
+[[MediaWiki_talk:Filestatus|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Copyright status
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Filestatus</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Fileuploaded&amp;action=edit fileuploaded]&lt;br&gt;
+[[MediaWiki_talk:Fileuploaded|Talk]]
+&lt;/td&gt;&lt;td&gt;
+File &amp;quot;$1&amp;quot; uploaded successfully.
+Please follow this link: $2 to the description page and fill
+in information about the file, such as where it came from, when it was
+created and by whom, and anything else you may know about it.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Fileuploaded</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Formerror&amp;action=edit formerror]&lt;br&gt;
+[[MediaWiki_talk:Formerror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Error: could not submit form
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Formerror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Fromwikipedia&amp;action=edit fromwikipedia]&lt;br&gt;
+[[MediaWiki_talk:Fromwikipedia|Talk]]
+&lt;/td&gt;&lt;td&gt;
+From Wiktionary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Fromwikipedia</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Getimagelist&amp;action=edit getimagelist]&lt;br&gt;
+[[MediaWiki_talk:Getimagelist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+fetching image list
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Getimagelist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Go&amp;action=edit go]&lt;br&gt;
+[[MediaWiki_talk:Go|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Go
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Go</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Googlesearch&amp;action=edit googlesearch]&lt;br&gt;
+[[MediaWiki_talk:Googlesearch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+
+&amp;lt;!-- SiteSearch Google --&amp;gt;
+&amp;lt;FORM method=GET action=&amp;quot;http&amp;#58;//www.google.com/search&amp;quot;&amp;gt;
+&amp;lt;TABLE bgcolor=&amp;quot;#FFFFFF&amp;quot;&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;
+&amp;lt;A HREF=&amp;quot;http&amp;#58;//www.google.com/&amp;quot;&amp;gt;
+&amp;lt;IMG SRC=&amp;quot;http&amp;#58;//www.google.com/logos/Logo_40wht.gif&amp;quot;
+border=&amp;quot;0&amp;quot; ALT=&amp;quot;Google&amp;quot;&amp;gt;&amp;lt;/A&amp;gt;
+&amp;lt;/td&amp;gt;
+&amp;lt;td&amp;gt;
+&amp;lt;INPUT TYPE=text name=q size=31 maxlength=255 value=&amp;quot;$1&amp;quot;&amp;gt;
+&amp;lt;INPUT type=submit name=btnG VALUE=&amp;quot;Google Search&amp;quot;&amp;gt;
+&amp;lt;font size=-1&amp;gt;
+&amp;lt;input type=hidden name=domains value=&amp;quot;http&amp;#58;//tl.wiktionary.org&amp;quot;&amp;gt;&amp;lt;br /&amp;gt;&amp;lt;input type=radio name=sitesearch value=&amp;quot;&amp;quot;&amp;gt; WWW &amp;lt;input type=radio name=sitesearch value=&amp;quot;http&amp;#58;//tl.wiktionary.org&amp;quot; checked&amp;gt; http&amp;#58;//tl.wiktionary.org &amp;lt;br /&amp;gt;
+&amp;lt;input type=&amp;#39;hidden&amp;#39; name=&amp;#39;ie&amp;#39; value=&amp;#39;$2&amp;#39;&amp;gt;
+&amp;lt;input type=&amp;#39;hidden&amp;#39; name=&amp;#39;oe&amp;#39; value=&amp;#39;$2&amp;#39;&amp;gt;
+&amp;lt;/font&amp;gt;
+&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/TABLE&amp;gt;
+&amp;lt;/FORM&amp;gt;
+&amp;lt;!-- SiteSearch Google --&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Googlesearch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Guesstimezone&amp;action=edit guesstimezone]&lt;br&gt;
+[[MediaWiki_talk:Guesstimezone|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Fill in from browser
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Guesstimezone</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Headline_sample&amp;action=edit headline_sample]&lt;br&gt;
+[[MediaWiki_talk:Headline_sample|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Headline text
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Headline_sample</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Headline_tip&amp;action=edit headline_tip]&lt;br&gt;
+[[MediaWiki_talk:Headline_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Level 2 headline
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Headline_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Help&amp;action=edit help]&lt;br&gt;
+[[MediaWiki_talk:Help|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Help
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Help</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Helppage&amp;action=edit helppage]&lt;br&gt;
+[[MediaWiki_talk:Helppage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Help:Contents
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Helppage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Hide&amp;action=edit hide]&lt;br&gt;
+[[MediaWiki_talk:Hide|Talk]]
+&lt;/td&gt;&lt;td&gt;
+hide
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Hide</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Hidetoc&amp;action=edit hidetoc]&lt;br&gt;
+[[MediaWiki_talk:Hidetoc|Talk]]
+&lt;/td&gt;&lt;td&gt;
+hide
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Hidetoc</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Hist&amp;action=edit hist]&lt;br&gt;
+[[MediaWiki_talk:Hist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+hist
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Hist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Histlegend&amp;action=edit histlegend]&lt;br&gt;
+[[MediaWiki_talk:Histlegend|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Diff selection: mark the radio boxes of the versions to compare and hit enter or the button at the bottom.&amp;lt;br/&amp;gt;
+Legend: (cur) = difference with current version,
+(last) = difference with preceding version, M = minor edit.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Histlegend</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:History&amp;action=edit history]&lt;br&gt;
+[[MediaWiki_talk:History|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Page history
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:History</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:History_short&amp;action=edit history_short]&lt;br&gt;
+[[MediaWiki_talk:History_short|Talk]]
+&lt;/td&gt;&lt;td&gt;
+History
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:History_short</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Historywarning&amp;action=edit historywarning]&lt;br&gt;
+[[MediaWiki_talk:Historywarning|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Warning: The page you are about to delete has a history:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Historywarning</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Hr_tip&amp;action=edit hr_tip]&lt;br&gt;
+[[MediaWiki_talk:Hr_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Horizontal line (use sparingly)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Hr_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ignorewarning&amp;action=edit ignorewarning]&lt;br&gt;
+[[MediaWiki_talk:Ignorewarning|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Ignore warning and save file anyway.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ignorewarning</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ilshowmatch&amp;action=edit ilshowmatch]&lt;br&gt;
+[[MediaWiki_talk:Ilshowmatch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Show all images with names matching
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ilshowmatch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ilsubmit&amp;action=edit ilsubmit]&lt;br&gt;
+[[MediaWiki_talk:Ilsubmit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Search
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ilsubmit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Image_sample&amp;action=edit image_sample]&lt;br&gt;
+[[MediaWiki_talk:Image_sample|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Example.jpg
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Image_sample</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Image_tip&amp;action=edit image_tip]&lt;br&gt;
+[[MediaWiki_talk:Image_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Embedded image
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Image_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagelinks&amp;action=edit imagelinks]&lt;br&gt;
+[[MediaWiki_talk:Imagelinks|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Image links
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imagelinks</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagelist&amp;action=edit imagelist]&lt;br&gt;
+[[MediaWiki_talk:Imagelist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Image list
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imagelist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagelisttext&amp;action=edit imagelisttext]&lt;br&gt;
+[[MediaWiki_talk:Imagelisttext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Below is a list of $1 images sorted $2.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imagelisttext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagepage&amp;action=edit imagepage]&lt;br&gt;
+[[MediaWiki_talk:Imagepage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View image page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imagepage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagereverted&amp;action=edit imagereverted]&lt;br&gt;
+[[MediaWiki_talk:Imagereverted|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Revert to earlier version was successful.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imagereverted</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imgdelete&amp;action=edit imgdelete]&lt;br&gt;
+[[MediaWiki_talk:Imgdelete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+del
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imgdelete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imgdesc&amp;action=edit imgdesc]&lt;br&gt;
+[[MediaWiki_talk:Imgdesc|Talk]]
+&lt;/td&gt;&lt;td&gt;
+desc
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imgdesc</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imghistlegend&amp;action=edit imghistlegend]&lt;br&gt;
+[[MediaWiki_talk:Imghistlegend|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Legend: (cur) = this is the current image, (del) = delete
+this old version, (rev) = revert to this old version.
+&amp;lt;br /&amp;gt;&amp;lt;i&amp;gt;Click on date to see image uploaded on that date&amp;lt;/i&amp;gt;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imghistlegend</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imghistory&amp;action=edit imghistory]&lt;br&gt;
+[[MediaWiki_talk:Imghistory|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Image history
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imghistory</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imglegend&amp;action=edit imglegend]&lt;br&gt;
+[[MediaWiki_talk:Imglegend|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Legend: (desc) = show/edit image description.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Imglegend</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Import&amp;action=edit import]&lt;br&gt;
+[[MediaWiki_talk:Import|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Import pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Import</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importfailed&amp;action=edit importfailed]&lt;br&gt;
+[[MediaWiki_talk:Importfailed|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Import failed: $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Importfailed</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importhistoryconflict&amp;action=edit importhistoryconflict]&lt;br&gt;
+[[MediaWiki_talk:Importhistoryconflict|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Conflicting history revision exists (may have imported this page before)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Importhistoryconflict</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importnotext&amp;action=edit importnotext]&lt;br&gt;
+[[MediaWiki_talk:Importnotext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Empty or no text
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Importnotext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importsuccess&amp;action=edit importsuccess]&lt;br&gt;
+[[MediaWiki_talk:Importsuccess|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Import succeeded!
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Importsuccess</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importtext&amp;action=edit importtext]&lt;br&gt;
+[[MediaWiki_talk:Importtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Please export the file from the source wiki using the Special:Export utility, save it to your disk and upload it here.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Importtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Infobox&amp;action=edit infobox]&lt;br&gt;
+[[MediaWiki_talk:Infobox|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Click a button to get an example text
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Infobox</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Infobox_alert&amp;action=edit infobox_alert]&lt;br&gt;
+[[MediaWiki_talk:Infobox_alert|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Please enter the text you want to be formatted.\n It will be shown in the infobox for copy and pasting.\nExample:\n$1\nwill become:\n$2
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Infobox_alert</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Internalerror&amp;action=edit internalerror]&lt;br&gt;
+[[MediaWiki_talk:Internalerror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Internal error
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Internalerror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Intl&amp;action=edit intl]&lt;br&gt;
+[[MediaWiki_talk:Intl|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Interlanguage links
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Intl</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ip_range_invalid&amp;action=edit ip_range_invalid]&lt;br&gt;
+[[MediaWiki_talk:Ip_range_invalid|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Invalid IP range.
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ip_range_invalid</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipaddress&amp;action=edit ipaddress]&lt;br&gt;
+[[MediaWiki_talk:Ipaddress|Talk]]
+&lt;/td&gt;&lt;td&gt;
+IP Address/username
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ipaddress</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipb_expiry_invalid&amp;action=edit ipb_expiry_invalid]&lt;br&gt;
+[[MediaWiki_talk:Ipb_expiry_invalid|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Expiry time invalid.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ipb_expiry_invalid</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipbexpiry&amp;action=edit ipbexpiry]&lt;br&gt;
+[[MediaWiki_talk:Ipbexpiry|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Expiry
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ipbexpiry</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipblocklist&amp;action=edit ipblocklist]&lt;br&gt;
+[[MediaWiki_talk:Ipblocklist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+List of blocked IP addresses and usernames
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ipblocklist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipbreason&amp;action=edit ipbreason]&lt;br&gt;
+[[MediaWiki_talk:Ipbreason|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Reason
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ipbreason</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipbsubmit&amp;action=edit ipbsubmit]&lt;br&gt;
+[[MediaWiki_talk:Ipbsubmit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Block this user
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ipbsubmit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipusubmit&amp;action=edit ipusubmit]&lt;br&gt;
+[[MediaWiki_talk:Ipusubmit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unblock this address
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ipusubmit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipusuccess&amp;action=edit ipusuccess]&lt;br&gt;
+[[MediaWiki_talk:Ipusuccess|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;quot;$1&amp;quot; unblocked
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ipusuccess</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Isbn&amp;action=edit isbn]&lt;br&gt;
+[[MediaWiki_talk:Isbn|Talk]]
+&lt;/td&gt;&lt;td&gt;
+ISBN
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Isbn</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Isredirect&amp;action=edit isredirect]&lt;br&gt;
+[[MediaWiki_talk:Isredirect|Talk]]
+&lt;/td&gt;&lt;td&gt;
+redirect page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Isredirect</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Italic_sample&amp;action=edit italic_sample]&lt;br&gt;
+[[MediaWiki_talk:Italic_sample|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Italic text
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Italic_sample</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Italic_tip&amp;action=edit italic_tip]&lt;br&gt;
+[[MediaWiki_talk:Italic_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Italic text
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Italic_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Iteminvalidname&amp;action=edit iteminvalidname]&lt;br&gt;
+[[MediaWiki_talk:Iteminvalidname|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Problem with item &amp;#39;$1&amp;#39;, invalid name...
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Iteminvalidname</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Largefile&amp;action=edit largefile]&lt;br&gt;
+[[MediaWiki_talk:Largefile|Talk]]
+&lt;/td&gt;&lt;td&gt;
+It is recommended that images not exceed 100k in size.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Largefile</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Last&amp;action=edit last]&lt;br&gt;
+[[MediaWiki_talk:Last|Talk]]
+&lt;/td&gt;&lt;td&gt;
+last
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Last</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lastmodified&amp;action=edit lastmodified]&lt;br&gt;
+[[MediaWiki_talk:Lastmodified|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This page was last modified $1.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lastmodified</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lastmodifiedby&amp;action=edit lastmodifiedby]&lt;br&gt;
+[[MediaWiki_talk:Lastmodifiedby|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This page was last modified $1 by $2.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lastmodifiedby</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lineno&amp;action=edit lineno]&lt;br&gt;
+[[MediaWiki_talk:Lineno|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Line $1:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lineno</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Link_sample&amp;action=edit link_sample]&lt;br&gt;
+[[MediaWiki_talk:Link_sample|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Link title
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Link_sample</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Link_tip&amp;action=edit link_tip]&lt;br&gt;
+[[MediaWiki_talk:Link_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Internal link
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Link_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Linklistsub&amp;action=edit linklistsub]&lt;br&gt;
+[[MediaWiki_talk:Linklistsub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(List of links)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Linklistsub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Linkshere&amp;action=edit linkshere]&lt;br&gt;
+[[MediaWiki_talk:Linkshere|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The following pages link to here:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Linkshere</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Linkstoimage&amp;action=edit linkstoimage]&lt;br&gt;
+[[MediaWiki_talk:Linkstoimage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The following pages link to this image:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Linkstoimage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Linktrail&amp;action=edit linktrail]&lt;br&gt;
+[[MediaWiki_talk:Linktrail|Talk]]
+&lt;/td&gt;&lt;td&gt;
+/^(&amp;#91;a-z]+)(.*)$/sD
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Linktrail</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Listform&amp;action=edit listform]&lt;br&gt;
+[[MediaWiki_talk:Listform|Talk]]
+&lt;/td&gt;&lt;td&gt;
+list
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Listform</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Listusers&amp;action=edit listusers]&lt;br&gt;
+[[MediaWiki_talk:Listusers|Talk]]
+&lt;/td&gt;&lt;td&gt;
+User list
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Listusers</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loadhist&amp;action=edit loadhist]&lt;br&gt;
+[[MediaWiki_talk:Loadhist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Loading page history
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loadhist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loadingrev&amp;action=edit loadingrev]&lt;br&gt;
+[[MediaWiki_talk:Loadingrev|Talk]]
+&lt;/td&gt;&lt;td&gt;
+loading revision for diff
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loadingrev</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Localtime&amp;action=edit localtime]&lt;br&gt;
+[[MediaWiki_talk:Localtime|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Local time display
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Localtime</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockbtn&amp;action=edit lockbtn]&lt;br&gt;
+[[MediaWiki_talk:Lockbtn|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Lock database
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lockbtn</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockconfirm&amp;action=edit lockconfirm]&lt;br&gt;
+[[MediaWiki_talk:Lockconfirm|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Yes, I really want to lock the database.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lockconfirm</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockdb&amp;action=edit lockdb]&lt;br&gt;
+[[MediaWiki_talk:Lockdb|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Lock database
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lockdb</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockdbsuccesssub&amp;action=edit lockdbsuccesssub]&lt;br&gt;
+[[MediaWiki_talk:Lockdbsuccesssub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Database lock succeeded
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lockdbsuccesssub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockdbsuccesstext&amp;action=edit lockdbsuccesstext]&lt;br&gt;
+[[MediaWiki_talk:Lockdbsuccesstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The database has been locked.
+&amp;lt;br /&amp;gt;Remember to remove the lock after your maintenance is complete.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lockdbsuccesstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockdbtext&amp;action=edit lockdbtext]&lt;br&gt;
+[[MediaWiki_talk:Lockdbtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Locking the database will suspend the ability of all
+users to edit pages, change their preferences, edit their watchlists, and
+other things requiring changes in the database.
+Please confirm that this is what you intend to do, and that you will
+unlock the database when your maintenance is done.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lockdbtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Locknoconfirm&amp;action=edit locknoconfirm]&lt;br&gt;
+[[MediaWiki_talk:Locknoconfirm|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You did not check the confirmation box.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Locknoconfirm</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Login&amp;action=edit login]&lt;br&gt;
+[[MediaWiki_talk:Login|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Log in
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Login</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginend&amp;action=edit loginend]&lt;br&gt;
+[[MediaWiki_talk:Loginend|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;amp;nbsp;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loginend</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginerror&amp;action=edit loginerror]&lt;br&gt;
+[[MediaWiki_talk:Loginerror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Login error
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loginerror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginpagetitle&amp;action=edit loginpagetitle]&lt;br&gt;
+[[MediaWiki_talk:Loginpagetitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+User login
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loginpagetitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginproblem&amp;action=edit loginproblem]&lt;br&gt;
+[[MediaWiki_talk:Loginproblem|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;b&amp;gt;There has been a problem with your login.&amp;lt;/b&amp;gt;&amp;lt;br /&amp;gt;Try again!
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loginproblem</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginprompt&amp;action=edit loginprompt]&lt;br&gt;
+[[MediaWiki_talk:Loginprompt|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You must have cookies enabled to log in to Wiktionary.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loginprompt</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginreqtext&amp;action=edit loginreqtext]&lt;br&gt;
+[[MediaWiki_talk:Loginreqtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You must &amp;#91;&amp;#91;special:Userlogin&amp;#124;login]] to view other pages.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loginreqtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginreqtitle&amp;action=edit loginreqtitle]&lt;br&gt;
+[[MediaWiki_talk:Loginreqtitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Login Required
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loginreqtitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginsuccess&amp;action=edit loginsuccess]&lt;br&gt;
+[[MediaWiki_talk:Loginsuccess|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You are now logged in to Wiktionary as &amp;quot;$1&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loginsuccess</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginsuccesstitle&amp;action=edit loginsuccesstitle]&lt;br&gt;
+[[MediaWiki_talk:Loginsuccesstitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Login successful
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Loginsuccesstitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Logout&amp;action=edit logout]&lt;br&gt;
+[[MediaWiki_talk:Logout|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Log out
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Logout</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Logouttext&amp;action=edit logouttext]&lt;br&gt;
+[[MediaWiki_talk:Logouttext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You are now logged out.
+You can continue to use Wiktionary anonymously, or you can log in
+again as the same or as a different user. Note that some pages may
+continue to be displayed as if you were still logged in, until you clear
+your browser cache
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Logouttext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Logouttitle&amp;action=edit logouttitle]&lt;br&gt;
+[[MediaWiki_talk:Logouttitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+User logout
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Logouttitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lonelypages&amp;action=edit lonelypages]&lt;br&gt;
+[[MediaWiki_talk:Lonelypages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Orphaned pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Lonelypages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Longpages&amp;action=edit longpages]&lt;br&gt;
+[[MediaWiki_talk:Longpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Long pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Longpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Longpagewarning&amp;action=edit longpagewarning]&lt;br&gt;
+[[MediaWiki_talk:Longpagewarning|Talk]]
+&lt;/td&gt;&lt;td&gt;
+WARNING: This page is $1 kilobytes long; some
+browsers may have problems editing pages approaching or longer than 32kb.
+Please consider breaking the page into smaller sections.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Longpagewarning</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mailerror&amp;action=edit mailerror]&lt;br&gt;
+[[MediaWiki_talk:Mailerror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Error sending mail: $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mailerror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mailmypassword&amp;action=edit mailmypassword]&lt;br&gt;
+[[MediaWiki_talk:Mailmypassword|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Mail me a new password
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mailmypassword</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mailnologin&amp;action=edit mailnologin]&lt;br&gt;
+[[MediaWiki_talk:Mailnologin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No send address
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mailnologin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mailnologintext&amp;action=edit mailnologintext]&lt;br&gt;
+[[MediaWiki_talk:Mailnologintext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You must be &amp;lt;a href=&amp;quot;{{localurl:Special:Userlogin&amp;quot;&amp;gt;logged in&amp;lt;/a&amp;gt;
+and have a valid e-mail address in your &amp;lt;a href=&amp;quot;/wiki/Special:Preferences&amp;quot;&amp;gt;preferences&amp;lt;/a&amp;gt;
+to send e-mail to other users.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mailnologintext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mainpage&amp;action=edit mainpage]&lt;br&gt;
+[[MediaWiki_talk:Mainpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Main Page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mainpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mainpagedocfooter&amp;action=edit mainpagedocfooter]&lt;br&gt;
+[[MediaWiki_talk:Mainpagedocfooter|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Please see &amp;#91;http&amp;#58;//meta.wikipedia.org/wiki/MediaWiki_i18n documentation on customizing the interface]
+and the &amp;#91;http&amp;#58;//meta.wikipedia.org/wiki/MediaWiki_User%27s_Guide User&amp;#39;s Guide] for usage and configuration help.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mainpagedocfooter</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mainpagetext&amp;action=edit mainpagetext]&lt;br&gt;
+[[MediaWiki_talk:Mainpagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiki software successfully installed.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mainpagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Maintenance&amp;action=edit maintenance]&lt;br&gt;
+[[MediaWiki_talk:Maintenance|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Maintenance page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Maintenance</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Maintenancebacklink&amp;action=edit maintenancebacklink]&lt;br&gt;
+[[MediaWiki_talk:Maintenancebacklink|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Back to Maintenance Page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Maintenancebacklink</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Maintnancepagetext&amp;action=edit maintnancepagetext]&lt;br&gt;
+[[MediaWiki_talk:Maintnancepagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This page includes several handy tools for everyday maintenance. Some of these functions tend to stress the database, so please do not hit reload after every item you fixed ;-)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Maintnancepagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysop&amp;action=edit makesysop]&lt;br&gt;
+[[MediaWiki_talk:Makesysop|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Make a user into a sysop
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Makesysop</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysopfail&amp;action=edit makesysopfail]&lt;br&gt;
+[[MediaWiki_talk:Makesysopfail|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;b&amp;gt;User &amp;quot;$1&amp;quot; could not be made into a sysop. (Did you enter the name correctly?)&amp;lt;/b&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Makesysopfail</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysopname&amp;action=edit makesysopname]&lt;br&gt;
+[[MediaWiki_talk:Makesysopname|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Name of the user:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Makesysopname</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysopok&amp;action=edit makesysopok]&lt;br&gt;
+[[MediaWiki_talk:Makesysopok|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;b&amp;gt;User &amp;quot;$1&amp;quot; is now a sysop&amp;lt;/b&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Makesysopok</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysopsubmit&amp;action=edit makesysopsubmit]&lt;br&gt;
+[[MediaWiki_talk:Makesysopsubmit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Make this user into a sysop
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Makesysopsubmit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysoptext&amp;action=edit makesysoptext]&lt;br&gt;
+[[MediaWiki_talk:Makesysoptext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This form is used by bureaucrats to turn ordinary users into administrators.
+Type the name of the user in the box and press the button to make the user an administrator
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Makesysoptext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysoptitle&amp;action=edit makesysoptitle]&lt;br&gt;
+[[MediaWiki_talk:Makesysoptitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Make a user into a sysop
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Makesysoptitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Matchtotals&amp;action=edit matchtotals]&lt;br&gt;
+[[MediaWiki_talk:Matchtotals|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The query &amp;quot;$1&amp;quot; matched $2 page titles
+and the text of $3 pages.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Matchtotals</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math&amp;action=edit math]&lt;br&gt;
+[[MediaWiki_talk:Math|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Rendering math
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_bad_output&amp;action=edit math_bad_output]&lt;br&gt;
+[[MediaWiki_talk:Math_bad_output|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Can&amp;#39;t write to or create math output directory
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_bad_output</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_bad_tmpdir&amp;action=edit math_bad_tmpdir]&lt;br&gt;
+[[MediaWiki_talk:Math_bad_tmpdir|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Can&amp;#39;t write to or create math temp directory
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_bad_tmpdir</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_failure&amp;action=edit math_failure]&lt;br&gt;
+[[MediaWiki_talk:Math_failure|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Failed to parse
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_failure</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_image_error&amp;action=edit math_image_error]&lt;br&gt;
+[[MediaWiki_talk:Math_image_error|Talk]]
+&lt;/td&gt;&lt;td&gt;
+PNG conversion failed; check for correct installation of latex, dvips, gs, and convert
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_image_error</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_lexing_error&amp;action=edit math_lexing_error]&lt;br&gt;
+[[MediaWiki_talk:Math_lexing_error|Talk]]
+&lt;/td&gt;&lt;td&gt;
+lexing error
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_lexing_error</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_notexvc&amp;action=edit math_notexvc]&lt;br&gt;
+[[MediaWiki_talk:Math_notexvc|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Missing texvc executable; please see math/README to configure.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_notexvc</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_sample&amp;action=edit math_sample]&lt;br&gt;
+[[MediaWiki_talk:Math_sample|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Insert formula here
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_sample</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_syntax_error&amp;action=edit math_syntax_error]&lt;br&gt;
+[[MediaWiki_talk:Math_syntax_error|Talk]]
+&lt;/td&gt;&lt;td&gt;
+syntax error
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_syntax_error</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_tip&amp;action=edit math_tip]&lt;br&gt;
+[[MediaWiki_talk:Math_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Mathematical formula (LaTeX)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_unknown_error&amp;action=edit math_unknown_error]&lt;br&gt;
+[[MediaWiki_talk:Math_unknown_error|Talk]]
+&lt;/td&gt;&lt;td&gt;
+unknown error
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_unknown_error</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_unknown_function&amp;action=edit math_unknown_function]&lt;br&gt;
+[[MediaWiki_talk:Math_unknown_function|Talk]]
+&lt;/td&gt;&lt;td&gt;
+unknown function
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Math_unknown_function</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Media_sample&amp;action=edit media_sample]&lt;br&gt;
+[[MediaWiki_talk:Media_sample|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Example.mp3
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Media_sample</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Media_tip&amp;action=edit media_tip]&lt;br&gt;
+[[MediaWiki_talk:Media_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Media file link
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Media_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Minlength&amp;action=edit minlength]&lt;br&gt;
+[[MediaWiki_talk:Minlength|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Image names must be at least three letters.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Minlength</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Minoredit&amp;action=edit minoredit]&lt;br&gt;
+[[MediaWiki_talk:Minoredit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This is a minor edit
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Minoredit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Minoreditletter&amp;action=edit minoreditletter]&lt;br&gt;
+[[MediaWiki_talk:Minoreditletter|Talk]]
+&lt;/td&gt;&lt;td&gt;
+M
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Minoreditletter</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mispeelings&amp;action=edit mispeelings]&lt;br&gt;
+[[MediaWiki_talk:Mispeelings|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Pages with misspellings
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mispeelings</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mispeelingspage&amp;action=edit mispeelingspage]&lt;br&gt;
+[[MediaWiki_talk:Mispeelingspage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+List of common misspellings
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mispeelingspage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mispeelingstext&amp;action=edit mispeelingstext]&lt;br&gt;
+[[MediaWiki_talk:Mispeelingstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The following pages contain a common misspelling, which are listed on $1. The correct spelling might be given (like this).
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mispeelingstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missingarticle&amp;action=edit missingarticle]&lt;br&gt;
+[[MediaWiki_talk:Missingarticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The database did not find the text of a page
+that it should have found, named &amp;quot;$1&amp;quot;.
+
+&amp;lt;p&amp;gt;This is usually caused by following an outdated diff or history link to a
+page that has been deleted.
+
+&amp;lt;p&amp;gt;If this is not the case, you may have found a bug in the software.
+Please report this to an administrator, making note of the URL.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Missingarticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missingimage&amp;action=edit missingimage]&lt;br&gt;
+[[MediaWiki_talk:Missingimage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;b&amp;gt;Missing image&amp;lt;/b&amp;gt;&amp;lt;br /&amp;gt;&amp;lt;i&amp;gt;$1&amp;lt;/i&amp;gt;
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Missingimage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missinglanguagelinks&amp;action=edit missinglanguagelinks]&lt;br&gt;
+[[MediaWiki_talk:Missinglanguagelinks|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Missing Language Links
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Missinglanguagelinks</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missinglanguagelinksbutton&amp;action=edit missinglanguagelinksbutton]&lt;br&gt;
+[[MediaWiki_talk:Missinglanguagelinksbutton|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Find missing language links for
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Missinglanguagelinksbutton</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missinglanguagelinkstext&amp;action=edit missinglanguagelinkstext]&lt;br&gt;
+[[MediaWiki_talk:Missinglanguagelinkstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+These pages do &amp;lt;i&amp;gt;not&amp;lt;/i&amp;gt; link to their counterpart in $1. Redirects and subpages are &amp;lt;i&amp;gt;not&amp;lt;/i&amp;gt; shown.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Missinglanguagelinkstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Moredotdotdot&amp;action=edit moredotdotdot]&lt;br&gt;
+[[MediaWiki_talk:Moredotdotdot|Talk]]
+&lt;/td&gt;&lt;td&gt;
+More...
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Moredotdotdot</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Move&amp;action=edit move]&lt;br&gt;
+[[MediaWiki_talk:Move|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Move
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Move</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movearticle&amp;action=edit movearticle]&lt;br&gt;
+[[MediaWiki_talk:Movearticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Move page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movearticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movedto&amp;action=edit movedto]&lt;br&gt;
+[[MediaWiki_talk:Movedto|Talk]]
+&lt;/td&gt;&lt;td&gt;
+moved to
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movedto</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movenologin&amp;action=edit movenologin]&lt;br&gt;
+[[MediaWiki_talk:Movenologin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Not logged in
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movenologin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movenologintext&amp;action=edit movenologintext]&lt;br&gt;
+[[MediaWiki_talk:Movenologintext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You must be a registered user and &amp;lt;a href=&amp;quot;/wiki/Special:Userlogin&amp;quot;&amp;gt;logged in&amp;lt;/a&amp;gt;
+to move a page.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movenologintext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movepage&amp;action=edit movepage]&lt;br&gt;
+[[MediaWiki_talk:Movepage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Move page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movepage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movepagebtn&amp;action=edit movepagebtn]&lt;br&gt;
+[[MediaWiki_talk:Movepagebtn|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Move page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movepagebtn</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movepagetalktext&amp;action=edit movepagetalktext]&lt;br&gt;
+[[MediaWiki_talk:Movepagetalktext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The associated talk page, if any, will be automatically moved along with it &amp;#39;&amp;#39;&amp;#39;unless:&amp;#39;&amp;#39;&amp;#39;
+*You are moving the page across namespaces,
+*A non-empty talk page already exists under the new name, or
+*You uncheck the box below.
+
+In those cases, you will have to move or merge the page manually if desired.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movepagetalktext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movepagetext&amp;action=edit movepagetext]&lt;br&gt;
+[[MediaWiki_talk:Movepagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Using the form below will rename a page, moving all
+of its history to the new name.
+The old title will become a redirect page to the new title.
+Links to the old page title will not be changed; be sure to
+&amp;#91;&amp;#91;Special:Maintenance&amp;#124;check]] for double or broken redirects.
+You are responsible for making sure that links continue to
+point where they are supposed to go.
+
+Note that the page will &amp;#39;&amp;#39;&amp;#39;not&amp;#39;&amp;#39;&amp;#39; be moved if there is already
+a page at the new title, unless it is empty or a redirect and has no
+past edit history. This means that you can rename a page back to where
+it was just renamed from if you make a mistake, and you cannot overwrite
+an existing page.
+
+&amp;lt;b&amp;gt;WARNING!&amp;lt;/b&amp;gt;
+This can be a drastic and unexpected change for a popular page;
+please be sure you understand the consequences of this before
+proceeding.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movepagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movetalk&amp;action=edit movetalk]&lt;br&gt;
+[[MediaWiki_talk:Movetalk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Move &amp;quot;talk&amp;quot; page as well, if applicable.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movetalk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movethispage&amp;action=edit movethispage]&lt;br&gt;
+[[MediaWiki_talk:Movethispage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Move this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Movethispage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mycontris&amp;action=edit mycontris]&lt;br&gt;
+[[MediaWiki_talk:Mycontris|Talk]]
+&lt;/td&gt;&lt;td&gt;
+My contributions
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mycontris</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mypage&amp;action=edit mypage]&lt;br&gt;
+[[MediaWiki_talk:Mypage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+My page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mypage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mytalk&amp;action=edit mytalk]&lt;br&gt;
+[[MediaWiki_talk:Mytalk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+My talk
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Mytalk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Navigation&amp;action=edit navigation]&lt;br&gt;
+[[MediaWiki_talk:Navigation|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Navigation
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Navigation</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nbytes&amp;action=edit nbytes]&lt;br&gt;
+[[MediaWiki_talk:Nbytes|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 bytes
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nbytes</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nchanges&amp;action=edit nchanges]&lt;br&gt;
+[[MediaWiki_talk:Nchanges|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 changes
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nchanges</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newarticle&amp;action=edit newarticle]&lt;br&gt;
+[[MediaWiki_talk:Newarticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(New)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newarticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newarticletext&amp;action=edit newarticletext]&lt;br&gt;
+[[MediaWiki_talk:Newarticletext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You&amp;#39;ve followed a link to a page that doesn&amp;#39;t exist yet.
+To create the page, start typing in the box below
+(see the &amp;#91;&amp;#91;Wiktionary:Help&amp;#124;help page]] for more info).
+If you are here by mistake, just click your browser&amp;#39;s &amp;#39;&amp;#39;&amp;#39;back&amp;#39;&amp;#39;&amp;#39; button.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newarticletext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newmessages&amp;action=edit newmessages]&lt;br&gt;
+[[MediaWiki_talk:Newmessages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You have $1.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newmessages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newmessageslink&amp;action=edit newmessageslink]&lt;br&gt;
+[[MediaWiki_talk:Newmessageslink|Talk]]
+&lt;/td&gt;&lt;td&gt;
+new messages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newmessageslink</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newpage&amp;action=edit newpage]&lt;br&gt;
+[[MediaWiki_talk:Newpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+New page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newpageletter&amp;action=edit newpageletter]&lt;br&gt;
+[[MediaWiki_talk:Newpageletter|Talk]]
+&lt;/td&gt;&lt;td&gt;
+N
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newpageletter</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newpages&amp;action=edit newpages]&lt;br&gt;
+[[MediaWiki_talk:Newpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+New pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newpassword&amp;action=edit newpassword]&lt;br&gt;
+[[MediaWiki_talk:Newpassword|Talk]]
+&lt;/td&gt;&lt;td&gt;
+New password
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newpassword</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newtitle&amp;action=edit newtitle]&lt;br&gt;
+[[MediaWiki_talk:Newtitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+To new title
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newtitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newusersonly&amp;action=edit newusersonly]&lt;br&gt;
+[[MediaWiki_talk:Newusersonly|Talk]]
+&lt;/td&gt;&lt;td&gt;
+ (new users only)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Newusersonly</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Next&amp;action=edit next]&lt;br&gt;
+[[MediaWiki_talk:Next|Talk]]
+&lt;/td&gt;&lt;td&gt;
+next
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Next</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nextn&amp;action=edit nextn]&lt;br&gt;
+[[MediaWiki_talk:Nextn|Talk]]
+&lt;/td&gt;&lt;td&gt;
+next $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nextn</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nlinks&amp;action=edit nlinks]&lt;br&gt;
+[[MediaWiki_talk:Nlinks|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 links
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nlinks</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noaffirmation&amp;action=edit noaffirmation]&lt;br&gt;
+[[MediaWiki_talk:Noaffirmation|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You must affirm that your upload does not violate
+any copyrights.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Noaffirmation</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noarticletext&amp;action=edit noarticletext]&lt;br&gt;
+[[MediaWiki_talk:Noarticletext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(There is currently no text in this page)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Noarticletext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noblockreason&amp;action=edit noblockreason]&lt;br&gt;
+[[MediaWiki_talk:Noblockreason|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You must supply a reason for the block.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Noblockreason</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noconnect&amp;action=edit noconnect]&lt;br&gt;
+[[MediaWiki_talk:Noconnect|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Sorry! The wiki is experiencing some technical difficulties, and cannot contact the database server.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Noconnect</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nocontribs&amp;action=edit nocontribs]&lt;br&gt;
+[[MediaWiki_talk:Nocontribs|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No changes were found matching these criteria.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nocontribs</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nocookieslogin&amp;action=edit nocookieslogin]&lt;br&gt;
+[[MediaWiki_talk:Nocookieslogin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary uses cookies to log in users. You have cookies disabled. Please enable them and try again.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nocookieslogin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nocookiesnew&amp;action=edit nocookiesnew]&lt;br&gt;
+[[MediaWiki_talk:Nocookiesnew|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The user account was created, but you are not logged in. Wiktionary uses cookies to log in users. You have cookies disabled. Please enable them, then log in with your new username and password.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nocookiesnew</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nocreativecommons&amp;action=edit nocreativecommons]&lt;br&gt;
+[[MediaWiki_talk:Nocreativecommons|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Creative Commons RDF metadata disabled for this server.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nocreativecommons</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nodb&amp;action=edit nodb]&lt;br&gt;
+[[MediaWiki_talk:Nodb|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Could not select database $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nodb</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nodublincore&amp;action=edit nodublincore]&lt;br&gt;
+[[MediaWiki_talk:Nodublincore|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Dublin Core RDF metadata disabled for this server.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nodublincore</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noemail&amp;action=edit noemail]&lt;br&gt;
+[[MediaWiki_talk:Noemail|Talk]]
+&lt;/td&gt;&lt;td&gt;
+There is no e-mail address recorded for user &amp;quot;$1&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Noemail</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noemailtext&amp;action=edit noemailtext]&lt;br&gt;
+[[MediaWiki_talk:Noemailtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This user has not specified a valid e-mail address,
+or has chosen not to receive e-mail from other users.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Noemailtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noemailtitle&amp;action=edit noemailtitle]&lt;br&gt;
+[[MediaWiki_talk:Noemailtitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No e-mail address
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Noemailtitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nogomatch&amp;action=edit nogomatch]&lt;br&gt;
+[[MediaWiki_talk:Nogomatch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No page with this exact title exists, trying full text search.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nogomatch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nohistory&amp;action=edit nohistory]&lt;br&gt;
+[[MediaWiki_talk:Nohistory|Talk]]
+&lt;/td&gt;&lt;td&gt;
+There is no edit history for this page.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nohistory</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nolinkshere&amp;action=edit nolinkshere]&lt;br&gt;
+[[MediaWiki_talk:Nolinkshere|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No pages link to here.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nolinkshere</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nolinkstoimage&amp;action=edit nolinkstoimage]&lt;br&gt;
+[[MediaWiki_talk:Nolinkstoimage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+There are no pages that link to this image.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nolinkstoimage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noname&amp;action=edit noname]&lt;br&gt;
+[[MediaWiki_talk:Noname|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You have not specified a valid user name.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Noname</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nonefound&amp;action=edit nonefound]&lt;br&gt;
+[[MediaWiki_talk:Nonefound|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;strong&amp;gt;Note&amp;lt;/strong&amp;gt;: unsuccessful searches are
+often caused by searching for common words like &amp;quot;have&amp;quot; and &amp;quot;from&amp;quot;,
+which are not indexed, or by specifying more than one search term (only pages
+containing all of the search terms will appear in the result).
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nonefound</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nospecialpagetext&amp;action=edit nospecialpagetext]&lt;br&gt;
+[[MediaWiki_talk:Nospecialpagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You have requested a special page that is not
+recognized by the wiki.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nospecialpagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nosuchaction&amp;action=edit nosuchaction]&lt;br&gt;
+[[MediaWiki_talk:Nosuchaction|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No such action
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nosuchaction</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nosuchactiontext&amp;action=edit nosuchactiontext]&lt;br&gt;
+[[MediaWiki_talk:Nosuchactiontext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The action specified by the URL is not
+recognized by the wiki
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nosuchactiontext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nosuchspecialpage&amp;action=edit nosuchspecialpage]&lt;br&gt;
+[[MediaWiki_talk:Nosuchspecialpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No such special page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nosuchspecialpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nosuchuser&amp;action=edit nosuchuser]&lt;br&gt;
+[[MediaWiki_talk:Nosuchuser|Talk]]
+&lt;/td&gt;&lt;td&gt;
+There is no user by the name &amp;quot;$1&amp;quot;.
+Check your spelling, or use the form below to create a new user account.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nosuchuser</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notacceptable&amp;action=edit notacceptable]&lt;br&gt;
+[[MediaWiki_talk:Notacceptable|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The wiki server can&amp;#39;t provide data in a format your client can read.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Notacceptable</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notanarticle&amp;action=edit notanarticle]&lt;br&gt;
+[[MediaWiki_talk:Notanarticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Not a content page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Notanarticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notargettext&amp;action=edit notargettext]&lt;br&gt;
+[[MediaWiki_talk:Notargettext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You have not specified a target page or user
+to perform this function on.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Notargettext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notargettitle&amp;action=edit notargettitle]&lt;br&gt;
+[[MediaWiki_talk:Notargettitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No target
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Notargettitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Note&amp;action=edit note]&lt;br&gt;
+[[MediaWiki_talk:Note|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;strong&amp;gt;Note:&amp;lt;/strong&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Note</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notextmatches&amp;action=edit notextmatches]&lt;br&gt;
+[[MediaWiki_talk:Notextmatches|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No page text matches
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Notextmatches</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notitlematches&amp;action=edit notitlematches]&lt;br&gt;
+[[MediaWiki_talk:Notitlematches|Talk]]
+&lt;/td&gt;&lt;td&gt;
+No page title matches
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Notitlematches</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notloggedin&amp;action=edit notloggedin]&lt;br&gt;
+[[MediaWiki_talk:Notloggedin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Not logged in
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Notloggedin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nowatchlist&amp;action=edit nowatchlist]&lt;br&gt;
+[[MediaWiki_talk:Nowatchlist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You have no items on your watchlist.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nowatchlist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nowiki_sample&amp;action=edit nowiki_sample]&lt;br&gt;
+[[MediaWiki_talk:Nowiki_sample|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Insert non-formatted text here
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nowiki_sample</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nowiki_tip&amp;action=edit nowiki_tip]&lt;br&gt;
+[[MediaWiki_talk:Nowiki_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Ignore wiki formatting
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nowiki_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-category&amp;action=edit nstab-category]&lt;br&gt;
+[[MediaWiki_talk:Nstab-category|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Category
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-category</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-help&amp;action=edit nstab-help]&lt;br&gt;
+[[MediaWiki_talk:Nstab-help|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Help
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-help</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-image&amp;action=edit nstab-image]&lt;br&gt;
+[[MediaWiki_talk:Nstab-image|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Image
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-image</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-main&amp;action=edit nstab-main]&lt;br&gt;
+[[MediaWiki_talk:Nstab-main|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Article
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-main</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-media&amp;action=edit nstab-media]&lt;br&gt;
+[[MediaWiki_talk:Nstab-media|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Media
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-media</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-mediawiki&amp;action=edit nstab-mediawiki]&lt;br&gt;
+[[MediaWiki_talk:Nstab-mediawiki|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Message
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-mediawiki</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-special&amp;action=edit nstab-special]&lt;br&gt;
+[[MediaWiki_talk:Nstab-special|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Special
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-special</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-template&amp;action=edit nstab-template]&lt;br&gt;
+[[MediaWiki_talk:Nstab-template|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Template
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-template</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-user&amp;action=edit nstab-user]&lt;br&gt;
+[[MediaWiki_talk:Nstab-user|Talk]]
+&lt;/td&gt;&lt;td&gt;
+User page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-user</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-wp&amp;action=edit nstab-wp]&lt;br&gt;
+[[MediaWiki_talk:Nstab-wp|Talk]]
+&lt;/td&gt;&lt;td&gt;
+About
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nstab-wp</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nviews&amp;action=edit nviews]&lt;br&gt;
+[[MediaWiki_talk:Nviews|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 views
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Nviews</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ok&amp;action=edit ok]&lt;br&gt;
+[[MediaWiki_talk:Ok|Talk]]
+&lt;/td&gt;&lt;td&gt;
+OK
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ok</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Oldpassword&amp;action=edit oldpassword]&lt;br&gt;
+[[MediaWiki_talk:Oldpassword|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Old password
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Oldpassword</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Orig&amp;action=edit orig]&lt;br&gt;
+[[MediaWiki_talk:Orig|Talk]]
+&lt;/td&gt;&lt;td&gt;
+orig
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Orig</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Orphans&amp;action=edit orphans]&lt;br&gt;
+[[MediaWiki_talk:Orphans|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Orphaned pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Orphans</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Othercontribs&amp;action=edit othercontribs]&lt;br&gt;
+[[MediaWiki_talk:Othercontribs|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Based on work by $1.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Othercontribs</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Otherlanguages&amp;action=edit otherlanguages]&lt;br&gt;
+[[MediaWiki_talk:Otherlanguages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Other languages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Otherlanguages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Pagemovedsub&amp;action=edit pagemovedsub]&lt;br&gt;
+[[MediaWiki_talk:Pagemovedsub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Move succeeded
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Pagemovedsub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Pagemovedtext&amp;action=edit pagemovedtext]&lt;br&gt;
+[[MediaWiki_talk:Pagemovedtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Page &amp;quot;&amp;#91;&amp;#91;$1]]&amp;quot; moved to &amp;quot;&amp;#91;&amp;#91;$2]]&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Pagemovedtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Pagetitle&amp;action=edit pagetitle]&lt;br&gt;
+[[MediaWiki_talk:Pagetitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 - Wiktionary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Pagetitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Passwordremindertext&amp;action=edit passwordremindertext]&lt;br&gt;
+[[MediaWiki_talk:Passwordremindertext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Someone (probably you, from IP address $1)
+requested that we send you a new Wiktionary login password.
+The password for user &amp;quot;$2&amp;quot; is now &amp;quot;$3&amp;quot;.
+You should log in and change your password now.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Passwordremindertext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Passwordremindertitle&amp;action=edit passwordremindertitle]&lt;br&gt;
+[[MediaWiki_talk:Passwordremindertitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Password reminder from Wiktionary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Passwordremindertitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Passwordsent&amp;action=edit passwordsent]&lt;br&gt;
+[[MediaWiki_talk:Passwordsent|Talk]]
+&lt;/td&gt;&lt;td&gt;
+A new password has been sent to the e-mail address
+registered for &amp;quot;$1&amp;quot;.
+Please log in again after you receive it.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Passwordsent</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Perfcached&amp;action=edit perfcached]&lt;br&gt;
+[[MediaWiki_talk:Perfcached|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The following data is cached and may not be completely up to date:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Perfcached</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Perfdisabled&amp;action=edit perfdisabled]&lt;br&gt;
+[[MediaWiki_talk:Perfdisabled|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Sorry! This feature has been temporarily disabled
+because it slows the database down to the point that no one can use
+the wiki.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Perfdisabled</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Perfdisabledsub&amp;action=edit perfdisabledsub]&lt;br&gt;
+[[MediaWiki_talk:Perfdisabledsub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Here&amp;#39;s a saved copy from $1:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Perfdisabledsub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Personaltools&amp;action=edit personaltools]&lt;br&gt;
+[[MediaWiki_talk:Personaltools|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Personal tools
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Personaltools</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Popularpages&amp;action=edit popularpages]&lt;br&gt;
+[[MediaWiki_talk:Popularpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Popular pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Popularpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Portal&amp;action=edit portal]&lt;br&gt;
+[[MediaWiki_talk:Portal|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Community portal
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Portal</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Portal-url&amp;action=edit portal-url]&lt;br&gt;
+[[MediaWiki_talk:Portal-url|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary:Community Portal
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Portal-url</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Postcomment&amp;action=edit postcomment]&lt;br&gt;
+[[MediaWiki_talk:Postcomment|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Post a comment
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Postcomment</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Poweredby&amp;action=edit poweredby]&lt;br&gt;
+[[MediaWiki_talk:Poweredby|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary is powered by &amp;#91;http&amp;#58;//www.mediawiki.org/ MediaWiki], an open source wiki engine.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Poweredby</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Powersearch&amp;action=edit powersearch]&lt;br&gt;
+[[MediaWiki_talk:Powersearch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Search
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Powersearch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Powersearchtext&amp;action=edit powersearchtext]&lt;br&gt;
+[[MediaWiki_talk:Powersearchtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+
+Search in namespaces :&amp;lt;br /&amp;gt;
+$1&amp;lt;br /&amp;gt;
+$2 List redirects &amp;amp;nbsp; Search for $3 $9
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Powersearchtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Preferences&amp;action=edit preferences]&lt;br&gt;
+[[MediaWiki_talk:Preferences|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Preferences
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Preferences</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefs-help-userdata&amp;action=edit prefs-help-userdata]&lt;br&gt;
+[[MediaWiki_talk:Prefs-help-userdata|Talk]]
+&lt;/td&gt;&lt;td&gt;
+* &amp;lt;strong&amp;gt;Real name&amp;lt;/strong&amp;gt; (optional): if you choose to provide it this will be used for giving you attribution for your work.&amp;lt;br/&amp;gt;
+* &amp;lt;strong&amp;gt;Email&amp;lt;/strong&amp;gt; (optional): Enables people to contact you through the website without you having to reveal your
+email address to them, and it can be used to send you a new password if you forget it.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Prefs-help-userdata</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefs-misc&amp;action=edit prefs-misc]&lt;br&gt;
+[[MediaWiki_talk:Prefs-misc|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Misc settings
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Prefs-misc</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefs-personal&amp;action=edit prefs-personal]&lt;br&gt;
+[[MediaWiki_talk:Prefs-personal|Talk]]
+&lt;/td&gt;&lt;td&gt;
+User data
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Prefs-personal</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefs-rc&amp;action=edit prefs-rc]&lt;br&gt;
+[[MediaWiki_talk:Prefs-rc|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Recent changes and stub display
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Prefs-rc</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefslogintext&amp;action=edit prefslogintext]&lt;br&gt;
+[[MediaWiki_talk:Prefslogintext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You are logged in as &amp;quot;$1&amp;quot;.
+Your internal ID number is $2.
+
+See &amp;#91;&amp;#91;Wiktionary:User preferences help]] for help deciphering the options.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Prefslogintext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefsnologin&amp;action=edit prefsnologin]&lt;br&gt;
+[[MediaWiki_talk:Prefsnologin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Not logged in
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Prefsnologin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefsnologintext&amp;action=edit prefsnologintext]&lt;br&gt;
+[[MediaWiki_talk:Prefsnologintext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You must be &amp;lt;a href=&amp;quot;/wiki/Special:Userlogin&amp;quot;&amp;gt;logged in&amp;lt;/a&amp;gt;
+to set user preferences.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Prefsnologintext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefsreset&amp;action=edit prefsreset]&lt;br&gt;
+[[MediaWiki_talk:Prefsreset|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Preferences have been reset from storage.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Prefsreset</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Preview&amp;action=edit preview]&lt;br&gt;
+[[MediaWiki_talk:Preview|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Preview
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Preview</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Previewconflict&amp;action=edit previewconflict]&lt;br&gt;
+[[MediaWiki_talk:Previewconflict|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This preview reflects the text in the upper
+text editing area as it will appear if you choose to save.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Previewconflict</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Previewnote&amp;action=edit previewnote]&lt;br&gt;
+[[MediaWiki_talk:Previewnote|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Remember that this is only a preview, and has not yet been saved!
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Previewnote</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prevn&amp;action=edit prevn]&lt;br&gt;
+[[MediaWiki_talk:Prevn|Talk]]
+&lt;/td&gt;&lt;td&gt;
+previous $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Prevn</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Printableversion&amp;action=edit printableversion]&lt;br&gt;
+[[MediaWiki_talk:Printableversion|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Printable version
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Printableversion</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Printsubtitle&amp;action=edit printsubtitle]&lt;br&gt;
+[[MediaWiki_talk:Printsubtitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(From http&amp;#58;//tl.wiktionary.org)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Printsubtitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protect&amp;action=edit protect]&lt;br&gt;
+[[MediaWiki_talk:Protect|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Protect
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protect</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectcomment&amp;action=edit protectcomment]&lt;br&gt;
+[[MediaWiki_talk:Protectcomment|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Reason for protecting
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectcomment</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectedarticle&amp;action=edit protectedarticle]&lt;br&gt;
+[[MediaWiki_talk:Protectedarticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+protected &amp;#91;&amp;#91;$1]]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectedarticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectedpage&amp;action=edit protectedpage]&lt;br&gt;
+[[MediaWiki_talk:Protectedpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Protected page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectedpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectedpagewarning&amp;action=edit protectedpagewarning]&lt;br&gt;
+[[MediaWiki_talk:Protectedpagewarning|Talk]]
+&lt;/td&gt;&lt;td&gt;
+WARNING: This page has been locked so that only
+users with sysop privileges can edit it. Be sure you are following the
+&amp;lt;a href=&amp;#39;/w/wiki.phtml/Wiktionary:Protected_page_guidelines&amp;#39;&amp;gt;protected page
+guidelines&amp;lt;/a&amp;gt;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectedpagewarning</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectedtext&amp;action=edit protectedtext]&lt;br&gt;
+[[MediaWiki_talk:Protectedtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This page has been locked to prevent editing; there are
+a number of reasons why this may be so, please see
+&amp;#91;&amp;#91;Wiktionary:Protected page]].
+
+You can view and copy the source of this page:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectedtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectlogpage&amp;action=edit protectlogpage]&lt;br&gt;
+[[MediaWiki_talk:Protectlogpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Protection_log
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectlogpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectlogtext&amp;action=edit protectlogtext]&lt;br&gt;
+[[MediaWiki_talk:Protectlogtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Below is a list of page locks/unlocks.
+See &amp;#91;&amp;#91;Wiktionary:Protected page]] for more information.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectlogtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectpage&amp;action=edit protectpage]&lt;br&gt;
+[[MediaWiki_talk:Protectpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Protect page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectreason&amp;action=edit protectreason]&lt;br&gt;
+[[MediaWiki_talk:Protectreason|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(give a reason)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectreason</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectsub&amp;action=edit protectsub]&lt;br&gt;
+[[MediaWiki_talk:Protectsub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(Protecting &amp;quot;$1&amp;quot;)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectsub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectthispage&amp;action=edit protectthispage]&lt;br&gt;
+[[MediaWiki_talk:Protectthispage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Protect this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Protectthispage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Proxyblocker&amp;action=edit proxyblocker]&lt;br&gt;
+[[MediaWiki_talk:Proxyblocker|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Proxy blocker
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Proxyblocker</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Proxyblockreason&amp;action=edit proxyblockreason]&lt;br&gt;
+[[MediaWiki_talk:Proxyblockreason|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your IP address has been blocked because it is an open proxy. Please contact your Internet service provider or tech support and inform them of this serious security problem.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Proxyblockreason</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Proxyblocksuccess&amp;action=edit proxyblocksuccess]&lt;br&gt;
+[[MediaWiki_talk:Proxyblocksuccess|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Done.
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Proxyblocksuccess</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbbrowse&amp;action=edit qbbrowse]&lt;br&gt;
+[[MediaWiki_talk:Qbbrowse|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Browse
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Qbbrowse</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbedit&amp;action=edit qbedit]&lt;br&gt;
+[[MediaWiki_talk:Qbedit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Edit
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Qbedit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbfind&amp;action=edit qbfind]&lt;br&gt;
+[[MediaWiki_talk:Qbfind|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Find
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Qbfind</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbmyoptions&amp;action=edit qbmyoptions]&lt;br&gt;
+[[MediaWiki_talk:Qbmyoptions|Talk]]
+&lt;/td&gt;&lt;td&gt;
+My pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Qbmyoptions</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbpageinfo&amp;action=edit qbpageinfo]&lt;br&gt;
+[[MediaWiki_talk:Qbpageinfo|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Context
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Qbpageinfo</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbpageoptions&amp;action=edit qbpageoptions]&lt;br&gt;
+[[MediaWiki_talk:Qbpageoptions|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Qbpageoptions</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbsettings&amp;action=edit qbsettings]&lt;br&gt;
+[[MediaWiki_talk:Qbsettings|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Quickbar settings
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Qbsettings</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbspecialpages&amp;action=edit qbspecialpages]&lt;br&gt;
+[[MediaWiki_talk:Qbspecialpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Special pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Qbspecialpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Querybtn&amp;action=edit querybtn]&lt;br&gt;
+[[MediaWiki_talk:Querybtn|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Submit query
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Querybtn</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Querysuccessful&amp;action=edit querysuccessful]&lt;br&gt;
+[[MediaWiki_talk:Querysuccessful|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Query successful
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Querysuccessful</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Randompage&amp;action=edit randompage]&lt;br&gt;
+[[MediaWiki_talk:Randompage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Random page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Randompage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Range_block_disabled&amp;action=edit range_block_disabled]&lt;br&gt;
+[[MediaWiki_talk:Range_block_disabled|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The sysop ability to create range blocks is disabled.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Range_block_disabled</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rchide&amp;action=edit rchide]&lt;br&gt;
+[[MediaWiki_talk:Rchide|Talk]]
+&lt;/td&gt;&lt;td&gt;
+in $4 form; $1 minor edits; $2 secondary namespaces; $3 multiple edits.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rchide</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rclinks&amp;action=edit rclinks]&lt;br&gt;
+[[MediaWiki_talk:Rclinks|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Show last $1 changes in last $2 days&amp;lt;br /&amp;gt;$3
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rclinks</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rclistfrom&amp;action=edit rclistfrom]&lt;br&gt;
+[[MediaWiki_talk:Rclistfrom|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Show new changes starting from $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rclistfrom</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rcliu&amp;action=edit rcliu]&lt;br&gt;
+[[MediaWiki_talk:Rcliu|Talk]]
+&lt;/td&gt;&lt;td&gt;
+; $1 edits from logged in users
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rcliu</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rcloaderr&amp;action=edit rcloaderr]&lt;br&gt;
+[[MediaWiki_talk:Rcloaderr|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Loading recent changes
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rcloaderr</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rclsub&amp;action=edit rclsub]&lt;br&gt;
+[[MediaWiki_talk:Rclsub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(to pages linked from &amp;quot;$1&amp;quot;)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rclsub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rcnote&amp;action=edit rcnote]&lt;br&gt;
+[[MediaWiki_talk:Rcnote|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Below are the last &amp;lt;strong&amp;gt;$1&amp;lt;/strong&amp;gt; changes in last &amp;lt;strong&amp;gt;$2&amp;lt;/strong&amp;gt; days.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rcnote</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rcnotefrom&amp;action=edit rcnotefrom]&lt;br&gt;
+[[MediaWiki_talk:Rcnotefrom|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Below are the changes since &amp;lt;b&amp;gt;$2&amp;lt;/b&amp;gt; (up to &amp;lt;b&amp;gt;$1&amp;lt;/b&amp;gt; shown).
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rcnotefrom</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Readonly&amp;action=edit readonly]&lt;br&gt;
+[[MediaWiki_talk:Readonly|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Database locked
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Readonly</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Readonlytext&amp;action=edit readonlytext]&lt;br&gt;
+[[MediaWiki_talk:Readonlytext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The database is currently locked to new
+entries and other modifications, probably for routine database maintenance,
+after which it will be back to normal.
+The administrator who locked it offered this explanation:
+&amp;lt;p&amp;gt;$1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Readonlytext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Readonlywarning&amp;action=edit readonlywarning]&lt;br&gt;
+[[MediaWiki_talk:Readonlywarning|Talk]]
+&lt;/td&gt;&lt;td&gt;
+WARNING: The database has been locked for maintenance,
+so you will not be able to save your edits right now. You may wish to cut-n-paste
+the text into a text file and save it for later.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Readonlywarning</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Recentchanges&amp;action=edit recentchanges]&lt;br&gt;
+[[MediaWiki_talk:Recentchanges|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Recent changes
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Recentchanges</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Recentchangescount&amp;action=edit recentchangescount]&lt;br&gt;
+[[MediaWiki_talk:Recentchangescount|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Number of titles in recent changes
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Recentchangescount</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Recentchangeslinked&amp;action=edit recentchangeslinked]&lt;br&gt;
+[[MediaWiki_talk:Recentchangeslinked|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Related changes
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Recentchangeslinked</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Recentchangestext&amp;action=edit recentchangestext]&lt;br&gt;
+[[MediaWiki_talk:Recentchangestext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Track the most recent changes to the wiki on this page.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Recentchangestext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Redirectedfrom&amp;action=edit redirectedfrom]&lt;br&gt;
+[[MediaWiki_talk:Redirectedfrom|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(Redirected from $1)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Redirectedfrom</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Remembermypassword&amp;action=edit remembermypassword]&lt;br&gt;
+[[MediaWiki_talk:Remembermypassword|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Remember my password across sessions.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Remembermypassword</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Removechecked&amp;action=edit removechecked]&lt;br&gt;
+[[MediaWiki_talk:Removechecked|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Remove checked items from watchlist
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Removechecked</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Removedwatch&amp;action=edit removedwatch]&lt;br&gt;
+[[MediaWiki_talk:Removedwatch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Removed from watchlist
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Removedwatch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Removedwatchtext&amp;action=edit removedwatchtext]&lt;br&gt;
+[[MediaWiki_talk:Removedwatchtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The page &amp;quot;$1&amp;quot; has been removed from your watchlist.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Removedwatchtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Removingchecked&amp;action=edit removingchecked]&lt;br&gt;
+[[MediaWiki_talk:Removingchecked|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Removing requested items from watchlist...
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Removingchecked</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Resetprefs&amp;action=edit resetprefs]&lt;br&gt;
+[[MediaWiki_talk:Resetprefs|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Reset preferences
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Resetprefs</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Restorelink&amp;action=edit restorelink]&lt;br&gt;
+[[MediaWiki_talk:Restorelink|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 deleted edits
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Restorelink</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Resultsperpage&amp;action=edit resultsperpage]&lt;br&gt;
+[[MediaWiki_talk:Resultsperpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Hits to show per page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Resultsperpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Retrievedfrom&amp;action=edit retrievedfrom]&lt;br&gt;
+[[MediaWiki_talk:Retrievedfrom|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Retrieved from &amp;quot;$1&amp;quot;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Retrievedfrom</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Returnto&amp;action=edit returnto]&lt;br&gt;
+[[MediaWiki_talk:Returnto|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Return to $1.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Returnto</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Retypenew&amp;action=edit retypenew]&lt;br&gt;
+[[MediaWiki_talk:Retypenew|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Retype new password
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Retypenew</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Reupload&amp;action=edit reupload]&lt;br&gt;
+[[MediaWiki_talk:Reupload|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Re-upload
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Reupload</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Reuploaddesc&amp;action=edit reuploaddesc]&lt;br&gt;
+[[MediaWiki_talk:Reuploaddesc|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Return to the upload form.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Reuploaddesc</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Reverted&amp;action=edit reverted]&lt;br&gt;
+[[MediaWiki_talk:Reverted|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Reverted to earlier revision
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Reverted</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revertimg&amp;action=edit revertimg]&lt;br&gt;
+[[MediaWiki_talk:Revertimg|Talk]]
+&lt;/td&gt;&lt;td&gt;
+rev
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Revertimg</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revertpage&amp;action=edit revertpage]&lt;br&gt;
+[[MediaWiki_talk:Revertpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Reverted edit of $2, changed back to last version by $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Revertpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revhistory&amp;action=edit revhistory]&lt;br&gt;
+[[MediaWiki_talk:Revhistory|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Revision history
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Revhistory</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revisionasof&amp;action=edit revisionasof]&lt;br&gt;
+[[MediaWiki_talk:Revisionasof|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Revision as of $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Revisionasof</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revnotfound&amp;action=edit revnotfound]&lt;br&gt;
+[[MediaWiki_talk:Revnotfound|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Revision not found
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Revnotfound</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revnotfoundtext&amp;action=edit revnotfoundtext]&lt;br&gt;
+[[MediaWiki_talk:Revnotfoundtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The old revision of the page you asked for could not be found.
+Please check the URL you used to access this page.
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Revnotfoundtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rfcurl&amp;action=edit rfcurl]&lt;br&gt;
+[[MediaWiki_talk:Rfcurl|Talk]]
+&lt;/td&gt;&lt;td&gt;
+http&amp;#58;//www.faqs.org/rfcs/rfc$1.html
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rfcurl</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rights&amp;action=edit rights]&lt;br&gt;
+[[MediaWiki_talk:Rights|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Rights:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rights</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rollback&amp;action=edit rollback]&lt;br&gt;
+[[MediaWiki_talk:Rollback|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Roll back edits
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rollback</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rollback_short&amp;action=edit rollback_short]&lt;br&gt;
+[[MediaWiki_talk:Rollback_short|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Rollback
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rollback_short</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rollbackfailed&amp;action=edit rollbackfailed]&lt;br&gt;
+[[MediaWiki_talk:Rollbackfailed|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Rollback failed
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rollbackfailed</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rollbacklink&amp;action=edit rollbacklink]&lt;br&gt;
+[[MediaWiki_talk:Rollbacklink|Talk]]
+&lt;/td&gt;&lt;td&gt;
+rollback
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rollbacklink</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rows&amp;action=edit rows]&lt;br&gt;
+[[MediaWiki_talk:Rows|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Rows
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Rows</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Savearticle&amp;action=edit savearticle]&lt;br&gt;
+[[MediaWiki_talk:Savearticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Save page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Savearticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Savedprefs&amp;action=edit savedprefs]&lt;br&gt;
+[[MediaWiki_talk:Savedprefs|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your preferences have been saved.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Savedprefs</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Savefile&amp;action=edit savefile]&lt;br&gt;
+[[MediaWiki_talk:Savefile|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Save file
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Savefile</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Saveprefs&amp;action=edit saveprefs]&lt;br&gt;
+[[MediaWiki_talk:Saveprefs|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Save preferences
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Saveprefs</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Search&amp;action=edit search]&lt;br&gt;
+[[MediaWiki_talk:Search|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Search
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Search</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchdisabled&amp;action=edit searchdisabled]&lt;br&gt;
+[[MediaWiki_talk:Searchdisabled|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;p&amp;gt;Sorry! Full text search has been disabled temporarily, for performance reasons. In the meantime, you can use the Google search below, which may be out of date.&amp;lt;/p&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Searchdisabled</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchhelppage&amp;action=edit searchhelppage]&lt;br&gt;
+[[MediaWiki_talk:Searchhelppage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary:Searching
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Searchhelppage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchingwikipedia&amp;action=edit searchingwikipedia]&lt;br&gt;
+[[MediaWiki_talk:Searchingwikipedia|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Searching Wiktionary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Searchingwikipedia</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchquery&amp;action=edit searchquery]&lt;br&gt;
+[[MediaWiki_talk:Searchquery|Talk]]
+&lt;/td&gt;&lt;td&gt;
+For query &amp;quot;$1&amp;quot;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Searchquery</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchresults&amp;action=edit searchresults]&lt;br&gt;
+[[MediaWiki_talk:Searchresults|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Search results
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Searchresults</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchresultshead&amp;action=edit searchresultshead]&lt;br&gt;
+[[MediaWiki_talk:Searchresultshead|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Search result settings
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Searchresultshead</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchresulttext&amp;action=edit searchresulttext]&lt;br&gt;
+[[MediaWiki_talk:Searchresulttext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+For more information about searching Wiktionary, see $1.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Searchresulttext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sectionedit&amp;action=edit sectionedit]&lt;br&gt;
+[[MediaWiki_talk:Sectionedit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+ (section)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sectionedit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selectnewerversionfordiff&amp;action=edit selectnewerversionfordiff]&lt;br&gt;
+[[MediaWiki_talk:Selectnewerversionfordiff|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Select a newer version for comparison
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Selectnewerversionfordiff</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selectolderversionfordiff&amp;action=edit selectolderversionfordiff]&lt;br&gt;
+[[MediaWiki_talk:Selectolderversionfordiff|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Select an older version for comparison
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Selectolderversionfordiff</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selectonly&amp;action=edit selectonly]&lt;br&gt;
+[[MediaWiki_talk:Selectonly|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Only read-only queries are allowed.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Selectonly</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selflinks&amp;action=edit selflinks]&lt;br&gt;
+[[MediaWiki_talk:Selflinks|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Pages with Self Links
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Selflinks</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selflinkstext&amp;action=edit selflinkstext]&lt;br&gt;
+[[MediaWiki_talk:Selflinkstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The following pages contain a link to themselves, which they should not.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Selflinkstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Seriousxhtmlerrors&amp;action=edit seriousxhtmlerrors]&lt;br&gt;
+[[MediaWiki_talk:Seriousxhtmlerrors|Talk]]
+&lt;/td&gt;&lt;td&gt;
+There were serious xhtml markup errors detected by tidy.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Seriousxhtmlerrors</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Servertime&amp;action=edit servertime]&lt;br&gt;
+[[MediaWiki_talk:Servertime|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Server time is now
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Servertime</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Set_rights_fail&amp;action=edit set_rights_fail]&lt;br&gt;
+[[MediaWiki_talk:Set_rights_fail|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;b&amp;gt;User rights for &amp;quot;$1&amp;quot; could not be set. (Did you enter the name correctly?)&amp;lt;/b&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Set_rights_fail</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Set_user_rights&amp;action=edit set_user_rights]&lt;br&gt;
+[[MediaWiki_talk:Set_user_rights|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Set user rights
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Set_user_rights</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Setbureaucratflag&amp;action=edit setbureaucratflag]&lt;br&gt;
+[[MediaWiki_talk:Setbureaucratflag|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Set bureaucrat flag
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Setbureaucratflag</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Shortpages&amp;action=edit shortpages]&lt;br&gt;
+[[MediaWiki_talk:Shortpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Short pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Shortpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Show&amp;action=edit show]&lt;br&gt;
+[[MediaWiki_talk:Show|Talk]]
+&lt;/td&gt;&lt;td&gt;
+show
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Show</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showhideminor&amp;action=edit showhideminor]&lt;br&gt;
+[[MediaWiki_talk:Showhideminor|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 minor edits &amp;#124; $2 bots &amp;#124; $3 logged in users
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Showhideminor</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showingresults&amp;action=edit showingresults]&lt;br&gt;
+[[MediaWiki_talk:Showingresults|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Showing below &amp;lt;b&amp;gt;$1&amp;lt;/b&amp;gt; results starting with #&amp;lt;b&amp;gt;$2&amp;lt;/b&amp;gt;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Showingresults</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showingresultsnum&amp;action=edit showingresultsnum]&lt;br&gt;
+[[MediaWiki_talk:Showingresultsnum|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Showing below &amp;lt;b&amp;gt;$3&amp;lt;/b&amp;gt; results starting with #&amp;lt;b&amp;gt;$2&amp;lt;/b&amp;gt;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Showingresultsnum</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showlast&amp;action=edit showlast]&lt;br&gt;
+[[MediaWiki_talk:Showlast|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Show last $1 images sorted $2.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Showlast</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showpreview&amp;action=edit showpreview]&lt;br&gt;
+[[MediaWiki_talk:Showpreview|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Show preview
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Showpreview</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showtoc&amp;action=edit showtoc]&lt;br&gt;
+[[MediaWiki_talk:Showtoc|Talk]]
+&lt;/td&gt;&lt;td&gt;
+show
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Showtoc</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sig_tip&amp;action=edit sig_tip]&lt;br&gt;
+[[MediaWiki_talk:Sig_tip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your signature with timestamp
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sig_tip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitestats&amp;action=edit sitestats]&lt;br&gt;
+[[MediaWiki_talk:Sitestats|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Site statistics
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sitestats</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitestatstext&amp;action=edit sitestatstext]&lt;br&gt;
+[[MediaWiki_talk:Sitestatstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+There are &amp;#39;&amp;#39;&amp;#39;$1&amp;#39;&amp;#39;&amp;#39; total pages in the database.
+This includes &amp;quot;talk&amp;quot; pages, pages about Wiktionary, minimal &amp;quot;stub&amp;quot;
+pages, redirects, and others that probably don&amp;#39;t qualify as content pages.
+Excluding those, there are &amp;#39;&amp;#39;&amp;#39;$2&amp;#39;&amp;#39;&amp;#39; pages that are probably legitimate
+content pages.
+
+There have been a total of &amp;#39;&amp;#39;&amp;#39;$3&amp;#39;&amp;#39;&amp;#39; page views, and &amp;#39;&amp;#39;&amp;#39;$4&amp;#39;&amp;#39;&amp;#39; page edits
+since the wiki was setup.
+That comes to &amp;#39;&amp;#39;&amp;#39;$5&amp;#39;&amp;#39;&amp;#39; average edits per page, and &amp;#39;&amp;#39;&amp;#39;$6&amp;#39;&amp;#39;&amp;#39; views per edit.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sitestatstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitesubtitle&amp;action=edit sitesubtitle]&lt;br&gt;
+[[MediaWiki_talk:Sitesubtitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The Free Encyclopedia
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sitesubtitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitesupport&amp;action=edit sitesupport]&lt;br&gt;
+[[MediaWiki_talk:Sitesupport|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Donations
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sitesupport</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitetitle&amp;action=edit sitetitle]&lt;br&gt;
+[[MediaWiki_talk:Sitetitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sitetitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Siteuser&amp;action=edit siteuser]&lt;br&gt;
+[[MediaWiki_talk:Siteuser|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary user $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Siteuser</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Siteusers&amp;action=edit siteusers]&lt;br&gt;
+[[MediaWiki_talk:Siteusers|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary user(s) $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Siteusers</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Skin&amp;action=edit skin]&lt;br&gt;
+[[MediaWiki_talk:Skin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Skin
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Skin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Spamprotectiontext&amp;action=edit spamprotectiontext]&lt;br&gt;
+[[MediaWiki_talk:Spamprotectiontext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The page you wanted to save was blocked by the spam filter. This is probably caused by a link to an external site.
+
+You might want to check the following regular expression for patterns that are currently blocked:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Spamprotectiontext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Spamprotectiontitle&amp;action=edit spamprotectiontitle]&lt;br&gt;
+[[MediaWiki_talk:Spamprotectiontitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Spam protection filter
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Spamprotectiontitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Specialpage&amp;action=edit specialpage]&lt;br&gt;
+[[MediaWiki_talk:Specialpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Special Page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Specialpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Specialpages&amp;action=edit specialpages]&lt;br&gt;
+[[MediaWiki_talk:Specialpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Special pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Specialpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Spheading&amp;action=edit spheading]&lt;br&gt;
+[[MediaWiki_talk:Spheading|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Special pages for all users
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Spheading</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sqlislogged&amp;action=edit sqlislogged]&lt;br&gt;
+[[MediaWiki_talk:Sqlislogged|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Please note that all queries are logged.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sqlislogged</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sqlquery&amp;action=edit sqlquery]&lt;br&gt;
+[[MediaWiki_talk:Sqlquery|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Enter query
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sqlquery</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Statistics&amp;action=edit statistics]&lt;br&gt;
+[[MediaWiki_talk:Statistics|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Statistics
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Statistics</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Storedversion&amp;action=edit storedversion]&lt;br&gt;
+[[MediaWiki_talk:Storedversion|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Stored version
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Storedversion</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Stubthreshold&amp;action=edit stubthreshold]&lt;br&gt;
+[[MediaWiki_talk:Stubthreshold|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Threshold for stub display
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Stubthreshold</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Subcategories&amp;action=edit subcategories]&lt;br&gt;
+[[MediaWiki_talk:Subcategories|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Subcategories
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Subcategories</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Subject&amp;action=edit subject]&lt;br&gt;
+[[MediaWiki_talk:Subject|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Subject/headline
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Subject</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Subjectpage&amp;action=edit subjectpage]&lt;br&gt;
+[[MediaWiki_talk:Subjectpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View subject
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Subjectpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Successfulupload&amp;action=edit successfulupload]&lt;br&gt;
+[[MediaWiki_talk:Successfulupload|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Successful upload
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Successfulupload</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Summary&amp;action=edit summary]&lt;br&gt;
+[[MediaWiki_talk:Summary|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Summary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Summary</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sysopspheading&amp;action=edit sysopspheading]&lt;br&gt;
+[[MediaWiki_talk:Sysopspheading|Talk]]
+&lt;/td&gt;&lt;td&gt;
+For sysop use only
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sysopspheading</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sysoptext&amp;action=edit sysoptext]&lt;br&gt;
+[[MediaWiki_talk:Sysoptext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The action you have requested can only be
+performed by users with &amp;quot;sysop&amp;quot; status.
+See $1.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sysoptext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sysoptitle&amp;action=edit sysoptitle]&lt;br&gt;
+[[MediaWiki_talk:Sysoptitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Sysop access required
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Sysoptitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tableform&amp;action=edit tableform]&lt;br&gt;
+[[MediaWiki_talk:Tableform|Talk]]
+&lt;/td&gt;&lt;td&gt;
+table
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tableform</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talk&amp;action=edit talk]&lt;br&gt;
+[[MediaWiki_talk:Talk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Discussion
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Talk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkexists&amp;action=edit talkexists]&lt;br&gt;
+[[MediaWiki_talk:Talkexists|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The page itself was moved successfully, but the
+talk page could not be moved because one already exists at the new
+title. Please merge them manually.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Talkexists</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkpage&amp;action=edit talkpage]&lt;br&gt;
+[[MediaWiki_talk:Talkpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Discuss this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Talkpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkpagemoved&amp;action=edit talkpagemoved]&lt;br&gt;
+[[MediaWiki_talk:Talkpagemoved|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The corresponding talk page was also moved.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Talkpagemoved</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkpagenotmoved&amp;action=edit talkpagenotmoved]&lt;br&gt;
+[[MediaWiki_talk:Talkpagenotmoved|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The corresponding talk page was &amp;lt;strong&amp;gt;not&amp;lt;/strong&amp;gt; moved.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Talkpagenotmoved</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkpagetext&amp;action=edit talkpagetext]&lt;br&gt;
+[[MediaWiki_talk:Talkpagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;!-- MediaWiki:talkpagetext --&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Talkpagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Textboxsize&amp;action=edit textboxsize]&lt;br&gt;
+[[MediaWiki_talk:Textboxsize|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Textbox dimensions
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Textboxsize</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Textmatches&amp;action=edit textmatches]&lt;br&gt;
+[[MediaWiki_talk:Textmatches|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Page text matches
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Textmatches</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Thisisdeleted&amp;action=edit thisisdeleted]&lt;br&gt;
+[[MediaWiki_talk:Thisisdeleted|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View or restore $1?
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Thisisdeleted</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Thumbnail-more&amp;action=edit thumbnail-more]&lt;br&gt;
+[[MediaWiki_talk:Thumbnail-more|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Enlarge
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Thumbnail-more</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Timezonelegend&amp;action=edit timezonelegend]&lt;br&gt;
+[[MediaWiki_talk:Timezonelegend|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Time zone
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Timezonelegend</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Timezoneoffset&amp;action=edit timezoneoffset]&lt;br&gt;
+[[MediaWiki_talk:Timezoneoffset|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Offset
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Timezoneoffset</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Timezonetext&amp;action=edit timezonetext]&lt;br&gt;
+[[MediaWiki_talk:Timezonetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Enter number of hours your local time differs
+from server time (UTC).
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Timezonetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Titlematches&amp;action=edit titlematches]&lt;br&gt;
+[[MediaWiki_talk:Titlematches|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Article title matches
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Titlematches</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Toc&amp;action=edit toc]&lt;br&gt;
+[[MediaWiki_talk:Toc|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Table of contents
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Toc</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Toolbox&amp;action=edit toolbox]&lt;br&gt;
+[[MediaWiki_talk:Toolbox|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Toolbox
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Toolbox</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-addsection&amp;action=edit tooltip-addsection]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-addsection|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Add a comment to this page. &amp;#91;alt-+]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-addsection</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-anontalk&amp;action=edit tooltip-anontalk]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-anontalk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Discussion about edits from this ip address &amp;#91;alt-n]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-anontalk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-anonuserpage&amp;action=edit tooltip-anonuserpage]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-anonuserpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The user page for the ip you&amp;#39;re editing as &amp;#91;alt-.]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-anonuserpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-article&amp;action=edit tooltip-article]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-article|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View the content page &amp;#91;alt-a]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-article</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-atom&amp;action=edit tooltip-atom]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-atom|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Atom feed for this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-atom</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-compareselectedversions&amp;action=edit tooltip-compareselectedversions]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-compareselectedversions|Talk]]
+&lt;/td&gt;&lt;td&gt;
+See the differences between the two selected versions of this page. &amp;#91;alt-v]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-compareselectedversions</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-contributions&amp;action=edit tooltip-contributions]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-contributions|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View the list of contributions of this user
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-contributions</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-currentevents&amp;action=edit tooltip-currentevents]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-currentevents|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Find background information on current events
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-currentevents</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-delete&amp;action=edit tooltip-delete]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-delete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Delete this page &amp;#91;alt-d]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-delete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-edit&amp;action=edit tooltip-edit]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-edit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You can edit this page. Please use the preview button before saving. &amp;#91;alt-e]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-edit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-emailuser&amp;action=edit tooltip-emailuser]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-emailuser|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Send a mail to this user
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-emailuser</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-help&amp;action=edit tooltip-help]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-help|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The place to find out.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-help</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-history&amp;action=edit tooltip-history]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-history|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Past versions of this page, &amp;#91;alt-h]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-history</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-login&amp;action=edit tooltip-login]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-login|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You are encouraged to log in, it is not mandatory however. &amp;#91;alt-o]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-login</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-logout&amp;action=edit tooltip-logout]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-logout|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Log out &amp;#91;alt-o]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-logout</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-mainpage&amp;action=edit tooltip-mainpage]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-mainpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Visit the Main Page &amp;#91;alt-z]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-mainpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-minoredit&amp;action=edit tooltip-minoredit]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-minoredit|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Mark this as a minor edit &amp;#91;alt-i]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-minoredit</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-move&amp;action=edit tooltip-move]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-move|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Move this page &amp;#91;alt-m]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-move</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-mycontris&amp;action=edit tooltip-mycontris]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-mycontris|Talk]]
+&lt;/td&gt;&lt;td&gt;
+List of my contributions &amp;#91;alt-y]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-mycontris</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-mytalk&amp;action=edit tooltip-mytalk]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-mytalk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+My talk page &amp;#91;alt-n]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-mytalk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-nomove&amp;action=edit tooltip-nomove]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-nomove|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You don&amp;#39;t have the permissions to move this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-nomove</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-portal&amp;action=edit tooltip-portal]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-portal|Talk]]
+&lt;/td&gt;&lt;td&gt;
+About the project, what you can do, where to find things
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-portal</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-preferences&amp;action=edit tooltip-preferences]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-preferences|Talk]]
+&lt;/td&gt;&lt;td&gt;
+My preferences
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-preferences</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-preview&amp;action=edit tooltip-preview]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-preview|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Preview your changes, please use this before saving! &amp;#91;alt-p]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-preview</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-protect&amp;action=edit tooltip-protect]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-protect|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Protect this page &amp;#91;alt-=]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-protect</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-randompage&amp;action=edit tooltip-randompage]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-randompage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Load a random page &amp;#91;alt-x]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-randompage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-recentchanges&amp;action=edit tooltip-recentchanges]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-recentchanges|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The list of recent changes in the wiki. &amp;#91;alt-r]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-recentchanges</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-recentchangeslinked&amp;action=edit tooltip-recentchangeslinked]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-recentchangeslinked|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Recent changes in pages linking to this page &amp;#91;alt-c]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-recentchangeslinked</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-rss&amp;action=edit tooltip-rss]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-rss|Talk]]
+&lt;/td&gt;&lt;td&gt;
+RSS feed for this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-rss</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-save&amp;action=edit tooltip-save]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-save|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Save your changes &amp;#91;alt-s]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-save</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-search&amp;action=edit tooltip-search]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-search|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Search this wiki &amp;#91;alt-f]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-search</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-sitesupport&amp;action=edit tooltip-sitesupport]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-sitesupport|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Support Wiktionary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-sitesupport</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-specialpage&amp;action=edit tooltip-specialpage]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-specialpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This is a special page, you can&amp;#39;t edit the page itself.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-specialpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-specialpages&amp;action=edit tooltip-specialpages]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-specialpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+List of all special pages &amp;#91;alt-q]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-specialpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-talk&amp;action=edit tooltip-talk]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-talk|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Discussion about the content page &amp;#91;alt-t]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-talk</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-undelete&amp;action=edit tooltip-undelete]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-undelete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Restore the $1 edits done to this page before it was deleted &amp;#91;alt-d]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-undelete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-unwatch&amp;action=edit tooltip-unwatch]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-unwatch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Remove this page from your watchlist &amp;#91;alt-w]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-unwatch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-upload&amp;action=edit tooltip-upload]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-upload|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Upload images or media files &amp;#91;alt-u]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-upload</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-userpage&amp;action=edit tooltip-userpage]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-userpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+My user page &amp;#91;alt-.]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-userpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-viewsource&amp;action=edit tooltip-viewsource]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-viewsource|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This page is protected. You can view its source. &amp;#91;alt-e]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-viewsource</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-watch&amp;action=edit tooltip-watch]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-watch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Add this page to your watchlist &amp;#91;alt-w]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-watch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-watchlist&amp;action=edit tooltip-watchlist]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-watchlist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The list of pages you&amp;#39;re monitoring for changes. &amp;#91;alt-l]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-watchlist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-whatlinkshere&amp;action=edit tooltip-whatlinkshere]&lt;br&gt;
+[[MediaWiki_talk:Tooltip-whatlinkshere|Talk]]
+&lt;/td&gt;&lt;td&gt;
+List of all wiki pages that link here &amp;#91;alt-b]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Tooltip-whatlinkshere</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uclinks&amp;action=edit uclinks]&lt;br&gt;
+[[MediaWiki_talk:Uclinks|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View the last $1 changes; view the last $2 days.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uclinks</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ucnote&amp;action=edit ucnote]&lt;br&gt;
+[[MediaWiki_talk:Ucnote|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Below are this user&amp;#39;s last &amp;lt;b&amp;gt;$1&amp;lt;/b&amp;gt; changes in the last &amp;lt;b&amp;gt;$2&amp;lt;/b&amp;gt; days.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Ucnote</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uctop&amp;action=edit uctop]&lt;br&gt;
+[[MediaWiki_talk:Uctop|Talk]]
+&lt;/td&gt;&lt;td&gt;
+ (top)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uctop</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unblockip&amp;action=edit unblockip]&lt;br&gt;
+[[MediaWiki_talk:Unblockip|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unblock user
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unblockip</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unblockiptext&amp;action=edit unblockiptext]&lt;br&gt;
+[[MediaWiki_talk:Unblockiptext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Use the form below to restore write access
+to a previously blocked IP address or username.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unblockiptext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unblocklink&amp;action=edit unblocklink]&lt;br&gt;
+[[MediaWiki_talk:Unblocklink|Talk]]
+&lt;/td&gt;&lt;td&gt;
+unblock
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unblocklink</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unblocklogentry&amp;action=edit unblocklogentry]&lt;br&gt;
+[[MediaWiki_talk:Unblocklogentry|Talk]]
+&lt;/td&gt;&lt;td&gt;
+unblocked &amp;quot;$1&amp;quot;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unblocklogentry</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undelete&amp;action=edit undelete]&lt;br&gt;
+[[MediaWiki_talk:Undelete|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Restore deleted page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undelete</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undelete_short&amp;action=edit undelete_short]&lt;br&gt;
+[[MediaWiki_talk:Undelete_short|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Undelete $1 edits
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undelete_short</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletearticle&amp;action=edit undeletearticle]&lt;br&gt;
+[[MediaWiki_talk:Undeletearticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Restore deleted page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undeletearticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletebtn&amp;action=edit undeletebtn]&lt;br&gt;
+[[MediaWiki_talk:Undeletebtn|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Restore!
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undeletebtn</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletedarticle&amp;action=edit undeletedarticle]&lt;br&gt;
+[[MediaWiki_talk:Undeletedarticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+restored &amp;quot;$1&amp;quot;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undeletedarticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletedtext&amp;action=edit undeletedtext]&lt;br&gt;
+[[MediaWiki_talk:Undeletedtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;#91;&amp;#91;$1]] has been successfully restored.
+See &amp;#91;&amp;#91;Wiktionary:Deletion_log]] for a record of recent deletions and restorations.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undeletedtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletehistory&amp;action=edit undeletehistory]&lt;br&gt;
+[[MediaWiki_talk:Undeletehistory|Talk]]
+&lt;/td&gt;&lt;td&gt;
+If you restore the page, all revisions will be restored to the history.
+If a new page with the same name has been created since the deletion, the restored
+revisions will appear in the prior history, and the current revision of the live page
+will not be automatically replaced.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undeletehistory</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletepage&amp;action=edit undeletepage]&lt;br&gt;
+[[MediaWiki_talk:Undeletepage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View and restore deleted pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undeletepage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletepagetext&amp;action=edit undeletepagetext]&lt;br&gt;
+[[MediaWiki_talk:Undeletepagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The following pages have been deleted but are still in the archive and
+can be restored. The archive may be periodically cleaned out.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undeletepagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeleterevision&amp;action=edit undeleterevision]&lt;br&gt;
+[[MediaWiki_talk:Undeleterevision|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Deleted revision as of $1
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undeleterevision</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeleterevisions&amp;action=edit undeleterevisions]&lt;br&gt;
+[[MediaWiki_talk:Undeleterevisions|Talk]]
+&lt;/td&gt;&lt;td&gt;
+$1 revisions archived
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Undeleterevisions</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unexpected&amp;action=edit unexpected]&lt;br&gt;
+[[MediaWiki_talk:Unexpected|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unexpected value: &amp;quot;$1&amp;quot;=&amp;quot;$2&amp;quot;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unexpected</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockbtn&amp;action=edit unlockbtn]&lt;br&gt;
+[[MediaWiki_talk:Unlockbtn|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unlock database
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unlockbtn</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockconfirm&amp;action=edit unlockconfirm]&lt;br&gt;
+[[MediaWiki_talk:Unlockconfirm|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Yes, I really want to unlock the database.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unlockconfirm</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockdb&amp;action=edit unlockdb]&lt;br&gt;
+[[MediaWiki_talk:Unlockdb|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unlock database
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unlockdb</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockdbsuccesssub&amp;action=edit unlockdbsuccesssub]&lt;br&gt;
+[[MediaWiki_talk:Unlockdbsuccesssub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Database lock removed
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unlockdbsuccesssub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockdbsuccesstext&amp;action=edit unlockdbsuccesstext]&lt;br&gt;
+[[MediaWiki_talk:Unlockdbsuccesstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The database has been unlocked.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unlockdbsuccesstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockdbtext&amp;action=edit unlockdbtext]&lt;br&gt;
+[[MediaWiki_talk:Unlockdbtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unlocking the database will restore the ability of all
+users to edit pages, change their preferences, edit their watchlists, and
+other things requiring changes in the database.
+Please confirm that this is what you intend to do.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unlockdbtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotect&amp;action=edit unprotect]&lt;br&gt;
+[[MediaWiki_talk:Unprotect|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unprotect
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unprotect</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotectcomment&amp;action=edit unprotectcomment]&lt;br&gt;
+[[MediaWiki_talk:Unprotectcomment|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Reason for unprotecting
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unprotectcomment</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotectedarticle&amp;action=edit unprotectedarticle]&lt;br&gt;
+[[MediaWiki_talk:Unprotectedarticle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+unprotected &amp;#91;&amp;#91;$1]]
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unprotectedarticle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotectsub&amp;action=edit unprotectsub]&lt;br&gt;
+[[MediaWiki_talk:Unprotectsub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(Unprotecting &amp;quot;$1&amp;quot;)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unprotectsub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotectthispage&amp;action=edit unprotectthispage]&lt;br&gt;
+[[MediaWiki_talk:Unprotectthispage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unprotect this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unprotectthispage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unusedimages&amp;action=edit unusedimages]&lt;br&gt;
+[[MediaWiki_talk:Unusedimages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unused images
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unusedimages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unusedimagestext&amp;action=edit unusedimagestext]&lt;br&gt;
+[[MediaWiki_talk:Unusedimagestext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;p&amp;gt;Please note that other web sites may link to an image with
+a direct URL, and so may still be listed here despite being
+in active use.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unusedimagestext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unwatch&amp;action=edit unwatch]&lt;br&gt;
+[[MediaWiki_talk:Unwatch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Unwatch
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unwatch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unwatchthispage&amp;action=edit unwatchthispage]&lt;br&gt;
+[[MediaWiki_talk:Unwatchthispage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Stop watching
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Unwatchthispage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Updated&amp;action=edit updated]&lt;br&gt;
+[[MediaWiki_talk:Updated|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(Updated)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Updated</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Upload&amp;action=edit upload]&lt;br&gt;
+[[MediaWiki_talk:Upload|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Upload file
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Upload</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadbtn&amp;action=edit uploadbtn]&lt;br&gt;
+[[MediaWiki_talk:Uploadbtn|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Upload file
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadbtn</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploaddisabled&amp;action=edit uploaddisabled]&lt;br&gt;
+[[MediaWiki_talk:Uploaddisabled|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Sorry, uploading is disabled.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploaddisabled</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadedfiles&amp;action=edit uploadedfiles]&lt;br&gt;
+[[MediaWiki_talk:Uploadedfiles|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Uploaded files
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadedfiles</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadedimage&amp;action=edit uploadedimage]&lt;br&gt;
+[[MediaWiki_talk:Uploadedimage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+uploaded &amp;quot;$1&amp;quot;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadedimage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploaderror&amp;action=edit uploaderror]&lt;br&gt;
+[[MediaWiki_talk:Uploaderror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Upload error
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploaderror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadfile&amp;action=edit uploadfile]&lt;br&gt;
+[[MediaWiki_talk:Uploadfile|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Upload images, sounds, documents etc.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadfile</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadlink&amp;action=edit uploadlink]&lt;br&gt;
+[[MediaWiki_talk:Uploadlink|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Upload images
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadlink</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadlog&amp;action=edit uploadlog]&lt;br&gt;
+[[MediaWiki_talk:Uploadlog|Talk]]
+&lt;/td&gt;&lt;td&gt;
+upload log
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadlog</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadlogpage&amp;action=edit uploadlogpage]&lt;br&gt;
+[[MediaWiki_talk:Uploadlogpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Upload_log
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadlogpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadlogpagetext&amp;action=edit uploadlogpagetext]&lt;br&gt;
+[[MediaWiki_talk:Uploadlogpagetext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Below is a list of the most recent file uploads.
+All times shown are server time (UTC).
+&amp;lt;ul&amp;gt;
+&amp;lt;/ul&amp;gt;
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadlogpagetext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadnologin&amp;action=edit uploadnologin]&lt;br&gt;
+[[MediaWiki_talk:Uploadnologin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Not logged in
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadnologin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadnologintext&amp;action=edit uploadnologintext]&lt;br&gt;
+[[MediaWiki_talk:Uploadnologintext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You must be &amp;lt;a href=&amp;quot;/wiki/Special:Userlogin&amp;quot;&amp;gt;logged in&amp;lt;/a&amp;gt;
+to upload files.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadnologintext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadtext&amp;action=edit uploadtext]&lt;br&gt;
+[[MediaWiki_talk:Uploadtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;strong&amp;gt;STOP!&amp;lt;/strong&amp;gt; Before you upload here,
+make sure to read and follow the &amp;lt;a href=&amp;quot;/wiki/Special:Image_use_policy&amp;quot;&amp;gt;image use policy&amp;lt;/a&amp;gt;.
+&amp;lt;p&amp;gt;If a file with the name you are specifying already
+exists on the wiki, it&amp;#39;ll be replaced without warning.
+So unless you mean to update a file, it&amp;#39;s a good idea
+to first check if such a file exists.
+&amp;lt;p&amp;gt;To view or search previously uploaded images,
+go to the &amp;lt;a href=&amp;quot;/wiki/Special:Imagelist&amp;quot;&amp;gt;list of uploaded images&amp;lt;/a&amp;gt;.
+Uploads and deletions are logged on the &amp;lt;a href=&amp;quot;/wiki/Wiktionary:Upload_log&amp;quot;&amp;gt;upload log&amp;lt;/a&amp;gt;.
+&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;Use the form below to upload new image files for use in
+illustrating your pages.
+On most browsers, you will see a &amp;quot;Browse...&amp;quot; button, which will
+bring up your operating system&amp;#39;s standard file open dialog.
+Choosing a file will fill the name of that file into the text
+field next to the button.
+You must also check the box affirming that you are not
+violating any copyrights by uploading the file.
+Press the &amp;quot;Upload&amp;quot; button to finish the upload.
+This may take some time if you have a slow internet connection.
+&amp;lt;p&amp;gt;The preferred formats are JPEG for photographic images, PNG
+for drawings and other iconic images, and OGG for sounds.
+Please name your files descriptively to avoid confusion.
+To include the image in a page, use a link in the form
+&amp;lt;b&amp;gt;&amp;#91;&amp;#91;Image:file.jpg]]&amp;lt;/b&amp;gt; or &amp;lt;b&amp;gt;&amp;#91;&amp;#91;Image:file.png&amp;#124;alt text]]&amp;lt;/b&amp;gt;
+or &amp;lt;b&amp;gt;&amp;#91;&amp;#91;Media:file.ogg]]&amp;lt;/b&amp;gt; for sounds.
+&amp;lt;p&amp;gt;Please note that as with wiki pages, others may edit or
+delete your uploads if they think it serves the project, and
+you may be blocked from uploading if you abuse the system.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadwarning&amp;action=edit uploadwarning]&lt;br&gt;
+[[MediaWiki_talk:Uploadwarning|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Upload warning
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Uploadwarning</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:User_rights_set&amp;action=edit user_rights_set]&lt;br&gt;
+[[MediaWiki_talk:User_rights_set|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;b&amp;gt;User rights for &amp;quot;$1&amp;quot; updated&amp;lt;/b&amp;gt;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:User_rights_set</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Usercssjs&amp;action=edit usercssjs]&lt;br&gt;
+[[MediaWiki_talk:Usercssjs|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;#39;&amp;#39;&amp;#39;Note:&amp;#39;&amp;#39;&amp;#39; After saving, you have to tell your bowser to get the new version: &amp;#39;&amp;#39;&amp;#39;Mozilla:&amp;#39;&amp;#39;&amp;#39; click &amp;#39;&amp;#39;reload&amp;#39;&amp;#39;(or &amp;#39;&amp;#39;ctrl-r&amp;#39;&amp;#39;), &amp;#39;&amp;#39;&amp;#39;IE / Opera:&amp;#39;&amp;#39;&amp;#39; &amp;#39;&amp;#39;ctrl-f5&amp;#39;&amp;#39;, &amp;#39;&amp;#39;&amp;#39;Safari:&amp;#39;&amp;#39;&amp;#39; &amp;#39;&amp;#39;cmd-r&amp;#39;&amp;#39;, &amp;#39;&amp;#39;&amp;#39;Konqueror&amp;#39;&amp;#39;&amp;#39; &amp;#39;&amp;#39;ctrl-r&amp;#39;&amp;#39;.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Usercssjs</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Usercssjsyoucanpreview&amp;action=edit usercssjsyoucanpreview]&lt;br&gt;
+[[MediaWiki_talk:Usercssjsyoucanpreview|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;strong&amp;gt;Tip:&amp;lt;/strong&amp;gt; Use the &amp;#39;Show preview&amp;#39; button to test your new css/js before saving.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Usercssjsyoucanpreview</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Usercsspreview&amp;action=edit usercsspreview]&lt;br&gt;
+[[MediaWiki_talk:Usercsspreview|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;#39;&amp;#39;&amp;#39;Remember that you are only previewing your user css, it has not yet been saved!&amp;#39;&amp;#39;&amp;#39;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Usercsspreview</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userexists&amp;action=edit userexists]&lt;br&gt;
+[[MediaWiki_talk:Userexists|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The user name you entered is already in use. Please choose a different name.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Userexists</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userjspreview&amp;action=edit userjspreview]&lt;br&gt;
+[[MediaWiki_talk:Userjspreview|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;#39;&amp;#39;&amp;#39;Remember that you are only testing/previewing your user javascript, it has not yet been saved!&amp;#39;&amp;#39;&amp;#39;
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Userjspreview</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userlogin&amp;action=edit userlogin]&lt;br&gt;
+[[MediaWiki_talk:Userlogin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Log in
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Userlogin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userlogout&amp;action=edit userlogout]&lt;br&gt;
+[[MediaWiki_talk:Userlogout|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Log out
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Userlogout</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Usermailererror&amp;action=edit usermailererror]&lt;br&gt;
+[[MediaWiki_talk:Usermailererror|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Mail object returned error:
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Usermailererror</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userpage&amp;action=edit userpage]&lt;br&gt;
+[[MediaWiki_talk:Userpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View user page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Userpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userstats&amp;action=edit userstats]&lt;br&gt;
+[[MediaWiki_talk:Userstats|Talk]]
+&lt;/td&gt;&lt;td&gt;
+User statistics
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Userstats</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userstatstext&amp;action=edit userstatstext]&lt;br&gt;
+[[MediaWiki_talk:Userstatstext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+There are &amp;#39;&amp;#39;&amp;#39;$1&amp;#39;&amp;#39;&amp;#39; registered users.
+&amp;#39;&amp;#39;&amp;#39;$2&amp;#39;&amp;#39;&amp;#39; of these are administrators (see $3).
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Userstatstext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Version&amp;action=edit version]&lt;br&gt;
+[[MediaWiki_talk:Version|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Version
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Version</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Viewcount&amp;action=edit viewcount]&lt;br&gt;
+[[MediaWiki_talk:Viewcount|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This page has been accessed $1 times.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Viewcount</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Viewprevnext&amp;action=edit viewprevnext]&lt;br&gt;
+[[MediaWiki_talk:Viewprevnext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View ($1) ($2) ($3).
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Viewprevnext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Viewsource&amp;action=edit viewsource]&lt;br&gt;
+[[MediaWiki_talk:Viewsource|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View source
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Viewsource</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Viewtalkpage&amp;action=edit viewtalkpage]&lt;br&gt;
+[[MediaWiki_talk:Viewtalkpage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View discussion
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Viewtalkpage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wantedpages&amp;action=edit wantedpages]&lt;br&gt;
+[[MediaWiki_talk:Wantedpages|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wanted pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Wantedpages</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watch&amp;action=edit watch]&lt;br&gt;
+[[MediaWiki_talk:Watch|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Watch
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watch</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchdetails&amp;action=edit watchdetails]&lt;br&gt;
+[[MediaWiki_talk:Watchdetails|Talk]]
+&lt;/td&gt;&lt;td&gt;
+($1 pages watched not counting talk pages;
+$2 total pages edited since cutoff;
+$3...
+&amp;lt;a href=&amp;#39;$4&amp;#39;&amp;gt;show and edit complete list&amp;lt;/a&amp;gt;.)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchdetails</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watcheditlist&amp;action=edit watcheditlist]&lt;br&gt;
+[[MediaWiki_talk:Watcheditlist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Here&amp;#39;s an alphabetical list of your
+watched pages. Check the boxes of pages you want to remove
+from your watchlist and click the &amp;#39;remove checked&amp;#39; button
+at the bottom of the screen.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watcheditlist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchlist&amp;action=edit watchlist]&lt;br&gt;
+[[MediaWiki_talk:Watchlist|Talk]]
+&lt;/td&gt;&lt;td&gt;
+My watchlist
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchlist</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchlistcontains&amp;action=edit watchlistcontains]&lt;br&gt;
+[[MediaWiki_talk:Watchlistcontains|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your watchlist contains $1 pages.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchlistcontains</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchlistsub&amp;action=edit watchlistsub]&lt;br&gt;
+[[MediaWiki_talk:Watchlistsub|Talk]]
+&lt;/td&gt;&lt;td&gt;
+(for user &amp;quot;$1&amp;quot;)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchlistsub</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchmethod-list&amp;action=edit watchmethod-list]&lt;br&gt;
+[[MediaWiki_talk:Watchmethod-list|Talk]]
+&lt;/td&gt;&lt;td&gt;
+checking watched pages for recent edits
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchmethod-list</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchmethod-recent&amp;action=edit watchmethod-recent]&lt;br&gt;
+[[MediaWiki_talk:Watchmethod-recent|Talk]]
+&lt;/td&gt;&lt;td&gt;
+checking recent edits for watched pages
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchmethod-recent</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchnochange&amp;action=edit watchnochange]&lt;br&gt;
+[[MediaWiki_talk:Watchnochange|Talk]]
+&lt;/td&gt;&lt;td&gt;
+None of your watched items were edited in the time period displayed.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchnochange</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchnologin&amp;action=edit watchnologin]&lt;br&gt;
+[[MediaWiki_talk:Watchnologin|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Not logged in
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchnologin</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchnologintext&amp;action=edit watchnologintext]&lt;br&gt;
+[[MediaWiki_talk:Watchnologintext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You must be &amp;lt;a href=&amp;quot;/wiki/Special:Userlogin&amp;quot;&amp;gt;logged in&amp;lt;/a&amp;gt;
+to modify your watchlist.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchnologintext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchthis&amp;action=edit watchthis]&lt;br&gt;
+[[MediaWiki_talk:Watchthis|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Watch this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchthis</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchthispage&amp;action=edit watchthispage]&lt;br&gt;
+[[MediaWiki_talk:Watchthispage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Watch this page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Watchthispage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Welcomecreation&amp;action=edit welcomecreation]&lt;br&gt;
+[[MediaWiki_talk:Welcomecreation|Talk]]
+&lt;/td&gt;&lt;td&gt;
+&amp;lt;h2&amp;gt;Welcome, $1!&amp;lt;/h2&amp;gt;&amp;lt;p&amp;gt;Your account has been created.
+Don&amp;#39;t forget to change your Wiktionary preferences.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Welcomecreation</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whatlinkshere&amp;action=edit whatlinkshere]&lt;br&gt;
+[[MediaWiki_talk:Whatlinkshere|Talk]]
+&lt;/td&gt;&lt;td&gt;
+What links here
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Whatlinkshere</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistacctext&amp;action=edit whitelistacctext]&lt;br&gt;
+[[MediaWiki_talk:Whitelistacctext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+To be allowed to create accounts in this Wiki you have to &amp;#91;&amp;#91;Special:Userlogin&amp;#124;log]] in and have the appropriate permissions.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Whitelistacctext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistacctitle&amp;action=edit whitelistacctitle]&lt;br&gt;
+[[MediaWiki_talk:Whitelistacctitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You are not allowed to create an account
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Whitelistacctitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistedittext&amp;action=edit whitelistedittext]&lt;br&gt;
+[[MediaWiki_talk:Whitelistedittext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You have to &amp;#91;&amp;#91;Special:Userlogin&amp;#124;login]] to edit pages.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Whitelistedittext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistedittitle&amp;action=edit whitelistedittitle]&lt;br&gt;
+[[MediaWiki_talk:Whitelistedittitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Login required to edit
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Whitelistedittitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistreadtext&amp;action=edit whitelistreadtext]&lt;br&gt;
+[[MediaWiki_talk:Whitelistreadtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+You have to &amp;#91;&amp;#91;Special:Userlogin&amp;#124;login]] to read pages.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Whitelistreadtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistreadtitle&amp;action=edit whitelistreadtitle]&lt;br&gt;
+[[MediaWiki_talk:Whitelistreadtitle|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Login required to read
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Whitelistreadtitle</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wikipediapage&amp;action=edit wikipediapage]&lt;br&gt;
+[[MediaWiki_talk:Wikipediapage|Talk]]
+&lt;/td&gt;&lt;td&gt;
+View project page
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Wikipediapage</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wikititlesuffix&amp;action=edit wikititlesuffix]&lt;br&gt;
+[[MediaWiki_talk:Wikititlesuffix|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Wiktionary
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Wikititlesuffix</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wlnote&amp;action=edit wlnote]&lt;br&gt;
+[[MediaWiki_talk:Wlnote|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Below are the last $1 changes in the last &amp;lt;b&amp;gt;$2&amp;lt;/b&amp;gt; hours.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Wlnote</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wlsaved&amp;action=edit wlsaved]&lt;br&gt;
+[[MediaWiki_talk:Wlsaved|Talk]]
+&lt;/td&gt;&lt;td&gt;
+This is a saved version of your watchlist.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Wlsaved</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wlshowlast&amp;action=edit wlshowlast]&lt;br&gt;
+[[MediaWiki_talk:Wlshowlast|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Show last $1 hours $2 days $3
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Wlshowlast</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wrong_wfQuery_params&amp;action=edit wrong_wfQuery_params]&lt;br&gt;
+[[MediaWiki_talk:Wrong_wfQuery_params|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Incorrect parameters to wfQuery()&amp;lt;br /&amp;gt;
+Function: $1&amp;lt;br /&amp;gt;
+Query: $2
+
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Wrong_wfQuery_params</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wrongpassword&amp;action=edit wrongpassword]&lt;br&gt;
+[[MediaWiki_talk:Wrongpassword|Talk]]
+&lt;/td&gt;&lt;td&gt;
+The password you entered is incorrect. Please try again.
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Wrongpassword</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourdiff&amp;action=edit yourdiff]&lt;br&gt;
+[[MediaWiki_talk:Yourdiff|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Differences
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Yourdiff</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Youremail&amp;action=edit youremail]&lt;br&gt;
+[[MediaWiki_talk:Youremail|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your email*
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Youremail</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourname&amp;action=edit yourname]&lt;br&gt;
+[[MediaWiki_talk:Yourname|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your user name
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Yourname</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yournick&amp;action=edit yournick]&lt;br&gt;
+[[MediaWiki_talk:Yournick|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your nickname (for signatures)
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Yournick</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourpassword&amp;action=edit yourpassword]&lt;br&gt;
+[[MediaWiki_talk:Yourpassword|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your password
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Yourpassword</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourpasswordagain&amp;action=edit yourpasswordagain]&lt;br&gt;
+[[MediaWiki_talk:Yourpasswordagain|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Retype password
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Yourpasswordagain</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourrealname&amp;action=edit yourrealname]&lt;br&gt;
+[[MediaWiki_talk:Yourrealname|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your real name*
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Yourrealname</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourtext&amp;action=edit yourtext]&lt;br&gt;
+[[MediaWiki_talk:Yourtext|Talk]]
+&lt;/td&gt;&lt;td&gt;
+Your text
+&lt;/td&gt;&lt;td&gt;
+<template lineStart="1"><title>int:Yourtext</title></template>
+&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
+
+</root> \ No newline at end of file
diff --git a/tests/parser/preprocess/All_system_messages.txt b/tests/parser/preprocess/All_system_messages.txt
new file mode 100644
index 00000000..3c30da94
--- /dev/null
+++ b/tests/parser/preprocess/All_system_messages.txt
@@ -0,0 +1,5624 @@
+{{int:allmessagestext}}
+
+<table border=1 width=100%><tr><td>
+'''Name'''
+</td><td>
+'''Default text'''
+</td><td>
+'''Current text'''
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:1movedto2&action=edit 1movedto2]<br>
+[[MediaWiki_talk:1movedto2|Talk]]
+</td><td>
+$1 moved to $2
+</td><td>
+{{int:1movedto2}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Monobook.css&action=edit Monobook.css]<br>
+[[MediaWiki_talk:Monobook.css|Talk]]
+</td><td>
+/* edit this file to customize the monobook skin for the entire site */
+</td><td>
+{{int:Monobook.css}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:About&action=edit about]<br>
+[[MediaWiki_talk:About|Talk]]
+</td><td>
+About
+</td><td>
+{{int:About}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Aboutpage&action=edit aboutpage]<br>
+[[MediaWiki_talk:Aboutpage|Talk]]
+</td><td>
+Wiktionary:About
+</td><td>
+{{int:Aboutpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Aboutwikipedia&action=edit aboutwikipedia]<br>
+[[MediaWiki_talk:Aboutwikipedia|Talk]]
+</td><td>
+About Wiktionary
+</td><td>
+{{int:Aboutwikipedia}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-addsection&action=edit accesskey-addsection]<br>
+[[MediaWiki_talk:Accesskey-addsection|Talk]]
+</td><td>
++
+</td><td>
+{{int:Accesskey-addsection}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-anontalk&action=edit accesskey-anontalk]<br>
+[[MediaWiki_talk:Accesskey-anontalk|Talk]]
+</td><td>
+n
+</td><td>
+{{int:Accesskey-anontalk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-anonuserpage&action=edit accesskey-anonuserpage]<br>
+[[MediaWiki_talk:Accesskey-anonuserpage|Talk]]
+</td><td>
+.
+</td><td>
+{{int:Accesskey-anonuserpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-article&action=edit accesskey-article]<br>
+[[MediaWiki_talk:Accesskey-article|Talk]]
+</td><td>
+a
+</td><td>
+{{int:Accesskey-article}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-compareselectedversions&action=edit accesskey-compareselectedversions]<br>
+[[MediaWiki_talk:Accesskey-compareselectedversions|Talk]]
+</td><td>
+v
+</td><td>
+{{int:Accesskey-compareselectedversions}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-contributions&action=edit accesskey-contributions]<br>
+[[MediaWiki_talk:Accesskey-contributions|Talk]]
+</td><td>
+&amp;lt;accesskey-contributions&amp;gt;
+</td><td>
+{{int:Accesskey-contributions}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-currentevents&action=edit accesskey-currentevents]<br>
+[[MediaWiki_talk:Accesskey-currentevents|Talk]]
+</td><td>
+&amp;lt;accesskey-currentevents&amp;gt;
+</td><td>
+{{int:Accesskey-currentevents}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-delete&action=edit accesskey-delete]<br>
+[[MediaWiki_talk:Accesskey-delete|Talk]]
+</td><td>
+d
+</td><td>
+{{int:Accesskey-delete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-edit&action=edit accesskey-edit]<br>
+[[MediaWiki_talk:Accesskey-edit|Talk]]
+</td><td>
+e
+</td><td>
+{{int:Accesskey-edit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-emailuser&action=edit accesskey-emailuser]<br>
+[[MediaWiki_talk:Accesskey-emailuser|Talk]]
+</td><td>
+&amp;lt;accesskey-emailuser&amp;gt;
+</td><td>
+{{int:Accesskey-emailuser}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-help&action=edit accesskey-help]<br>
+[[MediaWiki_talk:Accesskey-help|Talk]]
+</td><td>
+&amp;lt;accesskey-help&amp;gt;
+</td><td>
+{{int:Accesskey-help}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-history&action=edit accesskey-history]<br>
+[[MediaWiki_talk:Accesskey-history|Talk]]
+</td><td>
+h
+</td><td>
+{{int:Accesskey-history}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-login&action=edit accesskey-login]<br>
+[[MediaWiki_talk:Accesskey-login|Talk]]
+</td><td>
+o
+</td><td>
+{{int:Accesskey-login}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-logout&action=edit accesskey-logout]<br>
+[[MediaWiki_talk:Accesskey-logout|Talk]]
+</td><td>
+o
+</td><td>
+{{int:Accesskey-logout}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-mainpage&action=edit accesskey-mainpage]<br>
+[[MediaWiki_talk:Accesskey-mainpage|Talk]]
+</td><td>
+z
+</td><td>
+{{int:Accesskey-mainpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-minoredit&action=edit accesskey-minoredit]<br>
+[[MediaWiki_talk:Accesskey-minoredit|Talk]]
+</td><td>
+i
+</td><td>
+{{int:Accesskey-minoredit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-move&action=edit accesskey-move]<br>
+[[MediaWiki_talk:Accesskey-move|Talk]]
+</td><td>
+m
+</td><td>
+{{int:Accesskey-move}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-mycontris&action=edit accesskey-mycontris]<br>
+[[MediaWiki_talk:Accesskey-mycontris|Talk]]
+</td><td>
+y
+</td><td>
+{{int:Accesskey-mycontris}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-mytalk&action=edit accesskey-mytalk]<br>
+[[MediaWiki_talk:Accesskey-mytalk|Talk]]
+</td><td>
+n
+</td><td>
+{{int:Accesskey-mytalk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-portal&action=edit accesskey-portal]<br>
+[[MediaWiki_talk:Accesskey-portal|Talk]]
+</td><td>
+&amp;lt;accesskey-portal&amp;gt;
+</td><td>
+{{int:Accesskey-portal}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-preferences&action=edit accesskey-preferences]<br>
+[[MediaWiki_talk:Accesskey-preferences|Talk]]
+</td><td>
+&amp;lt;accesskey-preferences&amp;gt;
+</td><td>
+{{int:Accesskey-preferences}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-preview&action=edit accesskey-preview]<br>
+[[MediaWiki_talk:Accesskey-preview|Talk]]
+</td><td>
+p
+</td><td>
+{{int:Accesskey-preview}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-protect&action=edit accesskey-protect]<br>
+[[MediaWiki_talk:Accesskey-protect|Talk]]
+</td><td>
+=
+</td><td>
+{{int:Accesskey-protect}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-randompage&action=edit accesskey-randompage]<br>
+[[MediaWiki_talk:Accesskey-randompage|Talk]]
+</td><td>
+x
+</td><td>
+{{int:Accesskey-randompage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-recentchanges&action=edit accesskey-recentchanges]<br>
+[[MediaWiki_talk:Accesskey-recentchanges|Talk]]
+</td><td>
+r
+</td><td>
+{{int:Accesskey-recentchanges}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-recentchangeslinked&action=edit accesskey-recentchangeslinked]<br>
+[[MediaWiki_talk:Accesskey-recentchangeslinked|Talk]]
+</td><td>
+c
+</td><td>
+{{int:Accesskey-recentchangeslinked}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-save&action=edit accesskey-save]<br>
+[[MediaWiki_talk:Accesskey-save|Talk]]
+</td><td>
+s
+</td><td>
+{{int:Accesskey-save}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-search&action=edit accesskey-search]<br>
+[[MediaWiki_talk:Accesskey-search|Talk]]
+</td><td>
+f
+</td><td>
+{{int:Accesskey-search}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-sitesupport&action=edit accesskey-sitesupport]<br>
+[[MediaWiki_talk:Accesskey-sitesupport|Talk]]
+</td><td>
+&amp;lt;accesskey-sitesupport&amp;gt;
+</td><td>
+{{int:Accesskey-sitesupport}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-specialpage&action=edit accesskey-specialpage]<br>
+[[MediaWiki_talk:Accesskey-specialpage|Talk]]
+</td><td>
+&amp;lt;accesskey-specialpage&amp;gt;
+</td><td>
+{{int:Accesskey-specialpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-specialpages&action=edit accesskey-specialpages]<br>
+[[MediaWiki_talk:Accesskey-specialpages|Talk]]
+</td><td>
+q
+</td><td>
+{{int:Accesskey-specialpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-talk&action=edit accesskey-talk]<br>
+[[MediaWiki_talk:Accesskey-talk|Talk]]
+</td><td>
+t
+</td><td>
+{{int:Accesskey-talk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-undelete&action=edit accesskey-undelete]<br>
+[[MediaWiki_talk:Accesskey-undelete|Talk]]
+</td><td>
+d
+</td><td>
+{{int:Accesskey-undelete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-unwatch&action=edit accesskey-unwatch]<br>
+[[MediaWiki_talk:Accesskey-unwatch|Talk]]
+</td><td>
+w
+</td><td>
+{{int:Accesskey-unwatch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-upload&action=edit accesskey-upload]<br>
+[[MediaWiki_talk:Accesskey-upload|Talk]]
+</td><td>
+u
+</td><td>
+{{int:Accesskey-upload}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-userpage&action=edit accesskey-userpage]<br>
+[[MediaWiki_talk:Accesskey-userpage|Talk]]
+</td><td>
+.
+</td><td>
+{{int:Accesskey-userpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-viewsource&action=edit accesskey-viewsource]<br>
+[[MediaWiki_talk:Accesskey-viewsource|Talk]]
+</td><td>
+e
+</td><td>
+{{int:Accesskey-viewsource}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-watch&action=edit accesskey-watch]<br>
+[[MediaWiki_talk:Accesskey-watch|Talk]]
+</td><td>
+w
+</td><td>
+{{int:Accesskey-watch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-watchlist&action=edit accesskey-watchlist]<br>
+[[MediaWiki_talk:Accesskey-watchlist|Talk]]
+</td><td>
+l
+</td><td>
+{{int:Accesskey-watchlist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accesskey-whatlinkshere&action=edit accesskey-whatlinkshere]<br>
+[[MediaWiki_talk:Accesskey-whatlinkshere|Talk]]
+</td><td>
+b
+</td><td>
+{{int:Accesskey-whatlinkshere}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accmailtext&action=edit accmailtext]<br>
+[[MediaWiki_talk:Accmailtext|Talk]]
+</td><td>
+The Password for &#39;$1&#39; has been sent to $2.
+</td><td>
+{{int:Accmailtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Accmailtitle&action=edit accmailtitle]<br>
+[[MediaWiki_talk:Accmailtitle|Talk]]
+</td><td>
+Password sent.
+</td><td>
+{{int:Accmailtitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Actioncomplete&action=edit actioncomplete]<br>
+[[MediaWiki_talk:Actioncomplete|Talk]]
+</td><td>
+Action complete
+</td><td>
+{{int:Actioncomplete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Addedwatch&action=edit addedwatch]<br>
+[[MediaWiki_talk:Addedwatch|Talk]]
+</td><td>
+Added to watchlist
+</td><td>
+{{int:Addedwatch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Addedwatchtext&action=edit addedwatchtext]<br>
+[[MediaWiki_talk:Addedwatchtext|Talk]]
+</td><td>
+The page &quot;$1&quot; has been added to your &#91;&#91;Special:Watchlist&#124;watchlist]].
+Future changes to this page and its associated Talk page will be listed there,
+and the page will appear &#39;&#39;&#39;bolded&#39;&#39;&#39; in the &#91;&#91;Special:Recentchanges&#124;list of recent changes]] to
+make it easier to pick out.
+
+&lt;p&gt;If you want to remove the page from your watchlist later, click &quot;Stop watching&quot; in the sidebar.
+</td><td>
+{{int:Addedwatchtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Addsection&action=edit addsection]<br>
+[[MediaWiki_talk:Addsection|Talk]]
+</td><td>
++
+</td><td>
+{{int:Addsection}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Administrators&action=edit administrators]<br>
+[[MediaWiki_talk:Administrators|Talk]]
+</td><td>
+Wiktionary:Administrators
+</td><td>
+{{int:Administrators}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Affirmation&action=edit affirmation]<br>
+[[MediaWiki_talk:Affirmation|Talk]]
+</td><td>
+I affirm that the copyright holder of this file
+agrees to license it under the terms of the $1.
+</td><td>
+{{int:Affirmation}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:All&action=edit all]<br>
+[[MediaWiki_talk:All|Talk]]
+</td><td>
+all
+</td><td>
+{{int:All}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Allmessages&action=edit allmessages]<br>
+[[MediaWiki_talk:Allmessages|Talk]]
+</td><td>
+All system messages
+</td><td>
+{{int:Allmessages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Allmessagestext&action=edit allmessagestext]<br>
+[[MediaWiki_talk:Allmessagestext|Talk]]
+</td><td>
+This is a list of all system messages available in the MediaWiki: namespace.
+</td><td>
+{{int:Allmessagestext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Allpages&action=edit allpages]<br>
+[[MediaWiki_talk:Allpages|Talk]]
+</td><td>
+All pages
+</td><td>
+{{int:Allpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Alphaindexline&action=edit alphaindexline]<br>
+[[MediaWiki_talk:Alphaindexline|Talk]]
+</td><td>
+$1 to $2
+</td><td>
+{{int:Alphaindexline}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Alreadyloggedin&action=edit alreadyloggedin]<br>
+[[MediaWiki_talk:Alreadyloggedin|Talk]]
+</td><td>
+&lt;font color=red&gt;&lt;b&gt;User $1, you are already logged in!&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;
+
+</td><td>
+{{int:Alreadyloggedin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Alreadyrolled&action=edit alreadyrolled]<br>
+[[MediaWiki_talk:Alreadyrolled|Talk]]
+</td><td>
+Cannot rollback last edit of &#91;&#91;$1]]
+by &#91;&#91;User:$2&#124;$2]] (&#91;&#91;User talk:$2&#124;Talk]]); someone else has edited or rolled back the page already.
+
+Last edit was by &#91;&#91;User:$3&#124;$3]] (&#91;&#91;User talk:$3&#124;Talk]]).
+</td><td>
+{{int:Alreadyrolled}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ancientpages&action=edit ancientpages]<br>
+[[MediaWiki_talk:Ancientpages|Talk]]
+</td><td>
+Oldest pages
+</td><td>
+{{int:Ancientpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:And&action=edit and]<br>
+[[MediaWiki_talk:And|Talk]]
+</td><td>
+and
+</td><td>
+{{int:And}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Anontalk&action=edit anontalk]<br>
+[[MediaWiki_talk:Anontalk|Talk]]
+</td><td>
+Talk for this IP
+</td><td>
+{{int:Anontalk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Anontalkpagetext&action=edit anontalkpagetext]<br>
+[[MediaWiki_talk:Anontalkpagetext|Talk]]
+</td><td>
+----&#39;&#39;This is the discussion page for an anonymous user who has not created an account yet or who does not use it. We therefore have to use the numerical &#91;&#91;IP address]] to identify him/her. Such an IP address can be shared by several users. If you are an anonymous user and feel that irrelevant comments have been directed at you, please &#91;&#91;Special:Userlogin&#124;create an account or log in]] to avoid future confusion with other anonymous users.&#39;&#39;
+</td><td>
+{{int:Anontalkpagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Anonymous&action=edit anonymous]<br>
+[[MediaWiki_talk:Anonymous|Talk]]
+</td><td>
+Anonymous user(s) of Wiktionary
+</td><td>
+{{int:Anonymous}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Article&action=edit article]<br>
+[[MediaWiki_talk:Article|Talk]]
+</td><td>
+Content page
+</td><td>
+{{int:Article}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Articleexists&action=edit articleexists]<br>
+[[MediaWiki_talk:Articleexists|Talk]]
+</td><td>
+A page of that name already exists, or the
+name you have chosen is not valid.
+Please choose another name.
+</td><td>
+{{int:Articleexists}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Articlepage&action=edit articlepage]<br>
+[[MediaWiki_talk:Articlepage|Talk]]
+</td><td>
+View content page
+</td><td>
+{{int:Articlepage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Asksql&action=edit asksql]<br>
+[[MediaWiki_talk:Asksql|Talk]]
+</td><td>
+SQL query
+</td><td>
+{{int:Asksql}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Asksqltext&action=edit asksqltext]<br>
+[[MediaWiki_talk:Asksqltext|Talk]]
+</td><td>
+Use the form below to make a direct query of the
+database.
+Use single quotes (&#39;like this&#39;) to delimit string literals.
+This can often add considerable load to the server, so please use
+this function sparingly.
+</td><td>
+{{int:Asksqltext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Autoblocker&action=edit autoblocker]<br>
+[[MediaWiki_talk:Autoblocker|Talk]]
+</td><td>
+Autoblocked because you share an IP address with &quot;$1&quot;. Reason &quot;$2&quot;.
+</td><td>
+{{int:Autoblocker}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badarticleerror&action=edit badarticleerror]<br>
+[[MediaWiki_talk:Badarticleerror|Talk]]
+</td><td>
+This action cannot be performed on this page.
+</td><td>
+{{int:Badarticleerror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badfilename&action=edit badfilename]<br>
+[[MediaWiki_talk:Badfilename|Talk]]
+</td><td>
+Image name has been changed to &quot;$1&quot;.
+</td><td>
+{{int:Badfilename}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badfiletype&action=edit badfiletype]<br>
+[[MediaWiki_talk:Badfiletype|Talk]]
+</td><td>
+&quot;.$1&quot; is not a recommended image file format.
+</td><td>
+{{int:Badfiletype}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badipaddress&action=edit badipaddress]<br>
+[[MediaWiki_talk:Badipaddress|Talk]]
+</td><td>
+Invalid IP address
+</td><td>
+{{int:Badipaddress}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badquery&action=edit badquery]<br>
+[[MediaWiki_talk:Badquery|Talk]]
+</td><td>
+Badly formed search query
+</td><td>
+{{int:Badquery}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badquerytext&action=edit badquerytext]<br>
+[[MediaWiki_talk:Badquerytext|Talk]]
+</td><td>
+We could not process your query.
+This is probably because you have attempted to search for a
+word fewer than three letters long, which is not yet supported.
+It could also be that you have mistyped the expression, for
+example &quot;fish and and scales&quot;.
+Please try another query.
+</td><td>
+{{int:Badquerytext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badretype&action=edit badretype]<br>
+[[MediaWiki_talk:Badretype|Talk]]
+</td><td>
+The passwords you entered do not match.
+</td><td>
+{{int:Badretype}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badtitle&action=edit badtitle]<br>
+[[MediaWiki_talk:Badtitle|Talk]]
+</td><td>
+Bad title
+</td><td>
+{{int:Badtitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Badtitletext&action=edit badtitletext]<br>
+[[MediaWiki_talk:Badtitletext|Talk]]
+</td><td>
+The requested page title was invalid, empty, or
+an incorrectly linked inter-language or inter-wiki title.
+</td><td>
+{{int:Badtitletext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blanknamespace&action=edit blanknamespace]<br>
+[[MediaWiki_talk:Blanknamespace|Talk]]
+</td><td>
+(Main)
+</td><td>
+{{int:Blanknamespace}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockedtext&action=edit blockedtext]<br>
+[[MediaWiki_talk:Blockedtext|Talk]]
+</td><td>
+Your user name or IP address has been blocked by $1.
+The reason given is this:&lt;br /&gt;&#39;&#39;$2&#39;&#39;&lt;p&gt;You may contact $1 or one of the other
+&#91;&#91;Wiktionary:Administrators&#124;administrators]] to discuss the block.
+
+Note that you may not use the &quot;email this user&quot; feature unless you have a valid email address registered in your &#91;&#91;Special:Preferences&#124;user preferences]].
+
+Your IP address is $3. Please include this address in any queries you make.
+
+</td><td>
+{{int:Blockedtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockedtitle&action=edit blockedtitle]<br>
+[[MediaWiki_talk:Blockedtitle|Talk]]
+</td><td>
+User is blocked
+</td><td>
+{{int:Blockedtitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockip&action=edit blockip]<br>
+[[MediaWiki_talk:Blockip|Talk]]
+</td><td>
+Block user
+</td><td>
+{{int:Blockip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockipsuccesssub&action=edit blockipsuccesssub]<br>
+[[MediaWiki_talk:Blockipsuccesssub|Talk]]
+</td><td>
+Block succeeded
+</td><td>
+{{int:Blockipsuccesssub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockipsuccesstext&action=edit blockipsuccesstext]<br>
+[[MediaWiki_talk:Blockipsuccesstext|Talk]]
+</td><td>
+&quot;$1&quot; has been blocked.
+&lt;br /&gt;See &#91;&#91;Special:Ipblocklist&#124;IP block list]] to review blocks.
+</td><td>
+{{int:Blockipsuccesstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blockiptext&action=edit blockiptext]<br>
+[[MediaWiki_talk:Blockiptext|Talk]]
+</td><td>
+Use the form below to block write access
+from a specific IP address or username.
+This should be done only only to prevent vandalism, and in
+accordance with &#91;&#91;Wiktionary:Policy&#124;policy]].
+Fill in a specific reason below (for example, citing particular
+pages that were vandalized).
+</td><td>
+{{int:Blockiptext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklink&action=edit blocklink]<br>
+[[MediaWiki_talk:Blocklink|Talk]]
+</td><td>
+block
+</td><td>
+{{int:Blocklink}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklistline&action=edit blocklistline]<br>
+[[MediaWiki_talk:Blocklistline|Talk]]
+</td><td>
+$1, $2 blocked $3 (expires $4)
+</td><td>
+{{int:Blocklistline}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklogentry&action=edit blocklogentry]<br>
+[[MediaWiki_talk:Blocklogentry|Talk]]
+</td><td>
+blocked &quot;$1&quot; with an expiry time of $2
+</td><td>
+{{int:Blocklogentry}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklogpage&action=edit blocklogpage]<br>
+[[MediaWiki_talk:Blocklogpage|Talk]]
+</td><td>
+Block_log
+</td><td>
+{{int:Blocklogpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Blocklogtext&action=edit blocklogtext]<br>
+[[MediaWiki_talk:Blocklogtext|Talk]]
+</td><td>
+This is a log of user blocking and unblocking actions. Automatically
+blocked IP addresses are not be listed. See the &#91;&#91;Special:Ipblocklist&#124;IP block list]] for
+the list of currently operational bans and blocks.
+</td><td>
+{{int:Blocklogtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bold_sample&action=edit bold_sample]<br>
+[[MediaWiki_talk:Bold_sample|Talk]]
+</td><td>
+Bold text
+</td><td>
+{{int:Bold_sample}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bold_tip&action=edit bold_tip]<br>
+[[MediaWiki_talk:Bold_tip|Talk]]
+</td><td>
+Bold text
+</td><td>
+{{int:Bold_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Booksources&action=edit booksources]<br>
+[[MediaWiki_talk:Booksources|Talk]]
+</td><td>
+Book sources
+</td><td>
+{{int:Booksources}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Booksourcetext&action=edit booksourcetext]<br>
+[[MediaWiki_talk:Booksourcetext|Talk]]
+</td><td>
+Below is a list of links to other sites that
+sell new and used books, and may also have further information
+about books you are looking for.Wiktionary is not affiliated with any of these businesses, and
+this list should not be construed as an endorsement.
+</td><td>
+{{int:Booksourcetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Brokenredirects&action=edit brokenredirects]<br>
+[[MediaWiki_talk:Brokenredirects|Talk]]
+</td><td>
+Broken Redirects
+</td><td>
+{{int:Brokenredirects}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Brokenredirectstext&action=edit brokenredirectstext]<br>
+[[MediaWiki_talk:Brokenredirectstext|Talk]]
+</td><td>
+The following redirects link to a non-existing pages.
+</td><td>
+{{int:Brokenredirectstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bugreports&action=edit bugreports]<br>
+[[MediaWiki_talk:Bugreports|Talk]]
+</td><td>
+Bug reports
+</td><td>
+{{int:Bugreports}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bugreportspage&action=edit bugreportspage]<br>
+[[MediaWiki_talk:Bugreportspage|Talk]]
+</td><td>
+Wiktionary:Bug_reports
+</td><td>
+{{int:Bugreportspage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bureaucratlog&action=edit bureaucratlog]<br>
+[[MediaWiki_talk:Bureaucratlog|Talk]]
+</td><td>
+Bureaucrat_log
+</td><td>
+{{int:Bureaucratlog}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bureaucratlogentry&action=edit bureaucratlogentry]<br>
+[[MediaWiki_talk:Bureaucratlogentry|Talk]]
+</td><td>
+Rights for user &quot;$1&quot; set &quot;$2&quot;
+</td><td>
+{{int:Bureaucratlogentry}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bureaucrattext&action=edit bureaucrattext]<br>
+[[MediaWiki_talk:Bureaucrattext|Talk]]
+</td><td>
+The action you have requested can only be
+performed by sysops with &quot;bureaucrat&quot; status.
+</td><td>
+{{int:Bureaucrattext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bureaucrattitle&action=edit bureaucrattitle]<br>
+[[MediaWiki_talk:Bureaucrattitle|Talk]]
+</td><td>
+Bureaucrat access required
+</td><td>
+{{int:Bureaucrattitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bydate&action=edit bydate]<br>
+[[MediaWiki_talk:Bydate|Talk]]
+</td><td>
+by date
+</td><td>
+{{int:Bydate}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Byname&action=edit byname]<br>
+[[MediaWiki_talk:Byname|Talk]]
+</td><td>
+by name
+</td><td>
+{{int:Byname}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Bysize&action=edit bysize]<br>
+[[MediaWiki_talk:Bysize|Talk]]
+</td><td>
+by size
+</td><td>
+{{int:Bysize}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cachederror&action=edit cachederror]<br>
+[[MediaWiki_talk:Cachederror|Talk]]
+</td><td>
+The following is a cached copy of the requested page, and may not be up to date.
+</td><td>
+{{int:Cachederror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cancel&action=edit cancel]<br>
+[[MediaWiki_talk:Cancel|Talk]]
+</td><td>
+Cancel
+</td><td>
+{{int:Cancel}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cannotdelete&action=edit cannotdelete]<br>
+[[MediaWiki_talk:Cannotdelete|Talk]]
+</td><td>
+Could not delete the page or image specified. (It may have already been deleted by someone else.)
+</td><td>
+{{int:Cannotdelete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cantrollback&action=edit cantrollback]<br>
+[[MediaWiki_talk:Cantrollback|Talk]]
+</td><td>
+Cannot revert edit; last contributor is only author of this page.
+</td><td>
+{{int:Cantrollback}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Categories&action=edit categories]<br>
+[[MediaWiki_talk:Categories|Talk]]
+</td><td>
+Categories
+</td><td>
+{{int:Categories}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Category&action=edit category]<br>
+[[MediaWiki_talk:Category|Talk]]
+</td><td>
+category
+</td><td>
+{{int:Category}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Category_header&action=edit category_header]<br>
+[[MediaWiki_talk:Category_header|Talk]]
+</td><td>
+Articles in category &quot;$1&quot;
+</td><td>
+{{int:Category_header}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Changepassword&action=edit changepassword]<br>
+[[MediaWiki_talk:Changepassword|Talk]]
+</td><td>
+Change password
+</td><td>
+{{int:Changepassword}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Changes&action=edit changes]<br>
+[[MediaWiki_talk:Changes|Talk]]
+</td><td>
+changes
+</td><td>
+{{int:Changes}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Columns&action=edit columns]<br>
+[[MediaWiki_talk:Columns|Talk]]
+</td><td>
+Columns
+</td><td>
+{{int:Columns}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Commentedit&action=edit commentedit]<br>
+[[MediaWiki_talk:Commentedit|Talk]]
+</td><td>
+ (comment)
+</td><td>
+{{int:Commentedit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Compareselectedversions&action=edit compareselectedversions]<br>
+[[MediaWiki_talk:Compareselectedversions|Talk]]
+</td><td>
+Compare selected versions
+</td><td>
+{{int:Compareselectedversions}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirm&action=edit confirm]<br>
+[[MediaWiki_talk:Confirm|Talk]]
+</td><td>
+Confirm
+</td><td>
+{{int:Confirm}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmcheck&action=edit confirmcheck]<br>
+[[MediaWiki_talk:Confirmcheck|Talk]]
+</td><td>
+Yes, I really want to delete this.
+</td><td>
+{{int:Confirmcheck}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmdelete&action=edit confirmdelete]<br>
+[[MediaWiki_talk:Confirmdelete|Talk]]
+</td><td>
+Confirm delete
+</td><td>
+{{int:Confirmdelete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmdeletetext&action=edit confirmdeletetext]<br>
+[[MediaWiki_talk:Confirmdeletetext|Talk]]
+</td><td>
+You are about to permanently delete a page
+or image along with all of its history from the database.
+Please confirm that you intend to do this, that you understand the
+consequences, and that you are doing this in accordance with
+&#91;&#91;Wiktionary:Policy]].
+</td><td>
+{{int:Confirmdeletetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmprotect&action=edit confirmprotect]<br>
+[[MediaWiki_talk:Confirmprotect|Talk]]
+</td><td>
+Confirm protection
+</td><td>
+{{int:Confirmprotect}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmprotecttext&action=edit confirmprotecttext]<br>
+[[MediaWiki_talk:Confirmprotecttext|Talk]]
+</td><td>
+Do you really want to protect this page?
+</td><td>
+{{int:Confirmprotecttext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmunprotect&action=edit confirmunprotect]<br>
+[[MediaWiki_talk:Confirmunprotect|Talk]]
+</td><td>
+Confirm unprotection
+</td><td>
+{{int:Confirmunprotect}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Confirmunprotecttext&action=edit confirmunprotecttext]<br>
+[[MediaWiki_talk:Confirmunprotecttext|Talk]]
+</td><td>
+Do you really want to unprotect this page?
+</td><td>
+{{int:Confirmunprotecttext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contextchars&action=edit contextchars]<br>
+[[MediaWiki_talk:Contextchars|Talk]]
+</td><td>
+Characters of context per line
+</td><td>
+{{int:Contextchars}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contextlines&action=edit contextlines]<br>
+[[MediaWiki_talk:Contextlines|Talk]]
+</td><td>
+Lines to show per hit
+</td><td>
+{{int:Contextlines}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contribslink&action=edit contribslink]<br>
+[[MediaWiki_talk:Contribslink|Talk]]
+</td><td>
+contribs
+</td><td>
+{{int:Contribslink}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contribsub&action=edit contribsub]<br>
+[[MediaWiki_talk:Contribsub|Talk]]
+</td><td>
+For $1
+</td><td>
+{{int:Contribsub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Contributions&action=edit contributions]<br>
+[[MediaWiki_talk:Contributions|Talk]]
+</td><td>
+User contributions
+</td><td>
+{{int:Contributions}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Copyright&action=edit copyright]<br>
+[[MediaWiki_talk:Copyright|Talk]]
+</td><td>
+Content is available under $1.
+</td><td>
+{{int:Copyright}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Copyrightpage&action=edit copyrightpage]<br>
+[[MediaWiki_talk:Copyrightpage|Talk]]
+</td><td>
+Wiktionary:Copyrights
+</td><td>
+{{int:Copyrightpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Copyrightpagename&action=edit copyrightpagename]<br>
+[[MediaWiki_talk:Copyrightpagename|Talk]]
+</td><td>
+Wiktionary copyright
+</td><td>
+{{int:Copyrightpagename}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Copyrightwarning&action=edit copyrightwarning]<br>
+[[MediaWiki_talk:Copyrightwarning|Talk]]
+</td><td>
+Please note that all contributions to Wiktionary are
+considered to be released under the GNU Free Documentation License
+(see $1 for details).
+If you don&#39;t want your writing to be edited mercilessly and redistributed
+at will, then don&#39;t submit it here.&lt;br /&gt;
+You are also promising us that you wrote this yourself, or copied it from a
+public domain or similar free resource.
+&lt;strong&gt;DO NOT SUBMIT COPYRIGHTED WORK WITHOUT PERMISSION!&lt;/strong&gt;
+</td><td>
+{{int:Copyrightwarning}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Couldntremove&action=edit couldntremove]<br>
+[[MediaWiki_talk:Couldntremove|Talk]]
+</td><td>
+Couldn&#39;t remove item &#39;$1&#39;...
+</td><td>
+{{int:Couldntremove}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Createaccount&action=edit createaccount]<br>
+[[MediaWiki_talk:Createaccount|Talk]]
+</td><td>
+Create new account
+</td><td>
+{{int:Createaccount}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Createaccountmail&action=edit createaccountmail]<br>
+[[MediaWiki_talk:Createaccountmail|Talk]]
+</td><td>
+by email
+</td><td>
+{{int:Createaccountmail}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Cur&action=edit cur]<br>
+[[MediaWiki_talk:Cur|Talk]]
+</td><td>
+cur
+</td><td>
+{{int:Cur}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Currentevents&action=edit currentevents]<br>
+[[MediaWiki_talk:Currentevents|Talk]]
+</td><td>
+Current events
+</td><td>
+{{int:Currentevents}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Currentrev&action=edit currentrev]<br>
+[[MediaWiki_talk:Currentrev|Talk]]
+</td><td>
+Current revision
+</td><td>
+{{int:Currentrev}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Databaseerror&action=edit databaseerror]<br>
+[[MediaWiki_talk:Databaseerror|Talk]]
+</td><td>
+Database error
+</td><td>
+{{int:Databaseerror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dateformat&action=edit dateformat]<br>
+[[MediaWiki_talk:Dateformat|Talk]]
+</td><td>
+Date format
+</td><td>
+{{int:Dateformat}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dberrortext&action=edit dberrortext]<br>
+[[MediaWiki_talk:Dberrortext|Talk]]
+</td><td>
+A database query syntax error has occurred.
+This could be because of an illegal search query (see $5),
+or it may indicate a bug in the software.
+The last attempted database query was:
+&lt;blockquote&gt;&lt;tt&gt;$1&lt;/tt&gt;&lt;/blockquote&gt;
+from within function &quot;&lt;tt&gt;$2&lt;/tt&gt;&quot;.
+MySQL returned error &quot;&lt;tt&gt;$3: $4&lt;/tt&gt;&quot;.
+</td><td>
+{{int:Dberrortext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dberrortextcl&action=edit dberrortextcl]<br>
+[[MediaWiki_talk:Dberrortextcl|Talk]]
+</td><td>
+A database query syntax error has occurred.
+The last attempted database query was:
+&quot;$1&quot;
+from within function &quot;$2&quot;.
+MySQL returned error &quot;$3: $4&quot;.
+
+</td><td>
+{{int:Dberrortextcl}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deadendpages&action=edit deadendpages]<br>
+[[MediaWiki_talk:Deadendpages|Talk]]
+</td><td>
+Dead-end pages
+</td><td>
+{{int:Deadendpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Debug&action=edit debug]<br>
+[[MediaWiki_talk:Debug|Talk]]
+</td><td>
+Debug
+</td><td>
+{{int:Debug}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Defaultns&action=edit defaultns]<br>
+[[MediaWiki_talk:Defaultns|Talk]]
+</td><td>
+Search in these namespaces by default:
+</td><td>
+{{int:Defaultns}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Defemailsubject&action=edit defemailsubject]<br>
+[[MediaWiki_talk:Defemailsubject|Talk]]
+</td><td>
+Wiktionary e-mail
+</td><td>
+{{int:Defemailsubject}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Delete&action=edit delete]<br>
+[[MediaWiki_talk:Delete|Talk]]
+</td><td>
+Delete
+</td><td>
+{{int:Delete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletecomment&action=edit deletecomment]<br>
+[[MediaWiki_talk:Deletecomment|Talk]]
+</td><td>
+Reason for deletion
+</td><td>
+{{int:Deletecomment}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletedarticle&action=edit deletedarticle]<br>
+[[MediaWiki_talk:Deletedarticle|Talk]]
+</td><td>
+deleted &quot;$1&quot;
+</td><td>
+{{int:Deletedarticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletedtext&action=edit deletedtext]<br>
+[[MediaWiki_talk:Deletedtext|Talk]]
+</td><td>
+&quot;$1&quot; has been deleted.
+See $2 for a record of recent deletions.
+</td><td>
+{{int:Deletedtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deleteimg&action=edit deleteimg]<br>
+[[MediaWiki_talk:Deleteimg|Talk]]
+</td><td>
+del
+</td><td>
+{{int:Deleteimg}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletepage&action=edit deletepage]<br>
+[[MediaWiki_talk:Deletepage|Talk]]
+</td><td>
+Delete page
+</td><td>
+{{int:Deletepage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletesub&action=edit deletesub]<br>
+[[MediaWiki_talk:Deletesub|Talk]]
+</td><td>
+(Deleting &quot;$1&quot;)
+</td><td>
+{{int:Deletesub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletethispage&action=edit deletethispage]<br>
+[[MediaWiki_talk:Deletethispage|Talk]]
+</td><td>
+Delete this page
+</td><td>
+{{int:Deletethispage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Deletionlog&action=edit deletionlog]<br>
+[[MediaWiki_talk:Deletionlog|Talk]]
+</td><td>
+deletion log
+</td><td>
+{{int:Deletionlog}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dellogpage&action=edit dellogpage]<br>
+[[MediaWiki_talk:Dellogpage|Talk]]
+</td><td>
+Deletion_log
+</td><td>
+{{int:Dellogpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Dellogpagetext&action=edit dellogpagetext]<br>
+[[MediaWiki_talk:Dellogpagetext|Talk]]
+</td><td>
+Below is a list of the most recent deletions.
+All times shown are server time (UTC).
+&lt;ul&gt;
+&lt;/ul&gt;
+
+</td><td>
+{{int:Dellogpagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Developerspheading&action=edit developerspheading]<br>
+[[MediaWiki_talk:Developerspheading|Talk]]
+</td><td>
+For developer use only
+</td><td>
+{{int:Developerspheading}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Developertext&action=edit developertext]<br>
+[[MediaWiki_talk:Developertext|Talk]]
+</td><td>
+The action you have requested can only be
+performed by users with &quot;developer&quot; status.
+See $1.
+</td><td>
+{{int:Developertext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Developertitle&action=edit developertitle]<br>
+[[MediaWiki_talk:Developertitle|Talk]]
+</td><td>
+Developer access required
+</td><td>
+{{int:Developertitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Diff&action=edit diff]<br>
+[[MediaWiki_talk:Diff|Talk]]
+</td><td>
+diff
+</td><td>
+{{int:Diff}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Difference&action=edit difference]<br>
+[[MediaWiki_talk:Difference|Talk]]
+</td><td>
+(Difference between revisions)
+</td><td>
+{{int:Difference}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Disclaimerpage&action=edit disclaimerpage]<br>
+[[MediaWiki_talk:Disclaimerpage|Talk]]
+</td><td>
+Wiktionary:General_disclaimer
+</td><td>
+{{int:Disclaimerpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Disclaimers&action=edit disclaimers]<br>
+[[MediaWiki_talk:Disclaimers|Talk]]
+</td><td>
+Disclaimers
+</td><td>
+{{int:Disclaimers}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Doubleredirects&action=edit doubleredirects]<br>
+[[MediaWiki_talk:Doubleredirects|Talk]]
+</td><td>
+Double Redirects
+</td><td>
+{{int:Doubleredirects}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Doubleredirectstext&action=edit doubleredirectstext]<br>
+[[MediaWiki_talk:Doubleredirectstext|Talk]]
+</td><td>
+&lt;b&gt;Attention:&lt;/b&gt; This list may contain false positives. That usually means there is additional text with links below the first #REDIRECT.&lt;br /&gt;
+Each row contains links to the first and second redirect, as well as the first line of the second redirect text, usually giving the &quot;real&quot; target page, which the first redirect should point to.
+</td><td>
+{{int:Doubleredirectstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Edit&action=edit edit]<br>
+[[MediaWiki_talk:Edit|Talk]]
+</td><td>
+Edit
+</td><td>
+{{int:Edit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editcomment&action=edit editcomment]<br>
+[[MediaWiki_talk:Editcomment|Talk]]
+</td><td>
+The edit comment was: &quot;&lt;i&gt;$1&lt;/i&gt;&quot;.
+</td><td>
+{{int:Editcomment}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editconflict&action=edit editconflict]<br>
+[[MediaWiki_talk:Editconflict|Talk]]
+</td><td>
+Edit conflict: $1
+</td><td>
+{{int:Editconflict}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editcurrent&action=edit editcurrent]<br>
+[[MediaWiki_talk:Editcurrent|Talk]]
+</td><td>
+Edit the current version of this page
+</td><td>
+{{int:Editcurrent}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Edithelp&action=edit edithelp]<br>
+[[MediaWiki_talk:Edithelp|Talk]]
+</td><td>
+Editing help
+</td><td>
+{{int:Edithelp}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Edithelppage&action=edit edithelppage]<br>
+[[MediaWiki_talk:Edithelppage|Talk]]
+</td><td>
+Help:Editing
+</td><td>
+{{int:Edithelppage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editing&action=edit editing]<br>
+[[MediaWiki_talk:Editing|Talk]]
+</td><td>
+Editing $1
+</td><td>
+{{int:Editing}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editingold&action=edit editingold]<br>
+[[MediaWiki_talk:Editingold|Talk]]
+</td><td>
+&lt;strong&gt;WARNING: You are editing an out-of-date
+revision of this page.
+If you save it, any changes made since this revision will be lost.&lt;/strong&gt;
+
+</td><td>
+{{int:Editingold}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editsection&action=edit editsection]<br>
+[[MediaWiki_talk:Editsection|Talk]]
+</td><td>
+edit
+</td><td>
+{{int:Editsection}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Editthispage&action=edit editthispage]<br>
+[[MediaWiki_talk:Editthispage|Talk]]
+</td><td>
+Edit this page
+</td><td>
+{{int:Editthispage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailflag&action=edit emailflag]<br>
+[[MediaWiki_talk:Emailflag|Talk]]
+</td><td>
+Disable e-mail from other users
+</td><td>
+{{int:Emailflag}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailforlost&action=edit emailforlost]<br>
+[[MediaWiki_talk:Emailforlost|Talk]]
+</td><td>
+Fields marked with a star (*) are optional. Storing an email address enables people to contact you through the website without you having to reveal your
+email address to them, and it can be used to send you a new password if you forget it.&lt;br /&gt;&lt;br /&gt;Your real name, if you choose to provide it, will be used for giving you attribution for your work.
+</td><td>
+{{int:Emailforlost}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailfrom&action=edit emailfrom]<br>
+[[MediaWiki_talk:Emailfrom|Talk]]
+</td><td>
+From
+</td><td>
+{{int:Emailfrom}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailmessage&action=edit emailmessage]<br>
+[[MediaWiki_talk:Emailmessage|Talk]]
+</td><td>
+Message
+</td><td>
+{{int:Emailmessage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailpage&action=edit emailpage]<br>
+[[MediaWiki_talk:Emailpage|Talk]]
+</td><td>
+E-mail user
+</td><td>
+{{int:Emailpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailpagetext&action=edit emailpagetext]<br>
+[[MediaWiki_talk:Emailpagetext|Talk]]
+</td><td>
+If this user has entered a valid e-mail address in
+his or her user preferences, the form below will send a single message.
+The e-mail address you entered in your user preferences will appear
+as the &quot;From&quot; address of the mail, so the recipient will be able
+to reply.
+</td><td>
+{{int:Emailpagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailsend&action=edit emailsend]<br>
+[[MediaWiki_talk:Emailsend|Talk]]
+</td><td>
+Send
+</td><td>
+{{int:Emailsend}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailsent&action=edit emailsent]<br>
+[[MediaWiki_talk:Emailsent|Talk]]
+</td><td>
+E-mail sent
+</td><td>
+{{int:Emailsent}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailsenttext&action=edit emailsenttext]<br>
+[[MediaWiki_talk:Emailsenttext|Talk]]
+</td><td>
+Your e-mail message has been sent.
+</td><td>
+{{int:Emailsenttext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailsubject&action=edit emailsubject]<br>
+[[MediaWiki_talk:Emailsubject|Talk]]
+</td><td>
+Subject
+</td><td>
+{{int:Emailsubject}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailto&action=edit emailto]<br>
+[[MediaWiki_talk:Emailto|Talk]]
+</td><td>
+To
+</td><td>
+{{int:Emailto}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Emailuser&action=edit emailuser]<br>
+[[MediaWiki_talk:Emailuser|Talk]]
+</td><td>
+E-mail this user
+</td><td>
+{{int:Emailuser}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Enterlockreason&action=edit enterlockreason]<br>
+[[MediaWiki_talk:Enterlockreason|Talk]]
+</td><td>
+Enter a reason for the lock, including an estimate
+of when the lock will be released
+</td><td>
+{{int:Enterlockreason}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Error&action=edit error]<br>
+[[MediaWiki_talk:Error|Talk]]
+</td><td>
+Error
+</td><td>
+{{int:Error}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Errorpagetitle&action=edit errorpagetitle]<br>
+[[MediaWiki_talk:Errorpagetitle|Talk]]
+</td><td>
+Error
+</td><td>
+{{int:Errorpagetitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Exbeforeblank&action=edit exbeforeblank]<br>
+[[MediaWiki_talk:Exbeforeblank|Talk]]
+</td><td>
+content before blanking was:
+</td><td>
+{{int:Exbeforeblank}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Exblank&action=edit exblank]<br>
+[[MediaWiki_talk:Exblank|Talk]]
+</td><td>
+page was empty
+</td><td>
+{{int:Exblank}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Excontent&action=edit excontent]<br>
+[[MediaWiki_talk:Excontent|Talk]]
+</td><td>
+content was:
+</td><td>
+{{int:Excontent}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Explainconflict&action=edit explainconflict]<br>
+[[MediaWiki_talk:Explainconflict|Talk]]
+</td><td>
+Someone else has changed this page since you
+started editing it.
+The upper text area contains the page text as it currently exists.
+Your changes are shown in the lower text area.
+You will have to merge your changes into the existing text.
+&lt;b&gt;Only&lt;/b&gt; the text in the upper text area will be saved when you
+press &quot;Save page&quot;.
+&lt;p&gt;
+</td><td>
+{{int:Explainconflict}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Export&action=edit export]<br>
+[[MediaWiki_talk:Export|Talk]]
+</td><td>
+Export pages
+</td><td>
+{{int:Export}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Exportcuronly&action=edit exportcuronly]<br>
+[[MediaWiki_talk:Exportcuronly|Talk]]
+</td><td>
+Include only the current revision, not the full history
+</td><td>
+{{int:Exportcuronly}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Exporttext&action=edit exporttext]<br>
+[[MediaWiki_talk:Exporttext|Talk]]
+</td><td>
+You can export the text and editing history of a particular
+page or set of pages wrapped in some XML; this can then be imported into another
+wiki running MediaWiki software, transformed, or just kept for your private
+amusement.
+</td><td>
+{{int:Exporttext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Extlink_sample&action=edit extlink_sample]<br>
+[[MediaWiki_talk:Extlink_sample|Talk]]
+</td><td>
+http&#58;//www.example.com link title
+</td><td>
+{{int:Extlink_sample}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Extlink_tip&action=edit extlink_tip]<br>
+[[MediaWiki_talk:Extlink_tip|Talk]]
+</td><td>
+External link (remember http&#58;// prefix)
+</td><td>
+{{int:Extlink_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Faq&action=edit faq]<br>
+[[MediaWiki_talk:Faq|Talk]]
+</td><td>
+FAQ
+</td><td>
+{{int:Faq}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Faqpage&action=edit faqpage]<br>
+[[MediaWiki_talk:Faqpage|Talk]]
+</td><td>
+Wiktionary:FAQ
+</td><td>
+{{int:Faqpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Feedlinks&action=edit feedlinks]<br>
+[[MediaWiki_talk:Feedlinks|Talk]]
+</td><td>
+Feed:
+</td><td>
+{{int:Feedlinks}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filecopyerror&action=edit filecopyerror]<br>
+[[MediaWiki_talk:Filecopyerror|Talk]]
+</td><td>
+Could not copy file &quot;$1&quot; to &quot;$2&quot;.
+</td><td>
+{{int:Filecopyerror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filedeleteerror&action=edit filedeleteerror]<br>
+[[MediaWiki_talk:Filedeleteerror|Talk]]
+</td><td>
+Could not delete file &quot;$1&quot;.
+</td><td>
+{{int:Filedeleteerror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filedesc&action=edit filedesc]<br>
+[[MediaWiki_talk:Filedesc|Talk]]
+</td><td>
+Summary
+</td><td>
+{{int:Filedesc}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filename&action=edit filename]<br>
+[[MediaWiki_talk:Filename|Talk]]
+</td><td>
+Filename
+</td><td>
+{{int:Filename}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filenotfound&action=edit filenotfound]<br>
+[[MediaWiki_talk:Filenotfound|Talk]]
+</td><td>
+Could not find file &quot;$1&quot;.
+</td><td>
+{{int:Filenotfound}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filerenameerror&action=edit filerenameerror]<br>
+[[MediaWiki_talk:Filerenameerror|Talk]]
+</td><td>
+Could not rename file &quot;$1&quot; to &quot;$2&quot;.
+</td><td>
+{{int:Filerenameerror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filesource&action=edit filesource]<br>
+[[MediaWiki_talk:Filesource|Talk]]
+</td><td>
+Source
+</td><td>
+{{int:Filesource}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Filestatus&action=edit filestatus]<br>
+[[MediaWiki_talk:Filestatus|Talk]]
+</td><td>
+Copyright status
+</td><td>
+{{int:Filestatus}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Fileuploaded&action=edit fileuploaded]<br>
+[[MediaWiki_talk:Fileuploaded|Talk]]
+</td><td>
+File &quot;$1&quot; uploaded successfully.
+Please follow this link: $2 to the description page and fill
+in information about the file, such as where it came from, when it was
+created and by whom, and anything else you may know about it.
+</td><td>
+{{int:Fileuploaded}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Formerror&action=edit formerror]<br>
+[[MediaWiki_talk:Formerror|Talk]]
+</td><td>
+Error: could not submit form
+</td><td>
+{{int:Formerror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Fromwikipedia&action=edit fromwikipedia]<br>
+[[MediaWiki_talk:Fromwikipedia|Talk]]
+</td><td>
+From Wiktionary
+</td><td>
+{{int:Fromwikipedia}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Getimagelist&action=edit getimagelist]<br>
+[[MediaWiki_talk:Getimagelist|Talk]]
+</td><td>
+fetching image list
+</td><td>
+{{int:Getimagelist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Go&action=edit go]<br>
+[[MediaWiki_talk:Go|Talk]]
+</td><td>
+Go
+</td><td>
+{{int:Go}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Googlesearch&action=edit googlesearch]<br>
+[[MediaWiki_talk:Googlesearch|Talk]]
+</td><td>
+
+&lt;!-- SiteSearch Google --&gt;
+&lt;FORM method=GET action=&quot;http&#58;//www.google.com/search&quot;&gt;
+&lt;TABLE bgcolor=&quot;#FFFFFF&quot;&gt;&lt;tr&gt;&lt;td&gt;
+&lt;A HREF=&quot;http&#58;//www.google.com/&quot;&gt;
+&lt;IMG SRC=&quot;http&#58;//www.google.com/logos/Logo_40wht.gif&quot;
+border=&quot;0&quot; ALT=&quot;Google&quot;&gt;&lt;/A&gt;
+&lt;/td&gt;
+&lt;td&gt;
+&lt;INPUT TYPE=text name=q size=31 maxlength=255 value=&quot;$1&quot;&gt;
+&lt;INPUT type=submit name=btnG VALUE=&quot;Google Search&quot;&gt;
+&lt;font size=-1&gt;
+&lt;input type=hidden name=domains value=&quot;http&#58;//tl.wiktionary.org&quot;&gt;&lt;br /&gt;&lt;input type=radio name=sitesearch value=&quot;&quot;&gt; WWW &lt;input type=radio name=sitesearch value=&quot;http&#58;//tl.wiktionary.org&quot; checked&gt; http&#58;//tl.wiktionary.org &lt;br /&gt;
+&lt;input type=&#39;hidden&#39; name=&#39;ie&#39; value=&#39;$2&#39;&gt;
+&lt;input type=&#39;hidden&#39; name=&#39;oe&#39; value=&#39;$2&#39;&gt;
+&lt;/font&gt;
+&lt;/td&gt;&lt;/tr&gt;&lt;/TABLE&gt;
+&lt;/FORM&gt;
+&lt;!-- SiteSearch Google --&gt;
+</td><td>
+{{int:Googlesearch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Guesstimezone&action=edit guesstimezone]<br>
+[[MediaWiki_talk:Guesstimezone|Talk]]
+</td><td>
+Fill in from browser
+</td><td>
+{{int:Guesstimezone}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Headline_sample&action=edit headline_sample]<br>
+[[MediaWiki_talk:Headline_sample|Talk]]
+</td><td>
+Headline text
+</td><td>
+{{int:Headline_sample}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Headline_tip&action=edit headline_tip]<br>
+[[MediaWiki_talk:Headline_tip|Talk]]
+</td><td>
+Level 2 headline
+</td><td>
+{{int:Headline_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Help&action=edit help]<br>
+[[MediaWiki_talk:Help|Talk]]
+</td><td>
+Help
+</td><td>
+{{int:Help}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Helppage&action=edit helppage]<br>
+[[MediaWiki_talk:Helppage|Talk]]
+</td><td>
+Help:Contents
+</td><td>
+{{int:Helppage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Hide&action=edit hide]<br>
+[[MediaWiki_talk:Hide|Talk]]
+</td><td>
+hide
+</td><td>
+{{int:Hide}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Hidetoc&action=edit hidetoc]<br>
+[[MediaWiki_talk:Hidetoc|Talk]]
+</td><td>
+hide
+</td><td>
+{{int:Hidetoc}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Hist&action=edit hist]<br>
+[[MediaWiki_talk:Hist|Talk]]
+</td><td>
+hist
+</td><td>
+{{int:Hist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Histlegend&action=edit histlegend]<br>
+[[MediaWiki_talk:Histlegend|Talk]]
+</td><td>
+Diff selection: mark the radio boxes of the versions to compare and hit enter or the button at the bottom.&lt;br/&gt;
+Legend: (cur) = difference with current version,
+(last) = difference with preceding version, M = minor edit.
+</td><td>
+{{int:Histlegend}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:History&action=edit history]<br>
+[[MediaWiki_talk:History|Talk]]
+</td><td>
+Page history
+</td><td>
+{{int:History}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:History_short&action=edit history_short]<br>
+[[MediaWiki_talk:History_short|Talk]]
+</td><td>
+History
+</td><td>
+{{int:History_short}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Historywarning&action=edit historywarning]<br>
+[[MediaWiki_talk:Historywarning|Talk]]
+</td><td>
+Warning: The page you are about to delete has a history:
+</td><td>
+{{int:Historywarning}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Hr_tip&action=edit hr_tip]<br>
+[[MediaWiki_talk:Hr_tip|Talk]]
+</td><td>
+Horizontal line (use sparingly)
+</td><td>
+{{int:Hr_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ignorewarning&action=edit ignorewarning]<br>
+[[MediaWiki_talk:Ignorewarning|Talk]]
+</td><td>
+Ignore warning and save file anyway.
+</td><td>
+{{int:Ignorewarning}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ilshowmatch&action=edit ilshowmatch]<br>
+[[MediaWiki_talk:Ilshowmatch|Talk]]
+</td><td>
+Show all images with names matching
+</td><td>
+{{int:Ilshowmatch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ilsubmit&action=edit ilsubmit]<br>
+[[MediaWiki_talk:Ilsubmit|Talk]]
+</td><td>
+Search
+</td><td>
+{{int:Ilsubmit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Image_sample&action=edit image_sample]<br>
+[[MediaWiki_talk:Image_sample|Talk]]
+</td><td>
+Example.jpg
+</td><td>
+{{int:Image_sample}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Image_tip&action=edit image_tip]<br>
+[[MediaWiki_talk:Image_tip|Talk]]
+</td><td>
+Embedded image
+</td><td>
+{{int:Image_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagelinks&action=edit imagelinks]<br>
+[[MediaWiki_talk:Imagelinks|Talk]]
+</td><td>
+Image links
+</td><td>
+{{int:Imagelinks}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagelist&action=edit imagelist]<br>
+[[MediaWiki_talk:Imagelist|Talk]]
+</td><td>
+Image list
+</td><td>
+{{int:Imagelist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagelisttext&action=edit imagelisttext]<br>
+[[MediaWiki_talk:Imagelisttext|Talk]]
+</td><td>
+Below is a list of $1 images sorted $2.
+</td><td>
+{{int:Imagelisttext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagepage&action=edit imagepage]<br>
+[[MediaWiki_talk:Imagepage|Talk]]
+</td><td>
+View image page
+</td><td>
+{{int:Imagepage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imagereverted&action=edit imagereverted]<br>
+[[MediaWiki_talk:Imagereverted|Talk]]
+</td><td>
+Revert to earlier version was successful.
+</td><td>
+{{int:Imagereverted}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imgdelete&action=edit imgdelete]<br>
+[[MediaWiki_talk:Imgdelete|Talk]]
+</td><td>
+del
+</td><td>
+{{int:Imgdelete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imgdesc&action=edit imgdesc]<br>
+[[MediaWiki_talk:Imgdesc|Talk]]
+</td><td>
+desc
+</td><td>
+{{int:Imgdesc}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imghistlegend&action=edit imghistlegend]<br>
+[[MediaWiki_talk:Imghistlegend|Talk]]
+</td><td>
+Legend: (cur) = this is the current image, (del) = delete
+this old version, (rev) = revert to this old version.
+&lt;br /&gt;&lt;i&gt;Click on date to see image uploaded on that date&lt;/i&gt;.
+</td><td>
+{{int:Imghistlegend}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imghistory&action=edit imghistory]<br>
+[[MediaWiki_talk:Imghistory|Talk]]
+</td><td>
+Image history
+</td><td>
+{{int:Imghistory}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Imglegend&action=edit imglegend]<br>
+[[MediaWiki_talk:Imglegend|Talk]]
+</td><td>
+Legend: (desc) = show/edit image description.
+</td><td>
+{{int:Imglegend}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Import&action=edit import]<br>
+[[MediaWiki_talk:Import|Talk]]
+</td><td>
+Import pages
+</td><td>
+{{int:Import}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importfailed&action=edit importfailed]<br>
+[[MediaWiki_talk:Importfailed|Talk]]
+</td><td>
+Import failed: $1
+</td><td>
+{{int:Importfailed}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importhistoryconflict&action=edit importhistoryconflict]<br>
+[[MediaWiki_talk:Importhistoryconflict|Talk]]
+</td><td>
+Conflicting history revision exists (may have imported this page before)
+</td><td>
+{{int:Importhistoryconflict}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importnotext&action=edit importnotext]<br>
+[[MediaWiki_talk:Importnotext|Talk]]
+</td><td>
+Empty or no text
+</td><td>
+{{int:Importnotext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importsuccess&action=edit importsuccess]<br>
+[[MediaWiki_talk:Importsuccess|Talk]]
+</td><td>
+Import succeeded!
+</td><td>
+{{int:Importsuccess}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Importtext&action=edit importtext]<br>
+[[MediaWiki_talk:Importtext|Talk]]
+</td><td>
+Please export the file from the source wiki using the Special:Export utility, save it to your disk and upload it here.
+</td><td>
+{{int:Importtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Infobox&action=edit infobox]<br>
+[[MediaWiki_talk:Infobox|Talk]]
+</td><td>
+Click a button to get an example text
+</td><td>
+{{int:Infobox}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Infobox_alert&action=edit infobox_alert]<br>
+[[MediaWiki_talk:Infobox_alert|Talk]]
+</td><td>
+Please enter the text you want to be formatted.\n It will be shown in the infobox for copy and pasting.\nExample:\n$1\nwill become:\n$2
+</td><td>
+{{int:Infobox_alert}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Internalerror&action=edit internalerror]<br>
+[[MediaWiki_talk:Internalerror|Talk]]
+</td><td>
+Internal error
+</td><td>
+{{int:Internalerror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Intl&action=edit intl]<br>
+[[MediaWiki_talk:Intl|Talk]]
+</td><td>
+Interlanguage links
+</td><td>
+{{int:Intl}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ip_range_invalid&action=edit ip_range_invalid]<br>
+[[MediaWiki_talk:Ip_range_invalid|Talk]]
+</td><td>
+Invalid IP range.
+
+</td><td>
+{{int:Ip_range_invalid}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipaddress&action=edit ipaddress]<br>
+[[MediaWiki_talk:Ipaddress|Talk]]
+</td><td>
+IP Address/username
+</td><td>
+{{int:Ipaddress}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipb_expiry_invalid&action=edit ipb_expiry_invalid]<br>
+[[MediaWiki_talk:Ipb_expiry_invalid|Talk]]
+</td><td>
+Expiry time invalid.
+</td><td>
+{{int:Ipb_expiry_invalid}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipbexpiry&action=edit ipbexpiry]<br>
+[[MediaWiki_talk:Ipbexpiry|Talk]]
+</td><td>
+Expiry
+</td><td>
+{{int:Ipbexpiry}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipblocklist&action=edit ipblocklist]<br>
+[[MediaWiki_talk:Ipblocklist|Talk]]
+</td><td>
+List of blocked IP addresses and usernames
+</td><td>
+{{int:Ipblocklist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipbreason&action=edit ipbreason]<br>
+[[MediaWiki_talk:Ipbreason|Talk]]
+</td><td>
+Reason
+</td><td>
+{{int:Ipbreason}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipbsubmit&action=edit ipbsubmit]<br>
+[[MediaWiki_talk:Ipbsubmit|Talk]]
+</td><td>
+Block this user
+</td><td>
+{{int:Ipbsubmit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipusubmit&action=edit ipusubmit]<br>
+[[MediaWiki_talk:Ipusubmit|Talk]]
+</td><td>
+Unblock this address
+</td><td>
+{{int:Ipusubmit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ipusuccess&action=edit ipusuccess]<br>
+[[MediaWiki_talk:Ipusuccess|Talk]]
+</td><td>
+&quot;$1&quot; unblocked
+</td><td>
+{{int:Ipusuccess}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Isbn&action=edit isbn]<br>
+[[MediaWiki_talk:Isbn|Talk]]
+</td><td>
+ISBN
+</td><td>
+{{int:Isbn}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Isredirect&action=edit isredirect]<br>
+[[MediaWiki_talk:Isredirect|Talk]]
+</td><td>
+redirect page
+</td><td>
+{{int:Isredirect}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Italic_sample&action=edit italic_sample]<br>
+[[MediaWiki_talk:Italic_sample|Talk]]
+</td><td>
+Italic text
+</td><td>
+{{int:Italic_sample}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Italic_tip&action=edit italic_tip]<br>
+[[MediaWiki_talk:Italic_tip|Talk]]
+</td><td>
+Italic text
+</td><td>
+{{int:Italic_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Iteminvalidname&action=edit iteminvalidname]<br>
+[[MediaWiki_talk:Iteminvalidname|Talk]]
+</td><td>
+Problem with item &#39;$1&#39;, invalid name...
+</td><td>
+{{int:Iteminvalidname}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Largefile&action=edit largefile]<br>
+[[MediaWiki_talk:Largefile|Talk]]
+</td><td>
+It is recommended that images not exceed 100k in size.
+</td><td>
+{{int:Largefile}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Last&action=edit last]<br>
+[[MediaWiki_talk:Last|Talk]]
+</td><td>
+last
+</td><td>
+{{int:Last}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lastmodified&action=edit lastmodified]<br>
+[[MediaWiki_talk:Lastmodified|Talk]]
+</td><td>
+This page was last modified $1.
+</td><td>
+{{int:Lastmodified}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lastmodifiedby&action=edit lastmodifiedby]<br>
+[[MediaWiki_talk:Lastmodifiedby|Talk]]
+</td><td>
+This page was last modified $1 by $2.
+</td><td>
+{{int:Lastmodifiedby}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lineno&action=edit lineno]<br>
+[[MediaWiki_talk:Lineno|Talk]]
+</td><td>
+Line $1:
+</td><td>
+{{int:Lineno}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Link_sample&action=edit link_sample]<br>
+[[MediaWiki_talk:Link_sample|Talk]]
+</td><td>
+Link title
+</td><td>
+{{int:Link_sample}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Link_tip&action=edit link_tip]<br>
+[[MediaWiki_talk:Link_tip|Talk]]
+</td><td>
+Internal link
+</td><td>
+{{int:Link_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Linklistsub&action=edit linklistsub]<br>
+[[MediaWiki_talk:Linklistsub|Talk]]
+</td><td>
+(List of links)
+</td><td>
+{{int:Linklistsub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Linkshere&action=edit linkshere]<br>
+[[MediaWiki_talk:Linkshere|Talk]]
+</td><td>
+The following pages link to here:
+</td><td>
+{{int:Linkshere}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Linkstoimage&action=edit linkstoimage]<br>
+[[MediaWiki_talk:Linkstoimage|Talk]]
+</td><td>
+The following pages link to this image:
+</td><td>
+{{int:Linkstoimage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Linktrail&action=edit linktrail]<br>
+[[MediaWiki_talk:Linktrail|Talk]]
+</td><td>
+/^(&#91;a-z]+)(.*)$/sD
+</td><td>
+{{int:Linktrail}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Listform&action=edit listform]<br>
+[[MediaWiki_talk:Listform|Talk]]
+</td><td>
+list
+</td><td>
+{{int:Listform}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Listusers&action=edit listusers]<br>
+[[MediaWiki_talk:Listusers|Talk]]
+</td><td>
+User list
+</td><td>
+{{int:Listusers}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loadhist&action=edit loadhist]<br>
+[[MediaWiki_talk:Loadhist|Talk]]
+</td><td>
+Loading page history
+</td><td>
+{{int:Loadhist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loadingrev&action=edit loadingrev]<br>
+[[MediaWiki_talk:Loadingrev|Talk]]
+</td><td>
+loading revision for diff
+</td><td>
+{{int:Loadingrev}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Localtime&action=edit localtime]<br>
+[[MediaWiki_talk:Localtime|Talk]]
+</td><td>
+Local time display
+</td><td>
+{{int:Localtime}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockbtn&action=edit lockbtn]<br>
+[[MediaWiki_talk:Lockbtn|Talk]]
+</td><td>
+Lock database
+</td><td>
+{{int:Lockbtn}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockconfirm&action=edit lockconfirm]<br>
+[[MediaWiki_talk:Lockconfirm|Talk]]
+</td><td>
+Yes, I really want to lock the database.
+</td><td>
+{{int:Lockconfirm}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockdb&action=edit lockdb]<br>
+[[MediaWiki_talk:Lockdb|Talk]]
+</td><td>
+Lock database
+</td><td>
+{{int:Lockdb}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockdbsuccesssub&action=edit lockdbsuccesssub]<br>
+[[MediaWiki_talk:Lockdbsuccesssub|Talk]]
+</td><td>
+Database lock succeeded
+</td><td>
+{{int:Lockdbsuccesssub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockdbsuccesstext&action=edit lockdbsuccesstext]<br>
+[[MediaWiki_talk:Lockdbsuccesstext|Talk]]
+</td><td>
+The database has been locked.
+&lt;br /&gt;Remember to remove the lock after your maintenance is complete.
+</td><td>
+{{int:Lockdbsuccesstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lockdbtext&action=edit lockdbtext]<br>
+[[MediaWiki_talk:Lockdbtext|Talk]]
+</td><td>
+Locking the database will suspend the ability of all
+users to edit pages, change their preferences, edit their watchlists, and
+other things requiring changes in the database.
+Please confirm that this is what you intend to do, and that you will
+unlock the database when your maintenance is done.
+</td><td>
+{{int:Lockdbtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Locknoconfirm&action=edit locknoconfirm]<br>
+[[MediaWiki_talk:Locknoconfirm|Talk]]
+</td><td>
+You did not check the confirmation box.
+</td><td>
+{{int:Locknoconfirm}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Login&action=edit login]<br>
+[[MediaWiki_talk:Login|Talk]]
+</td><td>
+Log in
+</td><td>
+{{int:Login}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginend&action=edit loginend]<br>
+[[MediaWiki_talk:Loginend|Talk]]
+</td><td>
+&amp;nbsp;
+</td><td>
+{{int:Loginend}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginerror&action=edit loginerror]<br>
+[[MediaWiki_talk:Loginerror|Talk]]
+</td><td>
+Login error
+</td><td>
+{{int:Loginerror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginpagetitle&action=edit loginpagetitle]<br>
+[[MediaWiki_talk:Loginpagetitle|Talk]]
+</td><td>
+User login
+</td><td>
+{{int:Loginpagetitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginproblem&action=edit loginproblem]<br>
+[[MediaWiki_talk:Loginproblem|Talk]]
+</td><td>
+&lt;b&gt;There has been a problem with your login.&lt;/b&gt;&lt;br /&gt;Try again!
+</td><td>
+{{int:Loginproblem}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginprompt&action=edit loginprompt]<br>
+[[MediaWiki_talk:Loginprompt|Talk]]
+</td><td>
+You must have cookies enabled to log in to Wiktionary.
+</td><td>
+{{int:Loginprompt}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginreqtext&action=edit loginreqtext]<br>
+[[MediaWiki_talk:Loginreqtext|Talk]]
+</td><td>
+You must &#91;&#91;special:Userlogin&#124;login]] to view other pages.
+</td><td>
+{{int:Loginreqtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginreqtitle&action=edit loginreqtitle]<br>
+[[MediaWiki_talk:Loginreqtitle|Talk]]
+</td><td>
+Login Required
+</td><td>
+{{int:Loginreqtitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginsuccess&action=edit loginsuccess]<br>
+[[MediaWiki_talk:Loginsuccess|Talk]]
+</td><td>
+You are now logged in to Wiktionary as &quot;$1&quot;.
+</td><td>
+{{int:Loginsuccess}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Loginsuccesstitle&action=edit loginsuccesstitle]<br>
+[[MediaWiki_talk:Loginsuccesstitle|Talk]]
+</td><td>
+Login successful
+</td><td>
+{{int:Loginsuccesstitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Logout&action=edit logout]<br>
+[[MediaWiki_talk:Logout|Talk]]
+</td><td>
+Log out
+</td><td>
+{{int:Logout}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Logouttext&action=edit logouttext]<br>
+[[MediaWiki_talk:Logouttext|Talk]]
+</td><td>
+You are now logged out.
+You can continue to use Wiktionary anonymously, or you can log in
+again as the same or as a different user. Note that some pages may
+continue to be displayed as if you were still logged in, until you clear
+your browser cache
+
+</td><td>
+{{int:Logouttext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Logouttitle&action=edit logouttitle]<br>
+[[MediaWiki_talk:Logouttitle|Talk]]
+</td><td>
+User logout
+</td><td>
+{{int:Logouttitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Lonelypages&action=edit lonelypages]<br>
+[[MediaWiki_talk:Lonelypages|Talk]]
+</td><td>
+Orphaned pages
+</td><td>
+{{int:Lonelypages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Longpages&action=edit longpages]<br>
+[[MediaWiki_talk:Longpages|Talk]]
+</td><td>
+Long pages
+</td><td>
+{{int:Longpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Longpagewarning&action=edit longpagewarning]<br>
+[[MediaWiki_talk:Longpagewarning|Talk]]
+</td><td>
+WARNING: This page is $1 kilobytes long; some
+browsers may have problems editing pages approaching or longer than 32kb.
+Please consider breaking the page into smaller sections.
+</td><td>
+{{int:Longpagewarning}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mailerror&action=edit mailerror]<br>
+[[MediaWiki_talk:Mailerror|Talk]]
+</td><td>
+Error sending mail: $1
+</td><td>
+{{int:Mailerror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mailmypassword&action=edit mailmypassword]<br>
+[[MediaWiki_talk:Mailmypassword|Talk]]
+</td><td>
+Mail me a new password
+</td><td>
+{{int:Mailmypassword}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mailnologin&action=edit mailnologin]<br>
+[[MediaWiki_talk:Mailnologin|Talk]]
+</td><td>
+No send address
+</td><td>
+{{int:Mailnologin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mailnologintext&action=edit mailnologintext]<br>
+[[MediaWiki_talk:Mailnologintext|Talk]]
+</td><td>
+You must be &lt;a href=&quot;{{localurl:Special:Userlogin&quot;&gt;logged in&lt;/a&gt;
+and have a valid e-mail address in your &lt;a href=&quot;/wiki/Special:Preferences&quot;&gt;preferences&lt;/a&gt;
+to send e-mail to other users.
+</td><td>
+{{int:Mailnologintext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mainpage&action=edit mainpage]<br>
+[[MediaWiki_talk:Mainpage|Talk]]
+</td><td>
+Main Page
+</td><td>
+{{int:Mainpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mainpagedocfooter&action=edit mainpagedocfooter]<br>
+[[MediaWiki_talk:Mainpagedocfooter|Talk]]
+</td><td>
+Please see &#91;http&#58;//meta.wikipedia.org/wiki/MediaWiki_i18n documentation on customizing the interface]
+and the &#91;http&#58;//meta.wikipedia.org/wiki/MediaWiki_User%27s_Guide User&#39;s Guide] for usage and configuration help.
+</td><td>
+{{int:Mainpagedocfooter}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mainpagetext&action=edit mainpagetext]<br>
+[[MediaWiki_talk:Mainpagetext|Talk]]
+</td><td>
+Wiki software successfully installed.
+</td><td>
+{{int:Mainpagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Maintenance&action=edit maintenance]<br>
+[[MediaWiki_talk:Maintenance|Talk]]
+</td><td>
+Maintenance page
+</td><td>
+{{int:Maintenance}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Maintenancebacklink&action=edit maintenancebacklink]<br>
+[[MediaWiki_talk:Maintenancebacklink|Talk]]
+</td><td>
+Back to Maintenance Page
+</td><td>
+{{int:Maintenancebacklink}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Maintnancepagetext&action=edit maintnancepagetext]<br>
+[[MediaWiki_talk:Maintnancepagetext|Talk]]
+</td><td>
+This page includes several handy tools for everyday maintenance. Some of these functions tend to stress the database, so please do not hit reload after every item you fixed ;-)
+</td><td>
+{{int:Maintnancepagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysop&action=edit makesysop]<br>
+[[MediaWiki_talk:Makesysop|Talk]]
+</td><td>
+Make a user into a sysop
+</td><td>
+{{int:Makesysop}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysopfail&action=edit makesysopfail]<br>
+[[MediaWiki_talk:Makesysopfail|Talk]]
+</td><td>
+&lt;b&gt;User &quot;$1&quot; could not be made into a sysop. (Did you enter the name correctly?)&lt;/b&gt;
+</td><td>
+{{int:Makesysopfail}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysopname&action=edit makesysopname]<br>
+[[MediaWiki_talk:Makesysopname|Talk]]
+</td><td>
+Name of the user:
+</td><td>
+{{int:Makesysopname}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysopok&action=edit makesysopok]<br>
+[[MediaWiki_talk:Makesysopok|Talk]]
+</td><td>
+&lt;b&gt;User &quot;$1&quot; is now a sysop&lt;/b&gt;
+</td><td>
+{{int:Makesysopok}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysopsubmit&action=edit makesysopsubmit]<br>
+[[MediaWiki_talk:Makesysopsubmit|Talk]]
+</td><td>
+Make this user into a sysop
+</td><td>
+{{int:Makesysopsubmit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysoptext&action=edit makesysoptext]<br>
+[[MediaWiki_talk:Makesysoptext|Talk]]
+</td><td>
+This form is used by bureaucrats to turn ordinary users into administrators.
+Type the name of the user in the box and press the button to make the user an administrator
+</td><td>
+{{int:Makesysoptext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Makesysoptitle&action=edit makesysoptitle]<br>
+[[MediaWiki_talk:Makesysoptitle|Talk]]
+</td><td>
+Make a user into a sysop
+</td><td>
+{{int:Makesysoptitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Matchtotals&action=edit matchtotals]<br>
+[[MediaWiki_talk:Matchtotals|Talk]]
+</td><td>
+The query &quot;$1&quot; matched $2 page titles
+and the text of $3 pages.
+</td><td>
+{{int:Matchtotals}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math&action=edit math]<br>
+[[MediaWiki_talk:Math|Talk]]
+</td><td>
+Rendering math
+</td><td>
+{{int:Math}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_bad_output&action=edit math_bad_output]<br>
+[[MediaWiki_talk:Math_bad_output|Talk]]
+</td><td>
+Can&#39;t write to or create math output directory
+</td><td>
+{{int:Math_bad_output}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_bad_tmpdir&action=edit math_bad_tmpdir]<br>
+[[MediaWiki_talk:Math_bad_tmpdir|Talk]]
+</td><td>
+Can&#39;t write to or create math temp directory
+</td><td>
+{{int:Math_bad_tmpdir}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_failure&action=edit math_failure]<br>
+[[MediaWiki_talk:Math_failure|Talk]]
+</td><td>
+Failed to parse
+</td><td>
+{{int:Math_failure}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_image_error&action=edit math_image_error]<br>
+[[MediaWiki_talk:Math_image_error|Talk]]
+</td><td>
+PNG conversion failed; check for correct installation of latex, dvips, gs, and convert
+</td><td>
+{{int:Math_image_error}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_lexing_error&action=edit math_lexing_error]<br>
+[[MediaWiki_talk:Math_lexing_error|Talk]]
+</td><td>
+lexing error
+</td><td>
+{{int:Math_lexing_error}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_notexvc&action=edit math_notexvc]<br>
+[[MediaWiki_talk:Math_notexvc|Talk]]
+</td><td>
+Missing texvc executable; please see math/README to configure.
+</td><td>
+{{int:Math_notexvc}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_sample&action=edit math_sample]<br>
+[[MediaWiki_talk:Math_sample|Talk]]
+</td><td>
+Insert formula here
+</td><td>
+{{int:Math_sample}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_syntax_error&action=edit math_syntax_error]<br>
+[[MediaWiki_talk:Math_syntax_error|Talk]]
+</td><td>
+syntax error
+</td><td>
+{{int:Math_syntax_error}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_tip&action=edit math_tip]<br>
+[[MediaWiki_talk:Math_tip|Talk]]
+</td><td>
+Mathematical formula (LaTeX)
+</td><td>
+{{int:Math_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_unknown_error&action=edit math_unknown_error]<br>
+[[MediaWiki_talk:Math_unknown_error|Talk]]
+</td><td>
+unknown error
+</td><td>
+{{int:Math_unknown_error}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Math_unknown_function&action=edit math_unknown_function]<br>
+[[MediaWiki_talk:Math_unknown_function|Talk]]
+</td><td>
+unknown function
+</td><td>
+{{int:Math_unknown_function}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Media_sample&action=edit media_sample]<br>
+[[MediaWiki_talk:Media_sample|Talk]]
+</td><td>
+Example.mp3
+</td><td>
+{{int:Media_sample}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Media_tip&action=edit media_tip]<br>
+[[MediaWiki_talk:Media_tip|Talk]]
+</td><td>
+Media file link
+</td><td>
+{{int:Media_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Minlength&action=edit minlength]<br>
+[[MediaWiki_talk:Minlength|Talk]]
+</td><td>
+Image names must be at least three letters.
+</td><td>
+{{int:Minlength}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Minoredit&action=edit minoredit]<br>
+[[MediaWiki_talk:Minoredit|Talk]]
+</td><td>
+This is a minor edit
+</td><td>
+{{int:Minoredit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Minoreditletter&action=edit minoreditletter]<br>
+[[MediaWiki_talk:Minoreditletter|Talk]]
+</td><td>
+M
+</td><td>
+{{int:Minoreditletter}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mispeelings&action=edit mispeelings]<br>
+[[MediaWiki_talk:Mispeelings|Talk]]
+</td><td>
+Pages with misspellings
+</td><td>
+{{int:Mispeelings}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mispeelingspage&action=edit mispeelingspage]<br>
+[[MediaWiki_talk:Mispeelingspage|Talk]]
+</td><td>
+List of common misspellings
+</td><td>
+{{int:Mispeelingspage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mispeelingstext&action=edit mispeelingstext]<br>
+[[MediaWiki_talk:Mispeelingstext|Talk]]
+</td><td>
+The following pages contain a common misspelling, which are listed on $1. The correct spelling might be given (like this).
+</td><td>
+{{int:Mispeelingstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missingarticle&action=edit missingarticle]<br>
+[[MediaWiki_talk:Missingarticle|Talk]]
+</td><td>
+The database did not find the text of a page
+that it should have found, named &quot;$1&quot;.
+
+&lt;p&gt;This is usually caused by following an outdated diff or history link to a
+page that has been deleted.
+
+&lt;p&gt;If this is not the case, you may have found a bug in the software.
+Please report this to an administrator, making note of the URL.
+</td><td>
+{{int:Missingarticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missingimage&action=edit missingimage]<br>
+[[MediaWiki_talk:Missingimage|Talk]]
+</td><td>
+&lt;b&gt;Missing image&lt;/b&gt;&lt;br /&gt;&lt;i&gt;$1&lt;/i&gt;
+
+</td><td>
+{{int:Missingimage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missinglanguagelinks&action=edit missinglanguagelinks]<br>
+[[MediaWiki_talk:Missinglanguagelinks|Talk]]
+</td><td>
+Missing Language Links
+</td><td>
+{{int:Missinglanguagelinks}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missinglanguagelinksbutton&action=edit missinglanguagelinksbutton]<br>
+[[MediaWiki_talk:Missinglanguagelinksbutton|Talk]]
+</td><td>
+Find missing language links for
+</td><td>
+{{int:Missinglanguagelinksbutton}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Missinglanguagelinkstext&action=edit missinglanguagelinkstext]<br>
+[[MediaWiki_talk:Missinglanguagelinkstext|Talk]]
+</td><td>
+These pages do &lt;i&gt;not&lt;/i&gt; link to their counterpart in $1. Redirects and subpages are &lt;i&gt;not&lt;/i&gt; shown.
+</td><td>
+{{int:Missinglanguagelinkstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Moredotdotdot&action=edit moredotdotdot]<br>
+[[MediaWiki_talk:Moredotdotdot|Talk]]
+</td><td>
+More...
+</td><td>
+{{int:Moredotdotdot}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Move&action=edit move]<br>
+[[MediaWiki_talk:Move|Talk]]
+</td><td>
+Move
+</td><td>
+{{int:Move}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movearticle&action=edit movearticle]<br>
+[[MediaWiki_talk:Movearticle|Talk]]
+</td><td>
+Move page
+</td><td>
+{{int:Movearticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movedto&action=edit movedto]<br>
+[[MediaWiki_talk:Movedto|Talk]]
+</td><td>
+moved to
+</td><td>
+{{int:Movedto}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movenologin&action=edit movenologin]<br>
+[[MediaWiki_talk:Movenologin|Talk]]
+</td><td>
+Not logged in
+</td><td>
+{{int:Movenologin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movenologintext&action=edit movenologintext]<br>
+[[MediaWiki_talk:Movenologintext|Talk]]
+</td><td>
+You must be a registered user and &lt;a href=&quot;/wiki/Special:Userlogin&quot;&gt;logged in&lt;/a&gt;
+to move a page.
+</td><td>
+{{int:Movenologintext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movepage&action=edit movepage]<br>
+[[MediaWiki_talk:Movepage|Talk]]
+</td><td>
+Move page
+</td><td>
+{{int:Movepage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movepagebtn&action=edit movepagebtn]<br>
+[[MediaWiki_talk:Movepagebtn|Talk]]
+</td><td>
+Move page
+</td><td>
+{{int:Movepagebtn}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movepagetalktext&action=edit movepagetalktext]<br>
+[[MediaWiki_talk:Movepagetalktext|Talk]]
+</td><td>
+The associated talk page, if any, will be automatically moved along with it &#39;&#39;&#39;unless:&#39;&#39;&#39;
+*You are moving the page across namespaces,
+*A non-empty talk page already exists under the new name, or
+*You uncheck the box below.
+
+In those cases, you will have to move or merge the page manually if desired.
+</td><td>
+{{int:Movepagetalktext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movepagetext&action=edit movepagetext]<br>
+[[MediaWiki_talk:Movepagetext|Talk]]
+</td><td>
+Using the form below will rename a page, moving all
+of its history to the new name.
+The old title will become a redirect page to the new title.
+Links to the old page title will not be changed; be sure to
+&#91;&#91;Special:Maintenance&#124;check]] for double or broken redirects.
+You are responsible for making sure that links continue to
+point where they are supposed to go.
+
+Note that the page will &#39;&#39;&#39;not&#39;&#39;&#39; be moved if there is already
+a page at the new title, unless it is empty or a redirect and has no
+past edit history. This means that you can rename a page back to where
+it was just renamed from if you make a mistake, and you cannot overwrite
+an existing page.
+
+&lt;b&gt;WARNING!&lt;/b&gt;
+This can be a drastic and unexpected change for a popular page;
+please be sure you understand the consequences of this before
+proceeding.
+</td><td>
+{{int:Movepagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movetalk&action=edit movetalk]<br>
+[[MediaWiki_talk:Movetalk|Talk]]
+</td><td>
+Move &quot;talk&quot; page as well, if applicable.
+</td><td>
+{{int:Movetalk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Movethispage&action=edit movethispage]<br>
+[[MediaWiki_talk:Movethispage|Talk]]
+</td><td>
+Move this page
+</td><td>
+{{int:Movethispage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mycontris&action=edit mycontris]<br>
+[[MediaWiki_talk:Mycontris|Talk]]
+</td><td>
+My contributions
+</td><td>
+{{int:Mycontris}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mypage&action=edit mypage]<br>
+[[MediaWiki_talk:Mypage|Talk]]
+</td><td>
+My page
+</td><td>
+{{int:Mypage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Mytalk&action=edit mytalk]<br>
+[[MediaWiki_talk:Mytalk|Talk]]
+</td><td>
+My talk
+</td><td>
+{{int:Mytalk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Navigation&action=edit navigation]<br>
+[[MediaWiki_talk:Navigation|Talk]]
+</td><td>
+Navigation
+</td><td>
+{{int:Navigation}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nbytes&action=edit nbytes]<br>
+[[MediaWiki_talk:Nbytes|Talk]]
+</td><td>
+$1 bytes
+</td><td>
+{{int:Nbytes}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nchanges&action=edit nchanges]<br>
+[[MediaWiki_talk:Nchanges|Talk]]
+</td><td>
+$1 changes
+</td><td>
+{{int:Nchanges}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newarticle&action=edit newarticle]<br>
+[[MediaWiki_talk:Newarticle|Talk]]
+</td><td>
+(New)
+</td><td>
+{{int:Newarticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newarticletext&action=edit newarticletext]<br>
+[[MediaWiki_talk:Newarticletext|Talk]]
+</td><td>
+You&#39;ve followed a link to a page that doesn&#39;t exist yet.
+To create the page, start typing in the box below
+(see the &#91;&#91;Wiktionary:Help&#124;help page]] for more info).
+If you are here by mistake, just click your browser&#39;s &#39;&#39;&#39;back&#39;&#39;&#39; button.
+</td><td>
+{{int:Newarticletext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newmessages&action=edit newmessages]<br>
+[[MediaWiki_talk:Newmessages|Talk]]
+</td><td>
+You have $1.
+</td><td>
+{{int:Newmessages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newmessageslink&action=edit newmessageslink]<br>
+[[MediaWiki_talk:Newmessageslink|Talk]]
+</td><td>
+new messages
+</td><td>
+{{int:Newmessageslink}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newpage&action=edit newpage]<br>
+[[MediaWiki_talk:Newpage|Talk]]
+</td><td>
+New page
+</td><td>
+{{int:Newpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newpageletter&action=edit newpageletter]<br>
+[[MediaWiki_talk:Newpageletter|Talk]]
+</td><td>
+N
+</td><td>
+{{int:Newpageletter}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newpages&action=edit newpages]<br>
+[[MediaWiki_talk:Newpages|Talk]]
+</td><td>
+New pages
+</td><td>
+{{int:Newpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newpassword&action=edit newpassword]<br>
+[[MediaWiki_talk:Newpassword|Talk]]
+</td><td>
+New password
+</td><td>
+{{int:Newpassword}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newtitle&action=edit newtitle]<br>
+[[MediaWiki_talk:Newtitle|Talk]]
+</td><td>
+To new title
+</td><td>
+{{int:Newtitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Newusersonly&action=edit newusersonly]<br>
+[[MediaWiki_talk:Newusersonly|Talk]]
+</td><td>
+ (new users only)
+</td><td>
+{{int:Newusersonly}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Next&action=edit next]<br>
+[[MediaWiki_talk:Next|Talk]]
+</td><td>
+next
+</td><td>
+{{int:Next}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nextn&action=edit nextn]<br>
+[[MediaWiki_talk:Nextn|Talk]]
+</td><td>
+next $1
+</td><td>
+{{int:Nextn}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nlinks&action=edit nlinks]<br>
+[[MediaWiki_talk:Nlinks|Talk]]
+</td><td>
+$1 links
+</td><td>
+{{int:Nlinks}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noaffirmation&action=edit noaffirmation]<br>
+[[MediaWiki_talk:Noaffirmation|Talk]]
+</td><td>
+You must affirm that your upload does not violate
+any copyrights.
+</td><td>
+{{int:Noaffirmation}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noarticletext&action=edit noarticletext]<br>
+[[MediaWiki_talk:Noarticletext|Talk]]
+</td><td>
+(There is currently no text in this page)
+</td><td>
+{{int:Noarticletext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noblockreason&action=edit noblockreason]<br>
+[[MediaWiki_talk:Noblockreason|Talk]]
+</td><td>
+You must supply a reason for the block.
+</td><td>
+{{int:Noblockreason}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noconnect&action=edit noconnect]<br>
+[[MediaWiki_talk:Noconnect|Talk]]
+</td><td>
+Sorry! The wiki is experiencing some technical difficulties, and cannot contact the database server.
+</td><td>
+{{int:Noconnect}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nocontribs&action=edit nocontribs]<br>
+[[MediaWiki_talk:Nocontribs|Talk]]
+</td><td>
+No changes were found matching these criteria.
+</td><td>
+{{int:Nocontribs}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nocookieslogin&action=edit nocookieslogin]<br>
+[[MediaWiki_talk:Nocookieslogin|Talk]]
+</td><td>
+Wiktionary uses cookies to log in users. You have cookies disabled. Please enable them and try again.
+</td><td>
+{{int:Nocookieslogin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nocookiesnew&action=edit nocookiesnew]<br>
+[[MediaWiki_talk:Nocookiesnew|Talk]]
+</td><td>
+The user account was created, but you are not logged in. Wiktionary uses cookies to log in users. You have cookies disabled. Please enable them, then log in with your new username and password.
+</td><td>
+{{int:Nocookiesnew}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nocreativecommons&action=edit nocreativecommons]<br>
+[[MediaWiki_talk:Nocreativecommons|Talk]]
+</td><td>
+Creative Commons RDF metadata disabled for this server.
+</td><td>
+{{int:Nocreativecommons}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nodb&action=edit nodb]<br>
+[[MediaWiki_talk:Nodb|Talk]]
+</td><td>
+Could not select database $1
+</td><td>
+{{int:Nodb}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nodublincore&action=edit nodublincore]<br>
+[[MediaWiki_talk:Nodublincore|Talk]]
+</td><td>
+Dublin Core RDF metadata disabled for this server.
+</td><td>
+{{int:Nodublincore}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noemail&action=edit noemail]<br>
+[[MediaWiki_talk:Noemail|Talk]]
+</td><td>
+There is no e-mail address recorded for user &quot;$1&quot;.
+</td><td>
+{{int:Noemail}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noemailtext&action=edit noemailtext]<br>
+[[MediaWiki_talk:Noemailtext|Talk]]
+</td><td>
+This user has not specified a valid e-mail address,
+or has chosen not to receive e-mail from other users.
+</td><td>
+{{int:Noemailtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noemailtitle&action=edit noemailtitle]<br>
+[[MediaWiki_talk:Noemailtitle|Talk]]
+</td><td>
+No e-mail address
+</td><td>
+{{int:Noemailtitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nogomatch&action=edit nogomatch]<br>
+[[MediaWiki_talk:Nogomatch|Talk]]
+</td><td>
+No page with this exact title exists, trying full text search.
+</td><td>
+{{int:Nogomatch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nohistory&action=edit nohistory]<br>
+[[MediaWiki_talk:Nohistory|Talk]]
+</td><td>
+There is no edit history for this page.
+</td><td>
+{{int:Nohistory}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nolinkshere&action=edit nolinkshere]<br>
+[[MediaWiki_talk:Nolinkshere|Talk]]
+</td><td>
+No pages link to here.
+</td><td>
+{{int:Nolinkshere}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nolinkstoimage&action=edit nolinkstoimage]<br>
+[[MediaWiki_talk:Nolinkstoimage|Talk]]
+</td><td>
+There are no pages that link to this image.
+</td><td>
+{{int:Nolinkstoimage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Noname&action=edit noname]<br>
+[[MediaWiki_talk:Noname|Talk]]
+</td><td>
+You have not specified a valid user name.
+</td><td>
+{{int:Noname}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nonefound&action=edit nonefound]<br>
+[[MediaWiki_talk:Nonefound|Talk]]
+</td><td>
+&lt;strong&gt;Note&lt;/strong&gt;: unsuccessful searches are
+often caused by searching for common words like &quot;have&quot; and &quot;from&quot;,
+which are not indexed, or by specifying more than one search term (only pages
+containing all of the search terms will appear in the result).
+</td><td>
+{{int:Nonefound}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nospecialpagetext&action=edit nospecialpagetext]<br>
+[[MediaWiki_talk:Nospecialpagetext|Talk]]
+</td><td>
+You have requested a special page that is not
+recognized by the wiki.
+</td><td>
+{{int:Nospecialpagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nosuchaction&action=edit nosuchaction]<br>
+[[MediaWiki_talk:Nosuchaction|Talk]]
+</td><td>
+No such action
+</td><td>
+{{int:Nosuchaction}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nosuchactiontext&action=edit nosuchactiontext]<br>
+[[MediaWiki_talk:Nosuchactiontext|Talk]]
+</td><td>
+The action specified by the URL is not
+recognized by the wiki
+</td><td>
+{{int:Nosuchactiontext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nosuchspecialpage&action=edit nosuchspecialpage]<br>
+[[MediaWiki_talk:Nosuchspecialpage|Talk]]
+</td><td>
+No such special page
+</td><td>
+{{int:Nosuchspecialpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nosuchuser&action=edit nosuchuser]<br>
+[[MediaWiki_talk:Nosuchuser|Talk]]
+</td><td>
+There is no user by the name &quot;$1&quot;.
+Check your spelling, or use the form below to create a new user account.
+</td><td>
+{{int:Nosuchuser}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notacceptable&action=edit notacceptable]<br>
+[[MediaWiki_talk:Notacceptable|Talk]]
+</td><td>
+The wiki server can&#39;t provide data in a format your client can read.
+</td><td>
+{{int:Notacceptable}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notanarticle&action=edit notanarticle]<br>
+[[MediaWiki_talk:Notanarticle|Talk]]
+</td><td>
+Not a content page
+</td><td>
+{{int:Notanarticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notargettext&action=edit notargettext]<br>
+[[MediaWiki_talk:Notargettext|Talk]]
+</td><td>
+You have not specified a target page or user
+to perform this function on.
+</td><td>
+{{int:Notargettext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notargettitle&action=edit notargettitle]<br>
+[[MediaWiki_talk:Notargettitle|Talk]]
+</td><td>
+No target
+</td><td>
+{{int:Notargettitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Note&action=edit note]<br>
+[[MediaWiki_talk:Note|Talk]]
+</td><td>
+&lt;strong&gt;Note:&lt;/strong&gt;
+</td><td>
+{{int:Note}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notextmatches&action=edit notextmatches]<br>
+[[MediaWiki_talk:Notextmatches|Talk]]
+</td><td>
+No page text matches
+</td><td>
+{{int:Notextmatches}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notitlematches&action=edit notitlematches]<br>
+[[MediaWiki_talk:Notitlematches|Talk]]
+</td><td>
+No page title matches
+</td><td>
+{{int:Notitlematches}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Notloggedin&action=edit notloggedin]<br>
+[[MediaWiki_talk:Notloggedin|Talk]]
+</td><td>
+Not logged in
+</td><td>
+{{int:Notloggedin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nowatchlist&action=edit nowatchlist]<br>
+[[MediaWiki_talk:Nowatchlist|Talk]]
+</td><td>
+You have no items on your watchlist.
+</td><td>
+{{int:Nowatchlist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nowiki_sample&action=edit nowiki_sample]<br>
+[[MediaWiki_talk:Nowiki_sample|Talk]]
+</td><td>
+Insert non-formatted text here
+</td><td>
+{{int:Nowiki_sample}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nowiki_tip&action=edit nowiki_tip]<br>
+[[MediaWiki_talk:Nowiki_tip|Talk]]
+</td><td>
+Ignore wiki formatting
+</td><td>
+{{int:Nowiki_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-category&action=edit nstab-category]<br>
+[[MediaWiki_talk:Nstab-category|Talk]]
+</td><td>
+Category
+</td><td>
+{{int:Nstab-category}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-help&action=edit nstab-help]<br>
+[[MediaWiki_talk:Nstab-help|Talk]]
+</td><td>
+Help
+</td><td>
+{{int:Nstab-help}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-image&action=edit nstab-image]<br>
+[[MediaWiki_talk:Nstab-image|Talk]]
+</td><td>
+Image
+</td><td>
+{{int:Nstab-image}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-main&action=edit nstab-main]<br>
+[[MediaWiki_talk:Nstab-main|Talk]]
+</td><td>
+Article
+</td><td>
+{{int:Nstab-main}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-media&action=edit nstab-media]<br>
+[[MediaWiki_talk:Nstab-media|Talk]]
+</td><td>
+Media
+</td><td>
+{{int:Nstab-media}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-mediawiki&action=edit nstab-mediawiki]<br>
+[[MediaWiki_talk:Nstab-mediawiki|Talk]]
+</td><td>
+Message
+</td><td>
+{{int:Nstab-mediawiki}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-special&action=edit nstab-special]<br>
+[[MediaWiki_talk:Nstab-special|Talk]]
+</td><td>
+Special
+</td><td>
+{{int:Nstab-special}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-template&action=edit nstab-template]<br>
+[[MediaWiki_talk:Nstab-template|Talk]]
+</td><td>
+Template
+</td><td>
+{{int:Nstab-template}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-user&action=edit nstab-user]<br>
+[[MediaWiki_talk:Nstab-user|Talk]]
+</td><td>
+User page
+</td><td>
+{{int:Nstab-user}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nstab-wp&action=edit nstab-wp]<br>
+[[MediaWiki_talk:Nstab-wp|Talk]]
+</td><td>
+About
+</td><td>
+{{int:Nstab-wp}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nviews&action=edit nviews]<br>
+[[MediaWiki_talk:Nviews|Talk]]
+</td><td>
+$1 views
+</td><td>
+{{int:Nviews}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ok&action=edit ok]<br>
+[[MediaWiki_talk:Ok|Talk]]
+</td><td>
+OK
+</td><td>
+{{int:Ok}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Oldpassword&action=edit oldpassword]<br>
+[[MediaWiki_talk:Oldpassword|Talk]]
+</td><td>
+Old password
+</td><td>
+{{int:Oldpassword}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Orig&action=edit orig]<br>
+[[MediaWiki_talk:Orig|Talk]]
+</td><td>
+orig
+</td><td>
+{{int:Orig}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Orphans&action=edit orphans]<br>
+[[MediaWiki_talk:Orphans|Talk]]
+</td><td>
+Orphaned pages
+</td><td>
+{{int:Orphans}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Othercontribs&action=edit othercontribs]<br>
+[[MediaWiki_talk:Othercontribs|Talk]]
+</td><td>
+Based on work by $1.
+</td><td>
+{{int:Othercontribs}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Otherlanguages&action=edit otherlanguages]<br>
+[[MediaWiki_talk:Otherlanguages|Talk]]
+</td><td>
+Other languages
+</td><td>
+{{int:Otherlanguages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Pagemovedsub&action=edit pagemovedsub]<br>
+[[MediaWiki_talk:Pagemovedsub|Talk]]
+</td><td>
+Move succeeded
+</td><td>
+{{int:Pagemovedsub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Pagemovedtext&action=edit pagemovedtext]<br>
+[[MediaWiki_talk:Pagemovedtext|Talk]]
+</td><td>
+Page &quot;&#91;&#91;$1]]&quot; moved to &quot;&#91;&#91;$2]]&quot;.
+</td><td>
+{{int:Pagemovedtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Pagetitle&action=edit pagetitle]<br>
+[[MediaWiki_talk:Pagetitle|Talk]]
+</td><td>
+$1 - Wiktionary
+</td><td>
+{{int:Pagetitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Passwordremindertext&action=edit passwordremindertext]<br>
+[[MediaWiki_talk:Passwordremindertext|Talk]]
+</td><td>
+Someone (probably you, from IP address $1)
+requested that we send you a new Wiktionary login password.
+The password for user &quot;$2&quot; is now &quot;$3&quot;.
+You should log in and change your password now.
+</td><td>
+{{int:Passwordremindertext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Passwordremindertitle&action=edit passwordremindertitle]<br>
+[[MediaWiki_talk:Passwordremindertitle|Talk]]
+</td><td>
+Password reminder from Wiktionary
+</td><td>
+{{int:Passwordremindertitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Passwordsent&action=edit passwordsent]<br>
+[[MediaWiki_talk:Passwordsent|Talk]]
+</td><td>
+A new password has been sent to the e-mail address
+registered for &quot;$1&quot;.
+Please log in again after you receive it.
+</td><td>
+{{int:Passwordsent}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Perfcached&action=edit perfcached]<br>
+[[MediaWiki_talk:Perfcached|Talk]]
+</td><td>
+The following data is cached and may not be completely up to date:
+</td><td>
+{{int:Perfcached}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Perfdisabled&action=edit perfdisabled]<br>
+[[MediaWiki_talk:Perfdisabled|Talk]]
+</td><td>
+Sorry! This feature has been temporarily disabled
+because it slows the database down to the point that no one can use
+the wiki.
+</td><td>
+{{int:Perfdisabled}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Perfdisabledsub&action=edit perfdisabledsub]<br>
+[[MediaWiki_talk:Perfdisabledsub|Talk]]
+</td><td>
+Here&#39;s a saved copy from $1:
+</td><td>
+{{int:Perfdisabledsub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Personaltools&action=edit personaltools]<br>
+[[MediaWiki_talk:Personaltools|Talk]]
+</td><td>
+Personal tools
+</td><td>
+{{int:Personaltools}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Popularpages&action=edit popularpages]<br>
+[[MediaWiki_talk:Popularpages|Talk]]
+</td><td>
+Popular pages
+</td><td>
+{{int:Popularpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Portal&action=edit portal]<br>
+[[MediaWiki_talk:Portal|Talk]]
+</td><td>
+Community portal
+</td><td>
+{{int:Portal}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Portal-url&action=edit portal-url]<br>
+[[MediaWiki_talk:Portal-url|Talk]]
+</td><td>
+Wiktionary:Community Portal
+</td><td>
+{{int:Portal-url}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Postcomment&action=edit postcomment]<br>
+[[MediaWiki_talk:Postcomment|Talk]]
+</td><td>
+Post a comment
+</td><td>
+{{int:Postcomment}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Poweredby&action=edit poweredby]<br>
+[[MediaWiki_talk:Poweredby|Talk]]
+</td><td>
+Wiktionary is powered by &#91;http&#58;//www.mediawiki.org/ MediaWiki], an open source wiki engine.
+</td><td>
+{{int:Poweredby}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Powersearch&action=edit powersearch]<br>
+[[MediaWiki_talk:Powersearch|Talk]]
+</td><td>
+Search
+</td><td>
+{{int:Powersearch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Powersearchtext&action=edit powersearchtext]<br>
+[[MediaWiki_talk:Powersearchtext|Talk]]
+</td><td>
+
+Search in namespaces :&lt;br /&gt;
+$1&lt;br /&gt;
+$2 List redirects &amp;nbsp; Search for $3 $9
+</td><td>
+{{int:Powersearchtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Preferences&action=edit preferences]<br>
+[[MediaWiki_talk:Preferences|Talk]]
+</td><td>
+Preferences
+</td><td>
+{{int:Preferences}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefs-help-userdata&action=edit prefs-help-userdata]<br>
+[[MediaWiki_talk:Prefs-help-userdata|Talk]]
+</td><td>
+* &lt;strong&gt;Real name&lt;/strong&gt; (optional): if you choose to provide it this will be used for giving you attribution for your work.&lt;br/&gt;
+* &lt;strong&gt;Email&lt;/strong&gt; (optional): Enables people to contact you through the website without you having to reveal your
+email address to them, and it can be used to send you a new password if you forget it.
+</td><td>
+{{int:Prefs-help-userdata}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefs-misc&action=edit prefs-misc]<br>
+[[MediaWiki_talk:Prefs-misc|Talk]]
+</td><td>
+Misc settings
+</td><td>
+{{int:Prefs-misc}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefs-personal&action=edit prefs-personal]<br>
+[[MediaWiki_talk:Prefs-personal|Talk]]
+</td><td>
+User data
+</td><td>
+{{int:Prefs-personal}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefs-rc&action=edit prefs-rc]<br>
+[[MediaWiki_talk:Prefs-rc|Talk]]
+</td><td>
+Recent changes and stub display
+</td><td>
+{{int:Prefs-rc}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefslogintext&action=edit prefslogintext]<br>
+[[MediaWiki_talk:Prefslogintext|Talk]]
+</td><td>
+You are logged in as &quot;$1&quot;.
+Your internal ID number is $2.
+
+See &#91;&#91;Wiktionary:User preferences help]] for help deciphering the options.
+</td><td>
+{{int:Prefslogintext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefsnologin&action=edit prefsnologin]<br>
+[[MediaWiki_talk:Prefsnologin|Talk]]
+</td><td>
+Not logged in
+</td><td>
+{{int:Prefsnologin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefsnologintext&action=edit prefsnologintext]<br>
+[[MediaWiki_talk:Prefsnologintext|Talk]]
+</td><td>
+You must be &lt;a href=&quot;/wiki/Special:Userlogin&quot;&gt;logged in&lt;/a&gt;
+to set user preferences.
+</td><td>
+{{int:Prefsnologintext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prefsreset&action=edit prefsreset]<br>
+[[MediaWiki_talk:Prefsreset|Talk]]
+</td><td>
+Preferences have been reset from storage.
+</td><td>
+{{int:Prefsreset}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Preview&action=edit preview]<br>
+[[MediaWiki_talk:Preview|Talk]]
+</td><td>
+Preview
+</td><td>
+{{int:Preview}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Previewconflict&action=edit previewconflict]<br>
+[[MediaWiki_talk:Previewconflict|Talk]]
+</td><td>
+This preview reflects the text in the upper
+text editing area as it will appear if you choose to save.
+</td><td>
+{{int:Previewconflict}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Previewnote&action=edit previewnote]<br>
+[[MediaWiki_talk:Previewnote|Talk]]
+</td><td>
+Remember that this is only a preview, and has not yet been saved!
+</td><td>
+{{int:Previewnote}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Prevn&action=edit prevn]<br>
+[[MediaWiki_talk:Prevn|Talk]]
+</td><td>
+previous $1
+</td><td>
+{{int:Prevn}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Printableversion&action=edit printableversion]<br>
+[[MediaWiki_talk:Printableversion|Talk]]
+</td><td>
+Printable version
+</td><td>
+{{int:Printableversion}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Printsubtitle&action=edit printsubtitle]<br>
+[[MediaWiki_talk:Printsubtitle|Talk]]
+</td><td>
+(From http&#58;//tl.wiktionary.org)
+</td><td>
+{{int:Printsubtitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protect&action=edit protect]<br>
+[[MediaWiki_talk:Protect|Talk]]
+</td><td>
+Protect
+</td><td>
+{{int:Protect}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectcomment&action=edit protectcomment]<br>
+[[MediaWiki_talk:Protectcomment|Talk]]
+</td><td>
+Reason for protecting
+</td><td>
+{{int:Protectcomment}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectedarticle&action=edit protectedarticle]<br>
+[[MediaWiki_talk:Protectedarticle|Talk]]
+</td><td>
+protected &#91;&#91;$1]]
+</td><td>
+{{int:Protectedarticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectedpage&action=edit protectedpage]<br>
+[[MediaWiki_talk:Protectedpage|Talk]]
+</td><td>
+Protected page
+</td><td>
+{{int:Protectedpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectedpagewarning&action=edit protectedpagewarning]<br>
+[[MediaWiki_talk:Protectedpagewarning|Talk]]
+</td><td>
+WARNING: This page has been locked so that only
+users with sysop privileges can edit it. Be sure you are following the
+&lt;a href=&#39;/w/wiki.phtml/Wiktionary:Protected_page_guidelines&#39;&gt;protected page
+guidelines&lt;/a&gt;.
+</td><td>
+{{int:Protectedpagewarning}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectedtext&action=edit protectedtext]<br>
+[[MediaWiki_talk:Protectedtext|Talk]]
+</td><td>
+This page has been locked to prevent editing; there are
+a number of reasons why this may be so, please see
+&#91;&#91;Wiktionary:Protected page]].
+
+You can view and copy the source of this page:
+</td><td>
+{{int:Protectedtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectlogpage&action=edit protectlogpage]<br>
+[[MediaWiki_talk:Protectlogpage|Talk]]
+</td><td>
+Protection_log
+</td><td>
+{{int:Protectlogpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectlogtext&action=edit protectlogtext]<br>
+[[MediaWiki_talk:Protectlogtext|Talk]]
+</td><td>
+Below is a list of page locks/unlocks.
+See &#91;&#91;Wiktionary:Protected page]] for more information.
+</td><td>
+{{int:Protectlogtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectpage&action=edit protectpage]<br>
+[[MediaWiki_talk:Protectpage|Talk]]
+</td><td>
+Protect page
+</td><td>
+{{int:Protectpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectreason&action=edit protectreason]<br>
+[[MediaWiki_talk:Protectreason|Talk]]
+</td><td>
+(give a reason)
+</td><td>
+{{int:Protectreason}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectsub&action=edit protectsub]<br>
+[[MediaWiki_talk:Protectsub|Talk]]
+</td><td>
+(Protecting &quot;$1&quot;)
+</td><td>
+{{int:Protectsub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Protectthispage&action=edit protectthispage]<br>
+[[MediaWiki_talk:Protectthispage|Talk]]
+</td><td>
+Protect this page
+</td><td>
+{{int:Protectthispage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Proxyblocker&action=edit proxyblocker]<br>
+[[MediaWiki_talk:Proxyblocker|Talk]]
+</td><td>
+Proxy blocker
+</td><td>
+{{int:Proxyblocker}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Proxyblockreason&action=edit proxyblockreason]<br>
+[[MediaWiki_talk:Proxyblockreason|Talk]]
+</td><td>
+Your IP address has been blocked because it is an open proxy. Please contact your Internet service provider or tech support and inform them of this serious security problem.
+</td><td>
+{{int:Proxyblockreason}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Proxyblocksuccess&action=edit proxyblocksuccess]<br>
+[[MediaWiki_talk:Proxyblocksuccess|Talk]]
+</td><td>
+Done.
+
+</td><td>
+{{int:Proxyblocksuccess}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbbrowse&action=edit qbbrowse]<br>
+[[MediaWiki_talk:Qbbrowse|Talk]]
+</td><td>
+Browse
+</td><td>
+{{int:Qbbrowse}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbedit&action=edit qbedit]<br>
+[[MediaWiki_talk:Qbedit|Talk]]
+</td><td>
+Edit
+</td><td>
+{{int:Qbedit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbfind&action=edit qbfind]<br>
+[[MediaWiki_talk:Qbfind|Talk]]
+</td><td>
+Find
+</td><td>
+{{int:Qbfind}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbmyoptions&action=edit qbmyoptions]<br>
+[[MediaWiki_talk:Qbmyoptions|Talk]]
+</td><td>
+My pages
+</td><td>
+{{int:Qbmyoptions}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbpageinfo&action=edit qbpageinfo]<br>
+[[MediaWiki_talk:Qbpageinfo|Talk]]
+</td><td>
+Context
+</td><td>
+{{int:Qbpageinfo}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbpageoptions&action=edit qbpageoptions]<br>
+[[MediaWiki_talk:Qbpageoptions|Talk]]
+</td><td>
+This page
+</td><td>
+{{int:Qbpageoptions}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbsettings&action=edit qbsettings]<br>
+[[MediaWiki_talk:Qbsettings|Talk]]
+</td><td>
+Quickbar settings
+</td><td>
+{{int:Qbsettings}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Qbspecialpages&action=edit qbspecialpages]<br>
+[[MediaWiki_talk:Qbspecialpages|Talk]]
+</td><td>
+Special pages
+</td><td>
+{{int:Qbspecialpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Querybtn&action=edit querybtn]<br>
+[[MediaWiki_talk:Querybtn|Talk]]
+</td><td>
+Submit query
+</td><td>
+{{int:Querybtn}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Querysuccessful&action=edit querysuccessful]<br>
+[[MediaWiki_talk:Querysuccessful|Talk]]
+</td><td>
+Query successful
+</td><td>
+{{int:Querysuccessful}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Randompage&action=edit randompage]<br>
+[[MediaWiki_talk:Randompage|Talk]]
+</td><td>
+Random page
+</td><td>
+{{int:Randompage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Range_block_disabled&action=edit range_block_disabled]<br>
+[[MediaWiki_talk:Range_block_disabled|Talk]]
+</td><td>
+The sysop ability to create range blocks is disabled.
+</td><td>
+{{int:Range_block_disabled}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rchide&action=edit rchide]<br>
+[[MediaWiki_talk:Rchide|Talk]]
+</td><td>
+in $4 form; $1 minor edits; $2 secondary namespaces; $3 multiple edits.
+</td><td>
+{{int:Rchide}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rclinks&action=edit rclinks]<br>
+[[MediaWiki_talk:Rclinks|Talk]]
+</td><td>
+Show last $1 changes in last $2 days&lt;br /&gt;$3
+</td><td>
+{{int:Rclinks}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rclistfrom&action=edit rclistfrom]<br>
+[[MediaWiki_talk:Rclistfrom|Talk]]
+</td><td>
+Show new changes starting from $1
+</td><td>
+{{int:Rclistfrom}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rcliu&action=edit rcliu]<br>
+[[MediaWiki_talk:Rcliu|Talk]]
+</td><td>
+; $1 edits from logged in users
+</td><td>
+{{int:Rcliu}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rcloaderr&action=edit rcloaderr]<br>
+[[MediaWiki_talk:Rcloaderr|Talk]]
+</td><td>
+Loading recent changes
+</td><td>
+{{int:Rcloaderr}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rclsub&action=edit rclsub]<br>
+[[MediaWiki_talk:Rclsub|Talk]]
+</td><td>
+(to pages linked from &quot;$1&quot;)
+</td><td>
+{{int:Rclsub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rcnote&action=edit rcnote]<br>
+[[MediaWiki_talk:Rcnote|Talk]]
+</td><td>
+Below are the last &lt;strong&gt;$1&lt;/strong&gt; changes in last &lt;strong&gt;$2&lt;/strong&gt; days.
+</td><td>
+{{int:Rcnote}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rcnotefrom&action=edit rcnotefrom]<br>
+[[MediaWiki_talk:Rcnotefrom|Talk]]
+</td><td>
+Below are the changes since &lt;b&gt;$2&lt;/b&gt; (up to &lt;b&gt;$1&lt;/b&gt; shown).
+</td><td>
+{{int:Rcnotefrom}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Readonly&action=edit readonly]<br>
+[[MediaWiki_talk:Readonly|Talk]]
+</td><td>
+Database locked
+</td><td>
+{{int:Readonly}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Readonlytext&action=edit readonlytext]<br>
+[[MediaWiki_talk:Readonlytext|Talk]]
+</td><td>
+The database is currently locked to new
+entries and other modifications, probably for routine database maintenance,
+after which it will be back to normal.
+The administrator who locked it offered this explanation:
+&lt;p&gt;$1
+</td><td>
+{{int:Readonlytext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Readonlywarning&action=edit readonlywarning]<br>
+[[MediaWiki_talk:Readonlywarning|Talk]]
+</td><td>
+WARNING: The database has been locked for maintenance,
+so you will not be able to save your edits right now. You may wish to cut-n-paste
+the text into a text file and save it for later.
+</td><td>
+{{int:Readonlywarning}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Recentchanges&action=edit recentchanges]<br>
+[[MediaWiki_talk:Recentchanges|Talk]]
+</td><td>
+Recent changes
+</td><td>
+{{int:Recentchanges}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Recentchangescount&action=edit recentchangescount]<br>
+[[MediaWiki_talk:Recentchangescount|Talk]]
+</td><td>
+Number of titles in recent changes
+</td><td>
+{{int:Recentchangescount}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Recentchangeslinked&action=edit recentchangeslinked]<br>
+[[MediaWiki_talk:Recentchangeslinked|Talk]]
+</td><td>
+Related changes
+</td><td>
+{{int:Recentchangeslinked}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Recentchangestext&action=edit recentchangestext]<br>
+[[MediaWiki_talk:Recentchangestext|Talk]]
+</td><td>
+Track the most recent changes to the wiki on this page.
+</td><td>
+{{int:Recentchangestext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Redirectedfrom&action=edit redirectedfrom]<br>
+[[MediaWiki_talk:Redirectedfrom|Talk]]
+</td><td>
+(Redirected from $1)
+</td><td>
+{{int:Redirectedfrom}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Remembermypassword&action=edit remembermypassword]<br>
+[[MediaWiki_talk:Remembermypassword|Talk]]
+</td><td>
+Remember my password across sessions.
+</td><td>
+{{int:Remembermypassword}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Removechecked&action=edit removechecked]<br>
+[[MediaWiki_talk:Removechecked|Talk]]
+</td><td>
+Remove checked items from watchlist
+</td><td>
+{{int:Removechecked}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Removedwatch&action=edit removedwatch]<br>
+[[MediaWiki_talk:Removedwatch|Talk]]
+</td><td>
+Removed from watchlist
+</td><td>
+{{int:Removedwatch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Removedwatchtext&action=edit removedwatchtext]<br>
+[[MediaWiki_talk:Removedwatchtext|Talk]]
+</td><td>
+The page &quot;$1&quot; has been removed from your watchlist.
+</td><td>
+{{int:Removedwatchtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Removingchecked&action=edit removingchecked]<br>
+[[MediaWiki_talk:Removingchecked|Talk]]
+</td><td>
+Removing requested items from watchlist...
+</td><td>
+{{int:Removingchecked}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Resetprefs&action=edit resetprefs]<br>
+[[MediaWiki_talk:Resetprefs|Talk]]
+</td><td>
+Reset preferences
+</td><td>
+{{int:Resetprefs}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Restorelink&action=edit restorelink]<br>
+[[MediaWiki_talk:Restorelink|Talk]]
+</td><td>
+$1 deleted edits
+</td><td>
+{{int:Restorelink}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Resultsperpage&action=edit resultsperpage]<br>
+[[MediaWiki_talk:Resultsperpage|Talk]]
+</td><td>
+Hits to show per page
+</td><td>
+{{int:Resultsperpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Retrievedfrom&action=edit retrievedfrom]<br>
+[[MediaWiki_talk:Retrievedfrom|Talk]]
+</td><td>
+Retrieved from &quot;$1&quot;
+</td><td>
+{{int:Retrievedfrom}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Returnto&action=edit returnto]<br>
+[[MediaWiki_talk:Returnto|Talk]]
+</td><td>
+Return to $1.
+</td><td>
+{{int:Returnto}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Retypenew&action=edit retypenew]<br>
+[[MediaWiki_talk:Retypenew|Talk]]
+</td><td>
+Retype new password
+</td><td>
+{{int:Retypenew}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Reupload&action=edit reupload]<br>
+[[MediaWiki_talk:Reupload|Talk]]
+</td><td>
+Re-upload
+</td><td>
+{{int:Reupload}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Reuploaddesc&action=edit reuploaddesc]<br>
+[[MediaWiki_talk:Reuploaddesc|Talk]]
+</td><td>
+Return to the upload form.
+</td><td>
+{{int:Reuploaddesc}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Reverted&action=edit reverted]<br>
+[[MediaWiki_talk:Reverted|Talk]]
+</td><td>
+Reverted to earlier revision
+</td><td>
+{{int:Reverted}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revertimg&action=edit revertimg]<br>
+[[MediaWiki_talk:Revertimg|Talk]]
+</td><td>
+rev
+</td><td>
+{{int:Revertimg}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revertpage&action=edit revertpage]<br>
+[[MediaWiki_talk:Revertpage|Talk]]
+</td><td>
+Reverted edit of $2, changed back to last version by $1
+</td><td>
+{{int:Revertpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revhistory&action=edit revhistory]<br>
+[[MediaWiki_talk:Revhistory|Talk]]
+</td><td>
+Revision history
+</td><td>
+{{int:Revhistory}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revisionasof&action=edit revisionasof]<br>
+[[MediaWiki_talk:Revisionasof|Talk]]
+</td><td>
+Revision as of $1
+</td><td>
+{{int:Revisionasof}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revnotfound&action=edit revnotfound]<br>
+[[MediaWiki_talk:Revnotfound|Talk]]
+</td><td>
+Revision not found
+</td><td>
+{{int:Revnotfound}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Revnotfoundtext&action=edit revnotfoundtext]<br>
+[[MediaWiki_talk:Revnotfoundtext|Talk]]
+</td><td>
+The old revision of the page you asked for could not be found.
+Please check the URL you used to access this page.
+
+</td><td>
+{{int:Revnotfoundtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rfcurl&action=edit rfcurl]<br>
+[[MediaWiki_talk:Rfcurl|Talk]]
+</td><td>
+http&#58;//www.faqs.org/rfcs/rfc$1.html
+</td><td>
+{{int:Rfcurl}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rights&action=edit rights]<br>
+[[MediaWiki_talk:Rights|Talk]]
+</td><td>
+Rights:
+</td><td>
+{{int:Rights}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rollback&action=edit rollback]<br>
+[[MediaWiki_talk:Rollback|Talk]]
+</td><td>
+Roll back edits
+</td><td>
+{{int:Rollback}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rollback_short&action=edit rollback_short]<br>
+[[MediaWiki_talk:Rollback_short|Talk]]
+</td><td>
+Rollback
+</td><td>
+{{int:Rollback_short}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rollbackfailed&action=edit rollbackfailed]<br>
+[[MediaWiki_talk:Rollbackfailed|Talk]]
+</td><td>
+Rollback failed
+</td><td>
+{{int:Rollbackfailed}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rollbacklink&action=edit rollbacklink]<br>
+[[MediaWiki_talk:Rollbacklink|Talk]]
+</td><td>
+rollback
+</td><td>
+{{int:Rollbacklink}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Rows&action=edit rows]<br>
+[[MediaWiki_talk:Rows|Talk]]
+</td><td>
+Rows
+</td><td>
+{{int:Rows}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Savearticle&action=edit savearticle]<br>
+[[MediaWiki_talk:Savearticle|Talk]]
+</td><td>
+Save page
+</td><td>
+{{int:Savearticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Savedprefs&action=edit savedprefs]<br>
+[[MediaWiki_talk:Savedprefs|Talk]]
+</td><td>
+Your preferences have been saved.
+</td><td>
+{{int:Savedprefs}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Savefile&action=edit savefile]<br>
+[[MediaWiki_talk:Savefile|Talk]]
+</td><td>
+Save file
+</td><td>
+{{int:Savefile}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Saveprefs&action=edit saveprefs]<br>
+[[MediaWiki_talk:Saveprefs|Talk]]
+</td><td>
+Save preferences
+</td><td>
+{{int:Saveprefs}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Search&action=edit search]<br>
+[[MediaWiki_talk:Search|Talk]]
+</td><td>
+Search
+</td><td>
+{{int:Search}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchdisabled&action=edit searchdisabled]<br>
+[[MediaWiki_talk:Searchdisabled|Talk]]
+</td><td>
+&lt;p&gt;Sorry! Full text search has been disabled temporarily, for performance reasons. In the meantime, you can use the Google search below, which may be out of date.&lt;/p&gt;
+</td><td>
+{{int:Searchdisabled}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchhelppage&action=edit searchhelppage]<br>
+[[MediaWiki_talk:Searchhelppage|Talk]]
+</td><td>
+Wiktionary:Searching
+</td><td>
+{{int:Searchhelppage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchingwikipedia&action=edit searchingwikipedia]<br>
+[[MediaWiki_talk:Searchingwikipedia|Talk]]
+</td><td>
+Searching Wiktionary
+</td><td>
+{{int:Searchingwikipedia}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchquery&action=edit searchquery]<br>
+[[MediaWiki_talk:Searchquery|Talk]]
+</td><td>
+For query &quot;$1&quot;
+</td><td>
+{{int:Searchquery}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchresults&action=edit searchresults]<br>
+[[MediaWiki_talk:Searchresults|Talk]]
+</td><td>
+Search results
+</td><td>
+{{int:Searchresults}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchresultshead&action=edit searchresultshead]<br>
+[[MediaWiki_talk:Searchresultshead|Talk]]
+</td><td>
+Search result settings
+</td><td>
+{{int:Searchresultshead}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Searchresulttext&action=edit searchresulttext]<br>
+[[MediaWiki_talk:Searchresulttext|Talk]]
+</td><td>
+For more information about searching Wiktionary, see $1.
+</td><td>
+{{int:Searchresulttext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sectionedit&action=edit sectionedit]<br>
+[[MediaWiki_talk:Sectionedit|Talk]]
+</td><td>
+ (section)
+</td><td>
+{{int:Sectionedit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selectnewerversionfordiff&action=edit selectnewerversionfordiff]<br>
+[[MediaWiki_talk:Selectnewerversionfordiff|Talk]]
+</td><td>
+Select a newer version for comparison
+</td><td>
+{{int:Selectnewerversionfordiff}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selectolderversionfordiff&action=edit selectolderversionfordiff]<br>
+[[MediaWiki_talk:Selectolderversionfordiff|Talk]]
+</td><td>
+Select an older version for comparison
+</td><td>
+{{int:Selectolderversionfordiff}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selectonly&action=edit selectonly]<br>
+[[MediaWiki_talk:Selectonly|Talk]]
+</td><td>
+Only read-only queries are allowed.
+</td><td>
+{{int:Selectonly}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selflinks&action=edit selflinks]<br>
+[[MediaWiki_talk:Selflinks|Talk]]
+</td><td>
+Pages with Self Links
+</td><td>
+{{int:Selflinks}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Selflinkstext&action=edit selflinkstext]<br>
+[[MediaWiki_talk:Selflinkstext|Talk]]
+</td><td>
+The following pages contain a link to themselves, which they should not.
+</td><td>
+{{int:Selflinkstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Seriousxhtmlerrors&action=edit seriousxhtmlerrors]<br>
+[[MediaWiki_talk:Seriousxhtmlerrors|Talk]]
+</td><td>
+There were serious xhtml markup errors detected by tidy.
+</td><td>
+{{int:Seriousxhtmlerrors}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Servertime&action=edit servertime]<br>
+[[MediaWiki_talk:Servertime|Talk]]
+</td><td>
+Server time is now
+</td><td>
+{{int:Servertime}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Set_rights_fail&action=edit set_rights_fail]<br>
+[[MediaWiki_talk:Set_rights_fail|Talk]]
+</td><td>
+&lt;b&gt;User rights for &quot;$1&quot; could not be set. (Did you enter the name correctly?)&lt;/b&gt;
+</td><td>
+{{int:Set_rights_fail}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Set_user_rights&action=edit set_user_rights]<br>
+[[MediaWiki_talk:Set_user_rights|Talk]]
+</td><td>
+Set user rights
+</td><td>
+{{int:Set_user_rights}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Setbureaucratflag&action=edit setbureaucratflag]<br>
+[[MediaWiki_talk:Setbureaucratflag|Talk]]
+</td><td>
+Set bureaucrat flag
+</td><td>
+{{int:Setbureaucratflag}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Shortpages&action=edit shortpages]<br>
+[[MediaWiki_talk:Shortpages|Talk]]
+</td><td>
+Short pages
+</td><td>
+{{int:Shortpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Show&action=edit show]<br>
+[[MediaWiki_talk:Show|Talk]]
+</td><td>
+show
+</td><td>
+{{int:Show}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showhideminor&action=edit showhideminor]<br>
+[[MediaWiki_talk:Showhideminor|Talk]]
+</td><td>
+$1 minor edits &#124; $2 bots &#124; $3 logged in users
+</td><td>
+{{int:Showhideminor}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showingresults&action=edit showingresults]<br>
+[[MediaWiki_talk:Showingresults|Talk]]
+</td><td>
+Showing below &lt;b&gt;$1&lt;/b&gt; results starting with #&lt;b&gt;$2&lt;/b&gt;.
+</td><td>
+{{int:Showingresults}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showingresultsnum&action=edit showingresultsnum]<br>
+[[MediaWiki_talk:Showingresultsnum|Talk]]
+</td><td>
+Showing below &lt;b&gt;$3&lt;/b&gt; results starting with #&lt;b&gt;$2&lt;/b&gt;.
+</td><td>
+{{int:Showingresultsnum}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showlast&action=edit showlast]<br>
+[[MediaWiki_talk:Showlast|Talk]]
+</td><td>
+Show last $1 images sorted $2.
+</td><td>
+{{int:Showlast}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showpreview&action=edit showpreview]<br>
+[[MediaWiki_talk:Showpreview|Talk]]
+</td><td>
+Show preview
+</td><td>
+{{int:Showpreview}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Showtoc&action=edit showtoc]<br>
+[[MediaWiki_talk:Showtoc|Talk]]
+</td><td>
+show
+</td><td>
+{{int:Showtoc}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sig_tip&action=edit sig_tip]<br>
+[[MediaWiki_talk:Sig_tip|Talk]]
+</td><td>
+Your signature with timestamp
+</td><td>
+{{int:Sig_tip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitestats&action=edit sitestats]<br>
+[[MediaWiki_talk:Sitestats|Talk]]
+</td><td>
+Site statistics
+</td><td>
+{{int:Sitestats}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitestatstext&action=edit sitestatstext]<br>
+[[MediaWiki_talk:Sitestatstext|Talk]]
+</td><td>
+There are &#39;&#39;&#39;$1&#39;&#39;&#39; total pages in the database.
+This includes &quot;talk&quot; pages, pages about Wiktionary, minimal &quot;stub&quot;
+pages, redirects, and others that probably don&#39;t qualify as content pages.
+Excluding those, there are &#39;&#39;&#39;$2&#39;&#39;&#39; pages that are probably legitimate
+content pages.
+
+There have been a total of &#39;&#39;&#39;$3&#39;&#39;&#39; page views, and &#39;&#39;&#39;$4&#39;&#39;&#39; page edits
+since the wiki was setup.
+That comes to &#39;&#39;&#39;$5&#39;&#39;&#39; average edits per page, and &#39;&#39;&#39;$6&#39;&#39;&#39; views per edit.
+</td><td>
+{{int:Sitestatstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitesubtitle&action=edit sitesubtitle]<br>
+[[MediaWiki_talk:Sitesubtitle|Talk]]
+</td><td>
+The Free Encyclopedia
+</td><td>
+{{int:Sitesubtitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitesupport&action=edit sitesupport]<br>
+[[MediaWiki_talk:Sitesupport|Talk]]
+</td><td>
+Donations
+</td><td>
+{{int:Sitesupport}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sitetitle&action=edit sitetitle]<br>
+[[MediaWiki_talk:Sitetitle|Talk]]
+</td><td>
+Wiktionary
+</td><td>
+{{int:Sitetitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Siteuser&action=edit siteuser]<br>
+[[MediaWiki_talk:Siteuser|Talk]]
+</td><td>
+Wiktionary user $1
+</td><td>
+{{int:Siteuser}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Siteusers&action=edit siteusers]<br>
+[[MediaWiki_talk:Siteusers|Talk]]
+</td><td>
+Wiktionary user(s) $1
+</td><td>
+{{int:Siteusers}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Skin&action=edit skin]<br>
+[[MediaWiki_talk:Skin|Talk]]
+</td><td>
+Skin
+</td><td>
+{{int:Skin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Spamprotectiontext&action=edit spamprotectiontext]<br>
+[[MediaWiki_talk:Spamprotectiontext|Talk]]
+</td><td>
+The page you wanted to save was blocked by the spam filter. This is probably caused by a link to an external site.
+
+You might want to check the following regular expression for patterns that are currently blocked:
+</td><td>
+{{int:Spamprotectiontext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Spamprotectiontitle&action=edit spamprotectiontitle]<br>
+[[MediaWiki_talk:Spamprotectiontitle|Talk]]
+</td><td>
+Spam protection filter
+</td><td>
+{{int:Spamprotectiontitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Specialpage&action=edit specialpage]<br>
+[[MediaWiki_talk:Specialpage|Talk]]
+</td><td>
+Special Page
+</td><td>
+{{int:Specialpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Specialpages&action=edit specialpages]<br>
+[[MediaWiki_talk:Specialpages|Talk]]
+</td><td>
+Special pages
+</td><td>
+{{int:Specialpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Spheading&action=edit spheading]<br>
+[[MediaWiki_talk:Spheading|Talk]]
+</td><td>
+Special pages for all users
+</td><td>
+{{int:Spheading}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sqlislogged&action=edit sqlislogged]<br>
+[[MediaWiki_talk:Sqlislogged|Talk]]
+</td><td>
+Please note that all queries are logged.
+</td><td>
+{{int:Sqlislogged}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sqlquery&action=edit sqlquery]<br>
+[[MediaWiki_talk:Sqlquery|Talk]]
+</td><td>
+Enter query
+</td><td>
+{{int:Sqlquery}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Statistics&action=edit statistics]<br>
+[[MediaWiki_talk:Statistics|Talk]]
+</td><td>
+Statistics
+</td><td>
+{{int:Statistics}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Storedversion&action=edit storedversion]<br>
+[[MediaWiki_talk:Storedversion|Talk]]
+</td><td>
+Stored version
+</td><td>
+{{int:Storedversion}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Stubthreshold&action=edit stubthreshold]<br>
+[[MediaWiki_talk:Stubthreshold|Talk]]
+</td><td>
+Threshold for stub display
+</td><td>
+{{int:Stubthreshold}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Subcategories&action=edit subcategories]<br>
+[[MediaWiki_talk:Subcategories|Talk]]
+</td><td>
+Subcategories
+</td><td>
+{{int:Subcategories}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Subject&action=edit subject]<br>
+[[MediaWiki_talk:Subject|Talk]]
+</td><td>
+Subject/headline
+</td><td>
+{{int:Subject}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Subjectpage&action=edit subjectpage]<br>
+[[MediaWiki_talk:Subjectpage|Talk]]
+</td><td>
+View subject
+</td><td>
+{{int:Subjectpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Successfulupload&action=edit successfulupload]<br>
+[[MediaWiki_talk:Successfulupload|Talk]]
+</td><td>
+Successful upload
+</td><td>
+{{int:Successfulupload}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Summary&action=edit summary]<br>
+[[MediaWiki_talk:Summary|Talk]]
+</td><td>
+Summary
+</td><td>
+{{int:Summary}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sysopspheading&action=edit sysopspheading]<br>
+[[MediaWiki_talk:Sysopspheading|Talk]]
+</td><td>
+For sysop use only
+</td><td>
+{{int:Sysopspheading}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sysoptext&action=edit sysoptext]<br>
+[[MediaWiki_talk:Sysoptext|Talk]]
+</td><td>
+The action you have requested can only be
+performed by users with &quot;sysop&quot; status.
+See $1.
+</td><td>
+{{int:Sysoptext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Sysoptitle&action=edit sysoptitle]<br>
+[[MediaWiki_talk:Sysoptitle|Talk]]
+</td><td>
+Sysop access required
+</td><td>
+{{int:Sysoptitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tableform&action=edit tableform]<br>
+[[MediaWiki_talk:Tableform|Talk]]
+</td><td>
+table
+</td><td>
+{{int:Tableform}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talk&action=edit talk]<br>
+[[MediaWiki_talk:Talk|Talk]]
+</td><td>
+Discussion
+</td><td>
+{{int:Talk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkexists&action=edit talkexists]<br>
+[[MediaWiki_talk:Talkexists|Talk]]
+</td><td>
+The page itself was moved successfully, but the
+talk page could not be moved because one already exists at the new
+title. Please merge them manually.
+</td><td>
+{{int:Talkexists}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkpage&action=edit talkpage]<br>
+[[MediaWiki_talk:Talkpage|Talk]]
+</td><td>
+Discuss this page
+</td><td>
+{{int:Talkpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkpagemoved&action=edit talkpagemoved]<br>
+[[MediaWiki_talk:Talkpagemoved|Talk]]
+</td><td>
+The corresponding talk page was also moved.
+</td><td>
+{{int:Talkpagemoved}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkpagenotmoved&action=edit talkpagenotmoved]<br>
+[[MediaWiki_talk:Talkpagenotmoved|Talk]]
+</td><td>
+The corresponding talk page was &lt;strong&gt;not&lt;/strong&gt; moved.
+</td><td>
+{{int:Talkpagenotmoved}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Talkpagetext&action=edit talkpagetext]<br>
+[[MediaWiki_talk:Talkpagetext|Talk]]
+</td><td>
+&lt;!-- MediaWiki:talkpagetext --&gt;
+</td><td>
+{{int:Talkpagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Textboxsize&action=edit textboxsize]<br>
+[[MediaWiki_talk:Textboxsize|Talk]]
+</td><td>
+Textbox dimensions
+</td><td>
+{{int:Textboxsize}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Textmatches&action=edit textmatches]<br>
+[[MediaWiki_talk:Textmatches|Talk]]
+</td><td>
+Page text matches
+</td><td>
+{{int:Textmatches}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Thisisdeleted&action=edit thisisdeleted]<br>
+[[MediaWiki_talk:Thisisdeleted|Talk]]
+</td><td>
+View or restore $1?
+</td><td>
+{{int:Thisisdeleted}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Thumbnail-more&action=edit thumbnail-more]<br>
+[[MediaWiki_talk:Thumbnail-more|Talk]]
+</td><td>
+Enlarge
+</td><td>
+{{int:Thumbnail-more}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Timezonelegend&action=edit timezonelegend]<br>
+[[MediaWiki_talk:Timezonelegend|Talk]]
+</td><td>
+Time zone
+</td><td>
+{{int:Timezonelegend}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Timezoneoffset&action=edit timezoneoffset]<br>
+[[MediaWiki_talk:Timezoneoffset|Talk]]
+</td><td>
+Offset
+</td><td>
+{{int:Timezoneoffset}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Timezonetext&action=edit timezonetext]<br>
+[[MediaWiki_talk:Timezonetext|Talk]]
+</td><td>
+Enter number of hours your local time differs
+from server time (UTC).
+</td><td>
+{{int:Timezonetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Titlematches&action=edit titlematches]<br>
+[[MediaWiki_talk:Titlematches|Talk]]
+</td><td>
+Article title matches
+</td><td>
+{{int:Titlematches}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Toc&action=edit toc]<br>
+[[MediaWiki_talk:Toc|Talk]]
+</td><td>
+Table of contents
+</td><td>
+{{int:Toc}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Toolbox&action=edit toolbox]<br>
+[[MediaWiki_talk:Toolbox|Talk]]
+</td><td>
+Toolbox
+</td><td>
+{{int:Toolbox}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-addsection&action=edit tooltip-addsection]<br>
+[[MediaWiki_talk:Tooltip-addsection|Talk]]
+</td><td>
+Add a comment to this page. &#91;alt-+]
+</td><td>
+{{int:Tooltip-addsection}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-anontalk&action=edit tooltip-anontalk]<br>
+[[MediaWiki_talk:Tooltip-anontalk|Talk]]
+</td><td>
+Discussion about edits from this ip address &#91;alt-n]
+</td><td>
+{{int:Tooltip-anontalk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-anonuserpage&action=edit tooltip-anonuserpage]<br>
+[[MediaWiki_talk:Tooltip-anonuserpage|Talk]]
+</td><td>
+The user page for the ip you&#39;re editing as &#91;alt-.]
+</td><td>
+{{int:Tooltip-anonuserpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-article&action=edit tooltip-article]<br>
+[[MediaWiki_talk:Tooltip-article|Talk]]
+</td><td>
+View the content page &#91;alt-a]
+</td><td>
+{{int:Tooltip-article}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-atom&action=edit tooltip-atom]<br>
+[[MediaWiki_talk:Tooltip-atom|Talk]]
+</td><td>
+Atom feed for this page
+</td><td>
+{{int:Tooltip-atom}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-compareselectedversions&action=edit tooltip-compareselectedversions]<br>
+[[MediaWiki_talk:Tooltip-compareselectedversions|Talk]]
+</td><td>
+See the differences between the two selected versions of this page. &#91;alt-v]
+</td><td>
+{{int:Tooltip-compareselectedversions}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-contributions&action=edit tooltip-contributions]<br>
+[[MediaWiki_talk:Tooltip-contributions|Talk]]
+</td><td>
+View the list of contributions of this user
+</td><td>
+{{int:Tooltip-contributions}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-currentevents&action=edit tooltip-currentevents]<br>
+[[MediaWiki_talk:Tooltip-currentevents|Talk]]
+</td><td>
+Find background information on current events
+</td><td>
+{{int:Tooltip-currentevents}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-delete&action=edit tooltip-delete]<br>
+[[MediaWiki_talk:Tooltip-delete|Talk]]
+</td><td>
+Delete this page &#91;alt-d]
+</td><td>
+{{int:Tooltip-delete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-edit&action=edit tooltip-edit]<br>
+[[MediaWiki_talk:Tooltip-edit|Talk]]
+</td><td>
+You can edit this page. Please use the preview button before saving. &#91;alt-e]
+</td><td>
+{{int:Tooltip-edit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-emailuser&action=edit tooltip-emailuser]<br>
+[[MediaWiki_talk:Tooltip-emailuser|Talk]]
+</td><td>
+Send a mail to this user
+</td><td>
+{{int:Tooltip-emailuser}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-help&action=edit tooltip-help]<br>
+[[MediaWiki_talk:Tooltip-help|Talk]]
+</td><td>
+The place to find out.
+</td><td>
+{{int:Tooltip-help}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-history&action=edit tooltip-history]<br>
+[[MediaWiki_talk:Tooltip-history|Talk]]
+</td><td>
+Past versions of this page, &#91;alt-h]
+</td><td>
+{{int:Tooltip-history}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-login&action=edit tooltip-login]<br>
+[[MediaWiki_talk:Tooltip-login|Talk]]
+</td><td>
+You are encouraged to log in, it is not mandatory however. &#91;alt-o]
+</td><td>
+{{int:Tooltip-login}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-logout&action=edit tooltip-logout]<br>
+[[MediaWiki_talk:Tooltip-logout|Talk]]
+</td><td>
+Log out &#91;alt-o]
+</td><td>
+{{int:Tooltip-logout}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-mainpage&action=edit tooltip-mainpage]<br>
+[[MediaWiki_talk:Tooltip-mainpage|Talk]]
+</td><td>
+Visit the Main Page &#91;alt-z]
+</td><td>
+{{int:Tooltip-mainpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-minoredit&action=edit tooltip-minoredit]<br>
+[[MediaWiki_talk:Tooltip-minoredit|Talk]]
+</td><td>
+Mark this as a minor edit &#91;alt-i]
+</td><td>
+{{int:Tooltip-minoredit}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-move&action=edit tooltip-move]<br>
+[[MediaWiki_talk:Tooltip-move|Talk]]
+</td><td>
+Move this page &#91;alt-m]
+</td><td>
+{{int:Tooltip-move}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-mycontris&action=edit tooltip-mycontris]<br>
+[[MediaWiki_talk:Tooltip-mycontris|Talk]]
+</td><td>
+List of my contributions &#91;alt-y]
+</td><td>
+{{int:Tooltip-mycontris}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-mytalk&action=edit tooltip-mytalk]<br>
+[[MediaWiki_talk:Tooltip-mytalk|Talk]]
+</td><td>
+My talk page &#91;alt-n]
+</td><td>
+{{int:Tooltip-mytalk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-nomove&action=edit tooltip-nomove]<br>
+[[MediaWiki_talk:Tooltip-nomove|Talk]]
+</td><td>
+You don&#39;t have the permissions to move this page
+</td><td>
+{{int:Tooltip-nomove}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-portal&action=edit tooltip-portal]<br>
+[[MediaWiki_talk:Tooltip-portal|Talk]]
+</td><td>
+About the project, what you can do, where to find things
+</td><td>
+{{int:Tooltip-portal}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-preferences&action=edit tooltip-preferences]<br>
+[[MediaWiki_talk:Tooltip-preferences|Talk]]
+</td><td>
+My preferences
+</td><td>
+{{int:Tooltip-preferences}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-preview&action=edit tooltip-preview]<br>
+[[MediaWiki_talk:Tooltip-preview|Talk]]
+</td><td>
+Preview your changes, please use this before saving! &#91;alt-p]
+</td><td>
+{{int:Tooltip-preview}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-protect&action=edit tooltip-protect]<br>
+[[MediaWiki_talk:Tooltip-protect|Talk]]
+</td><td>
+Protect this page &#91;alt-=]
+</td><td>
+{{int:Tooltip-protect}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-randompage&action=edit tooltip-randompage]<br>
+[[MediaWiki_talk:Tooltip-randompage|Talk]]
+</td><td>
+Load a random page &#91;alt-x]
+</td><td>
+{{int:Tooltip-randompage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-recentchanges&action=edit tooltip-recentchanges]<br>
+[[MediaWiki_talk:Tooltip-recentchanges|Talk]]
+</td><td>
+The list of recent changes in the wiki. &#91;alt-r]
+</td><td>
+{{int:Tooltip-recentchanges}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-recentchangeslinked&action=edit tooltip-recentchangeslinked]<br>
+[[MediaWiki_talk:Tooltip-recentchangeslinked|Talk]]
+</td><td>
+Recent changes in pages linking to this page &#91;alt-c]
+</td><td>
+{{int:Tooltip-recentchangeslinked}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-rss&action=edit tooltip-rss]<br>
+[[MediaWiki_talk:Tooltip-rss|Talk]]
+</td><td>
+RSS feed for this page
+</td><td>
+{{int:Tooltip-rss}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-save&action=edit tooltip-save]<br>
+[[MediaWiki_talk:Tooltip-save|Talk]]
+</td><td>
+Save your changes &#91;alt-s]
+</td><td>
+{{int:Tooltip-save}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-search&action=edit tooltip-search]<br>
+[[MediaWiki_talk:Tooltip-search|Talk]]
+</td><td>
+Search this wiki &#91;alt-f]
+</td><td>
+{{int:Tooltip-search}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-sitesupport&action=edit tooltip-sitesupport]<br>
+[[MediaWiki_talk:Tooltip-sitesupport|Talk]]
+</td><td>
+Support Wiktionary
+</td><td>
+{{int:Tooltip-sitesupport}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-specialpage&action=edit tooltip-specialpage]<br>
+[[MediaWiki_talk:Tooltip-specialpage|Talk]]
+</td><td>
+This is a special page, you can&#39;t edit the page itself.
+</td><td>
+{{int:Tooltip-specialpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-specialpages&action=edit tooltip-specialpages]<br>
+[[MediaWiki_talk:Tooltip-specialpages|Talk]]
+</td><td>
+List of all special pages &#91;alt-q]
+</td><td>
+{{int:Tooltip-specialpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-talk&action=edit tooltip-talk]<br>
+[[MediaWiki_talk:Tooltip-talk|Talk]]
+</td><td>
+Discussion about the content page &#91;alt-t]
+</td><td>
+{{int:Tooltip-talk}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-undelete&action=edit tooltip-undelete]<br>
+[[MediaWiki_talk:Tooltip-undelete|Talk]]
+</td><td>
+Restore the $1 edits done to this page before it was deleted &#91;alt-d]
+</td><td>
+{{int:Tooltip-undelete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-unwatch&action=edit tooltip-unwatch]<br>
+[[MediaWiki_talk:Tooltip-unwatch|Talk]]
+</td><td>
+Remove this page from your watchlist &#91;alt-w]
+</td><td>
+{{int:Tooltip-unwatch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-upload&action=edit tooltip-upload]<br>
+[[MediaWiki_talk:Tooltip-upload|Talk]]
+</td><td>
+Upload images or media files &#91;alt-u]
+</td><td>
+{{int:Tooltip-upload}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-userpage&action=edit tooltip-userpage]<br>
+[[MediaWiki_talk:Tooltip-userpage|Talk]]
+</td><td>
+My user page &#91;alt-.]
+</td><td>
+{{int:Tooltip-userpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-viewsource&action=edit tooltip-viewsource]<br>
+[[MediaWiki_talk:Tooltip-viewsource|Talk]]
+</td><td>
+This page is protected. You can view its source. &#91;alt-e]
+</td><td>
+{{int:Tooltip-viewsource}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-watch&action=edit tooltip-watch]<br>
+[[MediaWiki_talk:Tooltip-watch|Talk]]
+</td><td>
+Add this page to your watchlist &#91;alt-w]
+</td><td>
+{{int:Tooltip-watch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-watchlist&action=edit tooltip-watchlist]<br>
+[[MediaWiki_talk:Tooltip-watchlist|Talk]]
+</td><td>
+The list of pages you&#39;re monitoring for changes. &#91;alt-l]
+</td><td>
+{{int:Tooltip-watchlist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Tooltip-whatlinkshere&action=edit tooltip-whatlinkshere]<br>
+[[MediaWiki_talk:Tooltip-whatlinkshere|Talk]]
+</td><td>
+List of all wiki pages that link here &#91;alt-b]
+</td><td>
+{{int:Tooltip-whatlinkshere}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uclinks&action=edit uclinks]<br>
+[[MediaWiki_talk:Uclinks|Talk]]
+</td><td>
+View the last $1 changes; view the last $2 days.
+</td><td>
+{{int:Uclinks}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ucnote&action=edit ucnote]<br>
+[[MediaWiki_talk:Ucnote|Talk]]
+</td><td>
+Below are this user&#39;s last &lt;b&gt;$1&lt;/b&gt; changes in the last &lt;b&gt;$2&lt;/b&gt; days.
+</td><td>
+{{int:Ucnote}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uctop&action=edit uctop]<br>
+[[MediaWiki_talk:Uctop|Talk]]
+</td><td>
+ (top)
+</td><td>
+{{int:Uctop}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unblockip&action=edit unblockip]<br>
+[[MediaWiki_talk:Unblockip|Talk]]
+</td><td>
+Unblock user
+</td><td>
+{{int:Unblockip}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unblockiptext&action=edit unblockiptext]<br>
+[[MediaWiki_talk:Unblockiptext|Talk]]
+</td><td>
+Use the form below to restore write access
+to a previously blocked IP address or username.
+</td><td>
+{{int:Unblockiptext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unblocklink&action=edit unblocklink]<br>
+[[MediaWiki_talk:Unblocklink|Talk]]
+</td><td>
+unblock
+</td><td>
+{{int:Unblocklink}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unblocklogentry&action=edit unblocklogentry]<br>
+[[MediaWiki_talk:Unblocklogentry|Talk]]
+</td><td>
+unblocked &quot;$1&quot;
+</td><td>
+{{int:Unblocklogentry}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undelete&action=edit undelete]<br>
+[[MediaWiki_talk:Undelete|Talk]]
+</td><td>
+Restore deleted page
+</td><td>
+{{int:Undelete}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undelete_short&action=edit undelete_short]<br>
+[[MediaWiki_talk:Undelete_short|Talk]]
+</td><td>
+Undelete $1 edits
+</td><td>
+{{int:Undelete_short}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletearticle&action=edit undeletearticle]<br>
+[[MediaWiki_talk:Undeletearticle|Talk]]
+</td><td>
+Restore deleted page
+</td><td>
+{{int:Undeletearticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletebtn&action=edit undeletebtn]<br>
+[[MediaWiki_talk:Undeletebtn|Talk]]
+</td><td>
+Restore!
+</td><td>
+{{int:Undeletebtn}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletedarticle&action=edit undeletedarticle]<br>
+[[MediaWiki_talk:Undeletedarticle|Talk]]
+</td><td>
+restored &quot;$1&quot;
+</td><td>
+{{int:Undeletedarticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletedtext&action=edit undeletedtext]<br>
+[[MediaWiki_talk:Undeletedtext|Talk]]
+</td><td>
+&#91;&#91;$1]] has been successfully restored.
+See &#91;&#91;Wiktionary:Deletion_log]] for a record of recent deletions and restorations.
+</td><td>
+{{int:Undeletedtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletehistory&action=edit undeletehistory]<br>
+[[MediaWiki_talk:Undeletehistory|Talk]]
+</td><td>
+If you restore the page, all revisions will be restored to the history.
+If a new page with the same name has been created since the deletion, the restored
+revisions will appear in the prior history, and the current revision of the live page
+will not be automatically replaced.
+</td><td>
+{{int:Undeletehistory}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletepage&action=edit undeletepage]<br>
+[[MediaWiki_talk:Undeletepage|Talk]]
+</td><td>
+View and restore deleted pages
+</td><td>
+{{int:Undeletepage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeletepagetext&action=edit undeletepagetext]<br>
+[[MediaWiki_talk:Undeletepagetext|Talk]]
+</td><td>
+The following pages have been deleted but are still in the archive and
+can be restored. The archive may be periodically cleaned out.
+</td><td>
+{{int:Undeletepagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeleterevision&action=edit undeleterevision]<br>
+[[MediaWiki_talk:Undeleterevision|Talk]]
+</td><td>
+Deleted revision as of $1
+</td><td>
+{{int:Undeleterevision}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Undeleterevisions&action=edit undeleterevisions]<br>
+[[MediaWiki_talk:Undeleterevisions|Talk]]
+</td><td>
+$1 revisions archived
+</td><td>
+{{int:Undeleterevisions}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unexpected&action=edit unexpected]<br>
+[[MediaWiki_talk:Unexpected|Talk]]
+</td><td>
+Unexpected value: &quot;$1&quot;=&quot;$2&quot;.
+</td><td>
+{{int:Unexpected}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockbtn&action=edit unlockbtn]<br>
+[[MediaWiki_talk:Unlockbtn|Talk]]
+</td><td>
+Unlock database
+</td><td>
+{{int:Unlockbtn}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockconfirm&action=edit unlockconfirm]<br>
+[[MediaWiki_talk:Unlockconfirm|Talk]]
+</td><td>
+Yes, I really want to unlock the database.
+</td><td>
+{{int:Unlockconfirm}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockdb&action=edit unlockdb]<br>
+[[MediaWiki_talk:Unlockdb|Talk]]
+</td><td>
+Unlock database
+</td><td>
+{{int:Unlockdb}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockdbsuccesssub&action=edit unlockdbsuccesssub]<br>
+[[MediaWiki_talk:Unlockdbsuccesssub|Talk]]
+</td><td>
+Database lock removed
+</td><td>
+{{int:Unlockdbsuccesssub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockdbsuccesstext&action=edit unlockdbsuccesstext]<br>
+[[MediaWiki_talk:Unlockdbsuccesstext|Talk]]
+</td><td>
+The database has been unlocked.
+</td><td>
+{{int:Unlockdbsuccesstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unlockdbtext&action=edit unlockdbtext]<br>
+[[MediaWiki_talk:Unlockdbtext|Talk]]
+</td><td>
+Unlocking the database will restore the ability of all
+users to edit pages, change their preferences, edit their watchlists, and
+other things requiring changes in the database.
+Please confirm that this is what you intend to do.
+</td><td>
+{{int:Unlockdbtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotect&action=edit unprotect]<br>
+[[MediaWiki_talk:Unprotect|Talk]]
+</td><td>
+Unprotect
+</td><td>
+{{int:Unprotect}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotectcomment&action=edit unprotectcomment]<br>
+[[MediaWiki_talk:Unprotectcomment|Talk]]
+</td><td>
+Reason for unprotecting
+</td><td>
+{{int:Unprotectcomment}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotectedarticle&action=edit unprotectedarticle]<br>
+[[MediaWiki_talk:Unprotectedarticle|Talk]]
+</td><td>
+unprotected &#91;&#91;$1]]
+</td><td>
+{{int:Unprotectedarticle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotectsub&action=edit unprotectsub]<br>
+[[MediaWiki_talk:Unprotectsub|Talk]]
+</td><td>
+(Unprotecting &quot;$1&quot;)
+</td><td>
+{{int:Unprotectsub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unprotectthispage&action=edit unprotectthispage]<br>
+[[MediaWiki_talk:Unprotectthispage|Talk]]
+</td><td>
+Unprotect this page
+</td><td>
+{{int:Unprotectthispage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unusedimages&action=edit unusedimages]<br>
+[[MediaWiki_talk:Unusedimages|Talk]]
+</td><td>
+Unused images
+</td><td>
+{{int:Unusedimages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unusedimagestext&action=edit unusedimagestext]<br>
+[[MediaWiki_talk:Unusedimagestext|Talk]]
+</td><td>
+&lt;p&gt;Please note that other web sites may link to an image with
+a direct URL, and so may still be listed here despite being
+in active use.
+</td><td>
+{{int:Unusedimagestext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unwatch&action=edit unwatch]<br>
+[[MediaWiki_talk:Unwatch|Talk]]
+</td><td>
+Unwatch
+</td><td>
+{{int:Unwatch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Unwatchthispage&action=edit unwatchthispage]<br>
+[[MediaWiki_talk:Unwatchthispage|Talk]]
+</td><td>
+Stop watching
+</td><td>
+{{int:Unwatchthispage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Updated&action=edit updated]<br>
+[[MediaWiki_talk:Updated|Talk]]
+</td><td>
+(Updated)
+</td><td>
+{{int:Updated}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Upload&action=edit upload]<br>
+[[MediaWiki_talk:Upload|Talk]]
+</td><td>
+Upload file
+</td><td>
+{{int:Upload}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadbtn&action=edit uploadbtn]<br>
+[[MediaWiki_talk:Uploadbtn|Talk]]
+</td><td>
+Upload file
+</td><td>
+{{int:Uploadbtn}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploaddisabled&action=edit uploaddisabled]<br>
+[[MediaWiki_talk:Uploaddisabled|Talk]]
+</td><td>
+Sorry, uploading is disabled.
+</td><td>
+{{int:Uploaddisabled}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadedfiles&action=edit uploadedfiles]<br>
+[[MediaWiki_talk:Uploadedfiles|Talk]]
+</td><td>
+Uploaded files
+</td><td>
+{{int:Uploadedfiles}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadedimage&action=edit uploadedimage]<br>
+[[MediaWiki_talk:Uploadedimage|Talk]]
+</td><td>
+uploaded &quot;$1&quot;
+</td><td>
+{{int:Uploadedimage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploaderror&action=edit uploaderror]<br>
+[[MediaWiki_talk:Uploaderror|Talk]]
+</td><td>
+Upload error
+</td><td>
+{{int:Uploaderror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadfile&action=edit uploadfile]<br>
+[[MediaWiki_talk:Uploadfile|Talk]]
+</td><td>
+Upload images, sounds, documents etc.
+</td><td>
+{{int:Uploadfile}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadlink&action=edit uploadlink]<br>
+[[MediaWiki_talk:Uploadlink|Talk]]
+</td><td>
+Upload images
+</td><td>
+{{int:Uploadlink}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadlog&action=edit uploadlog]<br>
+[[MediaWiki_talk:Uploadlog|Talk]]
+</td><td>
+upload log
+</td><td>
+{{int:Uploadlog}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadlogpage&action=edit uploadlogpage]<br>
+[[MediaWiki_talk:Uploadlogpage|Talk]]
+</td><td>
+Upload_log
+</td><td>
+{{int:Uploadlogpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadlogpagetext&action=edit uploadlogpagetext]<br>
+[[MediaWiki_talk:Uploadlogpagetext|Talk]]
+</td><td>
+Below is a list of the most recent file uploads.
+All times shown are server time (UTC).
+&lt;ul&gt;
+&lt;/ul&gt;
+
+</td><td>
+{{int:Uploadlogpagetext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadnologin&action=edit uploadnologin]<br>
+[[MediaWiki_talk:Uploadnologin|Talk]]
+</td><td>
+Not logged in
+</td><td>
+{{int:Uploadnologin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadnologintext&action=edit uploadnologintext]<br>
+[[MediaWiki_talk:Uploadnologintext|Talk]]
+</td><td>
+You must be &lt;a href=&quot;/wiki/Special:Userlogin&quot;&gt;logged in&lt;/a&gt;
+to upload files.
+</td><td>
+{{int:Uploadnologintext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadtext&action=edit uploadtext]<br>
+[[MediaWiki_talk:Uploadtext|Talk]]
+</td><td>
+&lt;strong&gt;STOP!&lt;/strong&gt; Before you upload here,
+make sure to read and follow the &lt;a href=&quot;/wiki/Special:Image_use_policy&quot;&gt;image use policy&lt;/a&gt;.
+&lt;p&gt;If a file with the name you are specifying already
+exists on the wiki, it&#39;ll be replaced without warning.
+So unless you mean to update a file, it&#39;s a good idea
+to first check if such a file exists.
+&lt;p&gt;To view or search previously uploaded images,
+go to the &lt;a href=&quot;/wiki/Special:Imagelist&quot;&gt;list of uploaded images&lt;/a&gt;.
+Uploads and deletions are logged on the &lt;a href=&quot;/wiki/Wiktionary:Upload_log&quot;&gt;upload log&lt;/a&gt;.
+&lt;/p&gt;&lt;p&gt;Use the form below to upload new image files for use in
+illustrating your pages.
+On most browsers, you will see a &quot;Browse...&quot; button, which will
+bring up your operating system&#39;s standard file open dialog.
+Choosing a file will fill the name of that file into the text
+field next to the button.
+You must also check the box affirming that you are not
+violating any copyrights by uploading the file.
+Press the &quot;Upload&quot; button to finish the upload.
+This may take some time if you have a slow internet connection.
+&lt;p&gt;The preferred formats are JPEG for photographic images, PNG
+for drawings and other iconic images, and OGG for sounds.
+Please name your files descriptively to avoid confusion.
+To include the image in a page, use a link in the form
+&lt;b&gt;&#91;&#91;Image:file.jpg]]&lt;/b&gt; or &lt;b&gt;&#91;&#91;Image:file.png&#124;alt text]]&lt;/b&gt;
+or &lt;b&gt;&#91;&#91;Media:file.ogg]]&lt;/b&gt; for sounds.
+&lt;p&gt;Please note that as with wiki pages, others may edit or
+delete your uploads if they think it serves the project, and
+you may be blocked from uploading if you abuse the system.
+</td><td>
+{{int:Uploadtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Uploadwarning&action=edit uploadwarning]<br>
+[[MediaWiki_talk:Uploadwarning|Talk]]
+</td><td>
+Upload warning
+</td><td>
+{{int:Uploadwarning}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:User_rights_set&action=edit user_rights_set]<br>
+[[MediaWiki_talk:User_rights_set|Talk]]
+</td><td>
+&lt;b&gt;User rights for &quot;$1&quot; updated&lt;/b&gt;
+</td><td>
+{{int:User_rights_set}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Usercssjs&action=edit usercssjs]<br>
+[[MediaWiki_talk:Usercssjs|Talk]]
+</td><td>
+&#39;&#39;&#39;Note:&#39;&#39;&#39; After saving, you have to tell your bowser to get the new version: &#39;&#39;&#39;Mozilla:&#39;&#39;&#39; click &#39;&#39;reload&#39;&#39;(or &#39;&#39;ctrl-r&#39;&#39;), &#39;&#39;&#39;IE / Opera:&#39;&#39;&#39; &#39;&#39;ctrl-f5&#39;&#39;, &#39;&#39;&#39;Safari:&#39;&#39;&#39; &#39;&#39;cmd-r&#39;&#39;, &#39;&#39;&#39;Konqueror&#39;&#39;&#39; &#39;&#39;ctrl-r&#39;&#39;.
+</td><td>
+{{int:Usercssjs}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Usercssjsyoucanpreview&action=edit usercssjsyoucanpreview]<br>
+[[MediaWiki_talk:Usercssjsyoucanpreview|Talk]]
+</td><td>
+&lt;strong&gt;Tip:&lt;/strong&gt; Use the &#39;Show preview&#39; button to test your new css/js before saving.
+</td><td>
+{{int:Usercssjsyoucanpreview}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Usercsspreview&action=edit usercsspreview]<br>
+[[MediaWiki_talk:Usercsspreview|Talk]]
+</td><td>
+&#39;&#39;&#39;Remember that you are only previewing your user css, it has not yet been saved!&#39;&#39;&#39;
+</td><td>
+{{int:Usercsspreview}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userexists&action=edit userexists]<br>
+[[MediaWiki_talk:Userexists|Talk]]
+</td><td>
+The user name you entered is already in use. Please choose a different name.
+</td><td>
+{{int:Userexists}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userjspreview&action=edit userjspreview]<br>
+[[MediaWiki_talk:Userjspreview|Talk]]
+</td><td>
+&#39;&#39;&#39;Remember that you are only testing/previewing your user javascript, it has not yet been saved!&#39;&#39;&#39;
+</td><td>
+{{int:Userjspreview}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userlogin&action=edit userlogin]<br>
+[[MediaWiki_talk:Userlogin|Talk]]
+</td><td>
+Log in
+</td><td>
+{{int:Userlogin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userlogout&action=edit userlogout]<br>
+[[MediaWiki_talk:Userlogout|Talk]]
+</td><td>
+Log out
+</td><td>
+{{int:Userlogout}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Usermailererror&action=edit usermailererror]<br>
+[[MediaWiki_talk:Usermailererror|Talk]]
+</td><td>
+Mail object returned error:
+</td><td>
+{{int:Usermailererror}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userpage&action=edit userpage]<br>
+[[MediaWiki_talk:Userpage|Talk]]
+</td><td>
+View user page
+</td><td>
+{{int:Userpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userstats&action=edit userstats]<br>
+[[MediaWiki_talk:Userstats|Talk]]
+</td><td>
+User statistics
+</td><td>
+{{int:Userstats}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Userstatstext&action=edit userstatstext]<br>
+[[MediaWiki_talk:Userstatstext|Talk]]
+</td><td>
+There are &#39;&#39;&#39;$1&#39;&#39;&#39; registered users.
+&#39;&#39;&#39;$2&#39;&#39;&#39; of these are administrators (see $3).
+</td><td>
+{{int:Userstatstext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Version&action=edit version]<br>
+[[MediaWiki_talk:Version|Talk]]
+</td><td>
+Version
+</td><td>
+{{int:Version}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Viewcount&action=edit viewcount]<br>
+[[MediaWiki_talk:Viewcount|Talk]]
+</td><td>
+This page has been accessed $1 times.
+</td><td>
+{{int:Viewcount}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Viewprevnext&action=edit viewprevnext]<br>
+[[MediaWiki_talk:Viewprevnext|Talk]]
+</td><td>
+View ($1) ($2) ($3).
+</td><td>
+{{int:Viewprevnext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Viewsource&action=edit viewsource]<br>
+[[MediaWiki_talk:Viewsource|Talk]]
+</td><td>
+View source
+</td><td>
+{{int:Viewsource}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Viewtalkpage&action=edit viewtalkpage]<br>
+[[MediaWiki_talk:Viewtalkpage|Talk]]
+</td><td>
+View discussion
+</td><td>
+{{int:Viewtalkpage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wantedpages&action=edit wantedpages]<br>
+[[MediaWiki_talk:Wantedpages|Talk]]
+</td><td>
+Wanted pages
+</td><td>
+{{int:Wantedpages}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watch&action=edit watch]<br>
+[[MediaWiki_talk:Watch|Talk]]
+</td><td>
+Watch
+</td><td>
+{{int:Watch}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchdetails&action=edit watchdetails]<br>
+[[MediaWiki_talk:Watchdetails|Talk]]
+</td><td>
+($1 pages watched not counting talk pages;
+$2 total pages edited since cutoff;
+$3...
+&lt;a href=&#39;$4&#39;&gt;show and edit complete list&lt;/a&gt;.)
+</td><td>
+{{int:Watchdetails}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watcheditlist&action=edit watcheditlist]<br>
+[[MediaWiki_talk:Watcheditlist|Talk]]
+</td><td>
+Here&#39;s an alphabetical list of your
+watched pages. Check the boxes of pages you want to remove
+from your watchlist and click the &#39;remove checked&#39; button
+at the bottom of the screen.
+</td><td>
+{{int:Watcheditlist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchlist&action=edit watchlist]<br>
+[[MediaWiki_talk:Watchlist|Talk]]
+</td><td>
+My watchlist
+</td><td>
+{{int:Watchlist}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchlistcontains&action=edit watchlistcontains]<br>
+[[MediaWiki_talk:Watchlistcontains|Talk]]
+</td><td>
+Your watchlist contains $1 pages.
+</td><td>
+{{int:Watchlistcontains}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchlistsub&action=edit watchlistsub]<br>
+[[MediaWiki_talk:Watchlistsub|Talk]]
+</td><td>
+(for user &quot;$1&quot;)
+</td><td>
+{{int:Watchlistsub}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchmethod-list&action=edit watchmethod-list]<br>
+[[MediaWiki_talk:Watchmethod-list|Talk]]
+</td><td>
+checking watched pages for recent edits
+</td><td>
+{{int:Watchmethod-list}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchmethod-recent&action=edit watchmethod-recent]<br>
+[[MediaWiki_talk:Watchmethod-recent|Talk]]
+</td><td>
+checking recent edits for watched pages
+</td><td>
+{{int:Watchmethod-recent}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchnochange&action=edit watchnochange]<br>
+[[MediaWiki_talk:Watchnochange|Talk]]
+</td><td>
+None of your watched items were edited in the time period displayed.
+</td><td>
+{{int:Watchnochange}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchnologin&action=edit watchnologin]<br>
+[[MediaWiki_talk:Watchnologin|Talk]]
+</td><td>
+Not logged in
+</td><td>
+{{int:Watchnologin}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchnologintext&action=edit watchnologintext]<br>
+[[MediaWiki_talk:Watchnologintext|Talk]]
+</td><td>
+You must be &lt;a href=&quot;/wiki/Special:Userlogin&quot;&gt;logged in&lt;/a&gt;
+to modify your watchlist.
+</td><td>
+{{int:Watchnologintext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchthis&action=edit watchthis]<br>
+[[MediaWiki_talk:Watchthis|Talk]]
+</td><td>
+Watch this page
+</td><td>
+{{int:Watchthis}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Watchthispage&action=edit watchthispage]<br>
+[[MediaWiki_talk:Watchthispage|Talk]]
+</td><td>
+Watch this page
+</td><td>
+{{int:Watchthispage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Welcomecreation&action=edit welcomecreation]<br>
+[[MediaWiki_talk:Welcomecreation|Talk]]
+</td><td>
+&lt;h2&gt;Welcome, $1!&lt;/h2&gt;&lt;p&gt;Your account has been created.
+Don&#39;t forget to change your Wiktionary preferences.
+</td><td>
+{{int:Welcomecreation}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whatlinkshere&action=edit whatlinkshere]<br>
+[[MediaWiki_talk:Whatlinkshere|Talk]]
+</td><td>
+What links here
+</td><td>
+{{int:Whatlinkshere}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistacctext&action=edit whitelistacctext]<br>
+[[MediaWiki_talk:Whitelistacctext|Talk]]
+</td><td>
+To be allowed to create accounts in this Wiki you have to &#91;&#91;Special:Userlogin&#124;log]] in and have the appropriate permissions.
+</td><td>
+{{int:Whitelistacctext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistacctitle&action=edit whitelistacctitle]<br>
+[[MediaWiki_talk:Whitelistacctitle|Talk]]
+</td><td>
+You are not allowed to create an account
+</td><td>
+{{int:Whitelistacctitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistedittext&action=edit whitelistedittext]<br>
+[[MediaWiki_talk:Whitelistedittext|Talk]]
+</td><td>
+You have to &#91;&#91;Special:Userlogin&#124;login]] to edit pages.
+</td><td>
+{{int:Whitelistedittext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistedittitle&action=edit whitelistedittitle]<br>
+[[MediaWiki_talk:Whitelistedittitle|Talk]]
+</td><td>
+Login required to edit
+</td><td>
+{{int:Whitelistedittitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistreadtext&action=edit whitelistreadtext]<br>
+[[MediaWiki_talk:Whitelistreadtext|Talk]]
+</td><td>
+You have to &#91;&#91;Special:Userlogin&#124;login]] to read pages.
+</td><td>
+{{int:Whitelistreadtext}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Whitelistreadtitle&action=edit whitelistreadtitle]<br>
+[[MediaWiki_talk:Whitelistreadtitle|Talk]]
+</td><td>
+Login required to read
+</td><td>
+{{int:Whitelistreadtitle}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wikipediapage&action=edit wikipediapage]<br>
+[[MediaWiki_talk:Wikipediapage|Talk]]
+</td><td>
+View project page
+</td><td>
+{{int:Wikipediapage}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wikititlesuffix&action=edit wikititlesuffix]<br>
+[[MediaWiki_talk:Wikititlesuffix|Talk]]
+</td><td>
+Wiktionary
+</td><td>
+{{int:Wikititlesuffix}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wlnote&action=edit wlnote]<br>
+[[MediaWiki_talk:Wlnote|Talk]]
+</td><td>
+Below are the last $1 changes in the last &lt;b&gt;$2&lt;/b&gt; hours.
+</td><td>
+{{int:Wlnote}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wlsaved&action=edit wlsaved]<br>
+[[MediaWiki_talk:Wlsaved|Talk]]
+</td><td>
+This is a saved version of your watchlist.
+</td><td>
+{{int:Wlsaved}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wlshowlast&action=edit wlshowlast]<br>
+[[MediaWiki_talk:Wlshowlast|Talk]]
+</td><td>
+Show last $1 hours $2 days $3
+</td><td>
+{{int:Wlshowlast}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wrong_wfQuery_params&action=edit wrong_wfQuery_params]<br>
+[[MediaWiki_talk:Wrong_wfQuery_params|Talk]]
+</td><td>
+Incorrect parameters to wfQuery()&lt;br /&gt;
+Function: $1&lt;br /&gt;
+Query: $2
+
+</td><td>
+{{int:Wrong_wfQuery_params}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Wrongpassword&action=edit wrongpassword]<br>
+[[MediaWiki_talk:Wrongpassword|Talk]]
+</td><td>
+The password you entered is incorrect. Please try again.
+</td><td>
+{{int:Wrongpassword}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourdiff&action=edit yourdiff]<br>
+[[MediaWiki_talk:Yourdiff|Talk]]
+</td><td>
+Differences
+</td><td>
+{{int:Yourdiff}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Youremail&action=edit youremail]<br>
+[[MediaWiki_talk:Youremail|Talk]]
+</td><td>
+Your email*
+</td><td>
+{{int:Youremail}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourname&action=edit yourname]<br>
+[[MediaWiki_talk:Yourname|Talk]]
+</td><td>
+Your user name
+</td><td>
+{{int:Yourname}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yournick&action=edit yournick]<br>
+[[MediaWiki_talk:Yournick|Talk]]
+</td><td>
+Your nickname (for signatures)
+</td><td>
+{{int:Yournick}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourpassword&action=edit yourpassword]<br>
+[[MediaWiki_talk:Yourpassword|Talk]]
+</td><td>
+Your password
+</td><td>
+{{int:Yourpassword}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourpasswordagain&action=edit yourpasswordagain]<br>
+[[MediaWiki_talk:Yourpasswordagain|Talk]]
+</td><td>
+Retype password
+</td><td>
+{{int:Yourpasswordagain}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourrealname&action=edit yourrealname]<br>
+[[MediaWiki_talk:Yourrealname|Talk]]
+</td><td>
+Your real name*
+</td><td>
+{{int:Yourrealname}}
+</td></tr><tr><td>
+[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Yourtext&action=edit yourtext]<br>
+[[MediaWiki_talk:Yourtext|Talk]]
+</td><td>
+Your text
+</td><td>
+{{int:Yourtext}}
+</td></tr></table>
+
diff --git a/tests/parser/preprocess/Factorial.expected b/tests/parser/preprocess/Factorial.expected
new file mode 100644
index 00000000..a10fd6ca
--- /dev/null
+++ b/tests/parser/preprocess/Factorial.expected
@@ -0,0 +1,17 @@
+<root><template><title>#expr:<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=00</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>01<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=01</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*01<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=02</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*02<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=03</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*03<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=04</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*04<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=05</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*05<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=06</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*06<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=07</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*07<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=08</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*08<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=09</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*09<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=10</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*10<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=11</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*11<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=12</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*12<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=13</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*13<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=14</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*14<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=15</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*15<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=16</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*16<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=17</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*17<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=18</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*18<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=19</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*19<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=20</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*20<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=21</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*21<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=22</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*22<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=23</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*23<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=24</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*24<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=25</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*25<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=26</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*26<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=27</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*27<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=28</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*28<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=29</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*29<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=30</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*30<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=31</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*31<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=32</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*32<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=33</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*33<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=34</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*34<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=35</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*35<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=36</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*36<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=37</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*37<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=38</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*38<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=39</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*39<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=40</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*40<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=41</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*41<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=42</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*42<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=43</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*43<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=44</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*44<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=45</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*45<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=46</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*46<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=47</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*47<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=48</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*48<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=49</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*49<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=50</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*50<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=51</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*51<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=52</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*52<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=53</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*53<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=54</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*54<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=55</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*55<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=56</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*56<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=57</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*57<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=58</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*58<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=59</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*59<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=60</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*60<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=61</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*61<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=62</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*62<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=63</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*63<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=64</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*64<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=65</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*65<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=66</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*66<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=67</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*67<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=68</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*68<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=69</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*69<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=70</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*70<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=71</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*71<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=72</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*72<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=73</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*73<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=74</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*74<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=75</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*75<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=76</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*76<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=77</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*77<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=78</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*78<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=79</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*79<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=80</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*80<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=81</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*81<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=82</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*82<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=83</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*83<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=84</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*84<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=85</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*85<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=86</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*86<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=87</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*87<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=88</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*88<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=89</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*89<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=90</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*90<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=91</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*91<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=92</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*92<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=93</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*93<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=94</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*94<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=95</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*95<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=96</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*96<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=97</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*97<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=98</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*98<template><title>#ifeq:<template><title>#expr:<tplarg><title>1</title></tplarg>&gt;=99</title></template></title><part><name index="1" /><value>1</value></part><part><name index="2" /><value>*99</value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></value></part></template></title></template><ignore>&lt;noinclude&gt;</ignore>
+<template lineStart="1"><title>Template documentation</title></template>
+This template finds the [[factorial]] of a number. To use it, enter:&lt;br /&gt;
+&lt;code&gt;&lt;nowiki&gt;<template><title>factorial</title><part><name index="1" /><value>input</value></part></template>&lt;/nowiki&gt;&lt;/code&gt;&lt;br /&gt;
+The input must be a positive interger smaller than 100 (better than most calculators, which go up to only 69). This template works by repeating conditional multiplications. Examples:&lt;br /&gt;
+*&lt;nowiki&gt;<template><title>factorial</title><part><name index="1" /><value>2</value></part></template>&lt;/nowiki&gt; gives <template><title>factorial</title><part><name index="1" /><value>2</value></part></template>
+*&lt;nowiki&gt;<template><title>factorial</title><part><name index="1" /><value>3</value></part></template>&lt;/nowiki&gt; gives <template><title>factorial</title><part><name index="1" /><value>3</value></part></template>
+*&lt;nowiki&gt;<template><title>factorial</title><part><name index="1" /><value>5</value></part></template>&lt;/nowiki&gt; gives <template><title>factorial</title><part><name index="1" /><value>5</value></part></template>
+*&lt;nowiki&gt;<template><title>factorial</title><part><name index="1" /><value>10</value></part></template>&lt;/nowiki&gt; gives <template><title>factorial</title><part><name index="1" /><value>10</value></part></template>
+*&lt;nowiki&gt;<template><title>factorial</title><part><name index="1" /><value>80</value></part></template>&lt;/nowiki&gt; gives <template><title>factorial</title><part><name index="1" /><value>80</value></part></template>
+*&lt;nowiki&gt;<template><title>factorial</title><part><name index="1" /><value>0.5</value></part></template>&lt;/nowiki&gt; gives <template><title>factorial</title><part><name index="1" /><value>0.5</value></part></template> (invalid input)
+*&lt;nowiki&gt;<template><title>factorial</title><part><name index="1" /><value>-1</value></part></template>&lt;/nowiki&gt; gives <template><title>factorial</title><part><name index="1" /><value>-1</value></part></template> (invalid input)
+<template lineStart="1"><title>esoteric</title></template>
+[[Category:Mathematical templates|<template><title>PAGENAME</title></template>]]
+<ignore>&lt;/noinclude&gt;</ignore>
+
+</root> \ No newline at end of file
diff --git a/tests/parser/preprocess/Factorial.txt b/tests/parser/preprocess/Factorial.txt
new file mode 100644
index 00000000..316f0792
--- /dev/null
+++ b/tests/parser/preprocess/Factorial.txt
@@ -0,0 +1,16 @@
+{{#expr:{{#ifeq:{{#expr:{{{1}}}>=00}}|1|01{{#ifeq:{{#expr:{{{1}}}>=01}}|1|*01{{#ifeq:{{#expr:{{{1}}}>=02}}|1|*02{{#ifeq:{{#expr:{{{1}}}>=03}}|1|*03{{#ifeq:{{#expr:{{{1}}}>=04}}|1|*04{{#ifeq:{{#expr:{{{1}}}>=05}}|1|*05{{#ifeq:{{#expr:{{{1}}}>=06}}|1|*06{{#ifeq:{{#expr:{{{1}}}>=07}}|1|*07{{#ifeq:{{#expr:{{{1}}}>=08}}|1|*08{{#ifeq:{{#expr:{{{1}}}>=09}}|1|*09{{#ifeq:{{#expr:{{{1}}}>=10}}|1|*10{{#ifeq:{{#expr:{{{1}}}>=11}}|1|*11{{#ifeq:{{#expr:{{{1}}}>=12}}|1|*12{{#ifeq:{{#expr:{{{1}}}>=13}}|1|*13{{#ifeq:{{#expr:{{{1}}}>=14}}|1|*14{{#ifeq:{{#expr:{{{1}}}>=15}}|1|*15{{#ifeq:{{#expr:{{{1}}}>=16}}|1|*16{{#ifeq:{{#expr:{{{1}}}>=17}}|1|*17{{#ifeq:{{#expr:{{{1}}}>=18}}|1|*18{{#ifeq:{{#expr:{{{1}}}>=19}}|1|*19{{#ifeq:{{#expr:{{{1}}}>=20}}|1|*20{{#ifeq:{{#expr:{{{1}}}>=21}}|1|*21{{#ifeq:{{#expr:{{{1}}}>=22}}|1|*22{{#ifeq:{{#expr:{{{1}}}>=23}}|1|*23{{#ifeq:{{#expr:{{{1}}}>=24}}|1|*24{{#ifeq:{{#expr:{{{1}}}>=25}}|1|*25{{#ifeq:{{#expr:{{{1}}}>=26}}|1|*26{{#ifeq:{{#expr:{{{1}}}>=27}}|1|*27{{#ifeq:{{#expr:{{{1}}}>=28}}|1|*28{{#ifeq:{{#expr:{{{1}}}>=29}}|1|*29{{#ifeq:{{#expr:{{{1}}}>=30}}|1|*30{{#ifeq:{{#expr:{{{1}}}>=31}}|1|*31{{#ifeq:{{#expr:{{{1}}}>=32}}|1|*32{{#ifeq:{{#expr:{{{1}}}>=33}}|1|*33{{#ifeq:{{#expr:{{{1}}}>=34}}|1|*34{{#ifeq:{{#expr:{{{1}}}>=35}}|1|*35{{#ifeq:{{#expr:{{{1}}}>=36}}|1|*36{{#ifeq:{{#expr:{{{1}}}>=37}}|1|*37{{#ifeq:{{#expr:{{{1}}}>=38}}|1|*38{{#ifeq:{{#expr:{{{1}}}>=39}}|1|*39{{#ifeq:{{#expr:{{{1}}}>=40}}|1|*40{{#ifeq:{{#expr:{{{1}}}>=41}}|1|*41{{#ifeq:{{#expr:{{{1}}}>=42}}|1|*42{{#ifeq:{{#expr:{{{1}}}>=43}}|1|*43{{#ifeq:{{#expr:{{{1}}}>=44}}|1|*44{{#ifeq:{{#expr:{{{1}}}>=45}}|1|*45{{#ifeq:{{#expr:{{{1}}}>=46}}|1|*46{{#ifeq:{{#expr:{{{1}}}>=47}}|1|*47{{#ifeq:{{#expr:{{{1}}}>=48}}|1|*48{{#ifeq:{{#expr:{{{1}}}>=49}}|1|*49{{#ifeq:{{#expr:{{{1}}}>=50}}|1|*50{{#ifeq:{{#expr:{{{1}}}>=51}}|1|*51{{#ifeq:{{#expr:{{{1}}}>=52}}|1|*52{{#ifeq:{{#expr:{{{1}}}>=53}}|1|*53{{#ifeq:{{#expr:{{{1}}}>=54}}|1|*54{{#ifeq:{{#expr:{{{1}}}>=55}}|1|*55{{#ifeq:{{#expr:{{{1}}}>=56}}|1|*56{{#ifeq:{{#expr:{{{1}}}>=57}}|1|*57{{#ifeq:{{#expr:{{{1}}}>=58}}|1|*58{{#ifeq:{{#expr:{{{1}}}>=59}}|1|*59{{#ifeq:{{#expr:{{{1}}}>=60}}|1|*60{{#ifeq:{{#expr:{{{1}}}>=61}}|1|*61{{#ifeq:{{#expr:{{{1}}}>=62}}|1|*62{{#ifeq:{{#expr:{{{1}}}>=63}}|1|*63{{#ifeq:{{#expr:{{{1}}}>=64}}|1|*64{{#ifeq:{{#expr:{{{1}}}>=65}}|1|*65{{#ifeq:{{#expr:{{{1}}}>=66}}|1|*66{{#ifeq:{{#expr:{{{1}}}>=67}}|1|*67{{#ifeq:{{#expr:{{{1}}}>=68}}|1|*68{{#ifeq:{{#expr:{{{1}}}>=69}}|1|*69{{#ifeq:{{#expr:{{{1}}}>=70}}|1|*70{{#ifeq:{{#expr:{{{1}}}>=71}}|1|*71{{#ifeq:{{#expr:{{{1}}}>=72}}|1|*72{{#ifeq:{{#expr:{{{1}}}>=73}}|1|*73{{#ifeq:{{#expr:{{{1}}}>=74}}|1|*74{{#ifeq:{{#expr:{{{1}}}>=75}}|1|*75{{#ifeq:{{#expr:{{{1}}}>=76}}|1|*76{{#ifeq:{{#expr:{{{1}}}>=77}}|1|*77{{#ifeq:{{#expr:{{{1}}}>=78}}|1|*78{{#ifeq:{{#expr:{{{1}}}>=79}}|1|*79{{#ifeq:{{#expr:{{{1}}}>=80}}|1|*80{{#ifeq:{{#expr:{{{1}}}>=81}}|1|*81{{#ifeq:{{#expr:{{{1}}}>=82}}|1|*82{{#ifeq:{{#expr:{{{1}}}>=83}}|1|*83{{#ifeq:{{#expr:{{{1}}}>=84}}|1|*84{{#ifeq:{{#expr:{{{1}}}>=85}}|1|*85{{#ifeq:{{#expr:{{{1}}}>=86}}|1|*86{{#ifeq:{{#expr:{{{1}}}>=87}}|1|*87{{#ifeq:{{#expr:{{{1}}}>=88}}|1|*88{{#ifeq:{{#expr:{{{1}}}>=89}}|1|*89{{#ifeq:{{#expr:{{{1}}}>=90}}|1|*90{{#ifeq:{{#expr:{{{1}}}>=91}}|1|*91{{#ifeq:{{#expr:{{{1}}}>=92}}|1|*92{{#ifeq:{{#expr:{{{1}}}>=93}}|1|*93{{#ifeq:{{#expr:{{{1}}}>=94}}|1|*94{{#ifeq:{{#expr:{{{1}}}>=95}}|1|*95{{#ifeq:{{#expr:{{{1}}}>=96}}|1|*96{{#ifeq:{{#expr:{{{1}}}>=97}}|1|*97{{#ifeq:{{#expr:{{{1}}}>=98}}|1|*98{{#ifeq:{{#expr:{{{1}}}>=99}}|1|*99}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}<noinclude>
+{{Template documentation}}
+This template finds the [[factorial]] of a number. To use it, enter:<br />
+<code><nowiki>{{factorial|input}}</nowiki></code><br />
+The input must be a positive interger smaller than 100 (better than most calculators, which go up to only 69). This template works by repeating conditional multiplications. Examples:<br />
+*<nowiki>{{factorial|2}}</nowiki> gives {{factorial|2}}
+*<nowiki>{{factorial|3}}</nowiki> gives {{factorial|3}}
+*<nowiki>{{factorial|5}}</nowiki> gives {{factorial|5}}
+*<nowiki>{{factorial|10}}</nowiki> gives {{factorial|10}}
+*<nowiki>{{factorial|80}}</nowiki> gives {{factorial|80}}
+*<nowiki>{{factorial|0.5}}</nowiki> gives {{factorial|0.5}} (invalid input)
+*<nowiki>{{factorial|-1}}</nowiki> gives {{factorial|-1}} (invalid input)
+{{esoteric}}
+[[Category:Mathematical templates|{{PAGENAME}}]]
+</noinclude>
+
diff --git a/tests/parser/preprocess/Fundraising.expected b/tests/parser/preprocess/Fundraising.expected
new file mode 100644
index 00000000..f5b32cc5
--- /dev/null
+++ b/tests/parser/preprocess/Fundraising.expected
@@ -0,0 +1,18 @@
+<root>&lt;div name=&quot;fundraising&quot; id=&quot;fundraising&quot; class=&quot;plainlinks&quot; style=&quot;margin-top:5px; text-align: center; background-color: #ffffe0; border: solid 1px #e0e0c0&quot;&gt;
+'''Pwede kang [[Wikimedia:give the gift of knowledge|maghandog ng kaalaman]] sa paraan ng [[Wikimedia:Fundraising#Donation_methods|pagbibigay ng donasyon sa Pundasyong Wikimedia!]]'''
+&lt;br /&gt;
+&lt;fundraising/&gt;
+&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
+&lt;fundraisinglogo/&gt;
+&lt;br /&gt;
+&lt;b&gt;Ngayon, ang iyong [[Wikimedia:Fundraising|kontribusyon]] ay [[Wikimedia:Fundraising FAQ|itatambal]] ng isang anonimong kaibigan.&lt;/b&gt;
+&lt;br /&gt;
+&lt;small&gt;
+[[Wikimedia:Deductibility of donations|Pagbabawas sa mga buwis ng donasyon]]
+|
+[[Wikimedia:Fundraising FAQ|FAQ]]
+|
+[http://upload.wikimedia.org/wikipedia/foundation/2/28/Wikimedia_2006_fs.pdf Mga pampananalaping pahayag]
+&lt;/small&gt;
+&lt;/div&gt;
+</root> \ No newline at end of file
diff --git a/tests/parser/preprocess/Fundraising.txt b/tests/parser/preprocess/Fundraising.txt
new file mode 100644
index 00000000..b868b4d8
--- /dev/null
+++ b/tests/parser/preprocess/Fundraising.txt
@@ -0,0 +1,17 @@
+<div name="fundraising" id="fundraising" class="plainlinks" style="margin-top:5px; text-align: center; background-color: #ffffe0; border: solid 1px #e0e0c0">
+'''Pwede kang [[Wikimedia:give the gift of knowledge|maghandog ng kaalaman]] sa paraan ng [[Wikimedia:Fundraising#Donation_methods|pagbibigay ng donasyon sa Pundasyong Wikimedia!]]'''
+<br />
+<fundraising/>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+<fundraisinglogo/>
+<br />
+<b>Ngayon, ang iyong [[Wikimedia:Fundraising|kontribusyon]] ay [[Wikimedia:Fundraising FAQ|itatambal]] ng isang anonimong kaibigan.</b>
+<br />
+<small>
+[[Wikimedia:Deductibility of donations|Pagbabawas sa mga buwis ng donasyon]]
+|
+[[Wikimedia:Fundraising FAQ|FAQ]]
+|
+[http://upload.wikimedia.org/wikipedia/foundation/2/28/Wikimedia_2006_fs.pdf Mga pampananalaping pahayag]
+</small>
+</div>
diff --git a/tests/parser/preprocess/NestedTemplates.expected b/tests/parser/preprocess/NestedTemplates.expected
new file mode 100644
index 00000000..645626df
--- /dev/null
+++ b/tests/parser/preprocess/NestedTemplates.expected
@@ -0,0 +1,90 @@
+<root><template><title>vorlage</title></template>
+
+<tplarg lineStart="1"><title>argument</title></tplarg>
+
+Nach [[:meta:Help:Expansion#XML parse tree]]
+{<tplarg><title>vorlagenname</title></tplarg>}
+<template lineStart="1"><title> <template><title>vorlagenname</title></template></title></template>
+<template lineStart="1"><title><template><title>vorlagenname</title></template> </title></template>
+<template lineStart="1"><title><template><title>vorlagenname</title></template>erweiterung</title></template>
+
+<template lineStart="1"><title><tplarg><title>vorlagenname</title></tplarg></title></template>
+<tplarg lineStart="1"><title> <template><title>vorlagenname</title></template></title></tplarg>
+<template lineStart="1"><title> <tplarg><title>vorlagenname</title></tplarg></title></template>
+<tplarg lineStart="1"><title><template><title>vorlagenname</title></template> </title></tplarg>
+<template lineStart="1"><title><tplarg><title>vorlagenname</title></tplarg> </title></template>
+
+nur etwas erweitert
+<tplarg lineStart="1"><title><tplarg><title>vorlagenname</title></tplarg></title></tplarg>
+<tplarg lineStart="1"><title> <tplarg><title>vorlagenname</title></tplarg></title></tplarg>
+<tplarg lineStart="1"><title><tplarg><title>vorlagenname</title></tplarg> </title></tplarg>
+<template lineStart="1"><title> {<tplarg><title>vorlagenname</title></tplarg></title></template>}
+{<tplarg><title> <template><title>vorlagenname</title></template></title></tplarg>}
+<template lineStart="1"><title> <template><title> <template><title>vorlagenname</title></template></title></template></title></template>
+{<tplarg><title> <template><title>vorlagenname</title></template>} </title></tplarg>
+{<template><title><tplarg><title>vorlagenname</title></tplarg>} </title></template>
+{<tplarg><title><template><title>vorlagenname</title></template> </title></tplarg>}
+<template lineStart="1"><title> <template><title><template><title>vorlagenname</title></template> </title></template></title></template>
+<tplarg lineStart="1"><title> {<template><title>vorlagenname</title></template> </title></tplarg>}
+
+{<tplarg><title><tplarg><title> </title></tplarg></title></tplarg>}
+
+<template lineStart="1"><title><tplarg><title><tplarg><title> </title></tplarg></title></tplarg></title></template>
+<tplarg lineStart="1"><title><tplarg><title><template><title> </title></template> </title></tplarg></title></tplarg>
+<template lineStart="1"><title><tplarg><title><tplarg><title> </title></tplarg> </title></tplarg></title></template>
+{{<tplarg><title><tplarg><title> </title></tplarg>} </title></tplarg>}
+<tplarg lineStart="1"><title><template><title><tplarg><title> </title></tplarg></title></template> </title></tplarg>
+<template lineStart="1"><title><tplarg><title><tplarg><title> </title></tplarg></title></tplarg> </title></template>
+{<tplarg><title><template><title><template><title> </title></template> </title></template> </title></tplarg>}
+{<template><title><tplarg><title><template><title> </title></template> </title></tplarg>} </title></template>
+{<template><title><template><title><tplarg><title> </title></tplarg>} </title></template> </title></template>
+<template lineStart="1"><title><tplarg><title><tplarg><title> </title></tplarg> </title></tplarg> </title></template>
+<tplarg lineStart="1"><title><template><title><tplarg><title> </title></tplarg> </title></template> </title></tplarg>
+<tplarg lineStart="1"><title><tplarg><title><template><title> </title></template> </title></tplarg> </title></tplarg>
+<template lineStart="1"><title><template><title><template><title><template><title> </title></template> </title></template> </title></template> </title></template>
+
+<template lineStart="1"><title>vorlage</title></template>
+
+<tplarg lineStart="1"><title>argument</title></tplarg>
+
+Nach [[:meta:Help:Expansion#XML parse tree]]
+{<tplarg><title>vorlagenname</title></tplarg>}
+<template lineStart="1"><title> <template><title>vorlagenname</title></template></title></template>
+<template lineStart="1"><title><template><title>vorlagenname</title></template> </title></template>
+<template lineStart="1"><title><template><title>vorlagenname</title></template>erweiterung</title></template>
+
+<template lineStart="1"><title><tplarg><title>vorlagenname</title></tplarg></title></template>
+<tplarg lineStart="1"><title> <template><title>vorlagenname</title></template></title></tplarg>
+<template lineStart="1"><title> <tplarg><title>vorlagenname</title></tplarg></title></template>
+<tplarg lineStart="1"><title><template><title>vorlagenname</title></template> </title></tplarg>
+<template lineStart="1"><title><tplarg><title>vorlagenname</title></tplarg> </title></template>
+
+nur etwas erweitert
+<tplarg lineStart="1"><title><tplarg><title>vorlagenname</title></tplarg></title></tplarg>
+<tplarg lineStart="1"><title> <tplarg><title>vorlagenname</title></tplarg></title></tplarg>
+<tplarg lineStart="1"><title><tplarg><title>vorlagenname</title></tplarg> </title></tplarg>
+<template lineStart="1"><title> {<tplarg><title>vorlagenname</title></tplarg></title></template>}
+{<tplarg><title> <template><title>vorlagenname</title></template></title></tplarg>}
+<template lineStart="1"><title> <template><title> <template><title>vorlagenname</title></template></title></template></title></template>
+{<tplarg><title> <template><title>vorlagenname</title></template>} </title></tplarg>
+{<template><title><tplarg><title>vorlagenname</title></tplarg>} </title></template>
+{<tplarg><title><template><title>vorlagenname</title></template> </title></tplarg>}
+<template lineStart="1"><title> <template><title><template><title>vorlagenname</title></template> </title></template></title></template>
+<tplarg lineStart="1"><title> {<template><title>vorlagenname</title></template> </title></tplarg>}
+
+{<tplarg><title><tplarg><title> </title></tplarg></title></tplarg>}
+
+<template lineStart="1"><title><tplarg><title><tplarg><title> </title></tplarg></title></tplarg></title></template>
+<tplarg lineStart="1"><title><tplarg><title><template><title> </title></template> </title></tplarg></title></tplarg>
+<template lineStart="1"><title><tplarg><title><tplarg><title> </title></tplarg> </title></tplarg></title></template>
+{{<tplarg><title><tplarg><title> </title></tplarg>} </title></tplarg>}
+<tplarg lineStart="1"><title><template><title><tplarg><title> </title></tplarg></title></template> </title></tplarg>
+<template lineStart="1"><title><tplarg><title><tplarg><title> </title></tplarg></title></tplarg> </title></template>
+{<tplarg><title><template><title><template><title> </title></template> </title></template> </title></tplarg>}
+{<template><title><tplarg><title><template><title> </title></template> </title></tplarg>} </title></template>
+{<template><title><template><title><tplarg><title> </title></tplarg>} </title></template> </title></template>
+<template lineStart="1"><title><tplarg><title><tplarg><title> </title></tplarg> </title></tplarg> </title></template>
+<tplarg lineStart="1"><title><template><title><tplarg><title> </title></tplarg> </title></template> </title></tplarg>
+<tplarg lineStart="1"><title><tplarg><title><template><title> </title></template> </title></tplarg> </title></tplarg>
+<template lineStart="1"><title><template><title><template><title><template><title> </title></template> </title></template> </title></template> </title></template>
+</root> \ No newline at end of file
diff --git a/tests/parser/preprocess/NestedTemplates.txt b/tests/parser/preprocess/NestedTemplates.txt
new file mode 100644
index 00000000..aa9a472d
--- /dev/null
+++ b/tests/parser/preprocess/NestedTemplates.txt
@@ -0,0 +1,89 @@
+{{vorlage}}
+
+{{{argument}}}
+
+Nach [[:meta:Help:Expansion#XML parse tree]]
+{{{{vorlagenname}}}}
+{{ {{vorlagenname}}}}
+{{{{vorlagenname}} }}
+{{{{vorlagenname}}erweiterung}}
+
+{{{{{vorlagenname}}}}}
+{{{ {{vorlagenname}}}}}
+{{ {{{vorlagenname}}}}}
+{{{{{vorlagenname}} }}}
+{{{{{vorlagenname}}} }}
+
+nur etwas erweitert
+{{{{{{vorlagenname}}}}}}
+{{{ {{{vorlagenname}}}}}}
+{{{{{{vorlagenname}}} }}}
+{{ {{{{vorlagenname}}}}}}
+{{{{ {{vorlagenname}}}}}}
+{{ {{ {{vorlagenname}}}}}}
+{{{{ {{vorlagenname}}} }}}
+{{{{{{vorlagenname}}}} }}
+{{{{{{vorlagenname}} }}}}
+{{ {{{{vorlagenname}} }}}}
+{{{ {{{vorlagenname}} }}}}
+
+{{{{{{{ }}}}}}}
+
+{{{{{{{{ }}}}}}}}
+{{{{{{{{ }} }}}}}}
+{{{{{{{{ }}} }}}}}
+{{{{{{{{ }}}} }}}}
+{{{{{{{{ }}}}} }}}
+{{{{{{{{ }}}}}} }}
+{{{{{{{{ }} }} }}}}
+{{{{{{{{ }} }}}} }}
+{{{{{{{{ }}}} }} }}
+{{{{{{{{ }}} }}} }}
+{{{{{{{{ }}} }} }}}
+{{{{{{{{ }} }}} }}}
+{{{{{{{{ }} }} }} }}
+
+{{vorlage}}
+
+{{{argument}}}
+
+Nach [[:meta:Help:Expansion#XML parse tree]]
+{{{{vorlagenname}}}}
+{{ {{vorlagenname}}}}
+{{{{vorlagenname}} }}
+{{{{vorlagenname}}erweiterung}}
+
+{{{{{vorlagenname}}}}}
+{{{ {{vorlagenname}}}}}
+{{ {{{vorlagenname}}}}}
+{{{{{vorlagenname}} }}}
+{{{{{vorlagenname}}} }}
+
+nur etwas erweitert
+{{{{{{vorlagenname}}}}}}
+{{{ {{{vorlagenname}}}}}}
+{{{{{{vorlagenname}}} }}}
+{{ {{{{vorlagenname}}}}}}
+{{{{ {{vorlagenname}}}}}}
+{{ {{ {{vorlagenname}}}}}}
+{{{{ {{vorlagenname}}} }}}
+{{{{{{vorlagenname}}}} }}
+{{{{{{vorlagenname}} }}}}
+{{ {{{{vorlagenname}} }}}}
+{{{ {{{vorlagenname}} }}}}
+
+{{{{{{{ }}}}}}}
+
+{{{{{{{{ }}}}}}}}
+{{{{{{{{ }} }}}}}}
+{{{{{{{{ }}} }}}}}
+{{{{{{{{ }}}} }}}}
+{{{{{{{{ }}}}} }}}
+{{{{{{{{ }}}}}} }}
+{{{{{{{{ }} }} }}}}
+{{{{{{{{ }} }}}} }}
+{{{{{{{{ }}}} }} }}
+{{{{{{{{ }}} }}} }}
+{{{{{{{{ }}} }} }}}
+{{{{{{{{ }} }}} }}}
+{{{{{{{{ }} }} }} }}
diff --git a/tests/parser/preprocess/QuoteQuran.expected b/tests/parser/preprocess/QuoteQuran.expected
new file mode 100644
index 00000000..e9a78e46
--- /dev/null
+++ b/tests/parser/preprocess/QuoteQuran.expected
@@ -0,0 +1,140 @@
+<root><ignore>&lt;noinclude&gt;</ignore><template><title>Template sandbox notice</title></template><ignore>&lt;/noinclude&gt;</ignore>
+&lt;div class=&quot;boilerplate metadata rfa&quot; style=&quot;background-color:#FFFFF5; margin: 2em 0 0 0; padding: 0 10px 0 10px; border: 1px solid #AAAAAA;&quot;&gt;The [[Qur'an]], [[sura|chapter]] <template><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 1) </title><part><name index="1" /><value> 1 ([[Al-Fatiha]]) </value></part><part><name index="2" /><value>
+<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> 2 ([[Al-Baqara]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 3) </title><part><name index="1" /><value> 3 ([[Ali Imran]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 4) </title><part><name index="1" /><value> 4 ([[An-Nisa]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 5) </title><part><name index="1" /><value> 5 ([[Al-Ma'ida]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 6) </title><part><name index="1" /><value> 6 ([[Al-An'am]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 7) </title><part><name index="1" /><value> 7 ([[Al-A'raf]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 8) </title><part><name index="1" /><value> 8 ([[Al-Anfal]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 9) </title><part><name index="1" /><value> 9 ([[At-Tawba]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 10) </title><part><name index="1" /><value> 10 ([[Yunus (sura)|Yunus]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 11) </title><part><name index="1" /><value> 11 ([[Hud (sura)|Hud]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 12) </title><part><name index="1" /><value> 12 ([[Yusuf (sura)|Yusuf]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 13) </title><part><name index="1" /><value> 13 ([[Ar-Ra'd]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 14) </title><part><name index="1" /><value> 14 ([[Ibrahim (sura)|Ibrahim]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 15) </title><part><name index="1" /><value> 15 ([[Al-Hijr]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 16) </title><part><name index="1" /><value> 16 ([[An-Nahl]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 17) </title><part><name index="1" /><value> 17 ([[Al-Isra]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 18) </title><part><name index="1" /><value> 18 ([[Al-Kahf]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 19) </title><part><name index="1" /><value> 19 ([[Maryam (sura)|Maryam]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 20) </title><part><name index="1" /><value> 20 ([[Ta-Ha]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 21) </title><part><name index="1" /><value> 21 ([[Al-Anbiya]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 22) </title><part><name index="1" /><value> 22 ([[Al-Hajj]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 23) </title><part><name index="1" /><value> 23 ([[Al-Muminun]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 24) </title><part><name index="1" /><value> 24 ([[An-Noor]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 25) </title><part><name index="1" /><value> 25 ([[Al-Furqan]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 26) </title><part><name index="1" /><value> 26 ([[Ash-Shu'ara]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 27) </title><part><name index="1" /><value> 27 ([[An-Naml]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 28) </title><part><name index="1" /><value> 28 ([[Al-Qisas]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 29) </title><part><name index="1" /><value> 29 ([[Al-Ankabut]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 30) </title><part><name index="1" /><value> 30 ([[Ar-Rum]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 31) </title><part><name index="1" /><value> 31 ([[Luqman (sura)|Luqman]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 32) </title><part><name index="1" /><value> 32 ([[As-Sajda]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 33) </title><part><name index="1" /><value> 33 ([[Al-Ahzab]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 34) </title><part><name index="1" /><value> 34 ([[Saba (sura)|Saba]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 35) </title><part><name index="1" /><value> 35 ([[Fatir]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 36) </title><part><name index="1" /><value> 36 ([[Ya-Seen]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 37) </title><part><name index="1" /><value> 37 ([[As-Saaffat]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 38) </title><part><name index="1" /><value> 38 ([[Sad (sura)|Sad]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 39) </title><part><name index="1" /><value> 39 ([[Az-Zumar]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 40) </title><part><name index="1" /><value> 40 ([[Ghafir]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 41) </title><part><name index="1" /><value> 41 ([[Fussilat]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 42) </title><part><name index="1" /><value> 42 ([[Ash-Shura]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 43) </title><part><name index="1" /><value> 43 ([[Az-Zukhruf]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 44) </title><part><name index="1" /><value> 44 ([[Ad-Dukhan]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 45) </title><part><name index="1" /><value> 45 ([[Al-Jathiya]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 46) </title><part><name index="1" /><value> 46 ([[Al-Ahqaf]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 47) </title><part><name index="1" /><value> 47 ([[Muhammad (sura)|Muhammad]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 48) </title><part><name index="1" /><value> 48 ([[Al-Fath]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 49) </title><part><name index="1" /><value> 49 ([[Al-Hujraat]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 50) </title><part><name index="1" /><value> 50 ([[Qaf (sura)|Qaf]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 51) </title><part><name index="1" /><value> 51 ([[Adh-Dhariyat]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 52) </title><part><name index="1" /><value> 52 ([[At-Tur]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 53) </title><part><name index="1" /><value> 53 ([[An-Najm]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 54) </title><part><name index="1" /><value> 54 ([[Al-Qamar]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 55) </title><part><name index="1" /><value> 55 ([[Ar-Rahman]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 56) </title><part><name index="1" /><value> 56 ([[Al-Waqia]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 57) </title><part><name index="1" /><value> 57 ([[Al-Hadid]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 58) </title><part><name index="1" /><value> 58 ([[Al-Mujadila]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 59) </title><part><name index="1" /><value> 59 ([[Al-Hashr]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 60) </title><part><name index="1" /><value> 60 ([[Al-Mumtahina]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 61) </title><part><name index="1" /><value> 61 ([[As-Saff]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 62) </title><part><name index="1" /><value> 62 ([[Al-Jumua]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 63) </title><part><name index="1" /><value> 63 ([[Al-Munafiqoon]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 64) </title><part><name index="1" /><value> 64 ([[At-Taghabun]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 65) </title><part><name index="1" /><value> 65 ([[At-Talaq]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 66) </title><part><name index="1" /><value> 66 ([[At-Tahrim]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 67) </title><part><name index="1" /><value> 67 ([[Al-Mulk]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 68) </title><part><name index="1" /><value> 68 ([[Al-Qalam]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 69) </title><part><name index="1" /><value> 69 ([[Al-Haaqqa]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 70) </title><part><name index="1" /><value> 70 ([[Al-Maarij]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 71) </title><part><name index="1" /><value> 71 ([[Nooh (sura)|Nooh]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 72) </title><part><name index="1" /><value> 72 ([[Al-Jinn]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 73) </title><part><name index="1" /><value> 73 ([[Al-Muzzammil]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 74) </title><part><name index="1" /><value> 74 ([[Al-Muddaththir]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 75) </title><part><name index="1" /><value> 75 ([[Al-Qiyama]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 76) </title><part><name index="1" /><value> 76 ([[Al-Insan]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 77) </title><part><name index="1" /><value> 77 ([[Al-Mursalat]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 78) </title><part><name index="1" /><value> 78 ([[An-Naba]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 79) </title><part><name index="1" /><value> 79 ([[An-Naziat]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 80) </title><part><name index="1" /><value> 80 ([[Abasa]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 81) </title><part><name index="1" /><value> 81 ([[At-Takwir]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 82) </title><part><name index="1" /><value> 82 ([[Al-Infitar]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 83) </title><part><name index="1" /><value> 83 ([[Al-Mutaffifin]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 84) </title><part><name index="1" /><value> 84 ([[Al-Inshiqaq]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 85) </title><part><name index="1" /><value> 85 ([[Al-Burooj]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 86) </title><part><name index="1" /><value> 86 ([[At-Tariq]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 87) </title><part><name index="1" /><value> 87 ([[Al-Ala]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 88) </title><part><name index="1" /><value> 88 ([[Al-Ghashiya]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 89) </title><part><name index="1" /><value> 89 ([[Al-Fajr (sura)|Al-Fajr]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 90) </title><part><name index="1" /><value> 90 ([[Al-Balad]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 91) </title><part><name index="1" /><value> 91 ([[Ash-Shams]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 92) </title><part><name index="1" /><value> 92 ([[Al-Lail]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 93) </title><part><name index="1" /><value> 93 ([[Ad-Dhuha]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 94) </title><part><name index="1" /><value> 94 ([[Al-Inshirah]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 95) </title><part><name index="1" /><value> 95 ([[At-Tin]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 96) </title><part><name index="1" /><value> 96 ([[Al-Alaq]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 97) </title><part><name index="1" /><value> 97 ([[Al-Qadr]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 98) </title><part><name index="1" /><value> 98 ([[Al-Bayyina]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 99) </title><part><name index="1" /><value> 99 ([[Az-Zalzala]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 100) </title><part><name index="1" /><value> 100 ([[Al-Adiyat]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 101) </title><part><name index="1" /><value> 101 ([[Al-Qaria]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 102) </title><part><name index="1" /><value> 102 ([[At-Takathur]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 103) </title><part><name index="1" /><value> 103 ([[Al-Asr]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 104) </title><part><name index="1" /><value> 104 ([[Al-Humaza]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 105) </title><part><name index="1" /><value> 105 ([[Al-Fil]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 106) </title><part><name index="1" /><value> 106 ([[Quraysh (sura)|Quraysh]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 107) </title><part><name index="1" /><value> 107 ([[Al-Ma'un]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 108) </title><part><name index="1" /><value> 108 ([[Al-Kawthar]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 109) </title><part><name index="1" /><value> 109 ([[Al-Kafirun]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 110) </title><part><name index="1" /><value> 110 ([[An-Nasr]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 111) </title><part><name index="1" /><value> 111 ([[Al-Masadd]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 112) </title><part><name index="1" /><value> 112 ([[Al-Ikhlas]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 113) </title><part><name index="1" /><value> 113 ([[Al-Falaq]]) </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg> = 114) </title><part><name index="1" /><value> 114 ([[An-Nas]]) </value></part><part><name index="2" /><value>
+error </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template>, [[ayat|verse]] [http://www.usc.edu/dept/MSA/quran/<template><title>three digit</title><part><name index="1" /><value><tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg></value></part></template>.qmt.html#<template><title>three digit</title><part><name index="1" /><value><tplarg><title>1</title><part><name index="1" /><value>1</value></part></tplarg></value></part></template>.<template><title>three digit</title><part><name index="1" /><value><tplarg><title>2</title><part><name index="1" /><value>1</value></part></tplarg></value></part></template> <tplarg><title>2</title><part><name index="1" /><value>1</value></part></tplarg>]''':'''<template><title>cquote</title><part><name index="1" /><value> <tplarg><title>3</title><part><name index="1" /><value>Default text</value></part></tplarg>&amp;mdash; &lt;small&gt;[[Qur'an translations|translated]] by <template><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 0) </title><part><name index="1" /><value> Unknown </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 1) </title><part><name index="1" /><value> [[Salman the Persian]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 101) </title><part><name index="1" /><value> [[Marmaduke Pickthall]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 102) </title><part><name index="1" /><value> [[Abdullah Yusuf Ali]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 601) </title><part><name index="1" /><value> [[Muhammad Muhsin Khan]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 701) </title><part><name index="1" /><value> [[Mohammed Habib Shakir|M. H. Shakir]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 901) </title><part><name index="1" /><value> [[Maulana Muhammad Ali]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 902) </title><part><name index="1" /><value> [[Rashad Khalifa]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 1001) </title><part><name index="1" /><value> [[Theodor Bibliander]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 1002) </title><part><name index="1" /><value> [[Robert of Ketton]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 1003) </title><part><name index="1" /><value> [[Andre du Ryer]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 1004) </title><part><name index="1" /><value> [[Alexander Ross (writer)|Alexander Ross]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 1005) </title><part><name index="1" /><value> [[Abraham Hinckelmann]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 1006) </title><part><name index="1" /><value> [[George Sale]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 1007) </title><part><name index="1" /><value> [[John Medows Rodwell]] </value></part><part><name index="2" /><value>
+<template lineStart="1"><title>#ifexpr: (<tplarg><title>4</title><part><name index="1" /><value>0</value></part></tplarg> = 1008) </title><part><name index="1" /><value> [[Arthur John Arberry]] </value></part><part><name index="2" /><value>
+error </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template> </value></part></template>&lt;/small&gt;
+<template lineStart="1"><title>#if:<tplarg><title>trans</title><part><name index="1" /><value></value></part></tplarg></title><part><name index="1" /><value>
+----
+[[Transliteration]]: <tplarg><title>trans</title></tplarg></value></part><part><name index="2" /><value> </value></part></template>
+<template lineStart="1"><title>#if:<tplarg><title>arab</title><part><name index="1" /><value></value></part></tplarg></title><part><name index="1" /><value>
+----
+[[Arabic language|Arabic]]: <tplarg><title>arab</title></tplarg></value></part><part><name index="2" /><value> </value></part></template> </value></part></template>&lt;/font&gt;&lt;/div&gt;
+
+</root> \ No newline at end of file
diff --git a/tests/parser/preprocess/QuoteQuran.txt b/tests/parser/preprocess/QuoteQuran.txt
new file mode 100644
index 00000000..3cfac5b2
--- /dev/null
+++ b/tests/parser/preprocess/QuoteQuran.txt
@@ -0,0 +1,139 @@
+<noinclude>{{Template sandbox notice}}</noinclude>
+<div class="boilerplate metadata rfa" style="background-color:#FFFFF5; margin: 2em 0 0 0; padding: 0 10px 0 10px; border: 1px solid #AAAAAA;">The [[Qur'an]], [[sura|chapter]] {{#ifexpr: ({{{1|1}}} = 1) | 1 ([[Al-Fatiha]]) |
+{{#ifexpr: ({{{1|1}}} = 2) | 2 ([[Al-Baqara]]) |
+{{#ifexpr: ({{{1|1}}} = 3) | 3 ([[Ali Imran]]) |
+{{#ifexpr: ({{{1|1}}} = 4) | 4 ([[An-Nisa]]) |
+{{#ifexpr: ({{{1|1}}} = 5) | 5 ([[Al-Ma'ida]]) |
+{{#ifexpr: ({{{1|1}}} = 6) | 6 ([[Al-An'am]]) |
+{{#ifexpr: ({{{1|1}}} = 7) | 7 ([[Al-A'raf]]) |
+{{#ifexpr: ({{{1|1}}} = 8) | 8 ([[Al-Anfal]]) |
+{{#ifexpr: ({{{1|1}}} = 9) | 9 ([[At-Tawba]]) |
+{{#ifexpr: ({{{1|1}}} = 10) | 10 ([[Yunus (sura)|Yunus]]) |
+{{#ifexpr: ({{{1|1}}} = 11) | 11 ([[Hud (sura)|Hud]]) |
+{{#ifexpr: ({{{1|1}}} = 12) | 12 ([[Yusuf (sura)|Yusuf]]) |
+{{#ifexpr: ({{{1|1}}} = 13) | 13 ([[Ar-Ra'd]]) |
+{{#ifexpr: ({{{1|1}}} = 14) | 14 ([[Ibrahim (sura)|Ibrahim]]) |
+{{#ifexpr: ({{{1|1}}} = 15) | 15 ([[Al-Hijr]]) |
+{{#ifexpr: ({{{1|1}}} = 16) | 16 ([[An-Nahl]]) |
+{{#ifexpr: ({{{1|1}}} = 17) | 17 ([[Al-Isra]]) |
+{{#ifexpr: ({{{1|1}}} = 18) | 18 ([[Al-Kahf]]) |
+{{#ifexpr: ({{{1|1}}} = 19) | 19 ([[Maryam (sura)|Maryam]]) |
+{{#ifexpr: ({{{1|1}}} = 20) | 20 ([[Ta-Ha]]) |
+{{#ifexpr: ({{{1|1}}} = 21) | 21 ([[Al-Anbiya]]) |
+{{#ifexpr: ({{{1|1}}} = 22) | 22 ([[Al-Hajj]]) |
+{{#ifexpr: ({{{1|1}}} = 23) | 23 ([[Al-Muminun]]) |
+{{#ifexpr: ({{{1|1}}} = 24) | 24 ([[An-Noor]]) |
+{{#ifexpr: ({{{1|1}}} = 25) | 25 ([[Al-Furqan]]) |
+{{#ifexpr: ({{{1|1}}} = 26) | 26 ([[Ash-Shu'ara]]) |
+{{#ifexpr: ({{{1|1}}} = 27) | 27 ([[An-Naml]]) |
+{{#ifexpr: ({{{1|1}}} = 28) | 28 ([[Al-Qisas]]) |
+{{#ifexpr: ({{{1|1}}} = 29) | 29 ([[Al-Ankabut]]) |
+{{#ifexpr: ({{{1|1}}} = 30) | 30 ([[Ar-Rum]]) |
+{{#ifexpr: ({{{1|1}}} = 31) | 31 ([[Luqman (sura)|Luqman]]) |
+{{#ifexpr: ({{{1|1}}} = 32) | 32 ([[As-Sajda]]) |
+{{#ifexpr: ({{{1|1}}} = 33) | 33 ([[Al-Ahzab]]) |
+{{#ifexpr: ({{{1|1}}} = 34) | 34 ([[Saba (sura)|Saba]]) |
+{{#ifexpr: ({{{1|1}}} = 35) | 35 ([[Fatir]]) |
+{{#ifexpr: ({{{1|1}}} = 36) | 36 ([[Ya-Seen]]) |
+{{#ifexpr: ({{{1|1}}} = 37) | 37 ([[As-Saaffat]]) |
+{{#ifexpr: ({{{1|1}}} = 38) | 38 ([[Sad (sura)|Sad]]) |
+{{#ifexpr: ({{{1|1}}} = 39) | 39 ([[Az-Zumar]]) |
+{{#ifexpr: ({{{1|1}}} = 40) | 40 ([[Ghafir]]) |
+{{#ifexpr: ({{{1|1}}} = 41) | 41 ([[Fussilat]]) |
+{{#ifexpr: ({{{1|1}}} = 42) | 42 ([[Ash-Shura]]) |
+{{#ifexpr: ({{{1|1}}} = 43) | 43 ([[Az-Zukhruf]]) |
+{{#ifexpr: ({{{1|1}}} = 44) | 44 ([[Ad-Dukhan]]) |
+{{#ifexpr: ({{{1|1}}} = 45) | 45 ([[Al-Jathiya]]) |
+{{#ifexpr: ({{{1|1}}} = 46) | 46 ([[Al-Ahqaf]]) |
+{{#ifexpr: ({{{1|1}}} = 47) | 47 ([[Muhammad (sura)|Muhammad]]) |
+{{#ifexpr: ({{{1|1}}} = 48) | 48 ([[Al-Fath]]) |
+{{#ifexpr: ({{{1|1}}} = 49) | 49 ([[Al-Hujraat]]) |
+{{#ifexpr: ({{{1|1}}} = 50) | 50 ([[Qaf (sura)|Qaf]]) |
+{{#ifexpr: ({{{1|1}}} = 51) | 51 ([[Adh-Dhariyat]]) |
+{{#ifexpr: ({{{1|1}}} = 52) | 52 ([[At-Tur]]) |
+{{#ifexpr: ({{{1|1}}} = 53) | 53 ([[An-Najm]]) |
+{{#ifexpr: ({{{1|1}}} = 54) | 54 ([[Al-Qamar]]) |
+{{#ifexpr: ({{{1|1}}} = 55) | 55 ([[Ar-Rahman]]) |
+{{#ifexpr: ({{{1|1}}} = 56) | 56 ([[Al-Waqia]]) |
+{{#ifexpr: ({{{1|1}}} = 57) | 57 ([[Al-Hadid]]) |
+{{#ifexpr: ({{{1|1}}} = 58) | 58 ([[Al-Mujadila]]) |
+{{#ifexpr: ({{{1|1}}} = 59) | 59 ([[Al-Hashr]]) |
+{{#ifexpr: ({{{1|1}}} = 60) | 60 ([[Al-Mumtahina]]) |
+{{#ifexpr: ({{{1|1}}} = 61) | 61 ([[As-Saff]]) |
+{{#ifexpr: ({{{1|1}}} = 62) | 62 ([[Al-Jumua]]) |
+{{#ifexpr: ({{{1|1}}} = 63) | 63 ([[Al-Munafiqoon]]) |
+{{#ifexpr: ({{{1|1}}} = 64) | 64 ([[At-Taghabun]]) |
+{{#ifexpr: ({{{1|1}}} = 65) | 65 ([[At-Talaq]]) |
+{{#ifexpr: ({{{1|1}}} = 66) | 66 ([[At-Tahrim]]) |
+{{#ifexpr: ({{{1|1}}} = 67) | 67 ([[Al-Mulk]]) |
+{{#ifexpr: ({{{1|1}}} = 68) | 68 ([[Al-Qalam]]) |
+{{#ifexpr: ({{{1|1}}} = 69) | 69 ([[Al-Haaqqa]]) |
+{{#ifexpr: ({{{1|1}}} = 70) | 70 ([[Al-Maarij]]) |
+{{#ifexpr: ({{{1|1}}} = 71) | 71 ([[Nooh (sura)|Nooh]]) |
+{{#ifexpr: ({{{1|1}}} = 72) | 72 ([[Al-Jinn]]) |
+{{#ifexpr: ({{{1|1}}} = 73) | 73 ([[Al-Muzzammil]]) |
+{{#ifexpr: ({{{1|1}}} = 74) | 74 ([[Al-Muddaththir]]) |
+{{#ifexpr: ({{{1|1}}} = 75) | 75 ([[Al-Qiyama]]) |
+{{#ifexpr: ({{{1|1}}} = 76) | 76 ([[Al-Insan]]) |
+{{#ifexpr: ({{{1|1}}} = 77) | 77 ([[Al-Mursalat]]) |
+{{#ifexpr: ({{{1|1}}} = 78) | 78 ([[An-Naba]]) |
+{{#ifexpr: ({{{1|1}}} = 79) | 79 ([[An-Naziat]]) |
+{{#ifexpr: ({{{1|1}}} = 80) | 80 ([[Abasa]]) |
+{{#ifexpr: ({{{1|1}}} = 81) | 81 ([[At-Takwir]]) |
+{{#ifexpr: ({{{1|1}}} = 82) | 82 ([[Al-Infitar]]) |
+{{#ifexpr: ({{{1|1}}} = 83) | 83 ([[Al-Mutaffifin]]) |
+{{#ifexpr: ({{{1|1}}} = 84) | 84 ([[Al-Inshiqaq]]) |
+{{#ifexpr: ({{{1|1}}} = 85) | 85 ([[Al-Burooj]]) |
+{{#ifexpr: ({{{1|1}}} = 86) | 86 ([[At-Tariq]]) |
+{{#ifexpr: ({{{1|1}}} = 87) | 87 ([[Al-Ala]]) |
+{{#ifexpr: ({{{1|1}}} = 88) | 88 ([[Al-Ghashiya]]) |
+{{#ifexpr: ({{{1|1}}} = 89) | 89 ([[Al-Fajr (sura)|Al-Fajr]]) |
+{{#ifexpr: ({{{1|1}}} = 90) | 90 ([[Al-Balad]]) |
+{{#ifexpr: ({{{1|1}}} = 91) | 91 ([[Ash-Shams]]) |
+{{#ifexpr: ({{{1|1}}} = 92) | 92 ([[Al-Lail]]) |
+{{#ifexpr: ({{{1|1}}} = 93) | 93 ([[Ad-Dhuha]]) |
+{{#ifexpr: ({{{1|1}}} = 94) | 94 ([[Al-Inshirah]]) |
+{{#ifexpr: ({{{1|1}}} = 95) | 95 ([[At-Tin]]) |
+{{#ifexpr: ({{{1|1}}} = 96) | 96 ([[Al-Alaq]]) |
+{{#ifexpr: ({{{1|1}}} = 97) | 97 ([[Al-Qadr]]) |
+{{#ifexpr: ({{{1|1}}} = 98) | 98 ([[Al-Bayyina]]) |
+{{#ifexpr: ({{{1|1}}} = 99) | 99 ([[Az-Zalzala]]) |
+{{#ifexpr: ({{{1|1}}} = 100) | 100 ([[Al-Adiyat]]) |
+{{#ifexpr: ({{{1|1}}} = 101) | 101 ([[Al-Qaria]]) |
+{{#ifexpr: ({{{1|1}}} = 102) | 102 ([[At-Takathur]]) |
+{{#ifexpr: ({{{1|1}}} = 103) | 103 ([[Al-Asr]]) |
+{{#ifexpr: ({{{1|1}}} = 104) | 104 ([[Al-Humaza]]) |
+{{#ifexpr: ({{{1|1}}} = 105) | 105 ([[Al-Fil]]) |
+{{#ifexpr: ({{{1|1}}} = 106) | 106 ([[Quraysh (sura)|Quraysh]]) |
+{{#ifexpr: ({{{1|1}}} = 107) | 107 ([[Al-Ma'un]]) |
+{{#ifexpr: ({{{1|1}}} = 108) | 108 ([[Al-Kawthar]]) |
+{{#ifexpr: ({{{1|1}}} = 109) | 109 ([[Al-Kafirun]]) |
+{{#ifexpr: ({{{1|1}}} = 110) | 110 ([[An-Nasr]]) |
+{{#ifexpr: ({{{1|1}}} = 111) | 111 ([[Al-Masadd]]) |
+{{#ifexpr: ({{{1|1}}} = 112) | 112 ([[Al-Ikhlas]]) |
+{{#ifexpr: ({{{1|1}}} = 113) | 113 ([[Al-Falaq]]) |
+{{#ifexpr: ({{{1|1}}} = 114) | 114 ([[An-Nas]]) |
+error }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }}, [[ayat|verse]] [http://www.usc.edu/dept/MSA/quran/{{three digit|{{{1|1}}}}}.qmt.html#{{three digit|{{{1|1}}}}}.{{three digit|{{{2|1}}}}} {{{2|1}}}]''':'''{{cquote| {{{3|Default text}}}&mdash; <small>[[Qur'an translations|translated]] by {{#ifexpr: ({{{4|0}}} = 0) | Unknown |
+{{#ifexpr: ({{{4|0}}} = 1) | [[Salman the Persian]] |
+{{#ifexpr: ({{{4|0}}} = 101) | [[Marmaduke Pickthall]] |
+{{#ifexpr: ({{{4|0}}} = 102) | [[Abdullah Yusuf Ali]] |
+{{#ifexpr: ({{{4|0}}} = 601) | [[Muhammad Muhsin Khan]] |
+{{#ifexpr: ({{{4|0}}} = 701) | [[Mohammed Habib Shakir|M. H. Shakir]] |
+{{#ifexpr: ({{{4|0}}} = 901) | [[Maulana Muhammad Ali]] |
+{{#ifexpr: ({{{4|0}}} = 902) | [[Rashad Khalifa]] |
+{{#ifexpr: ({{{4|0}}} = 1001) | [[Theodor Bibliander]] |
+{{#ifexpr: ({{{4|0}}} = 1002) | [[Robert of Ketton]] |
+{{#ifexpr: ({{{4|0}}} = 1003) | [[Andre du Ryer]] |
+{{#ifexpr: ({{{4|0}}} = 1004) | [[Alexander Ross (writer)|Alexander Ross]] |
+{{#ifexpr: ({{{4|0}}} = 1005) | [[Abraham Hinckelmann]] |
+{{#ifexpr: ({{{4|0}}} = 1006) | [[George Sale]] |
+{{#ifexpr: ({{{4|0}}} = 1007) | [[John Medows Rodwell]] |
+{{#ifexpr: ({{{4|0}}} = 1008) | [[Arthur John Arberry]] |
+error }} }} }} }} }} }} }} }} }} }} }} }} }} }} }} }}</small>
+{{#if:{{{trans|}}}|
+----
+[[Transliteration]]: {{{trans}}}| }}
+{{#if:{{{arab|}}}|
+----
+[[Arabic language|Arabic]]: {{{arab}}}| }} }}</font></div>
+
diff --git a/tests/parserTests.php b/tests/parserTests.php
new file mode 100644
index 00000000..9965c438
--- /dev/null
+++ b/tests/parserTests.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * MediaWiki parser test suite
+ *
+ * Copyright © 2004 Brion Vibber <brion@pobox.com>
+ * https://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
+ * @ingroup Testing
+ */
+
+$otions = array( 'quick', 'color', 'quiet', 'help', 'show-output',
+ 'record', 'run-disabled', 'run-parsoid' );
+$optionsWithArgs = array( 'regex', 'filter', 'seed', 'setversion' );
+
+require_once __DIR__ . '/../maintenance/commandLine.inc';
+require_once __DIR__ . '/TestsAutoLoader.php';
+
+if ( isset( $options['help'] ) ) {
+ echo <<<ENDS
+MediaWiki $wgVersion parser test suite
+Usage: php parserTests.php [options...]
+
+Options:
+ --quick Suppress diff output of failed tests
+ --quiet Suppress notification of passed tests (shows only failed tests)
+ --show-output Show expected and actual output
+ --color[=yes|no] Override terminal detection and force color output on or off
+ use wgCommandLineDarkBg = true; if your term is dark
+ --regex Only run tests whose descriptions which match given regex
+ --filter Alias for --regex
+ --file=<testfile> Run test cases from a custom file instead of parserTests.txt
+ --record Record tests in database
+ --compare Compare with recorded results, without updating the database.
+ --setversion When using --record, set the version string to use (useful
+ with git-svn so that you can get the exact revision)
+ --keep-uploads Re-use the same upload directory for each test, don't delete it
+ --fuzz Do a fuzz test instead of a normal test
+ --seed <n> Start the fuzz test from the specified seed
+ --help Show this help message
+ --run-disabled run disabled tests
+ --run-parsoid run parsoid tests (normally disabled)
+
+ENDS;
+ exit( 0 );
+}
+
+# Cases of weird db corruption were encountered when running tests on earlyish
+# versions of SQLite
+if ( $wgDBtype == 'sqlite' ) {
+ $db = wfGetDB( DB_MASTER );
+ $version = $db->getServerVersion();
+ if ( version_compare( $version, '3.6' ) < 0 ) {
+ die( "Parser tests require SQLite version 3.6 or later, you have $version\n" );
+ }
+}
+
+# There is a convention that the parser should never
+# refer to $wgTitle directly, but instead use the title
+# passed to it.
+$wgTitle = Title::newFromText( 'Parser test script do not use' );
+$tester = new ParserTest( $options );
+
+if ( isset( $options['file'] ) ) {
+ $files = array( $options['file'] );
+} else {
+ // Default parser tests and any set from extensions or local config
+ $files = $wgParserTestFiles;
+}
+
+# Print out software version to assist with locating regressions
+$version = SpecialVersion::getVersion();
+echo "This is MediaWiki version {$version}.\n\n";
+
+if ( isset( $options['fuzz'] ) ) {
+ $tester->fuzzTest( $files );
+} else {
+ $ok = $tester->runTestsFromFiles( $files );
+ exit( $ok ? 0 : 1 );
+}
diff --git a/tests/phpunit/LessFileCompilationTest.php b/tests/phpunit/LessFileCompilationTest.php
new file mode 100644
index 00000000..71e0f4b2
--- /dev/null
+++ b/tests/phpunit/LessFileCompilationTest.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * Modelled on Sebastian Bergmann's PHPUnit_Extensions_PhptTestCase class.
+ *
+ * @see https://github.com/sebastianbergmann/phpunit/blob/master/src/Extensions/PhptTestCase.php
+ * @author Sam Smith <samsmith@wikimedia.org>
+ */
+class LessFileCompilationTest extends ResourceLoaderTestCase {
+
+ /**
+ * @var string $file
+ */
+ protected $file;
+
+ /**
+ * @var ResourceLoaderModule The ResourceLoader module that contains
+ * the file
+ */
+ protected $module;
+
+ /**
+ * @param string $file
+ * @param ResourceLoaderModule $module The ResourceLoader module that
+ * contains the file
+ */
+ public function __construct( $file, ResourceLoaderModule $module ) {
+ parent::__construct( 'testLessFileCompilation' );
+
+ $this->file = $file;
+ $this->module = $module;
+ }
+
+ public function testLessFileCompilation() {
+ $thisString = $this->toString();
+ $this->assertTrue(
+ is_string( $this->file ) && is_file( $this->file ) && is_readable( $this->file ),
+ "$thisString must refer to a readable file"
+ );
+
+ $rlContext = static::getResourceLoaderContext();
+
+ // Bleh
+ $method = new ReflectionMethod( $this->module, 'getLessCompiler' );
+ $method->setAccessible( true );
+ $compiler = $method->invoke( $this->module, $rlContext );
+
+ $this->assertNotNull( $compiler->compileFile( $this->file ) );
+ }
+
+ public function getName( $withDataSet = true ) {
+ return $this->toString();
+ }
+
+ public function toString() {
+ $moduleName = $this->module->getName();
+
+ return "{$this->file} in the \"{$moduleName}\" module";
+ }
+}
diff --git a/tests/phpunit/Makefile b/tests/phpunit/Makefile
new file mode 100644
index 00000000..c3e2a303
--- /dev/null
+++ b/tests/phpunit/Makefile
@@ -0,0 +1,91 @@
+.PHONY: help test phpunit install coverage warning destructive parser noparser safe databaseless list-groups
+.DEFAULT: warning
+
+SHELL = /bin/sh
+CONFIG_FILE = ${PWD}/suite.xml
+PHP = php
+PU = ${PHP} phpunit.php --configuration ${CONFIG_FILE} ${FLAGS}
+
+all test: warning
+
+warning:
+ @echo "Run 'make help' to get usage"
+ @echo ""
+ @echo "WARNING -- some tests are DESTRUCTIVE and will alter your wiki."
+ @echo "DO NOT RUN THESE TESTS on a production wiki."
+ @echo ""
+ @echo "Until the default tests are made non-destructive, you can run"
+ @echo "the destructive tests like so:"
+ @echo ""
+ @echo " make destructive"
+ @echo ""
+ @echo "Some tests are expected to be safe, you can run them with"
+ @echo ""
+ @echo " make safe"
+ @echo ""
+ @echo "You are recommended to run the tests with read-only credentials."
+ @echo ""
+ @echo "If you don't have a database running, you can still run"
+ @echo ""
+ @echo " make databaseless"
+ @echo ""
+
+destructive: phpunit
+
+phpunit:
+ ${PU}
+
+install:
+ ./install-phpunit.sh
+
+tap:
+ ${PU} --tap
+
+coverage:
+ ${PU} --coverage-html ../../docs/code-coverage
+
+parser:
+ ${PU} --group Parser
+parserfuzz:
+ @echo "******************************************************************"
+ @echo "* This WILL kill your computer by eating all memory AND all swap *"
+ @echo "* *"
+ @echo "* If you are on a production machine. ABORT NOW!! *"
+ @echo "* Press control+C to stop *"
+ @echo "* *"
+ @echo "******************************************************************"
+ ${PU} --group Parser,ParserFuzz
+noparser:
+ ${PU} --exclude-group Parser,Broken,ParserFuzz,Stub
+
+safe:
+ ${PU} --exclude-group Broken,ParserFuzz,Destructive,Stub
+
+databaseless:
+ ${PU} --exclude-group Broken,ParserFuzz,Destructive,Database,Stub
+
+database:
+ ${PU} --exclude-group Broken,ParserFuzz,Destructive,Stub --group Database
+
+list-groups:
+ ${PU} --list-groups
+
+help:
+ # Usage:
+ # make <target> [OPTION=value]
+ #
+ # Targets:
+ # phpunit (default) Run all the tests with phpunit
+ # install Install PHPUnit from phpunit.de
+ # tap Run the tests individually through Test::Harness's prove(1)
+ # help You're looking at it!
+ # coverage Run the tests and generates an HTML code coverage report
+ # You will need the Xdebug PHP extension for the later.
+ # [no]parser Skip or only run Parser tests
+ #
+ # list-groups List availabe Tests groups.
+ #
+ # Options:
+ # CONFIG_FILE Path to a PHPUnit configuration file (default: suite.xml)
+ # FLAGS Additional flags to pass to PHPUnit
+ # PHP Path to php
diff --git a/tests/phpunit/MediaWikiLangTestCase.php b/tests/phpunit/MediaWikiLangTestCase.php
new file mode 100644
index 00000000..53e67224
--- /dev/null
+++ b/tests/phpunit/MediaWikiLangTestCase.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * Base class that store and restore the Language objects
+ */
+abstract class MediaWikiLangTestCase extends MediaWikiTestCase {
+ protected function setUp() {
+ global $wgLanguageCode, $wgContLang;
+ parent::setUp();
+
+ if ( $wgLanguageCode != $wgContLang->getCode() ) {
+ throw new MWException( "Error in MediaWikiLangTestCase::setUp(): " .
+ "\$wgLanguageCode ('$wgLanguageCode') is different from " .
+ "\$wgContLang->getCode() (" . $wgContLang->getCode() . ")" );
+ }
+
+ // HACK: Call getLanguage() so the real $wgContLang is cached as the user language
+ // rather than our fake one. This is to avoid breaking other, unrelated tests.
+ RequestContext::getMain()->getLanguage();
+
+ $langCode = 'en'; # For mainpage to be 'Main Page'
+ $langObj = Language::factory( $langCode );
+
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => $langCode,
+ 'wgLang' => $langObj,
+ 'wgContLang' => $langObj,
+ ) );
+
+ MessageCache::singleton()->disable();
+ }
+}
diff --git a/tests/phpunit/MediaWikiPHPUnitTestListener.php b/tests/phpunit/MediaWikiPHPUnitTestListener.php
new file mode 100644
index 00000000..08463f12
--- /dev/null
+++ b/tests/phpunit/MediaWikiPHPUnitTestListener.php
@@ -0,0 +1,129 @@
+<?php
+
+class MediaWikiPHPUnitTestListener extends PHPUnit_TextUI_ResultPrinter implements PHPUnit_Framework_TestListener {
+
+ /**
+ * @var string
+ */
+ protected $logChannel = 'PHPUnitCommand';
+
+ protected function getTestName( PHPUnit_Framework_Test $test ) {
+ $name = get_class( $test );
+
+ if ( $test instanceof PHPUnit_Framework_TestCase ) {
+ $name .= '::' . $test->getName( true );
+ }
+
+ return $name;
+ }
+
+ protected function getErrorName( Exception $exception ) {
+ $name = get_class( $exception );
+ $name = "[$name] " . $exception->getMessage();
+
+ return $name;
+ }
+
+ /**
+ * An error occurred.
+ *
+ * @param PHPUnit_Framework_Test $test
+ * @param Exception $e
+ * @param float $time
+ */
+ public function addError( PHPUnit_Framework_Test $test, Exception $e, $time ) {
+ parent::addError( $test, $e, $time );
+ wfDebugLog(
+ $this->logChannel,
+ 'ERROR in ' . $this->getTestName( $test ) . ': ' . $this->getErrorName( $e )
+ );
+ }
+
+ /**
+ * A failure occurred.
+ *
+ * @param PHPUnit_Framework_Test $test
+ * @param PHPUnit_Framework_AssertionFailedError $e
+ * @param float $time
+ */
+ public function addFailure( PHPUnit_Framework_Test $test,
+ PHPUnit_Framework_AssertionFailedError $e, $time
+ ) {
+ parent::addFailure( $test, $e, $time );
+ wfDebugLog(
+ $this->logChannel,
+ 'FAILURE in ' . $this->getTestName( $test ) . ': ' . $this->getErrorName( $e )
+ );
+ }
+
+ /**
+ * Incomplete test.
+ *
+ * @param PHPUnit_Framework_Test $test
+ * @param Exception $e
+ * @param float $time
+ */
+ public function addIncompleteTest( PHPUnit_Framework_Test $test, Exception $e, $time ) {
+ parent::addIncompleteTest( $test, $e, $time );
+ wfDebugLog(
+ $this->logChannel,
+ 'Incomplete test ' . $this->getTestName( $test ) . ': ' . $this->getErrorName( $e )
+ );
+ }
+
+ /**
+ * Skipped test.
+ *
+ * @param PHPUnit_Framework_Test $test
+ * @param Exception $e
+ * @param float $time
+ */
+ public function addSkippedTest( PHPUnit_Framework_Test $test, Exception $e, $time ) {
+ parent::addSkippedTest( $test, $e, $time );
+ wfDebugLog(
+ $this->logChannel,
+ 'Skipped test ' . $this->getTestName( $test ) . ': ' . $this->getErrorName( $e )
+ );
+ }
+
+ /**
+ * A test suite started.
+ *
+ * @param PHPUnit_Framework_TestSuite $suite
+ */
+ public function startTestSuite( PHPUnit_Framework_TestSuite $suite ) {
+ parent::startTestSuite( $suite );
+ wfDebugLog( $this->logChannel, 'START suite ' . $suite->getName() );
+ }
+
+ /**
+ * A test suite ended.
+ *
+ * @param PHPUnit_Framework_TestSuite $suite
+ */
+ public function endTestSuite( PHPUnit_Framework_TestSuite $suite ) {
+ parent::endTestSuite( $suite );
+ wfDebugLog( $this->logChannel, 'END suite ' . $suite->getName() );
+ }
+
+ /**
+ * A test started.
+ *
+ * @param PHPUnit_Framework_Test $test
+ */
+ public function startTest( PHPUnit_Framework_Test $test ) {
+ parent::startTest( $test );
+ wfDebugLog( $this->logChannel, 'Start test ' . $this->getTestName( $test ) );
+ }
+
+ /**
+ * A test ended.
+ *
+ * @param PHPUnit_Framework_Test $test
+ * @param float $time
+ */
+ public function endTest( PHPUnit_Framework_Test $test, $time ) {
+ parent::endTest( $test, $time );
+ wfDebugLog( $this->logChannel, 'End test ' . $this->getTestName( $test ) );
+ }
+}
diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php
new file mode 100644
index 00000000..995853ea
--- /dev/null
+++ b/tests/phpunit/MediaWikiTestCase.php
@@ -0,0 +1,1141 @@
+<?php
+
+/**
+ * @since 1.18
+ */
+abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
+ /**
+ * $called tracks whether the setUp and tearDown method has been called.
+ * class extending MediaWikiTestCase usually override setUp and tearDown
+ * but forget to call the parent.
+ *
+ * The array format takes a method name as key and anything as a value.
+ * By asserting the key exist, we know the child class has called the
+ * parent.
+ *
+ * This property must be private, we do not want child to override it,
+ * they should call the appropriate parent method instead.
+ */
+ private $called = array();
+
+ /**
+ * @var TestUser[]
+ * @since 1.20
+ */
+ public static $users;
+
+ /**
+ * @var DatabaseBase
+ * @since 1.18
+ */
+ protected $db;
+
+ /**
+ * @var array
+ * @since 1.19
+ */
+ protected $tablesUsed = array(); // tables with data
+
+ private static $useTemporaryTables = true;
+ private static $reuseDB = false;
+ private static $dbSetup = false;
+ private static $oldTablePrefix = false;
+
+ /**
+ * Original value of PHP's error_reporting setting.
+ *
+ * @var int
+ */
+ private $phpErrorLevel;
+
+ /**
+ * Holds the paths of temporary files/directories created through getNewTempFile,
+ * and getNewTempDirectory
+ *
+ * @var array
+ */
+ private $tmpFiles = array();
+
+ /**
+ * Holds original values of MediaWiki configuration settings
+ * to be restored in tearDown().
+ * See also setMwGlobals().
+ * @var array
+ */
+ private $mwGlobals = array();
+
+ /**
+ * Table name prefixes. Oracle likes it shorter.
+ */
+ const DB_PREFIX = 'unittest_';
+ const ORA_DB_PREFIX = 'ut_';
+
+ /**
+ * @var array
+ * @since 1.18
+ */
+ protected $supportedDBs = array(
+ 'mysql',
+ 'sqlite',
+ 'postgres',
+ 'oracle'
+ );
+
+ public function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->backupGlobals = false;
+ $this->backupStaticAttributes = false;
+ }
+
+ public function __destruct() {
+ // Complain if self::setUp() was called, but not self::tearDown()
+ // $this->called['setUp'] will be checked by self::testMediaWikiTestCaseParentSetupCalled()
+ if ( isset( $this->called['setUp'] ) && !isset( $this->called['tearDown'] ) ) {
+ throw new MWException( get_called_class() . "::tearDown() must call parent::tearDown()" );
+ }
+ }
+
+ public function run( PHPUnit_Framework_TestResult $result = null ) {
+ /* Some functions require some kind of caching, and will end up using the db,
+ * which we can't allow, as that would open a new connection for mysql.
+ * Replace with a HashBag. They would not be going to persist anyway.
+ */
+ ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
+
+ $needsResetDB = false;
+ $logName = get_class( $this ) . '::' . $this->getName( false );
+
+ if ( $this->needsDB() ) {
+ // set up a DB connection for this test to use
+
+ self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
+ self::$reuseDB = $this->getCliArg( 'reuse-db' );
+
+ $this->db = wfGetDB( DB_MASTER );
+
+ $this->checkDbIsSupported();
+
+ if ( !self::$dbSetup ) {
+ wfProfileIn( $logName . ' (clone-db)' );
+
+ // switch to a temporary clone of the database
+ self::setupTestDB( $this->db, $this->dbPrefix() );
+
+ if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
+ $this->resetDB();
+ }
+
+ wfProfileOut( $logName . ' (clone-db)' );
+ }
+
+ wfProfileIn( $logName . ' (prepare-db)' );
+ $this->addCoreDBData();
+ $this->addDBData();
+ wfProfileOut( $logName . ' (prepare-db)' );
+
+ $needsResetDB = true;
+ }
+
+ wfProfileIn( $logName );
+ parent::run( $result );
+ wfProfileOut( $logName );
+
+ if ( $needsResetDB ) {
+ wfProfileIn( $logName . ' (reset-db)' );
+ $this->resetDB();
+ wfProfileOut( $logName . ' (reset-db)' );
+ }
+ }
+
+ /**
+ * @since 1.21
+ *
+ * @return bool
+ */
+ public function usesTemporaryTables() {
+ return self::$useTemporaryTables;
+ }
+
+ /**
+ * Obtains a new temporary file name
+ *
+ * The obtained filename is enlisted to be removed upon tearDown
+ *
+ * @since 1.20
+ *
+ * @return string Absolute name of the temporary file
+ */
+ protected function getNewTempFile() {
+ $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' );
+ $this->tmpFiles[] = $fileName;
+
+ return $fileName;
+ }
+
+ /**
+ * obtains a new temporary directory
+ *
+ * The obtained directory is enlisted to be removed (recursively with all its contained
+ * files) upon tearDown.
+ *
+ * @since 1.20
+ *
+ * @return string Absolute name of the temporary directory
+ */
+ protected function getNewTempDirectory() {
+ // Starting of with a temporary /file/.
+ $fileName = $this->getNewTempFile();
+
+ // Converting the temporary /file/ to a /directory/
+ //
+ // The following is not atomic, but at least we now have a single place,
+ // where temporary directory creation is bundled and can be improved
+ unlink( $fileName );
+ $this->assertTrue( wfMkdirParents( $fileName ) );
+
+ return $fileName;
+ }
+
+ protected function setUp() {
+ wfProfileIn( __METHOD__ );
+ parent::setUp();
+ $this->called['setUp'] = true;
+
+ $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
+
+ // Cleaning up temporary files
+ foreach ( $this->tmpFiles as $fileName ) {
+ if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
+ unlink( $fileName );
+ } elseif ( is_dir( $fileName ) ) {
+ wfRecursiveRemoveDir( $fileName );
+ }
+ }
+
+ if ( $this->needsDB() && $this->db ) {
+ // Clean up open transactions
+ while ( $this->db->trxLevel() > 0 ) {
+ $this->db->rollback();
+ }
+
+ // don't ignore DB errors
+ $this->db->ignoreErrors( false );
+ }
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ protected function tearDown() {
+ wfProfileIn( __METHOD__ );
+
+ $this->called['tearDown'] = true;
+ // Cleaning up temporary files
+ foreach ( $this->tmpFiles as $fileName ) {
+ if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
+ unlink( $fileName );
+ } elseif ( is_dir( $fileName ) ) {
+ wfRecursiveRemoveDir( $fileName );
+ }
+ }
+
+ if ( $this->needsDB() && $this->db ) {
+ // Clean up open transactions
+ while ( $this->db->trxLevel() > 0 ) {
+ $this->db->rollback();
+ }
+
+ // don't ignore DB errors
+ $this->db->ignoreErrors( false );
+ }
+
+ // Restore mw globals
+ foreach ( $this->mwGlobals as $key => $value ) {
+ $GLOBALS[$key] = $value;
+ }
+ $this->mwGlobals = array();
+ RequestContext::resetMain();
+ MediaHandler::resetCache();
+
+ $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
+
+ if ( $phpErrorLevel !== $this->phpErrorLevel ) {
+ ini_set( 'error_reporting', $this->phpErrorLevel );
+
+ $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
+ $newHex = strtoupper( dechex( $phpErrorLevel ) );
+ $message = "PHP error_reporting setting was left dirty: "
+ . "was 0x$oldHex before test, 0x$newHex after test!";
+
+ $this->fail( $message );
+ }
+
+ parent::tearDown();
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Make sure MediaWikiTestCase extending classes have called their
+ * parent setUp method
+ */
+ final public function testMediaWikiTestCaseParentSetupCalled() {
+ $this->assertArrayHasKey( 'setUp', $this->called,
+ get_called_class() . "::setUp() must call parent::setUp()"
+ );
+ }
+
+ /**
+ * Sets a global, maintaining a stashed version of the previous global to be
+ * restored in tearDown
+ *
+ * The key is added to the array of globals that will be reset afterwards
+ * in the tearDown().
+ *
+ * @example
+ * <code>
+ * protected function setUp() {
+ * $this->setMwGlobals( 'wgRestrictStuff', true );
+ * }
+ *
+ * function testFoo() {}
+ *
+ * function testBar() {}
+ * $this->assertTrue( self::getX()->doStuff() );
+ *
+ * $this->setMwGlobals( 'wgRestrictStuff', false );
+ * $this->assertTrue( self::getX()->doStuff() );
+ * }
+ *
+ * function testQuux() {}
+ * </code>
+ *
+ * @param array|string $pairs Key to the global variable, or an array
+ * of key/value pairs.
+ * @param mixed $value Value to set the global to (ignored
+ * if an array is given as first argument).
+ *
+ * @since 1.21
+ */
+ protected function setMwGlobals( $pairs, $value = null ) {
+ if ( is_string( $pairs ) ) {
+ $pairs = array( $pairs => $value );
+ }
+
+ $this->stashMwGlobals( array_keys( $pairs ) );
+
+ foreach ( $pairs as $key => $value ) {
+ $GLOBALS[$key] = $value;
+ }
+ }
+
+ /**
+ * Stashes the global, will be restored in tearDown()
+ *
+ * Individual test functions may override globals through the setMwGlobals() function
+ * or directly. When directly overriding globals their keys should first be passed to this
+ * method in setUp to avoid breaking global state for other tests
+ *
+ * That way all other tests are executed with the same settings (instead of using the
+ * unreliable local settings for most tests and fix it only for some tests).
+ *
+ * @param array|string $globalKeys Key to the global variable, or an array of keys.
+ *
+ * @throws Exception When trying to stash an unset global
+ * @since 1.23
+ */
+ protected function stashMwGlobals( $globalKeys ) {
+ if ( is_string( $globalKeys ) ) {
+ $globalKeys = array( $globalKeys );
+ }
+
+ foreach ( $globalKeys as $globalKey ) {
+ // NOTE: make sure we only save the global once or a second call to
+ // setMwGlobals() on the same global would override the original
+ // value.
+ if ( !array_key_exists( $globalKey, $this->mwGlobals ) ) {
+ if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
+ throw new Exception( "Global with key {$globalKey} doesn't exist and cant be stashed" );
+ }
+ // NOTE: we serialize then unserialize the value in case it is an object
+ // this stops any objects being passed by reference. We could use clone
+ // and if is_object but this does account for objects within objects!
+ try {
+ $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
+ }
+ // NOTE; some things such as Closures are not serializable
+ // in this case just set the value!
+ catch ( Exception $e ) {
+ $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
+ }
+ }
+ }
+ }
+
+ /**
+ * Merges the given values into a MW global array variable.
+ * Useful for setting some entries in a configuration array, instead of
+ * setting the entire array.
+ *
+ * @param string $name The name of the global, as in wgFooBar
+ * @param array $values The array containing the entries to set in that global
+ *
+ * @throws MWException If the designated global is not an array.
+ *
+ * @since 1.21
+ */
+ protected function mergeMwGlobalArrayValue( $name, $values ) {
+ if ( !isset( $GLOBALS[$name] ) ) {
+ $merged = $values;
+ } else {
+ if ( !is_array( $GLOBALS[$name] ) ) {
+ throw new MWException( "MW global $name is not an array." );
+ }
+
+ // NOTE: do not use array_merge, it screws up for numeric keys.
+ $merged = $GLOBALS[$name];
+ foreach ( $values as $k => $v ) {
+ $merged[$k] = $v;
+ }
+ }
+
+ $this->setMwGlobals( $name, $merged );
+ }
+
+ /**
+ * @return string
+ * @since 1.18
+ */
+ public function dbPrefix() {
+ return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
+ }
+
+ /**
+ * @return bool
+ * @since 1.18
+ */
+ public function needsDB() {
+ # if the test says it uses database tables, it needs the database
+ if ( $this->tablesUsed ) {
+ return true;
+ }
+
+ # if the test says it belongs to the Database group, it needs the database
+ $rc = new ReflectionClass( $this );
+ if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Stub. If a test needs to add additional data to the database, it should
+ * implement this method and do so
+ *
+ * @since 1.18
+ */
+ public function addDBData() {
+ }
+
+ private function addCoreDBData() {
+ if ( $this->db->getType() == 'oracle' ) {
+
+ # Insert 0 user to prevent FK violations
+ # Anonymous user
+ $this->db->insert( 'user', array(
+ 'user_id' => 0,
+ 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) );
+
+ # Insert 0 page to prevent FK violations
+ # Blank page
+ $this->db->insert( 'page', array(
+ 'page_id' => 0,
+ 'page_namespace' => 0,
+ 'page_title' => ' ',
+ 'page_restrictions' => null,
+ 'page_counter' => 0,
+ 'page_is_redirect' => 0,
+ 'page_is_new' => 0,
+ 'page_random' => 0,
+ 'page_touched' => $this->db->timestamp(),
+ 'page_latest' => 0,
+ 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) );
+ }
+
+ User::resetIdByNameCache();
+
+ //Make sysop user
+ $user = User::newFromName( 'UTSysop' );
+
+ if ( $user->idForName() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTSysopPassword' );
+
+ $user->addGroup( 'sysop' );
+ $user->addGroup( 'bureaucrat' );
+ $user->saveSettings();
+ }
+
+ //Make 1 page with 1 revision
+ $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+ if ( $page->getId() == 0 ) {
+ $page->doEditContent(
+ new WikitextContent( 'UTContent' ),
+ 'UTPageSummary',
+ EDIT_NEW,
+ false,
+ User::newFromName( 'UTSysop' ) );
+ }
+ }
+
+ /**
+ * Restores MediaWiki to using the table set (table prefix) it was using before
+ * setupTestDB() was called. Useful if we need to perform database operations
+ * after the test run has finished (such as saving logs or profiling info).
+ *
+ * @since 1.21
+ */
+ public static function teardownTestDB() {
+ if ( !self::$dbSetup ) {
+ return;
+ }
+
+ CloneDatabase::changePrefix( self::$oldTablePrefix );
+
+ self::$oldTablePrefix = false;
+ self::$dbSetup = false;
+ }
+
+ /**
+ * Creates an empty skeleton of the wiki database by cloning its structure
+ * to equivalent tables using the given $prefix. Then sets MediaWiki to
+ * use the new set of tables (aka schema) instead of the original set.
+ *
+ * This is used to generate a dummy table set, typically consisting of temporary
+ * tables, that will be used by tests instead of the original wiki database tables.
+ *
+ * @since 1.21
+ *
+ * @note the original table prefix is stored in self::$oldTablePrefix. This is used
+ * by teardownTestDB() to return the wiki to using the original table set.
+ *
+ * @note this method only works when first called. Subsequent calls have no effect,
+ * even if using different parameters.
+ *
+ * @param DatabaseBase $db The database connection
+ * @param string $prefix The prefix to use for the new table set (aka schema).
+ *
+ * @throws MWException If the database table prefix is already $prefix
+ */
+ public static function setupTestDB( DatabaseBase $db, $prefix ) {
+ global $wgDBprefix;
+ if ( $wgDBprefix === $prefix ) {
+ throw new MWException(
+ 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
+ }
+
+ if ( self::$dbSetup ) {
+ return;
+ }
+
+ $tablesCloned = self::listTables( $db );
+ $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
+ $dbClone->useTemporaryTables( self::$useTemporaryTables );
+
+ self::$dbSetup = true;
+ self::$oldTablePrefix = $wgDBprefix;
+
+ if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
+ CloneDatabase::changePrefix( $prefix );
+
+ return;
+ } else {
+ $dbClone->cloneTableStructure();
+ }
+
+ if ( $db->getType() == 'oracle' ) {
+ $db->query( 'BEGIN FILL_WIKI_INFO; END;' );
+ }
+ }
+
+ /**
+ * Empty all tables so they can be repopulated for tests
+ */
+ private function resetDB() {
+ if ( $this->db ) {
+ if ( $this->db->getType() == 'oracle' ) {
+ if ( self::$useTemporaryTables ) {
+ wfGetLB()->closeAll();
+ $this->db = wfGetDB( DB_MASTER );
+ } else {
+ foreach ( $this->tablesUsed as $tbl ) {
+ if ( $tbl == 'interwiki' ) {
+ continue;
+ }
+ $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ );
+ }
+ }
+ } else {
+ foreach ( $this->tablesUsed as $tbl ) {
+ if ( $tbl == 'interwiki' || $tbl == 'user' ) {
+ continue;
+ }
+ $this->db->delete( $tbl, '*', __METHOD__ );
+ }
+ }
+ }
+ }
+
+ /**
+ * @since 1.18
+ *
+ * @param string $func
+ * @param array $args
+ *
+ * @return mixed
+ * @throws MWException
+ */
+ public function __call( $func, $args ) {
+ static $compatibility = array(
+ 'assertEmpty' => 'assertEmpty2', // assertEmpty was added in phpunit 3.7.32
+ );
+
+ if ( isset( $compatibility[$func] ) ) {
+ return call_user_func_array( array( $this, $compatibility[$func] ), $args );
+ } else {
+ throw new MWException( "Called non-existant $func method on "
+ . get_class( $this ) );
+ }
+ }
+
+ /**
+ * Used as a compatibility method for phpunit < 3.7.32
+ * @param string $value
+ * @param string $msg
+ */
+ private function assertEmpty2( $value, $msg ) {
+ return $this->assertTrue( $value == '', $msg );
+ }
+
+ private static function unprefixTable( $tableName ) {
+ global $wgDBprefix;
+
+ return substr( $tableName, strlen( $wgDBprefix ) );
+ }
+
+ private static function isNotUnittest( $table ) {
+ return strpos( $table, 'unittest_' ) !== 0;
+ }
+
+ /**
+ * @since 1.18
+ *
+ * @param DataBaseBase $db
+ *
+ * @return array
+ */
+ public static function listTables( $db ) {
+ global $wgDBprefix;
+
+ $tables = $db->listTables( $wgDBprefix, __METHOD__ );
+
+ if ( $db->getType() === 'mysql' ) {
+ # bug 43571: cannot clone VIEWs under MySQL
+ $views = $db->listViews( $wgDBprefix, __METHOD__ );
+ $tables = array_diff( $tables, $views );
+ }
+ $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables );
+
+ // Don't duplicate test tables from the previous fataled run
+ $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) );
+
+ if ( $db->getType() == 'sqlite' ) {
+ $tables = array_flip( $tables );
+ // these are subtables of searchindex and don't need to be duped/dropped separately
+ unset( $tables['searchindex_content'] );
+ unset( $tables['searchindex_segdir'] );
+ unset( $tables['searchindex_segments'] );
+ $tables = array_flip( $tables );
+ }
+
+ return $tables;
+ }
+
+ /**
+ * @throws MWException
+ * @since 1.18
+ */
+ protected function checkDbIsSupported() {
+ if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
+ throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
+ }
+ }
+
+ /**
+ * @since 1.18
+ * @param string $offset
+ * @return mixed
+ */
+ public function getCliArg( $offset ) {
+ if ( isset( PHPUnitMaintClass::$additionalOptions[$offset] ) ) {
+ return PHPUnitMaintClass::$additionalOptions[$offset];
+ }
+ }
+
+ /**
+ * @since 1.18
+ * @param string $offset
+ * @param mixed $value
+ */
+ public function setCliArg( $offset, $value ) {
+ PHPUnitMaintClass::$additionalOptions[$offset] = $value;
+ }
+
+ /**
+ * Don't throw a warning if $function is deprecated and called later
+ *
+ * @since 1.19
+ *
+ * @param string $function
+ */
+ public function hideDeprecated( $function ) {
+ wfSuppressWarnings();
+ wfDeprecated( $function );
+ wfRestoreWarnings();
+ }
+
+ /**
+ * Asserts that the given database query yields the rows given by $expectedRows.
+ * The expected rows should be given as indexed (not associative) arrays, with
+ * the values given in the order of the columns in the $fields parameter.
+ * Note that the rows are sorted by the columns given in $fields.
+ *
+ * @since 1.20
+ *
+ * @param string|array $table The table(s) to query
+ * @param string|array $fields The columns to include in the result (and to sort by)
+ * @param string|array $condition "where" condition(s)
+ * @param array $expectedRows An array of arrays giving the expected rows.
+ *
+ * @throws MWException If this test cases's needsDB() method doesn't return true.
+ * Test cases can use "@group Database" to enable database test support,
+ * or list the tables under testing in $this->tablesUsed, or override the
+ * needsDB() method.
+ */
+ protected function assertSelect( $table, $fields, $condition, array $expectedRows ) {
+ if ( !$this->needsDB() ) {
+ throw new MWException( 'When testing database state, the test cases\'s needDB()' .
+ ' method should return true. Use @group Database or $this->tablesUsed.' );
+ }
+
+ $db = wfGetDB( DB_SLAVE );
+
+ $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) );
+ $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
+
+ $i = 0;
+
+ foreach ( $expectedRows as $expected ) {
+ $r = $res->fetchRow();
+ self::stripStringKeys( $r );
+
+ $i += 1;
+ $this->assertNotEmpty( $r, "row #$i missing" );
+
+ $this->assertEquals( $expected, $r, "row #$i mismatches" );
+ }
+
+ $r = $res->fetchRow();
+ self::stripStringKeys( $r );
+
+ $this->assertFalse( $r, "found extra row (after #$i)" );
+ }
+
+ /**
+ * Utility method taking an array of elements and wrapping
+ * each element in it's own array. Useful for data providers
+ * that only return a single argument.
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ *
+ * @return array
+ */
+ protected function arrayWrap( array $elements ) {
+ return array_map(
+ function ( $element ) {
+ return array( $element );
+ },
+ $elements
+ );
+ }
+
+ /**
+ * Assert that two arrays are equal. By default this means that both arrays need to hold
+ * the same set of values. Using additional arguments, order and associated key can also
+ * be set as relevant.
+ *
+ * @since 1.20
+ *
+ * @param array $expected
+ * @param array $actual
+ * @param bool $ordered If the order of the values should match
+ * @param bool $named If the keys should match
+ */
+ protected function assertArrayEquals( array $expected, array $actual,
+ $ordered = false, $named = false
+ ) {
+ if ( !$ordered ) {
+ $this->objectAssociativeSort( $expected );
+ $this->objectAssociativeSort( $actual );
+ }
+
+ if ( !$named ) {
+ $expected = array_values( $expected );
+ $actual = array_values( $actual );
+ }
+
+ call_user_func_array(
+ array( $this, 'assertEquals' ),
+ array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) )
+ );
+ }
+
+ /**
+ * Put each HTML element on its own line and then equals() the results
+ *
+ * Use for nicely formatting of PHPUnit diff output when comparing very
+ * simple HTML
+ *
+ * @since 1.20
+ *
+ * @param string $expected HTML on oneline
+ * @param string $actual HTML on oneline
+ * @param string $msg Optional message
+ */
+ protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
+ $expected = str_replace( '>', ">\n", $expected );
+ $actual = str_replace( '>', ">\n", $actual );
+
+ $this->assertEquals( $expected, $actual, $msg );
+ }
+
+ /**
+ * Does an associative sort that works for objects.
+ *
+ * @since 1.20
+ *
+ * @param array $array
+ */
+ protected function objectAssociativeSort( array &$array ) {
+ uasort(
+ $array,
+ function ( $a, $b ) {
+ return serialize( $a ) > serialize( $b ) ? 1 : -1;
+ }
+ );
+ }
+
+ /**
+ * Utility function for eliminating all string keys from an array.
+ * Useful to turn a database result row as returned by fetchRow() into
+ * a pure indexed array.
+ *
+ * @since 1.20
+ *
+ * @param mixed $r The array to remove string keys from.
+ */
+ protected static function stripStringKeys( &$r ) {
+ if ( !is_array( $r ) ) {
+ return;
+ }
+
+ foreach ( $r as $k => $v ) {
+ if ( is_string( $k ) ) {
+ unset( $r[$k] );
+ }
+ }
+ }
+
+ /**
+ * Asserts that the provided variable is of the specified
+ * internal type or equals the $value argument. This is useful
+ * for testing return types of functions that return a certain
+ * type or *value* when not set or on error.
+ *
+ * @since 1.20
+ *
+ * @param string $type
+ * @param mixed $actual
+ * @param mixed $value
+ * @param string $message
+ */
+ protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
+ if ( $actual === $value ) {
+ $this->assertTrue( true, $message );
+ } else {
+ $this->assertType( $type, $actual, $message );
+ }
+ }
+
+ /**
+ * Asserts the type of the provided value. This can be either
+ * in internal type such as boolean or integer, or a class or
+ * interface the value extends or implements.
+ *
+ * @since 1.20
+ *
+ * @param string $type
+ * @param mixed $actual
+ * @param string $message
+ */
+ protected function assertType( $type, $actual, $message = '' ) {
+ if ( class_exists( $type ) || interface_exists( $type ) ) {
+ $this->assertInstanceOf( $type, $actual, $message );
+ } else {
+ $this->assertInternalType( $type, $actual, $message );
+ }
+ }
+
+ /**
+ * Returns true if the given namespace defaults to Wikitext
+ * according to $wgNamespaceContentModels
+ *
+ * @param int $ns The namespace ID to check
+ *
+ * @return bool
+ * @since 1.21
+ */
+ protected function isWikitextNS( $ns ) {
+ global $wgNamespaceContentModels;
+
+ if ( isset( $wgNamespaceContentModels[$ns] ) ) {
+ return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the ID of a namespace that defaults to Wikitext.
+ *
+ * @throws MWException If there is none.
+ * @return int The ID of the wikitext Namespace
+ * @since 1.21
+ */
+ protected function getDefaultWikitextNS() {
+ global $wgNamespaceContentModels;
+
+ static $wikitextNS = null; // this is not going to change
+ if ( $wikitextNS !== null ) {
+ return $wikitextNS;
+ }
+
+ // quickly short out on most common case:
+ if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
+ return NS_MAIN;
+ }
+
+ // NOTE: prefer content namespaces
+ $namespaces = array_unique( array_merge(
+ MWNamespace::getContentNamespaces(),
+ array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these
+ MWNamespace::getValidNamespaces()
+ ) );
+
+ $namespaces = array_diff( $namespaces, array(
+ NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
+ ) );
+
+ $talk = array_filter( $namespaces, function ( $ns ) {
+ return MWNamespace::isTalk( $ns );
+ } );
+
+ // prefer non-talk pages
+ $namespaces = array_diff( $namespaces, $talk );
+ $namespaces = array_merge( $namespaces, $talk );
+
+ // check default content model of each namespace
+ foreach ( $namespaces as $ns ) {
+ if ( !isset( $wgNamespaceContentModels[$ns] ) ||
+ $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
+ ) {
+
+ $wikitextNS = $ns;
+
+ return $wikitextNS;
+ }
+ }
+
+ // give up
+ // @todo Inside a test, we could skip the test as incomplete.
+ // But frequently, this is used in fixture setup.
+ throw new MWException( "No namespace defaults to wikitext!" );
+ }
+
+ /**
+ * Check, if $wgDiff3 is set and ready to merge
+ * Will mark the calling test as skipped, if not ready
+ *
+ * @since 1.21
+ */
+ protected function checkHasDiff3() {
+ global $wgDiff3;
+
+ # This check may also protect against code injection in
+ # case of broken installations.
+ wfSuppressWarnings();
+ $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
+ wfRestoreWarnings();
+
+ if ( !$haveDiff3 ) {
+ $this->markTestSkipped( "Skip test, since diff3 is not configured" );
+ }
+ }
+
+ /**
+ * Check whether we have the 'gzip' commandline utility, will skip
+ * the test whenever "gzip -V" fails.
+ *
+ * Result is cached at the process level.
+ *
+ * @return bool
+ *
+ * @since 1.21
+ */
+ protected function checkHasGzip() {
+ static $haveGzip;
+
+ if ( $haveGzip === null ) {
+ $retval = null;
+ wfShellExec( 'gzip -V', $retval );
+ $haveGzip = ( $retval === 0 );
+ }
+
+ if ( !$haveGzip ) {
+ $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" );
+ }
+
+ return $haveGzip;
+ }
+
+ /**
+ * Check if $extName is a loaded PHP extension, will skip the
+ * test whenever it is not loaded.
+ *
+ * @since 1.21
+ * @param string $extName
+ * @return bool
+ */
+ protected function checkPHPExtension( $extName ) {
+ $loaded = extension_loaded( $extName );
+ if ( !$loaded ) {
+ $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
+ }
+
+ return $loaded;
+ }
+
+ /**
+ * Asserts that an exception of the specified type occurs when running
+ * the provided code.
+ *
+ * @since 1.21
+ * @deprecated since 1.22 Use setExpectedException
+ *
+ * @param callable $code
+ * @param string $expected
+ * @param string $message
+ */
+ protected function assertException( $code, $expected = 'Exception', $message = '' ) {
+ $pokemons = null;
+
+ try {
+ call_user_func( $code );
+ } catch ( Exception $pokemons ) {
+ // Gotta Catch 'Em All!
+ }
+
+ if ( $message === '' ) {
+ $message = 'An exception of type "' . $expected . '" should have been thrown';
+ }
+
+ $this->assertInstanceOf( $expected, $pokemons, $message );
+ }
+
+ /**
+ * Asserts that the given string is a valid HTML snippet.
+ * Wraps the given string in the required top level tags and
+ * then calls assertValidHtmlDocument().
+ * The snippet is expected to be HTML 5.
+ *
+ * @since 1.23
+ *
+ * @note Will mark the test as skipped if the "tidy" module is not installed.
+ * @note This ignores $wgUseTidy, so we can check for valid HTML even (and especially)
+ * when automatic tidying is disabled.
+ *
+ * @param string $html An HTML snippet (treated as the contents of the body tag).
+ */
+ protected function assertValidHtmlSnippet( $html ) {
+ $html = '<!DOCTYPE html><html><head><title>test</title></head><body>' . $html . '</body></html>';
+ $this->assertValidHtmlDocument( $html );
+ }
+
+ /**
+ * Asserts that the given string is valid HTML document.
+ *
+ * @since 1.23
+ *
+ * @note Will mark the test as skipped if the "tidy" module is not installed.
+ * @note This ignores $wgUseTidy, so we can check for valid HTML even (and especially)
+ * when automatic tidying is disabled.
+ *
+ * @param string $html A complete HTML document
+ */
+ protected function assertValidHtmlDocument( $html ) {
+ // Note: we only validate if the tidy PHP extension is available.
+ // In case wgTidyInternal is false, MWTidy would fall back to the command line version
+ // of tidy. In that case however, we can not reliably detect whether a failing validation
+ // is due to malformed HTML, or caused by tidy not being installed as a command line tool.
+ // That would cause all HTML assertions to fail on a system that has no tidy installed.
+ if ( !$GLOBALS['wgTidyInternal'] ) {
+ $this->markTestSkipped( 'Tidy extension not installed' );
+ }
+
+ $errorBuffer = '';
+ MWTidy::checkErrors( $html, $errorBuffer );
+ $allErrors = preg_split( '/[\r\n]+/', $errorBuffer );
+
+ // Filter Tidy warnings which aren't useful for us.
+ // Tidy eg. often cries about parameters missing which have actually
+ // been deprecated since HTML4, thus we should not care about them.
+ $errors = preg_grep(
+ '/^(.*Warning: (trimming empty|.* lacks ".*?" attribute).*|\s*)$/m',
+ $allErrors, PREG_GREP_INVERT
+ );
+
+ $this->assertEmpty( $errors, implode( "\n", $errors ) );
+ }
+
+ /**
+ * Note: we are overriding this method to remove the deprecated error
+ * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=69505
+ * @see https://github.com/sebastianbergmann/phpunit/issues/1292
+ *
+ * @param array $matcher
+ * @param string $actual
+ * @param string $message
+ * @param bool $isHtml
+ */
+ public static function assertTag( $matcher, $actual, $message = '', $isHtml = true ) {
+ //trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED);
+
+ $dom = PHPUnit_Util_XML::load( $actual, $isHtml );
+ $tags = PHPUnit_Util_XML::findNodes( $dom, $matcher, $isHtml );
+ $matched = count( $tags ) > 0 && $tags[0] instanceof DOMNode;
+
+ self::assertTrue( $matched, $message );
+ }
+}
diff --git a/tests/phpunit/README b/tests/phpunit/README
new file mode 100644
index 00000000..0a32ba17
--- /dev/null
+++ b/tests/phpunit/README
@@ -0,0 +1,53 @@
+== MediaWiki PHPUnit Tests ==
+
+The unit tests for MediaWiki are implemented using the PHPUnit testing
+framework and require PHPUnit to run.
+
+
+=== WARNING ===
+
+Some of the unit tests are DESTRUCTIVE and WILL ALTER YOUR WIKI'S CONTENTS.
+
+DO NOT RUN THESE TESTS ON A PRODUCTION SYSTEM OR ON ANY SYSTEM WHERE YOU NEED
+TO RETAIN YOUR DATA.
+
+
+== Installation ==
+
+If PHPUnit is not installed, follow the installation instructions in the
+PHPUnit Manual at:
+
+ http://www.phpunit.de/manual/current/en/installation.html
+
+- or -
+
+On Unix-like operating systems, run:
+
+ make install
+
+
+== Running tests ==
+
+The tests are run from your operating system's command line.
+
+Ensure that you are in the tests/phpunit directory of your MediaWiki
+installation.
+
+
+On Unix-like operating systems, the tests runs are controlled with a makefile.
+Run command:
+
+ make help
+
+for a full list of options for running tests.
+
+
+On Windows-family operating systems, run the 'run-tests.bat' batch file.
+
+
+=== Writing tests ===
+
+A guide to writing unit tests for MediaWiki can be found at:
+
+ http://mediawiki.org/wiki/Unit_Testing
+
diff --git a/tests/phpunit/ResourceLoaderTestCase.php b/tests/phpunit/ResourceLoaderTestCase.php
new file mode 100644
index 00000000..f5f302e0
--- /dev/null
+++ b/tests/phpunit/ResourceLoaderTestCase.php
@@ -0,0 +1,95 @@
+<?php
+
+abstract class ResourceLoaderTestCase extends MediaWikiTestCase {
+ protected static function getResourceLoaderContext( $lang = 'en' ) {
+ $resourceLoader = new ResourceLoader();
+ $request = new FauxRequest( array(
+ 'lang' => $lang,
+ 'modules' => 'startup',
+ 'only' => 'scripts',
+ 'skin' => 'vector',
+ 'target' => 'test',
+ ) );
+ return new ResourceLoaderContext( $resourceLoader, $request );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ ResourceLoader::clearCache();
+
+ $this->setMwGlobals( array(
+ // For ResourceLoader::inDebugMode since it doesn't have context
+ 'wgResourceLoaderDebug' => true,
+
+ // Avoid influence from wgInvalidateCacheOnLocalSettingsChange
+ 'wgCacheEpoch' => '20140101000000',
+
+ // For ResourceLoader::__construct()
+ 'wgResourceLoaderSources' => array(),
+
+ // For wfScript()
+ 'wgScriptPath' => '/w',
+ 'wgScriptExtension' => '.php',
+ 'wgScript' => '/w/index.php',
+ 'wgLoadScript' => '/w/load.php',
+ ) );
+ }
+}
+
+/* Stubs */
+
+class ResourceLoaderTestModule extends ResourceLoaderModule {
+ protected $dependencies = array();
+ protected $group = null;
+ protected $source = 'local';
+ protected $script = '';
+ protected $styles = '';
+ protected $skipFunction = null;
+ protected $isRaw = false;
+ protected $targets = array( 'test' );
+
+ public function __construct( $options = array() ) {
+ foreach ( $options as $key => $value ) {
+ $this->$key = $value;
+ }
+ }
+
+ public function getScript( ResourceLoaderContext $context ) {
+ return $this->script;
+ }
+
+ public function getStyles( ResourceLoaderContext $context ) {
+ return array( '' => $this->styles );
+ }
+
+ public function getDependencies() {
+ return $this->dependencies;
+ }
+
+ public function getGroup() {
+ return $this->group;
+ }
+
+ public function getSource() {
+ return $this->source;
+ }
+
+ public function getSkipFunction() {
+ return $this->skipFunction;
+ }
+
+ public function isRaw() {
+ return $this->isRaw;
+ }
+}
+
+class ResourceLoaderFileModuleTestModule extends ResourceLoaderFileModule {
+}
+
+class ResourceLoaderWikiModuleTestModule extends ResourceLoaderWikiModule {
+ // Override expected via PHPUnit mocks and stubs
+ protected function getPages( ResourceLoaderContext $context ) {
+ return array();
+ }
+}
diff --git a/tests/phpunit/TODO b/tests/phpunit/TODO
new file mode 100644
index 00000000..cd9b9e2d
--- /dev/null
+++ b/tests/phpunit/TODO
@@ -0,0 +1,20 @@
+== Things To Do ==
+
+* Most of the tests are named poorly;
+ naming should describe a use case in story-like language,
+ not simply identify the unit under test.
+ An example would be the difference between "testCalculate"
+ and "testAddingIntegersTogetherWorks".
+
+* Many of the tests make multiple assertions, and are thus not unitary tests.
+ By using data-providers and more use-case oriented test selection
+ nearly all of these cases can be easily resolved.
+
+* Some of the test files are either incorrectly named or in the wrong folder.
+ Tests should be organized in a mirrored structure to the source they are testing,
+ and named the same, with the exception of the word "Test" at the end.
+
+* Shared set-up code or base classes are present,
+ but usually named improperly or appear to be poorly factored.
+ Support code should share as much of the same naming as the code it's supporting,
+ and test and test-case depenencies should be considered to resolve other shared needs.
diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php
new file mode 100644
index 00000000..121aade9
--- /dev/null
+++ b/tests/phpunit/bootstrap.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Bootstrapping for MediaWiki PHPUnit tests
+ * This file is included by phpunit and is NOT in the global scope.
+ *
+ * @file
+ */
+
+if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ echo <<<EOF
+You are running these tests directly from phpunit. You may not have all globals correctly set.
+Running phpunit.php instead is recommended.
+EOF;
+ require_once __DIR__ . "/phpunit.php";
+}
+
+class MediaWikiPHPUnitBootstrap {
+
+ public function __construct() {
+ wfProfileIn( __CLASS__ );
+ }
+
+ public function __destruct() {
+ wfProfileOut( __CLASS__ );
+
+ // Return to real wiki db, so profiling data is preserved
+ MediaWikiTestCase::teardownTestDB();
+
+ // Log profiling data, e.g. in the database or UDP
+ wfLogProfilingData();
+ }
+
+}
+
+// This will be destructed after all tests have been run
+$mediawikiPHPUnitBootstrap = new MediaWikiPHPUnitBootstrap();
diff --git a/tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php b/tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php
new file mode 100644
index 00000000..6dfce7a1
--- /dev/null
+++ b/tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php
@@ -0,0 +1,4 @@
+<?php
+
+class TestAutoloadedCamlClass {
+}
diff --git a/tests/phpunit/data/autoloader/TestAutoloadedClass.php b/tests/phpunit/data/autoloader/TestAutoloadedClass.php
new file mode 100644
index 00000000..9ceedf6b
--- /dev/null
+++ b/tests/phpunit/data/autoloader/TestAutoloadedClass.php
@@ -0,0 +1,4 @@
+<?php
+
+class TestAutoloadedClass {
+}
diff --git a/tests/phpunit/data/autoloader/TestAutoloadedLocalClass.php b/tests/phpunit/data/autoloader/TestAutoloadedLocalClass.php
new file mode 100644
index 00000000..1b397cd6
--- /dev/null
+++ b/tests/phpunit/data/autoloader/TestAutoloadedLocalClass.php
@@ -0,0 +1,4 @@
+<?php
+
+class TestAutoloadedLocalClass {
+}
diff --git a/tests/phpunit/data/autoloader/TestAutoloadedSerializedClass.php b/tests/phpunit/data/autoloader/TestAutoloadedSerializedClass.php
new file mode 100644
index 00000000..80b9d58d
--- /dev/null
+++ b/tests/phpunit/data/autoloader/TestAutoloadedSerializedClass.php
@@ -0,0 +1,4 @@
+<?php
+
+class TestAutoloadedSerializedClass {
+}
diff --git a/tests/phpunit/data/css/expected.css b/tests/phpunit/data/css/expected.css
new file mode 100644
index 00000000..03addcb7
--- /dev/null
+++ b/tests/phpunit/data/css/expected.css
@@ -0,0 +1,11 @@
+/* All of the combinations should result in the same output in LTR and RTL mode. */
+
+.selector { /*@embed*/ background-image: url(simple-ltr.gif); }
+
+.selector { /*@embed*/ background-image: url(simple-ltr.gif); }
+
+.selector { /*@embed*/ background-image: url(simple-ltr.gif); }
+
+.selector { /*@embed*/ background-image: url(simple-ltr.gif); }
+
+.selector { /*@embed*/ background-image: url(simple-ltr.gif); }
diff --git a/tests/phpunit/data/css/simple-ltr.gif b/tests/phpunit/data/css/simple-ltr.gif
new file mode 100644
index 00000000..13c43e90
--- /dev/null
+++ b/tests/phpunit/data/css/simple-ltr.gif
Binary files differ
diff --git a/tests/phpunit/data/css/simple-rtl.gif b/tests/phpunit/data/css/simple-rtl.gif
new file mode 100644
index 00000000..f9e75316
--- /dev/null
+++ b/tests/phpunit/data/css/simple-rtl.gif
Binary files differ
diff --git a/tests/phpunit/data/css/test.css b/tests/phpunit/data/css/test.css
new file mode 100644
index 00000000..8d0d6708
--- /dev/null
+++ b/tests/phpunit/data/css/test.css
@@ -0,0 +1,11 @@
+/* All of the combinations should result in the same output in LTR and RTL mode. */
+
+/*@noflip*/ .selector { /*@embed*/ background-image: url(simple-ltr.gif); }
+
+/*@noflip*/ .selector { background-image: /*@embed*/ url(simple-ltr.gif); }
+
+.selector { /*@noflip*/ /*@embed*/ background-image: url(simple-ltr.gif); }
+
+.selector { /*@embed*/ /*@noflip*/ background-image: url(simple-ltr.gif); }
+
+.selector { /*@noflip*/ background-image: /*@embed*/ url(simple-ltr.gif); }
diff --git a/tests/phpunit/data/cssmin/green.gif b/tests/phpunit/data/cssmin/green.gif
new file mode 100644
index 00000000..f9e75316
--- /dev/null
+++ b/tests/phpunit/data/cssmin/green.gif
Binary files differ
diff --git a/tests/phpunit/data/cssmin/large.png b/tests/phpunit/data/cssmin/large.png
new file mode 100644
index 00000000..64bf48aa
--- /dev/null
+++ b/tests/phpunit/data/cssmin/large.png
Binary files differ
diff --git a/tests/phpunit/data/cssmin/red.gif b/tests/phpunit/data/cssmin/red.gif
new file mode 100644
index 00000000..13c43e90
--- /dev/null
+++ b/tests/phpunit/data/cssmin/red.gif
Binary files differ
diff --git a/tests/phpunit/data/db/mysql/functions.sql b/tests/phpunit/data/db/mysql/functions.sql
new file mode 100644
index 00000000..9e5e470f
--- /dev/null
+++ b/tests/phpunit/data/db/mysql/functions.sql
@@ -0,0 +1,12 @@
+-- MySQL test file for DatabaseTest::testStoredFunctions()
+
+DELIMITER //
+
+CREATE FUNCTION mw_test_function()
+RETURNS int DETERMINISTIC
+BEGIN
+ SET @foo = 21;
+ RETURN @foo * 2;
+END//
+
+DELIMITER //
diff --git a/tests/phpunit/data/db/postgres/functions.sql b/tests/phpunit/data/db/postgres/functions.sql
new file mode 100644
index 00000000..3086d4d5
--- /dev/null
+++ b/tests/phpunit/data/db/postgres/functions.sql
@@ -0,0 +1,12 @@
+-- Postgres test file for DatabaseTest::testStoredFunctions()
+
+CREATE FUNCTION mw_test_function()
+RETURNS INTEGER
+LANGUAGE plpgsql AS
+$mw$
+DECLARE foo INTEGER;
+BEGIN
+ foo := 21;
+ RETURN foo * 2;
+END
+$mw$;
diff --git a/tests/phpunit/data/db/sqlite/tables-1.13.sql b/tests/phpunit/data/db/sqlite/tables-1.13.sql
new file mode 100644
index 00000000..2efb7a0e
--- /dev/null
+++ b/tests/phpunit/data/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/data/db/sqlite/tables-1.15.sql b/tests/phpunit/data/db/sqlite/tables-1.15.sql
new file mode 100644
index 00000000..6b3a628e
--- /dev/null
+++ b/tests/phpunit/data/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/data/db/sqlite/tables-1.16.sql b/tests/phpunit/data/db/sqlite/tables-1.16.sql
new file mode 100644
index 00000000..7e8f30ec
--- /dev/null
+++ b/tests/phpunit/data/db/sqlite/tables-1.16.sql
@@ -0,0 +1,478 @@
+-- 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 /*_*/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/data/db/sqlite/tables-1.17.sql b/tests/phpunit/data/db/sqlite/tables-1.17.sql
new file mode 100644
index 00000000..e02e3e14
--- /dev/null
+++ b/tests/phpunit/data/db/sqlite/tables-1.17.sql
@@ -0,0 +1,511 @@
+-- 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 /*_*/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/data/db/sqlite/tables-1.18.sql b/tests/phpunit/data/db/sqlite/tables-1.18.sql
new file mode 100644
index 00000000..8bfc28e2
--- /dev/null
+++ b/tests/phpunit/data/db/sqlite/tables-1.18.sql
@@ -0,0 +1,530 @@
+-- This is a copy of MediaWiki 1.18 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 INDEX /*i*/user_email ON /*_*/user (user_email(50));
+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_former_groups (
+ ufg_user int unsigned NOT NULL default 0,
+ ufg_group varbinary(16) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/ufg_user_group ON /*_*/user_former_groups (ufg_user,ufg_group);
+CREATE TABLE /*_*/user_newtalk (
+ user_id int NOT NULL default 0,
+ user_ip varbinary(40) NOT NULL default '',
+ user_last_timestamp varbinary(14) NULL default NULL
+) /*$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(255) 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 /*_*/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 /*_*/uploadstash (
+ us_id int unsigned NOT NULL PRIMARY KEY auto_increment,
+ us_user int unsigned NOT NULL,
+ us_key varchar(255) NOT NULL,
+ us_orig_path varchar(255) NOT NULL,
+ us_path varchar(255) NOT NULL,
+ us_source_type varchar(50),
+ us_timestamp varbinary(14) not null,
+ us_status varchar(50) not null,
+ us_size int unsigned NOT NULL,
+ us_sha1 varchar(31) NOT NULL,
+ us_mime varchar(255),
+ us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+ us_image_width int unsigned,
+ us_image_height int unsigned,
+ us_image_bits smallint unsigned
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user);
+CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key);
+CREATE INDEX /*i*/us_timestamp ON /*_*/uploadstash (us_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 /*_*/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/data/filerepo/video.png b/tests/phpunit/data/filerepo/video.png
new file mode 100644
index 00000000..d86dbe01
--- /dev/null
+++ b/tests/phpunit/data/filerepo/video.png
Binary files differ
diff --git a/tests/phpunit/data/filerepo/wiki.png b/tests/phpunit/data/filerepo/wiki.png
new file mode 100644
index 00000000..8c421183
--- /dev/null
+++ b/tests/phpunit/data/filerepo/wiki.png
Binary files differ
diff --git a/tests/phpunit/data/gitinfo/info-testValidJsonData.json b/tests/phpunit/data/gitinfo/info-testValidJsonData.json
new file mode 100644
index 00000000..e955a2b6
--- /dev/null
+++ b/tests/phpunit/data/gitinfo/info-testValidJsonData.json
@@ -0,0 +1 @@
+{ "head": "refs/heads/master", "headSHA1": "0123456789abcdef0123456789abcdef01234567", "headCommitDate": "1070884800", "branch": "master", "remoteURL": "https://gerrit.wikimedia.org/r/mediawiki/core" } \ No newline at end of file
diff --git a/tests/phpunit/data/less/common/test.common.mixins.less b/tests/phpunit/data/less/common/test.common.mixins.less
new file mode 100644
index 00000000..2fbe9b79
--- /dev/null
+++ b/tests/phpunit/data/less/common/test.common.mixins.less
@@ -0,0 +1,5 @@
+.test-mixin (@value) {
+ color: @value;
+ border: @foo solid @Foo;
+ line-height: test-sum(@bar, 10, 20);
+}
diff --git a/tests/phpunit/data/less/module/dependency.less b/tests/phpunit/data/less/module/dependency.less
new file mode 100644
index 00000000..c7725a25
--- /dev/null
+++ b/tests/phpunit/data/less/module/dependency.less
@@ -0,0 +1,3 @@
+@import "test.common.mixins";
+
+@unitTestColor: green;
diff --git a/tests/phpunit/data/less/module/styles.css b/tests/phpunit/data/less/module/styles.css
new file mode 100644
index 00000000..b78780a9
--- /dev/null
+++ b/tests/phpunit/data/less/module/styles.css
@@ -0,0 +1,6 @@
+/* @noflip */
+.unit-tests {
+ color: green;
+ border: 2px solid #eeeeee;
+ line-height: 35;
+}
diff --git a/tests/phpunit/data/less/module/styles.less b/tests/phpunit/data/less/module/styles.less
new file mode 100644
index 00000000..ecac8392
--- /dev/null
+++ b/tests/phpunit/data/less/module/styles.less
@@ -0,0 +1,6 @@
+@import "dependency";
+
+/* @noflip */
+.unit-tests {
+ .test-mixin(@unitTestColor);
+}
diff --git a/tests/phpunit/data/localisationcache/en.json b/tests/phpunit/data/localisationcache/en.json
new file mode 100644
index 00000000..27600cdb
--- /dev/null
+++ b/tests/phpunit/data/localisationcache/en.json
@@ -0,0 +1,5 @@
+{
+ "present-uk": "en",
+ "present-ru": "en",
+ "present-en": "en"
+}
diff --git a/tests/phpunit/data/localisationcache/ru.json b/tests/phpunit/data/localisationcache/ru.json
new file mode 100644
index 00000000..79e1444b
--- /dev/null
+++ b/tests/phpunit/data/localisationcache/ru.json
@@ -0,0 +1,4 @@
+{
+ "present-uk": "ru",
+ "present-ru": "ru"
+}
diff --git a/tests/phpunit/data/localisationcache/uk.json b/tests/phpunit/data/localisationcache/uk.json
new file mode 100644
index 00000000..f63ce5d3
--- /dev/null
+++ b/tests/phpunit/data/localisationcache/uk.json
@@ -0,0 +1,3 @@
+{
+ "present-uk": "uk"
+}
diff --git a/tests/phpunit/data/media/1bit-png.png b/tests/phpunit/data/media/1bit-png.png
new file mode 100644
index 00000000..254e403a
--- /dev/null
+++ b/tests/phpunit/data/media/1bit-png.png
Binary files differ
diff --git a/tests/phpunit/data/media/Animated_PNG_example_bouncing_beach_ball.png b/tests/phpunit/data/media/Animated_PNG_example_bouncing_beach_ball.png
new file mode 100644
index 00000000..c2f45d90
--- /dev/null
+++ b/tests/phpunit/data/media/Animated_PNG_example_bouncing_beach_ball.png
Binary files differ
diff --git a/tests/phpunit/data/media/Bishzilla_blink.gif b/tests/phpunit/data/media/Bishzilla_blink.gif
new file mode 100644
index 00000000..13e55362
--- /dev/null
+++ b/tests/phpunit/data/media/Bishzilla_blink.gif
Binary files differ
diff --git a/tests/phpunit/data/media/Gtk-media-play-ltr.svg b/tests/phpunit/data/media/Gtk-media-play-ltr.svg
new file mode 100644
index 00000000..fc22338a
--- /dev/null
+++ b/tests/phpunit/data/media/Gtk-media-play-ltr.svg
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [
+ <!ATTLIST svg
+ xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink">
+]>
+<!-- Created with Sodipodi ("http://www.sodipodi.com/") -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" version="1" x="0.00000000" y="0.00000000" width="60.0000000" height="60.0000000" viewBox="0 0 256 256" id="svg548">
+ <defs id="defs572"/>
+ <g style="font-size:12;stroke:#000000;" id="Layer_1">
+ <path d="M 256 256 L 0 256 L 0 0 L 256 0 L 256 256 z " style="fill:none;stroke:none;" id="path550"/>
+ </g>
+ <g style="font-size:12;stroke:#000000;" id="Layer_2">
+ <path d="M 35.159 8.29 C 32.18 10.01 30.329 13.216 30.329 16.656 L 30.329 245.539 C 30.329 248.978 32.179 252.184 35.158 253.902 C 38.138 255.623 41.839 255.623 44.817 253.904 L 243.037 139.463 C 246.016 137.742 247.867 134.537 247.867 131.098 C 247.867 127.658 246.016 124.452 243.037 122.731 L 44.818 8.29 C 41.839 6.57 38.138 6.57 35.159 8.29 z " style="opacity:0.2;stroke:none;" id="path552"/>
+ <path d="M 27.314 2.29 C 24.335 4.01 22.484 7.216 22.484 10.656 L 22.484 239.538 C 22.484 242.977 24.335 246.184 27.313 247.903 C 30.293 249.623 33.994 249.623 36.973 247.905 L 235.193 133.464 C 238.172 131.742 240.023 128.536 240.023 125.098 C 240.023 121.658 238.172 118.452 235.193 116.732 L 36.975 2.29 C 33.996 0.57 30.294 0.57 27.314 2.29 z " style="fill:#003399;stroke:none;" id="path553"/>
+ <path d="M 29.247 5.636 C 27.454 6.672 26.349 8.585 26.349 10.656 L 26.349 239.538 C 26.349 241.608 27.453 243.521 29.247 244.558 C 31.04 245.592 33.249 245.592 35.042 244.558 L 233.261 130.117 C 235.054 129.081 236.159 127.169 236.159 125.098 C 236.159 123.027 235.054 121.114 233.261 120.078 L 35.042 5.636 C 33.25 4.601 31.041 4.601 29.247 5.636 z " style="fill:#003399;stroke:none;" id="path554"/>
+ <path d="M 32.145 10.656 L 230.364 125.097 L 32.145 239.538 L 32.145 10.656 z " style="fill:#3399ff;stroke:none;" id="path555"/>
+ <linearGradient x1="109.971703" y1="8.70849991" x2="109.971703" y2="107.238800" id="XMLID_1_" gradientUnits="userSpaceOnUse" spreadMethod="pad">
+ <stop style="stop-color:#ffffff;stop-opacity:1;" offset="0.00000000" id="stop557"/>
+ <stop style="stop-color:#3399ff;stop-opacity:1;" offset="1.00000000" id="stop558"/>
+ <a:midPointStop offset="0" style="stop-color:#FFFFFF" id="midPointStop559"/>
+ <a:midPointStop offset="0.5" style="stop-color:#FFFFFF" id="midPointStop560"/>
+ <a:midPointStop offset="1" style="stop-color:#3399FF" id="midPointStop561"/>
+ </linearGradient>
+ <path d="M 32.145 141.057 C 36.775 141.258 41.456 141.368 46.183 141.368 C 105.41 141.368 157.526 125.124 187.799 100.524 L 32.145 10.656 L 32.145 141.057 z " style="fill:url(#XMLID_1_);stroke:none;" id="path562"/>
+ <linearGradient x1="109.972198" y1="264.875000" x2="109.972198" y2="145.249298" id="XMLID_2_" gradientUnits="userSpaceOnUse" spreadMethod="pad">
+ <stop style="stop-color:#ccffff;stop-opacity:1;" offset="0.00000000" id="stop564"/>
+ <stop style="stop-color:#3399ff;stop-opacity:1;" offset="1.00000000" id="stop565"/>
+ <a:midPointStop offset="0" style="stop-color:#CCFFFF" id="midPointStop566"/>
+ <a:midPointStop offset="0.5" style="stop-color:#CCFFFF" id="midPointStop567"/>
+ <a:midPointStop offset="1" style="stop-color:#3399FF" id="midPointStop568"/>
+ </linearGradient>
+ <path d="M 32.145 108.517 C 36.775 108.315 41.456 108.206 46.183 108.206 C 105.41 108.206 157.526 124.451 187.799 149.05 L 32.145 238.916 L 32.145 108.517 z " style="fill:url(#XMLID_2_);stroke:none;" id="path569"/>
+ <path d="M 37.145 19.316 C 36.526 19.673 36.145 20.334 36.145 21.048 L 36.145 162.69 C 36.145 163.768 36.999 198.629 38.077 198.667 C 39.154 198.703 40.41 48.03 48.066 40.375 C 55.722 32.72 212.492 122.951 213 122 C 213.507 121.049 186.703 104.509 185.77 103.97 L 39.145 19.316 C 38.526 18.959 37.764 18.959 37.145 19.316 z " style="opacity:0.5;fill:#ffffff;stroke:none;" id="path570"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/tests/phpunit/data/media/LoremIpsum.djvu b/tests/phpunit/data/media/LoremIpsum.djvu
new file mode 100644
index 00000000..42f47cd0
--- /dev/null
+++ b/tests/phpunit/data/media/LoremIpsum.djvu
Binary files differ
diff --git a/tests/phpunit/data/media/Png-native-test.png b/tests/phpunit/data/media/Png-native-test.png
new file mode 100644
index 00000000..a0b81ca9
--- /dev/null
+++ b/tests/phpunit/data/media/Png-native-test.png
Binary files differ
diff --git a/tests/phpunit/data/media/QA_icon.svg b/tests/phpunit/data/media/QA_icon.svg
new file mode 100644
index 00000000..6b5d86e4
--- /dev/null
+++ b/tests/phpunit/data/media/QA_icon.svg
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg:svg xmlns:svg="http://www.w3.org/2000/svg" version="1.0" width="60" height="60" viewBox="0 0 128 128" id="svg548">
+ <svg:defs id="defs601">
+ <svg:linearGradient id="linearGradient2802">
+ <svg:stop style="stop-color:#1d12aa;stop-opacity:1" offset="0" id="stop2804"/>
+ <svg:stop style="stop-color:#8b12aa;stop-opacity:0" offset="1" id="stop2806"/>
+ </svg:linearGradient>
+ <svg:linearGradient id="linearGradient2812">
+ <svg:stop style="stop-color:#1d25aa;stop-opacity:1" offset="0" id="stop2814"/>
+ <svg:stop style="stop-color:#8b12aa;stop-opacity:0" offset="1" id="stop2816"/>
+ </svg:linearGradient>
+ <svg:marker refX="0" refY="0" orient="auto" style="overflow:visible" id="Arrow1Lstart">
+ <svg:path d="M 0,0 L 5,-5 L -12.5,0 L 5,5 L 0,0 z " transform="scale(0.8)" style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none" id="path2991"/>
+ </svg:marker>
+ <svg:linearGradient id="linearGradient4766">
+ <svg:stop style="stop-color:#0447ff;stop-opacity:1" offset="0" id="stop4768"/>
+ <svg:stop style="stop-color:#000000;stop-opacity:0" offset="1" id="stop4770"/>
+ </svg:linearGradient>
+ <svg:linearGradient x1="55.4272" y1="102.1953" x2="55.4272" y2="-7.1773" id="XMLID_1_" gradientUnits="userSpaceOnUse" gradientTransform="translate(0, -0.496766)" spreadMethod="pad">
+ <svg:stop style="stop-color:#7c74ff;stop-opacity:1" offset="0" id="stop556"/>
+ <svg:stop style="stop-color:#b3caff;stop-opacity:1" offset="0.41010001" id="stop557"/>
+ <svg:stop style="stop-color:#dfeaff;stop-opacity:1" offset="0.8258" id="stop558"/>
+ <svg:stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop559"/>
+ <midPointStop offset="0" style="stop-color:#7C74FF" id="midPointStop560"/>
+ <midPointStop offset="0.5" style="stop-color:#7C74FF" id="midPointStop561"/>
+ <midPointStop offset="0.4101" style="stop-color:#B3CAFF" id="midPointStop562"/>
+ <midPointStop offset="0.5" style="stop-color:#B3CAFF" id="midPointStop563"/>
+ <midPointStop offset="0.8258" style="stop-color:#DFEAFF" id="midPointStop564"/>
+ <midPointStop offset="0.5" style="stop-color:#DFEAFF" id="midPointStop565"/>
+ <midPointStop offset="1" style="stop-color:#FFFFFF" id="midPointStop566"/>
+ </svg:linearGradient>
+ <svg:linearGradient x1="54.7607" y1="7.2758999" x2="54.7607" y2="57.487301" id="XMLID_2_" gradientUnits="userSpaceOnUse" spreadMethod="pad">
+ <svg:stop style="stop-color:#ffffff;stop-opacity:1" offset="0" id="stop569"/>
+ <svg:stop style="stop-color:#b3caff;stop-opacity:1" offset="1" id="stop570"/>
+ <midPointStop offset="0" style="stop-color:#FFFFFF" id="midPointStop571"/>
+ <midPointStop offset="0.5" style="stop-color:#FFFFFF" id="midPointStop572"/>
+ <midPointStop offset="1" style="stop-color:#B3CAFF" id="midPointStop573"/>
+ </svg:linearGradient>
+ <svg:linearGradient x1="83.637703" y1="119.3457" x2="83.637703" y2="42.033901" id="XMLID_3_" gradientUnits="userSpaceOnUse" spreadMethod="pad">
+ <svg:stop style="stop-color:#006dff;stop-opacity:1" offset="0" id="stop577"/>
+ <svg:stop style="stop-color:#94caff;stop-opacity:1" offset="0.41010001" id="stop578"/>
+ <svg:stop style="stop-color:#dcf0ff;stop-opacity:1" offset="0.8258" id="stop579"/>
+ <svg:stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop580"/>
+ <midPointStop offset="0" style="stop-color:#006DFF" id="midPointStop581"/>
+ <midPointStop offset="0.5" style="stop-color:#006DFF" id="midPointStop582"/>
+ <midPointStop offset="0.4101" style="stop-color:#94CAFF" id="midPointStop583"/>
+ <midPointStop offset="0.5" style="stop-color:#94CAFF" id="midPointStop584"/>
+ <midPointStop offset="0.8258" style="stop-color:#DCF0FF" id="midPointStop585"/>
+ <midPointStop offset="0.5" style="stop-color:#DCF0FF" id="midPointStop586"/>
+ <midPointStop offset="1" style="stop-color:#FFFFFF" id="midPointStop587"/>
+ </svg:linearGradient>
+ <svg:linearGradient x1="265.11331" y1="52.250999" x2="265.11331" y2="87.743599" id="XMLID_4_" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1, 0, 0, 1, 349, 0)" spreadMethod="pad">
+ <svg:stop style="stop-color:#ffffff;stop-opacity:1" offset="0" id="stop590"/>
+ <svg:stop style="stop-color:#94caff;stop-opacity:1" offset="1" id="stop591"/>
+ <midPointStop offset="0" style="stop-color:#FFFFFF" id="midPointStop592"/>
+ <midPointStop offset="0.5" style="stop-color:#FFFFFF" id="midPointStop593"/>
+ <midPointStop offset="1" style="stop-color:#94CAFF" id="midPointStop594"/>
+ </svg:linearGradient>
+ </svg:defs>
+ <svg:g style="font-size:12px;stroke:#000000" id="Layer_2">
+ <svg:path d="M 128,128 L 0,128 L 0,0 L 128,0 L 128,128 z " style="fill:none;stroke:none" id="path550"/>
+ </svg:g>
+ <svg:g style="font-size:12px;stroke:#000000" id="Layer_1"/>
+ <svg:path d="M 9.041,92.189 C 9.041,92.189 21.955,85.393 30.11,67.382 L 52.198,76.897 C 52.198,76.897 46.422,92.189 9.041,92.189 z " style="font-size:12px;fill:#00008d;stroke:none" id="path553"/>
+ <svg:path d="M 1.905,49.712 C 1.905,70.733 25.867,87.773 55.427,87.773 C 84.987,87.773 108.949,70.733 108.949,49.712 C 108.949,28.692 84.987,11.651 55.427,11.651 C 25.867,11.651 1.905,28.692 1.905,49.712 z " style="font-size:12px;fill:#00008d;stroke:none" id="path554"/>
+ <svg:path d="M 55.427,13.193234 C 27.039,13.193234 3.943,29.352234 3.943,49.214234 C 3.943,61.333234 12.55,72.067234 25.703,78.598234 C 22.202,83.521234 18.6,87.075234 15.722,89.464234 C 27.71,88.800234 35.664,86.388234 40.883,83.762234 C 45.498,84.716234 50.377,85.236234 55.427,85.236234 C 83.815,85.236234 106.91,69.077234 106.91,49.215234 C 106.91,29.353234 83.815,13.193234 55.427,13.193234 z " style="font-size:12px;fill:url(#XMLID_1_);stroke:none" id="path567"/>
+ <svg:path d="M 12.999,35.282 C 30.044,44.81 49.474,47.149 69.356,41.962 C 73.46,40.821 77.627,39.436 81.656,38.096 C 86.51,36.482 91.504,34.846 96.524,33.573 C 88.559,23.302 72.888,16.748 55.428,16.748 C 37.091,16.749 20.396,24.128 12.999,35.282 z " style="font-size:12px;fill:url(#XMLID_2_);stroke:none" id="path574"/>
+ <svg:text x="32.487015" y="68.006958" style="font-size:48px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Microsoft Sans Serif" id="text2303" xml:space="preserve"><svg:tspan x="32.487015" y="68.006958" style="font-size:64px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;font-family:sans" id="tspan2305">?</svg:tspan></svg:text>
+ <svg:path d="M 42.401,82.248 C 42.401,98.96 60.9,112.557 83.638,112.557 C 86.794,112.557 90.053,112.231 93.329,111.651 C 98.255,113.817 104.317,115.142 111.437,115.538 L 126.095,116.35 L 114.798,106.972 C 114.067,106.367 113.056,105.441 111.922,104.237 C 120.165,98.531 124.875,90.66 124.875,82.25 C 124.875,65.538 106.376,51.942 83.637,51.942 C 60.9,51.94 42.401,65.536 42.401,82.248 z " style="font-size:12px;fill:#0032a4;stroke:none" id="path575"/>
+ <svg:path d="M 44.823,82.248 C 44.823,97.624 62.236,110.133 83.637,110.133 C 87.009,110.133 90.357,109.784 93.616,109.163 C 98.327,111.368 104.334,112.717 111.571,113.118 L 118.9,113.524 L 113.251,108.835 C 111.96,107.763 110.162,106.071 108.268,103.754 C 117.176,98.487 122.454,90.629 122.454,82.248 C 122.454,66.871 105.042,54.363 83.639,54.363 C 62.236,54.363 44.823,66.871 44.823,82.248 z " style="font-size:12px;fill:url(#XMLID_3_);stroke:none" id="path588"/>
+ <svg:path d="M 83.638,57.505 C 98.257,57.505 110.759,63.777 115.655,72.576 C 102.935,80.147 88.183,82.013 73.429,78.165 C 66.228,76.163 59.247,73.276 52.12,71.719 C 57.332,63.374 69.498,57.505 83.638,57.505 z " style="font-size:12px;fill:url(#XMLID_4_);stroke:none" id="path595"/>
+ <svg:g transform="matrix(1.38561, 0, 0, 1.38561, -32.2514, -30.5491)" id="g4248">
+ <svg:path d="M 103.21356 24.205935 A 24.311146 23.627199 0 1 1 54.591267,24.205935 A 24.311146 23.627199 0 1 1 103.21356 24.205935 z" transform="matrix(0.148134, 0, 0, 0.152972, 71.9504, 64.0705)" style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:6.64218044;stroke-linecap:square;marker-start:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="path3881"/>
+ <svg:path d="M 87.539971,77.627004 C 87.539971,78.31674 87.591107,91.916423 87.60756,94.355578 C 87.61771,95.860439 89.879004,95.050778 90.026509,95.980703 C 90.2785,97.569343 86.888685,97.025111 86.718511,97.01762 C 85.743882,96.974724 82.425764,97.036144 81.376943,97.036144 C 81.101002,97.036144 77.578516,97.69007 77.314172,96.309196 C 77.071189,95.039902 79.49446,95.29146 79.833236,94.380195 C 81.070282,91.052684 81.154686,84.029315 80.322646,79.891188 C 79.902772,77.802954 76.928763,78.363984 77.263297,76.859643 C 77.479369,75.888015 78.579837,75.778912 79.35102,75.513942 C 81.049574,74.930337 83.123826,75.068206 84.579101,74.012707 C 86.187481,72.846162 87.539971,75.631913 87.539971,77.627004 z " style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.99986994;stroke-linecap:square;marker-start:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" id="path4774"/>
+ </svg:g>
+</svg:svg> \ No newline at end of file
diff --git a/tests/phpunit/data/media/README b/tests/phpunit/data/media/README
new file mode 100644
index 00000000..9913f685
--- /dev/null
+++ b/tests/phpunit/data/media/README
@@ -0,0 +1,61 @@
+This directory contains media files for use with the
+tests in includes/media directory.
+
+Image credits:
+
+QA_icon.svg:
+http://es.wikipedia.org/wiki/Archivo:QA_icon.svg
+GNU Lesser General Public License
+~~helix84 (16.4.2007), Philverney (6.12.2005) David Vignoni
+
+Gtk-media-play-ltr.svg
+http://commons.wikimedia.org/wiki/File:Gtk-media-play-ltr.svg
+GNU Lesser General Public License
+http://ftp.gnome.org/pub/GNOME/sources/gnome-themes-extras/0.9/gnome-themes-extras-0.9.0.tar.gz
+David Vignoni
+
+US_states_by_total_state_tax_revenue.svg
+http://commons.wikimedia.org/wiki/File:US_states_by_total_state_tax_revenue.svg
+CC BY 3.0
+TastyCakes on English Wikipedia
+
+greyscale-na-png.png, rgb-png.png, Xmp-exif-multilingual_test.jpg
+greyscale-png.png, 1bit-png.png, Png-native-test.png, rgb-na-png.png,
+test.tiff, test.jpg, jpeg-comment-multiple.jpg, jpeg-comment-utf.jpg,
+jpeg-comment-iso8859-1.jpg, jpeg-comment-binary.jpg, jpeg-xmp-psir.jpg,
+jpeg-xmp-alt.jpg, animated.gif, exif-user-comment.jpg, animated-xmp.gif,
+iptc-timetest-invalid.jpg, jpeg-iptc-bad-hash.jpg, iptc-timetest.jpg,
+xmp.png, nonanimated.gif, exif-gps.jpg, jpeg-xmp-psir.xmp, jpeg-iptc-good-hash.jpg,
+jpeg-padding-even.jpg, jpeg-padding-odd.jpg
+Are all by Bawolff. I don't think they contain enough originality to
+claim copyright, but on the off chance they do, feel free to use them
+however you feel fit, without restriction.
+
+Animated_PNG_example_bouncing_beach_ball.png
+http://commons.wikimedia.org/wiki/File:Animated_PNG_example_bouncing_beach_ball.png (originally http://www.treebuilder.de/default.asp?file=89031.xml )
+Public Domain
+Holger Will
+
+Tux.svg
+https://commons.wikimedia.org/wiki/File:Tux.svg
+Larry Ewing, Simon Budig, Anja Gerwinski
+"The copyright holder of this file allows anyone to use it for any purpose, provided that the copyright holder is properly attributed. Redistribution, derivative work, commercial use, and all other use is permitted."
+
+Speech_bubbles.svg (Modified slightly)
+https://commons.wikimedia.org/wiki/File:Speech_bubbles.svg
+CC BY-SA 3.0
+Jarry1250
+
+Soccer_ball_animated.svg
+https://commons.wikimedia.org/wiki/File:Soccer_ball_animated.svg
+GFDL 1.2 or later, CC-BY-SA 3.0 unported, CC-BY-SA 2.5 generic, CC-BY-SA 2.0 generic, or CC-BY-SA 1.0 generic
+Pumbaa80
+
+Bishzilla_blink.gif
+https://commons.wikimedia.org/wiki/File:Bishzilla_blink.gif
+Public domain
+Bishonen
+
+say-test.ogg
+Public domain
+Brian Wolff
diff --git a/tests/phpunit/data/media/Soccer_ball_animated.svg b/tests/phpunit/data/media/Soccer_ball_animated.svg
new file mode 100644
index 00000000..6bd82fc4
--- /dev/null
+++ b/tests/phpunit/data/media/Soccer_ball_animated.svg
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg>
+<svg width="150" height="150" viewBox="-105 -105 210 210" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <clipPath id="ball">
+ <circle r="100" stroke-width="0"/>
+ </clipPath>
+ <radialGradient id="shadow1" cx=".4" cy=".3" r=".8">
+ <stop offset="0" stop-color="white" stop-opacity="1"/>
+ <stop offset=".4" stop-color="white" stop-opacity="1"/>
+ <stop offset=".8" stop-color="#EEEEEE" stop-opacity="1"/>
+ </radialGradient>
+ <radialGradient id="shadow2" cx=".5" cy=".5" r=".5">
+ <stop offset="0" stop-color="white" stop-opacity="0"/>
+ <stop offset=".8" stop-color="white" stop-opacity="0"/>
+ <stop offset=".99" stop-color="black" stop-opacity=".3"/>
+ <stop offset="1" stop-color="black" stop-opacity="1"/>
+ </radialGradient>
+ <g id="black_stuff" stroke-linejoin="round" clip-path="url(#ball)">
+ <g fill="black">
+ <path d="M 6,-32 Q 26,-28 46,-19 Q 57,-35 64,-47 Q 50,-68 37,-76 Q 17,-75 1,-68 Q 4,-51 6,-32"/>
+ <path d="M -26,-2 Q -45,-8 -62,-11 Q -74,5 -76,22 Q -69,40 -50,54 Q -32,47 -17,39 Q -23,15 -26,-2"/>
+ <path d="M -95,22 Q -102,12 -102,-8 V 80 H -85 Q -95,45 -95,22"/>
+ <path d="M 55,24 Q 41,41 24,52 Q 28,65 31,79 Q 55,78 68,67 Q 78,50 80,35 Q 65,28 55,24"/>
+ <path d="M 0,120 L -3,95 Q -25,93 -42,82 Q -50,84 -60,81"/>
+ <path d="M -90,-48 Q -80,-52 -68,-49 Q -52,-71 -35,-77 Q -35,-100 -40,-100 H -100"/>
+ <path d="M 100,-55 L 87,-37 Q 98,-10 97,5 L 100,6"/>
+ </g>
+ <g fill="none">
+ <path d="M 6,-32 Q -18,-12 -26,-2
+ M 46,-19 Q 54,5 55,24
+ M 64,-47 Q 77,-44 87,-37
+ M 37,-76 Q 39,-90 36,-100
+ M 1,-68 Q -13,-77 -35,-77
+ M -62,-11 Q -67,-25 -68,-49
+ M -76,22 Q -85,24 -95,22
+ M -50,54 Q -49,70 -42,82
+ M -17,39 Q 0,48 24,52
+ M 31,79 Q 20,92 -3,95
+ M 68,67 L 80,80
+ M 80,35 Q 90,25 97,5
+ "/>
+ </g>
+ </g>
+ </defs>
+ <circle r="100" fill="white" stroke="none"/>
+ <circle r="100" fill="url(#shadow1)" stroke="none"/>
+ <g><animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0" to="360" begin="0s" dur="3s" repeatCount="indefinite"/>
+ <use xlink:href="#black_stuff" stroke="#EEE" stroke-width="7"/>
+ <use xlink:href="#black_stuff" stroke="#DDD" stroke-width="4"/>
+ <use xlink:href="#black_stuff" stroke="#999" stroke-width="2"/>
+ <use xlink:href="#black_stuff" stroke="black" stroke-width="1"/>
+ </g>
+ <circle r="100" fill="url(#shadow2)" stroke="none"/>
+</svg>
diff --git a/tests/phpunit/data/media/Speech_bubbles.svg b/tests/phpunit/data/media/Speech_bubbles.svg
new file mode 100644
index 00000000..6b1ef7a9
--- /dev/null
+++ b/tests/phpunit/data/media/Speech_bubbles.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="17.7cm" height="13cm" id="svg2" version="1.1" inkscape:version="0.48.2 r9819" sodipodi:docname="New document 1">
+ <defs id="defs4"/>
+ <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.7" inkscape:cx="296.43458" inkscape:cy="130.17435" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:window-width="1366" inkscape:window-height="706" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1"/>
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-0.28125,-1.21875)">
+ <switch style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><text xml:space="preserve" x="90" y="108.07646" id="text2985-de" sodipodi:linespacing="125%" systemLanguage="de"><tspan text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2987-de">Hallo!</tspan></text><text xml:space="preserve" x="90" y="108.07646" id="text2985-fr" sodipodi:linespacing="125%" systemLanguage="fr"><tspan x="80" y="108.07646" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2987-fr">Bonjour</tspan></text><text xml:space="preserve" x="90" y="108.07646" id="text2985-nl" sodipodi:linespacing="125%" systemLanguage="nl, tlh-ca"><tspan x="90" y="108.07646" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2987-nl">Hallo!</tspan></text><text xml:space="preserve" x="90" y="108.07646" id="text2985" sodipodi:linespacing="125%"><tspan x="90" y="108.07646" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2987" sodipodi:role="line">Hello!</tspan></text></switch>
+ <switch style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><text xml:space="preserve" x="330" y="188.07648" id="text2989-de" sodipodi:linespacing="125%" systemLanguage="de"><tspan x="323" y="188.07648" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2991-de">Hallo! Wie</tspan><tspan x="350" y="238.07648" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2993-de" sodipodi:role="line">geht's?</tspan></text><text xml:space="preserve" x="330" y="188.07648" id="text2989-fr" sodipodi:linespacing="125%" systemLanguage="fr"><tspan x="335" y="188.07648" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2991-fr">Bonjour,</tspan><tspan x="350" y="238.07648" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2993-fr">ça va?</tspan></text><text xml:space="preserve" x="330" y="188.07648" id="text2989-nl" sodipodi:linespacing="125%" systemLanguage="nl, tlh-ca"><tspan x="310" y="188.07648" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2991-nl">Hallo! Hoe</tspan><tspan x="330" y="238.07648" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2993-nl">gaat het?</tspan></text><text xml:space="preserve" x="330" y="188.07648" id="text2989" sodipodi:linespacing="125%"><tspan x="330" y="188.07648" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2991" sodipodi:role="line">Hello! How</tspan><tspan x="330" y="238.07648" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2993" sodipodi:role="line">are you?</tspan></text></switch>
+ <switch style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><text xml:space="preserve" x="101.42857" y="318.64789" id="text2995-fr" sodipodi:linespacing="125%" systemLanguage="fr"><tspan x="82" y="323" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2997-fr">Ça va bien,</tspan><tspan x="117.42857" y="368.64789" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2999-fr">et toi?</tspan></text><text xml:space="preserve" x="101.42857" y="318.64789" id="text2995-nl" sodipodi:linespacing="125%" systemLanguage="nl, tlh-ca"><tspan x="101.42857" y="318.64789" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2997-nl">Goed,</tspan><tspan x="101.42857" y="368.64789" font-size="90%" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2999-nl">met jou?</tspan></text><text xml:space="preserve" x="101.42857" y="318.64789" id="text2995" sodipodi:linespacing="125%"><tspan x="101.42857" y="318.64789" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2997" sodipodi:role="line">I'm well,</tspan><tspan x="101.42857" y="368.64789" text-decoration="normal" font-style="normal" font-weight="normal" id="tspan2999" sodipodi:role="line"> you?</tspan></text></switch>
+ <path style="color:#000000;fill:none;stroke:#808080;stroke-width:8.19999980999999960;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" d="m 145.41518,24.660714 c -54.439497,0 -98.562501,30.043022 -98.562501,67.125 0,9.936246 3.188468,19.358966 8.875,27.843746 -3.477405,24.25473 -24,58.71875 -24,58.71875 0,0 55.316401,-29.49598 68.544641,-28.55804 2.17169,0.15398 -0.660951,4.01645 -2.044641,0.93304 14.019951,5.22007 30.083661,8.21875 47.187501,8.21875 54.4395,0 98.59375,-30.07427 98.59375,-67.156246 0,-37.081978 -44.15425,-67.125 -98.59375,-67.125 z" id="path3769" inkscape:connector-curvature="0" sodipodi:nodetypes="ssccscsss"/>
+ <path style="color:#000000;fill:none;stroke:#808080;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" d="m 416.54255,99.214524 c 73.5252,0 133.11712,43.566276 133.11712,97.339926 0,14.40884 -4.3063,28.073 -11.98645,40.37703 4.69653,35.1725 32.41406,85.14978 32.41406,85.14978 0,0 -74.70955,-42.77297 -92.57542,-41.41284 -2.93306,0.22328 0.89266,5.82436 2.76145,1.35303 -18.93514,7.56977 -40.63057,11.91824 -63.73076,11.91824 -73.52523,0 -133.15935,-43.61157 -133.15935,-97.38524 0,-53.77365 59.63412,-97.339926 133.15935,-97.339926 z" id="path3769-1" inkscape:connector-curvature="0" sodipodi:nodetypes="ssccscsss"/>
+ <path style="color:#000000;fill:none;stroke:#808080;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" d="m 173.1621,250.34923 c -64.02996,0 -115.926026,34.29807 -115.926026,76.63201 0,11.34353 3.750173,22.1008 10.438488,31.7873 -4.090007,27.68997 -28.228023,67.03517 -28.228023,67.03517 0,0 65.061361,-33.67353 80.619991,-32.60275 2.55427,0.17578 -0.77738,4.5853 -2.40483,1.06519 16.4898,5.95939 35.38343,9.38278 55.5004,9.38278 64.02999,0 115.96279,-34.33373 115.96279,-76.66769 0,-42.33394 -51.9328,-76.63201 -115.96279,-76.63201 z" id="path3769-1-7" inkscape:connector-curvature="0" sodipodi:nodetypes="ssccscsss"/>
+ </g>
+</svg>
diff --git a/tests/phpunit/data/media/Toll_Texas_1.svg b/tests/phpunit/data/media/Toll_Texas_1.svg
new file mode 100644
index 00000000..73004e3e
--- /dev/null
+++ b/tests/phpunit/data/media/Toll_Texas_1.svg
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY ns_svg "http://www.w3.org/2000/svg">
+ <!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
+]>
+<svg version="1.1" id="Layer_1" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="385" height="385.0004883"
+ viewBox="0 0 385 385.0004883" overflow="visible" enable-background="new 0 0 385 385.0004883" xml:space="preserve">
+<g>
+ <g>
+ <g>
+ <path fill="#FFFFFF" d="M0.5,24.5c0-13.2548828,10.7451172-24,24-24h336c13.2548828,0,24,10.7451172,24,24v336.0004883
+ c0,13.2548828-10.7451172,24-24,24h-336c-13.2548828,0-24-10.7451172-24-24V24.5L0.5,24.5z"/>
+ <path fill="#FFFFFF" d="M192.5,192.5004883"/>
+ </g>
+ <g>
+ <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3.863693" d="M0.5,24.5
+ c0-13.2548828,10.7451172-24,24-24h336c13.2548828,0,24,10.7451172,24,24v336.0004883c0,13.2548828-10.7451172,24-24,24h-336
+ c-13.2548828,0-24-10.7451172-24-24V24.5L0.5,24.5z"/>
+ <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3.863693" d="
+ M192.5,192.5004883"/>
+ </g>
+ </g>
+ <g>
+ <path fill="#003882" d="M24.5,0.5h336c13.2548828,0,24,10.7451172,24,24v232.0004883H0.5V24.5
+ C0.5,11.2451172,11.2451172,0.5,24.5,0.5z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M10.5,24.5c0-7.7319336,6.2680664-14,14-14h336c7.7324219,0,14,6.2680664,14,14v222.0004883h-364V24.5z"/>
+ </g>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#003882" points="93.809082,348.2397461 91.6787109,347.8666992
+ 89.5478516,347.7368164 85.2929688,347.7368164 83.1640625,347.8666992 78.9042969,348.6157227 76.7763672,349.1166992
+ 72.7666016,350.3706055 70.7631836,351.246582 68.8837891,352.121582 67.0053711,353.1254883 65.1254883,354.253418
+ 63.3740234,355.5063477 60.1210938,358.2602539 58.4926758,359.762207 57.1132813,361.2641602 55.7338867,362.8959961
+ 54.3603516,364.6459961 21.2949219,301.3999023 21.168457,301.3999023 22.5478516,299.6469727 23.9248047,298.0200195
+ 25.3022461,296.5180664 26.9296875,295.0141602 30.1875,292.2592773 31.9404297,291.0073242 33.8188477,289.8793945
+ 35.6972656,288.8774414 37.5761719,288.0004883 39.5791016,287.1245117 43.5878906,285.8706055 45.7148438,285.3706055
+ 49.9755859,284.6176758 52.1020508,284.4946289 56.3632813,284.4946289 58.4926758,284.6176758 60.6201172,284.9946289
+ 60.6201172,284.8696289 "/>
+ </g>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="32.8154297,319.559082 45.0898438,312.4233398
+ 42.2080078,298.5209961 52.7299805,308.0375977 65.1254883,300.9008789 59.2421875,313.9243164 69.8867188,323.4428711
+ 55.7338867,321.9389648 49.8476563,334.965332 46.9677734,321.0629883 "/>
+ </g>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#B01C2E" points="132.0053711,306.6606445 148.5385742,338.2211914
+ 147.4101563,339.7241211 146.1577148,341.2270508 144.9052734,342.6020508 143.5302734,343.9848633 142.1494141,345.2358398
+ 140.6484375,346.4868164 139.2705078,347.6157227 137.765625,348.6157227 136.1381836,349.6176758 134.6357422,350.621582
+ 133.0083008,351.3696289 131.3798828,352.246582 129.625,352.8745117 128,353.5024414 126.2441406,354.0004883
+ 124.4921875,354.5043945 122.737793,354.8774414 120.9853516,355.1293945 117.4785156,355.3793945 113.9707031,355.3793945
+ 112.09375,355.2543945 110.3408203,355.003418 108.5854492,354.6274414 106.9589844,354.253418 103.4501953,353.2485352
+ 101.8232422,352.6254883 100.0688477,351.871582 98.4428711,351.1235352 96.9389648,350.1176758 95.3095703,349.2426758
+ 93.809082,348.2397461 93.809082,348.1147461 93.809082,348.2397461 77.1523438,316.4282227 77.2753906,316.4282227
+ 77.2753906,316.5551758 78.78125,317.5581055 80.4057617,318.4350586 81.9116211,319.4360352 83.5395508,320.1860352
+ 85.2929688,320.9399414 86.9208984,321.5649414 90.4257813,322.5668945 92.0551758,322.9418945 93.809082,323.3188477
+ 95.5620117,323.5688477 97.4423828,323.6948242 100.9462891,323.6948242 102.6992188,323.5688477 104.4521484,323.4428711
+ 106.2060547,323.1928711 107.9609375,322.8168945 111.4682617,321.8168945 113.0947266,321.1889648 114.8481445,320.5639648
+ 116.4765625,319.6879883 118.1035156,318.9350586 119.6083984,317.934082 121.2363281,316.9301758 122.737793,315.9282227
+ 124.1152344,314.8012695 125.6196289,313.5483398 126.9960938,312.2983398 128.3759766,310.9194336 129.625,309.5424805
+ 130.8789063,308.0375977 132.0053711,306.5366211 132.1328125,306.5366211 "/>
+ </g>
+ <g>
+
+ <polyline fill="none" stroke="#003882" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3.863693" points="
+ 60.7441406,285.1196289 63,286.4956055 65.2529297,287.7485352 67.5068359,288.8774414 69.8867188,289.8793945
+ 72.3916016,290.6313477 74.8955078,291.3813477 77.4008789,291.7583008 80.0302734,292.1333008 82.5366211,292.2592773
+ 85.1655273,292.2592773 87.6708984,292.0083008 90.3022461,291.6313477 92.8066406,291.1313477 95.3095703,290.3793945
+ 97.6904297,289.5024414 100.0688477,288.5004883 102.3256836,287.2504883 104.578125,285.8706055 106.7070313,284.4946289
+ 108.7124023,282.8637695 110.5888672,281.1108398 112.3427734,279.2329102 114.0986328,277.2290039 115.5976563,275.1010742 "/>
+ </g>
+ <g>
+
+ <line fill="none" stroke="#003882" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="3.863693" x1="115.4746094" y1="275.1010742" x2="131.8793945" y2="306.2836914"/>
+ </g>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#003882" points="215.1650391,349.1166992 213.1601563,348.2397461
+ 211.2832031,347.237793 207.7773438,344.7329102 206.1503906,343.2319336 204.6435547,341.7270508 203.2685547,340.0991211
+ 202.0166016,338.347168 200.8867188,336.5942383 199.8857422,334.590332 199.0097656,332.7114258 198.2578125,330.7075195
+ 197.6328125,328.5776367 197.1289063,326.5756836 196.7548828,324.4458008 196.6318359,322.3168945 196.6318359,320.0629883
+ 196.7548828,317.934082 197.0058594,315.8041992 197.3818359,313.6743164 198.6357422,309.6665039 199.5107422,307.6635742
+ 200.5126953,305.7827148 201.6386719,303.9067383 202.890625,302.152832 204.2685547,300.3989258 205.6464844,298.8969727
+ 207.2744141,297.3920898 208.9023438,296.0161133 210.6572266,294.887207 212.5361328,293.7602539 214.4130859,292.7602539
+ 216.4179688,291.8833008 218.5449219,291.0073242 220.6767578,290.2543945 222.9306641,289.6274414 227.4384766,288.8774414
+ 229.6933594,288.6254883 231.9482422,288.5004883 234.2011719,288.5004883 236.4541016,288.6254883 238.7119141,288.8774414
+ 240.9638672,289.253418 243.21875,289.7543945 245.3476563,290.3793945 247.6025391,291.1313477 249.6054688,292.0083008
+ 251.7363281,293.0083008 251.7363281,292.8852539 253.6132813,294.137207 255.4931641,295.5151367 257.2460938,297.0161133
+ 258.8740234,298.6459961 260.5019531,300.3989258 261.8808594,302.277832 263.1328125,304.1577148 264.2578125,306.2836914
+ 266.0117188,310.543457 266.6386719,312.7983398 267.390625,317.3051758 267.5136719,319.6879883 267.390625,321.9418945
+ 267.265625,324.3188477 266.2626953,328.8286133 265.5117188,330.9575195 264.6347656,333.2114258 263.6318359,335.2163086
+ 262.5058594,337.2192383 261.1298828,339.0981445 259.625,340.9770508 258.1240234,342.6020508 256.3701172,344.1079102
+ 254.4902344,345.6098633 252.6142578,346.8618164 250.609375,347.9926758 248.4785156,348.9926758 246.3496094,349.8696289
+ 244.2216797,350.621582 242.0927734,351.246582 239.8359375,351.7485352 237.5830078,352.121582 235.3291016,352.3754883
+ 232.9501953,352.4985352 230.6933594,352.4985352 228.4414063,352.3754883 226.1865234,352.121582 223.9296875,351.7485352
+ 221.6777344,351.246582 219.421875,350.746582 217.2949219,349.9946289 "/>
+ </g>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="219.9238281,303.6547852 221.9287109,301.5258789
+ 224.4335938,299.8999023 227.1904297,298.8969727 230.1943359,298.2700195 233.0742188,298.3959961 235.9550781,299.0219727
+ 238.7119141,300.2739258 241.0898438,301.9018555 243.0957031,304.1577148 244.5957031,306.5366211 245.9746094,308.9145508
+ 246.9765625,311.4204102 247.8535156,314.0512695 248.4785156,316.8051758 248.8535156,319.559082 248.9804688,322.3168945
+ 248.9804688,325.0727539 248.6035156,327.8256836 248.1035156,330.5805664 247.3496094,333.2114258 246.3496094,335.7172852
+ 244.9726563,337.8442383 243.34375,339.6000977 241.3408203,341.1020508 239.2109375,342.355957 236.8330078,343.2319336
+ 234.4511719,343.6049805 231.9482422,343.7319336 229.4414063,343.3579102 227.1904297,342.6020508 224.9326172,341.4770508
+ 222.9306641,340.0991211 221.1757813,338.347168 219.6748047,336.3422852 218.5449219,334.2114258 217.7949219,331.8334961
+ 217.0449219,329.7036133 216.0419922,325.4477539 215.7910156,323.1928711 215.6660156,320.9399414 215.6660156,318.684082
+ 215.9169922,316.4282227 216.1669922,314.300293 216.6679688,312.0454102 217.2949219,309.918457 218.0458984,307.7895508
+ 218.9208984,305.7827148 219.9238281,303.7817383 "/>
+ </g>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#003882" points="150.4169922,290.0063477 196.3789063,289.7543945
+ 192.7470703,304.4067383 192.6220703,304.5307617 192.1210938,303.7817383 191.4960938,303.1567383 190.8681641,302.5288086
+ 190.1162109,302.027832 189.2421875,301.652832 188.4887695,301.2768555 187.6113281,301.0258789 186.7353516,300.7758789
+ 179.9741211,300.7758789 179.8481445,345.1098633 179.8481445,345.2358398 180.0966797,346.1118164 180.3486328,347.1157227
+ 180.7246094,347.9926758 181.1005859,348.7407227 181.6015625,349.6176758 182.8549805,351.1235352 183.6054688,351.7485352
+ 158.5556641,351.7485352 158.5556641,351.871582 159.3095703,351.246582 160.0595703,350.4956055 160.6845703,349.7426758
+ 161.1875,348.9926758 161.6879883,347.9926758 161.9384766,347.1157227 162.1894531,346.1118164 162.3144531,345.1098633
+ 162.4375,300.9008789 156.5527344,300.9008789 155.1767578,301.0258789 153.9248047,301.2768555 152.671875,301.5258789
+ 151.4204102,301.9018555 150.1660156,302.4008789 148.9135742,303.0297852 146.6601563,304.2797852 146.6601563,304.4067383 "/>
+ </g>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#003882" points="275.7822266,344.3598633 276.03125,298.1450195
+ 275.90625,297.769043 275.90625,297.894043 275.7822266,296.7661133 275.5302734,295.7670898 275.15625,294.6391602
+ 274.7792969,293.637207 272.8984375,291.0073242 272.0234375,290.2543945 272.1484375,290.2543945 297.4492188,290.1313477
+ 296.4453125,290.8803711 295.5683594,291.6313477 294.6904297,292.5083008 294.0673828,293.5102539 293.5644531,294.6391602
+ 293.1904297,295.7670898 293.0644531,297.0161133 293.0644531,298.2700195 293.0644531,298.1450195 293.1904297,298.1450195
+ 293.0644531,340.7260742 292.9394531,340.7260742 294.8183594,341.3540039 296.8222656,341.8540039 298.7011719,342.1040039
+ 300.7050781,342.355957 302.7070313,342.4799805 304.5878906,342.355957 306.5917969,342.2299805 308.46875,341.8540039
+ 311.4746094,340.7260742 312.4765625,340.2231445 313.3535156,339.7241211 314.3535156,339.2241211 316.109375,337.972168
+ 311.8505859,351.621582 271.3984375,351.7485352 272.3994141,351.1235352 273.2753906,350.3706055 274.0292969,349.6176758
+ 275.2802734,347.6157227 275.53125,346.4868164 275.7822266,345.4858398 275.90625,344.2348633 "/>
+ </g>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#003882" points="327.5058594,344.3598633 327.7539063,298.1450195
+ 327.6308594,297.769043 327.6308594,297.894043 327.5058594,296.7661133 327.2539063,295.7670898 326.8769531,294.6391602
+ 326.5029297,293.637207 324.6259766,291.0073242 323.7480469,290.2543945 323.8740234,290.2543945 349.171875,290.1313477
+ 348.1708984,290.8803711 347.2929688,291.6313477 346.4179688,292.5083008 345.7890625,293.5102539 345.2871094,294.6391602
+ 344.9121094,295.7670898 344.7890625,297.0161133 344.7890625,298.2700195 344.7890625,298.1450195 344.9121094,298.1450195
+ 344.7890625,340.7260742 344.6640625,340.7260742 346.5410156,341.3540039 348.5458984,341.8540039 350.4238281,342.1040039
+ 352.4277344,342.355957 354.4316406,342.4799805 356.3105469,342.355957 358.3144531,342.2299805 360.1933594,341.8540039
+ 361.1933594,341.4770508 363.1992188,340.7260742 364.2011719,340.2231445 365.078125,339.7241211 366.0820313,339.2241211
+ 367.8320313,337.972168 363.5751953,351.621582 323.1220703,351.7485352 324.125,351.1235352 325.0019531,350.3706055
+ 325.7519531,349.6176758 327.0058594,347.6157227 327.2539063,346.4868164 327.5058594,345.4858398 327.6308594,344.2348633 "/>
+ </g>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#003882" d="M188.4228516,211.0395508V89.7011719h-23.2304688V66.4711914
+ c7.4140625-0.3291016,13.8393555-2.3886719,19.2763672-6.1782227c5.4365234-3.7890625,9.3085938-8.7314453,11.6152344-14.8276367
+ h23.7236328v165.5742188H188.4228516z"/>
+</svg>
diff --git a/tests/phpunit/data/media/Tux.svg b/tests/phpunit/data/media/Tux.svg
new file mode 100644
index 00000000..39561078
--- /dev/null
+++ b/tests/phpunit/data/media/Tux.svg
@@ -0,0 +1,902 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="100%" width="100%" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="0 0 349.46883 405.12272">
+ <title>Tux</title>
+ <desc>For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg</desc>
+ <radialGradient id="ag" gradientUnits="userSpaceOnUse" cy="-551.04" cx="274.822" gradientTransform="matrix(.5671 0 0 -.2835 81.263 201.645)" r="165.384">
+ <stop stop-opacity=".502" offset="0"/>
+ <stop stop-opacity="0" offset="1"/>
+ </radialGradient>
+ <path fill="url(#ag)" d="m330.892 357.885c0 25.898-41.989 46.893-93.785 46.893-51.795 0-93.784-20.994-93.784-46.893s41.989-46.893 93.784-46.893c51.795 0.001 93.785 20.995 93.785 46.893z"/>
+ <radialGradient id="ak" gradientUnits="userSpaceOnUse" cy="-551.042" cx="268.794" gradientTransform="matrix(.5823 0 0 -.2835 -61.6052 201.14)" r="165.383">
+ <stop stop-opacity=".502" offset="0"/>
+ <stop stop-opacity="0" offset="1"/>
+ </radialGradient>
+ <path fill="url(#ak)" d="m191.223 357.381c0 25.897-43.117 46.892-96.306 46.892-53.188 0-96.305-20.994-96.305-46.892s43.117-46.893 96.305-46.893c53.188 0.001 96.306 20.995 96.306 46.893z"/>
+ <g transform="translate(8.99996 9.00046)">
+ <path d="m292.327 256.606c-4.752 19.584-28.872 60.48-41.688 78.48-12.815 18.072-11.231 34.344-34.92 28.008-23.616-6.336-30.24-5.184-54.647-3.744-24.265 1.439-19.009-0.721-34.2 6.12-15.12 6.84-65.88-82.944-69.984-99.647-4.031-16.705-5.976-14.689 4.536-32.761 10.513-18.071 12.024-35.928 25.92-57.816 13.896-21.96 29.952-33.12 28.8-49.896-4.535-62.28-8.136-93.384 19.513-107.784 26.352-13.68 48.384-5.544 57.096-0.864 3.744 2.016 11.376 5.904 17.064 12.744 5.688 6.696 10.8 16.848 13.68 29.664 5.904 25.704-2.448 17.208 4.248 46.656 6.624 29.375 20.088 43.775 36.504 67.031 16.414 23.257 33.55 61.633 28.078 83.809z"/>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#666" d="m148.47 94.049c4.319-1.728 3.592-1.958 6.472-8.222 2.304-4.824 4.328-6.898 4.256-14.242 0-7.2-2.232-9.648-5.616-14.328-3.24-4.464-8.424-4.68-11.664-4.104-1.872 0.288-4.319 2.664-5.976 6.192-1.08 2.376-1.944 5.4-2.017 8.568-0.216 8.496 0.505 11.736 2.448 17.496 2.305 6.769 7.921 10.297 12.097 8.64z"/>
+ <path fill="#6d6d6d" d="m148.47 94.023c4.293-1.717 3.563-1.954 6.425-8.178 2.289-4.793 4.312-6.861 4.271-14.164 0.027-7.152-2.162-9.702-5.488-14.201-3.296-4.345-8.376-4.509-11.593-3.953-1.916 0.283-4.354 2.569-6.038 5.968-1.159 2.31-2.016 5.353-2.087 8.535-0.212 8.438 0.547 11.691 2.46 17.417 2.268 6.731 7.901 10.221 12.05 8.576z"/>
+ <path fill="#757575" d="m148.471 93.996c4.264-1.706 3.533-1.95 6.377-8.133 2.273-4.762 4.296-6.823 4.288-14.085 0.053-7.105-2.093-9.756-5.363-14.075-3.35-4.225-8.327-4.338-11.52-3.801-1.961 0.278-4.389 2.474-6.099 5.744-1.242 2.245-2.089 5.305-2.16 8.501-0.207 8.38 0.591 11.647 2.473 17.34 2.231 6.691 7.881 10.144 12.004 8.509z"/>
+ <path fill="#7c7c7c" d="m148.471 93.969c4.235-1.694 3.506-1.946 6.329-8.089 2.26-4.731 4.28-6.786 4.304-14.006 0.081-7.058-2.021-9.811-5.236-13.948-3.403-4.105-8.278-4.167-11.446-3.649-2.006 0.273-4.424 2.379-6.16 5.519-1.322 2.179-2.161 5.257-2.232 8.468-0.202 8.323 0.636 11.603 2.486 17.261 2.191 6.654 7.859 10.068 11.955 8.444z"/>
+ <path fill="#848484" d="m148.471 93.943c4.209-1.684 3.477-1.942 6.282-8.045 2.245-4.7 4.266-6.749 4.319-13.928 0.107-7.01-1.95-9.864-5.109-13.821-3.458-3.985-8.23-3.996-11.375-3.498-2.049 0.268-4.458 2.284-6.222 5.295-1.403 2.114-2.233 5.21-2.303 8.435-0.198 8.265 0.679 11.559 2.498 17.183 2.156 6.615 7.842 9.992 11.91 8.379z"/>
+ <path fill="#8c8c8c" d="m148.471 93.916c4.181-1.672 3.448-1.938 6.235-8 2.23-4.668 4.249-6.711 4.335-13.85 0.134-6.962-1.88-9.918-4.982-13.695-3.513-3.865-8.183-3.825-11.303-3.347-2.094 0.263-4.492 2.189-6.283 5.07-1.484 2.049-2.306 5.163-2.375 8.401-0.193 8.207 0.723 11.515 2.511 17.105 2.118 6.58 7.821 9.919 11.862 8.316z"/>
+ <path fill="#939393" d="m148.472 93.889c4.152-1.661 3.419-1.934 6.188-7.956 2.215-4.638 4.233-6.674 4.35-13.771 0.161-6.915-1.809-9.972-4.854-13.568-3.567-3.746-8.134-3.654-11.23-3.195-2.138 0.259-4.527 2.094-6.345 4.847-1.564 1.983-2.378 5.115-2.447 8.368-0.188 8.149 0.767 11.47 2.523 17.026 2.079 6.54 7.8 9.841 11.815 8.249z"/>
+ <path fill="#9b9b9b" d="m148.472 93.863c4.125-1.65 3.391-1.93 6.141-7.912 2.2-4.607 4.217-6.637 4.366-13.693 0.188-6.868-1.739-10.026-4.729-13.441-3.621-3.626-8.085-3.484-11.157-3.044-2.183 0.253-4.562 1.999-6.406 4.622-1.646 1.918-2.45 5.068-2.52 8.335-0.185 8.091 0.811 11.426 2.535 16.948 2.044 6.502 7.782 9.766 11.77 8.185z"/>
+ <path fill="#a3a3a3" d="m148.472 93.836c4.097-1.639 3.361-1.926 6.094-7.867 2.185-4.576 4.201-6.599 4.382-13.614 0.214-6.82-1.669-10.081-4.603-13.315-3.676-3.506-8.036-3.313-11.084-2.893-2.229 0.249-4.598 1.904-6.47 4.398-1.726 1.852-2.521 5.021-2.591 8.301-0.18 8.034 0.854 11.382 2.548 16.87 2.008 6.465 7.763 9.691 11.724 8.12z"/>
+ <path fill="#aaa" d="m148.472 93.809c4.069-1.628 3.334-1.922 6.047-7.823 2.17-4.544 4.185-6.562 4.396-13.536 0.242-6.772-1.597-10.134-4.475-13.188-3.73-3.387-7.989-3.142-11.013-2.741-2.271 0.243-4.632 1.809-6.53 4.173-1.808 1.787-2.594 4.974-2.662 8.268-0.176 7.976 0.897 11.337 2.56 16.792 1.97 6.427 7.743 9.615 11.677 8.055z"/>
+ <path fill="#b2b2b2" d="m148.473 93.782c4.041-1.617 3.304-1.918 5.999-7.778 2.154-4.514 4.169-6.524 4.412-13.458 0.269-6.725-1.526-10.188-4.349-13.062-3.784-3.267-7.939-2.971-10.939-2.589-2.316 0.238-4.666 1.714-6.592 3.949-1.888 1.721-2.667 4.926-2.734 8.234-0.171 7.918 0.941 11.293 2.572 16.713 1.933 6.391 7.723 9.541 11.631 7.991z"/>
+ <path fill="#bababa" d="m148.473 93.756c4.014-1.606 3.275-1.914 5.951-7.734 2.141-4.482 4.153-6.487 4.43-13.379 0.295-6.678-1.457-10.243-4.223-12.935-3.839-3.147-7.892-2.8-10.867-2.438-2.36 0.233-4.701 1.619-6.653 3.725-1.969 1.656-2.739 4.879-2.806 8.201-0.167 7.86 0.984 11.249 2.585 16.636 1.895 6.35 7.702 9.462 11.583 7.924z"/>
+ <path fill="#c1c1c1" d="m148.473 93.729c3.985-1.595 3.247-1.91 5.904-7.69 2.125-4.451 4.138-6.45 4.445-13.3 0.321-6.63-1.387-10.297-4.096-12.808-3.894-3.028-7.844-2.629-10.795-2.287-2.405 0.229-4.735 1.524-6.716 3.5-2.049 1.59-2.811 4.831-2.878 8.167-0.161 7.802 1.029 11.205 2.599 16.557 1.859 6.314 7.683 9.389 11.537 7.861z"/>
+ <path fill="#c9c9c9" d="m148.473 93.702c3.958-1.583 3.219-1.906 5.857-7.646 2.11-4.42 4.121-6.412 4.46-13.222 0.35-6.583-1.315-10.351-3.969-12.682-3.947-2.908-7.794-2.458-10.722-2.135-2.45 0.224-4.771 1.429-6.777 3.276-2.13 1.525-2.883 4.784-2.95 8.135-0.157 7.745 1.073 11.16 2.611 16.479 1.821 6.276 7.663 9.313 11.49 7.795z"/>
+ <path fill="#d1d1d1" d="m148.474 93.676c3.93-1.573 3.188-1.902 5.809-7.601 2.097-4.389 4.107-6.375 4.477-13.144 0.375-6.535-1.245-10.404-3.842-12.555-4.002-2.788-7.747-2.287-10.65-1.984-2.493 0.219-4.805 1.334-6.837 3.052-2.213 1.459-2.957 4.736-3.022 8.101-0.153 7.687 1.116 11.116 2.623 16.401 1.782 6.237 7.642 9.237 11.442 7.73z"/>
+ <path fill="#d8d8d8" d="m148.474 93.649c3.901-1.562 3.16-1.898 5.762-7.557 2.082-4.358 4.091-6.338 4.493-13.065 0.401-6.487-1.176-10.458-3.716-12.428-4.057-2.668-7.698-2.116-10.578-1.832-2.538 0.214-4.839 1.239-6.899 2.827-2.292 1.394-3.029 4.689-3.094 8.068-0.148 7.629 1.16 11.072 2.636 16.322 1.746 6.2 7.623 9.161 11.396 7.665z"/>
+ <path fill="#e0e0e0" d="m148.474 93.622c3.875-1.55 3.132-1.894 5.715-7.512 2.066-4.327 4.075-6.3 4.508-12.987 0.429-6.44-1.104-10.513-3.588-12.302-4.111-2.549-7.65-1.945-10.506-1.681-2.582 0.209-4.874 1.144-6.961 2.604-2.373 1.328-3.102 4.642-3.165 8.034-0.145 7.571 1.204 11.027 2.647 16.244 1.709 6.162 7.604 9.086 11.35 7.6z"/>
+ <path fill="#e8e8e8" d="m148.474 93.596c3.847-1.54 3.104-1.89 5.668-7.468 2.052-4.296 4.059-6.263 4.523-12.908 0.456-6.393-1.034-10.567-3.462-12.175-4.165-2.429-7.601-1.774-10.433-1.529-2.627 0.204-4.908 1.049-7.023 2.379-2.453 1.263-3.173 4.594-3.236 8.001-0.141 7.514 1.247 10.983 2.659 16.166 1.673 6.123 7.585 9.008 11.304 7.534z"/>
+ <path fill="#efefef" d="m148.475 93.569c3.817-1.528 3.073-1.886 5.62-7.424 2.036-4.265 4.043-6.226 4.539-12.83 0.482-6.345-0.964-10.621-3.336-12.048-4.219-2.31-7.552-1.604-10.359-1.378-2.672 0.199-4.943 0.954-7.084 2.155-2.535 1.197-3.246 4.546-3.311 7.967-0.135 7.456 1.292 10.939 2.673 16.087 1.636 6.087 7.565 8.935 11.258 7.471z"/>
+ <path fill="#f7f7f7" d="m148.475 93.542c3.791-1.517 3.046-1.882 5.572-7.379 2.022-4.234 4.027-6.188 4.556-12.751 0.51-6.297-0.894-10.675-3.208-11.921-4.274-2.19-7.505-1.433-10.289-1.227-2.715 0.194-4.978 0.859-7.146 1.93-2.614 1.132-3.317 4.5-3.381 7.935-0.131 7.398 1.335 10.895 2.686 16.009 1.597 6.047 7.544 8.858 11.21 7.404z"/>
+ <path fill="#fff" d="m148.475 93.516c3.763-1.506 3.017-1.878 5.525-7.335 2.007-4.203 4.012-6.151 4.571-12.673 0.536-6.25-0.823-10.729-3.082-11.795-4.328-2.07-7.456-1.262-10.216-1.075-2.76 0.189-5.012 0.764-7.207 1.706-2.696 1.066-3.39 4.452-3.453 7.901-0.126 7.34 1.379 10.85 2.698 15.931 1.561 6.01 7.525 8.782 11.164 7.34z"/>
+ </g>
+ <path d="m132.033 74.7465c2.16 0 4.896 1.44 6.191 3.384 1.368 1.944 2.376 4.68 2.376 7.776 0 4.608-0.504 9.72-3.239 11.304-0.864 0.504-2.736 0.936-3.816 0.936-2.448 0-2.664-1.584-4.968-3.96-0.792-0.864-3.168-5.04-3.168-8.496 0-2.16-0.504-5.256 1.368-7.992 1.296-2.016 2.952-2.952 5.256-2.952z"/>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m143.862 68.608c0.844-1.305 4.222-0.69 5.45 1.996 1.229 2.687 0.998 8.522 0.153 8.829-2.226 0.691-1.535-2.534-3.454-5.451-1.919-2.762-2.994-4.067-2.149-5.374z"/>
+ <path fill="#070707" d="m143.916 68.664c0.833-1.289 4.169-0.681 5.381 1.971 1.215 2.653 0.985 8.414 0.152 8.717-2.198 0.682-1.516-2.502-3.411-5.382-1.895-2.728-2.956-4.017-2.122-5.306z"/>
+ <path fill="#0f0f0f" d="m143.97 68.719c0.822-1.272 4.114-0.673 5.312 1.945 1.198 2.619 0.973 8.306 0.15 8.605-2.169 0.673-1.497-2.47-3.367-5.313-1.871-2.692-2.918-3.964-2.095-5.237z"/>
+ <path fill="#161616" d="m144.024 68.774c0.812-1.255 4.062-0.664 5.243 1.92 1.182 2.585 0.96 8.198 0.147 8.493-2.141 0.665-1.477-2.438-3.323-5.244-1.846-2.657-2.88-3.913-2.067-5.169z"/>
+ <path fill="#1e1e1e" d="m144.078 68.829c0.801-1.239 4.008-0.655 5.174 1.895 1.167 2.551 0.947 8.09 0.146 8.381-2.113 0.656-1.458-2.405-3.28-5.174-1.821-2.623-2.842-3.863-2.04-5.102z"/>
+ <path fill="#262626" d="m144.132 68.884c0.791-1.222 3.955-0.646 5.105 1.87 1.151 2.517 0.935 7.982 0.144 8.27-2.085 0.647-1.438-2.374-3.235-5.105-1.798-2.589-2.805-3.812-2.014-5.035z"/>
+ <path fill="#2d2d2d" d="m144.186 68.939c0.779-1.206 3.9-0.638 5.036 1.844 1.135 2.483 0.922 7.874 0.142 8.158-2.057 0.639-1.419-2.341-3.192-5.037-1.773-2.552-2.766-3.758-1.986-4.965z"/>
+ <path fill="#353535" d="m144.24 68.994c0.769-1.189 3.848-0.629 4.967 1.819 1.12 2.449 0.909 7.766 0.141 8.046-2.028 0.629-1.399-2.31-3.148-4.967-1.75-2.518-2.73-3.708-1.96-4.898z"/>
+ <path fill="#3d3d3d" d="m144.294 69.049c0.76-1.172 3.794-0.621 4.898 1.793 1.104 2.415 0.896 7.658 0.138 7.934-2 0.621-1.38-2.277-3.104-4.898-1.725-2.482-2.691-3.655-1.932-4.829z"/>
+ <path fill="#444" d="m144.348 69.104c0.748-1.156 3.74-0.612 4.829 1.768 1.088 2.38 0.884 7.55 0.136 7.822-1.973 0.612-1.36-2.245-3.062-4.829-1.699-2.448-2.651-3.604-1.903-4.761z"/>
+ <path fill="#4c4c4c" d="m144.402 69.16c0.737-1.14 3.687-0.603 4.76 1.743 1.073 2.347 0.871 7.442 0.134 7.71-1.943 0.604-1.341-2.213-3.017-4.76-1.676-2.414-2.614-3.554-1.877-4.693z"/>
+ <path fill="#545454" d="m144.456 69.215c0.727-1.123 3.634-0.595 4.691 1.717 1.057 2.313 0.857 7.334 0.132 7.598-1.916 0.595-1.321-2.181-2.973-4.691-1.652-2.378-2.577-3.501-1.85-4.624z"/>
+ <path fill="#5b5b5b" d="m144.51 69.27c0.717-1.106 3.58-0.585 4.622 1.692 1.041 2.278 0.847 7.226 0.131 7.486-1.888 0.586-1.303-2.149-2.93-4.622-1.628-2.343-2.539-3.45-1.823-4.556z"/>
+ <path fill="#636363" d="m144.564 69.325c0.705-1.09 3.526-0.577 4.553 1.667 1.026 2.245 0.833 7.118 0.128 7.375-1.858 0.577-1.282-2.117-2.885-4.553-1.604-2.309-2.501-3.399-1.796-4.489z"/>
+ <path fill="#6b6b6b" d="m144.618 69.38c0.694-1.073 3.473-0.568 4.483 1.642 1.011 2.21 0.82 7.01 0.127 7.263-1.831 0.568-1.264-2.084-2.842-4.484-1.578-2.274-2.462-3.347-1.768-4.421z"/>
+ <path fill="#727272" d="m144.672 69.435c0.685-1.057 3.42-0.56 4.414 1.617 0.995 2.176 0.81 6.902 0.125 7.15-1.803 0.56-1.243-2.053-2.798-4.415-1.554-2.238-2.425-3.295-1.741-4.352z"/>
+ <path fill="#7a7a7a" d="m144.726 69.49c0.673-1.041 3.365-0.551 4.345 1.591 0.979 2.143 0.796 6.794 0.123 7.039-1.775 0.551-1.224-2.021-2.754-4.346-1.53-2.203-2.387-3.244-1.714-4.284z"/>
+ <path fill="#828282" d="m144.78 69.545c0.662-1.023 3.313-0.542 4.276 1.566 0.964 2.108 0.782 6.686 0.121 6.926-1.746 0.542-1.204-1.988-2.711-4.276-1.505-2.167-2.348-3.192-1.686-4.216z"/>
+ <path fill="#898989" d="m144.834 69.6c0.652-1.007 3.259-0.533 4.207 1.541s0.771 6.578 0.119 6.815c-1.718 0.534-1.185-1.956-2.666-4.207-1.482-2.134-2.311-3.142-1.66-4.149z"/>
+ <path fill="#919191" d="m144.888 69.655c0.641-0.99 3.206-0.524 4.138 1.516 0.933 2.04 0.758 6.47 0.117 6.703-1.69 0.525-1.165-1.924-2.623-4.138-1.457-2.098-2.273-3.09-1.632-4.081z"/>
+ <path fill="#999" d="m144.942 69.71c0.63-0.974 3.152-0.516 4.069 1.49s0.744 6.362 0.114 6.591c-1.662 0.516-1.146-1.892-2.579-4.069-1.432-2.062-2.234-3.037-1.604-4.012z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#666" d="m193.11 94.985c10.8-1.152 14.616-5.328 16.56-12.6 1.729-6.48 1.801-13.68-3.023-22.104-4.536-8.063-7.128-9.36-13.681-9.864-10.079-0.864-14.832 6.192-17.063 11.232-2.376 5.472-1.872 4.68-1.729 11.592 0.145 7.272 4.245 9.299 6.766 13.835 2.519 4.465 10.946 7.982 12.17 7.909z"/>
+ <path fill="#6d6d6d" d="m193.115 94.944c10.759-1.131 14.618-5.354 16.515-12.569 1.701-6.525 1.785-13.686-3.002-21.912-4.434-7.797-7.038-9.081-13.512-9.581-10.049-0.861-14.941 5.873-17.181 10.874-2.304 5.28-1.878 4.718-1.726 11.539 0.16 7.268 4.268 9.223 6.784 13.76 2.521 4.475 10.898 7.962 12.122 7.889z"/>
+ <path fill="#757575" d="m193.12 94.902c10.718-1.11 14.62-5.379 16.469-12.538 1.676-6.57 1.771-13.692-2.979-21.721-4.331-7.53-6.947-8.801-13.344-9.297-10.018-0.858-15.05 5.553-17.298 10.516-2.229 5.087-1.885 4.757-1.722 11.487 0.176 7.264 4.289 9.146 6.803 13.686 2.52 4.485 10.848 7.942 12.071 7.867z"/>
+ <path fill="#7c7c7c" d="m193.126 94.861c10.675-1.09 14.621-5.405 16.423-12.507 1.648-6.616 1.756-13.698-2.958-21.529-4.229-7.263-6.856-8.522-13.176-9.014-9.985-0.854-15.158 5.234-17.414 10.158-2.156 4.895-1.891 4.795-1.719 11.434 0.193 7.26 4.31 9.07 6.822 13.611 2.52 4.495 10.798 7.922 12.022 7.847z"/>
+ <path fill="#848484" d="m193.131 94.82c10.635-1.069 14.623-5.431 16.377-12.476 1.622-6.661 1.741-13.704-2.936-21.337-4.126-6.996-6.767-8.242-13.008-8.73-9.955-0.852-15.267 4.915-17.53 9.8-2.084 4.703-1.896 4.833-1.716 11.38 0.209 7.256 4.332 8.995 6.841 13.537 2.52 4.505 10.748 7.902 11.972 7.826z"/>
+ <path fill="#8c8c8c" d="m193.136 94.778c10.593-1.048 14.625-5.457 16.331-12.445 1.596-6.706 1.726-13.709-2.913-21.145-4.025-6.729-6.678-7.963-12.841-8.447-9.924-0.848-15.375 4.595-17.647 9.441-2.01 4.51-1.903 4.872-1.712 11.328 0.225 7.251 4.354 8.918 6.858 13.462 2.521 4.517 10.7 7.883 11.924 7.806z"/>
+ <path fill="#939393" d="m193.141 94.737c10.552-1.027 14.627-5.482 16.286-12.414 1.568-6.751 1.711-13.715-2.893-20.954-3.922-6.462-6.586-7.683-12.672-8.163-9.892-0.845-15.483 4.276-17.764 9.083-1.938 4.318-1.909 4.91-1.709 11.275 0.24 7.247 4.375 8.842 6.878 13.387 2.521 4.528 10.651 7.863 11.874 7.786z"/>
+ <path fill="#9b9b9b" d="m193.146 94.695c10.51-1.007 14.63-5.508 16.241-12.382 1.542-6.796 1.694-13.721-2.87-20.762-3.82-6.195-6.496-7.404-12.504-7.879-9.861-0.842-15.592 3.956-17.882 8.725-1.863 4.126-1.915 4.949-1.706 11.223 0.258 7.243 4.397 8.766 6.897 13.313 2.521 4.535 10.601 7.841 11.824 7.762z"/>
+ <path fill="#a3a3a3" d="m193.151 94.654c10.469-0.986 14.632-5.534 16.196-12.351 1.515-6.842 1.68-13.727-2.85-20.57-3.717-5.928-6.405-7.125-12.335-7.596-9.83-0.839-15.7 3.637-17.998 8.367-1.791 3.933-1.922 4.987-1.703 11.169 0.273 7.239 4.419 8.689 6.916 13.238 2.521 4.547 10.551 7.822 11.774 7.743z"/>
+ <path fill="#aaa" d="m193.157 94.612c10.427-0.965 14.633-5.56 16.149-12.32 1.488-6.887 1.666-13.733-2.826-20.379-3.615-5.661-6.316-6.845-12.168-7.313-9.799-0.835-15.809 3.317-18.114 8.009-1.718 3.741-1.928 5.025-1.7 11.117 0.29 7.235 4.44 8.613 6.936 13.163 2.519 4.558 10.499 7.804 11.723 7.723z"/>
+ <path fill="#b2b2b2" d="m193.162 94.571c10.386-0.944 14.635-5.585 16.104-12.289 1.462-6.932 1.649-13.739-2.806-20.188-3.512-5.394-6.225-6.565-11.999-7.029-9.768-0.833-15.917 2.998-18.23 7.651-1.646 3.549-1.935 5.064-1.697 11.064 0.306 7.231 4.462 8.537 6.954 13.088 2.52 4.569 10.451 7.784 11.674 7.703z"/>
+ <path fill="#bababa" d="m193.167 94.529c10.345-0.923 14.638-5.611 16.059-12.258 1.436-6.977 1.636-13.744-2.782-19.995-3.41-5.127-6.135-6.286-11.832-6.746-9.736-0.829-16.025 2.679-18.347 7.293-1.572 3.356-1.941 5.103-1.694 11.011 0.322 7.227 4.484 8.461 6.973 13.014 2.519 4.579 10.4 7.764 11.623 7.681z"/>
+ <path fill="#c1c1c1" d="m193.172 94.488c10.304-0.903 14.64-5.637 16.014-12.227 1.409-7.022 1.62-13.75-2.762-19.804-3.308-4.86-6.044-6.006-11.662-6.462-9.705-0.826-16.135 2.359-18.466 6.935-1.498 3.164-1.945 5.141-1.689 10.958 0.338 7.223 4.506 8.385 6.991 12.939 2.519 4.59 10.351 7.744 11.574 7.661z"/>
+ <path fill="#c9c9c9" d="m193.177 94.447c10.262-0.882 14.641-5.663 15.967-12.196 1.383-7.068 1.605-13.756-2.738-19.612-3.206-4.593-5.954-5.727-11.496-6.179-9.673-0.823-16.242 2.04-18.581 6.577-1.425 2.972-1.952 5.179-1.687 10.906 0.354 7.219 4.526 8.308 7.01 12.865 2.52 4.598 10.302 7.723 11.525 7.639z"/>
+ <path fill="#d1d1d1" d="m193.182 94.405c10.221-0.861 14.643-5.688 15.922-12.165 1.355-7.113 1.591-13.762-2.717-19.42-3.104-4.326-5.864-5.448-11.327-5.895-9.644-0.82-16.352 1.721-18.698 6.219-1.353 2.779-1.959 5.217-1.684 10.853 0.369 7.214 4.549 8.232 7.028 12.79 2.521 4.609 10.254 7.703 11.476 7.618z"/>
+ <path fill="#d8d8d8" d="m193.187 94.364c10.179-0.841 14.645-5.714 15.876-12.133 1.33-7.158 1.576-13.768-2.694-19.229-3.001-4.059-5.773-5.168-11.16-5.612-9.61-0.817-16.459 1.401-18.813 5.861-1.279 2.586-1.965 5.256-1.682 10.8 0.387 7.21 4.571 8.156 7.049 12.715 2.519 4.619 10.202 7.684 11.424 7.598z"/>
+ <path fill="#e0e0e0" d="m193.193 94.322c10.137-0.82 14.646-5.74 15.83-12.103 1.303-7.203 1.561-13.773-2.673-19.037-2.898-3.792-5.684-4.889-10.991-5.328-9.58-0.813-16.568 1.082-18.931 5.502-1.206 2.395-1.972 5.294-1.679 10.747 0.403 7.207 4.592 8.08 7.067 12.641 2.521 4.631 10.154 7.666 11.377 7.578z"/>
+ <path fill="#e8e8e8" d="m193.198 94.281c10.096-0.799 14.648-5.766 15.785-12.071 1.275-7.249 1.545-13.779-2.651-18.845-2.796-3.525-5.593-4.609-10.823-5.044-9.549-0.81-16.677 0.762-19.048 5.145-1.133 2.202-1.978 5.333-1.675 10.694 0.419 7.202 4.614 8.003 7.086 12.566 2.52 4.638 10.103 7.643 11.326 7.555z"/>
+ <path fill="#efefef" d="m193.203 94.239c10.055-0.778 14.65-5.792 15.739-12.04 1.25-7.293 1.531-13.785-2.629-18.653-2.694-3.258-5.502-4.33-10.655-4.761-9.517-0.807-16.785 0.443-19.165 4.786-1.059 2.01-1.983 5.372-1.671 10.642 0.435 7.198 4.636 7.928 7.104 12.492 2.52 4.649 10.055 7.624 11.277 7.534z"/>
+ <path fill="#f7f7f7" d="m193.208 94.198c10.014-0.757 14.652-5.817 15.694-12.009 1.223-7.339 1.516-13.792-2.607-18.462-2.592-2.991-5.413-4.05-10.486-4.478-9.487-0.804-16.895 0.124-19.282 4.428-0.986 1.817-1.989 5.41-1.668 10.589 0.451 7.194 4.657 7.851 7.123 12.417 2.519 4.661 10.004 7.605 11.226 7.515z"/>
+ <path fill="#fff" d="m193.213 94.156c9.973-0.737 14.654-5.843 15.648-11.978 1.197-7.384 1.501-13.797-2.585-18.27-2.489-2.724-5.322-3.771-10.319-4.194-9.455-0.801-17.002-0.196-19.397 4.07-0.913 1.625-1.996 5.448-1.665 10.536 0.467 7.19 4.679 7.775 7.142 12.342 2.519 4.671 9.954 7.586 11.176 7.494z"/>
+ </g>
+ <path d="m179.841 74.4585c5.4 0 8.568 4.824 9.648 11.016 0.432 2.808-0.216 6.048-1.944 8.28-1.944 2.592-5.4 4.176-8.208 4.176-2.664 0-5.688 0.432-7.271-1.728-1.584-2.232-1.944-7.2-1.944-10.728 0-3.96 1.152-6.768 3.168-9 1.511-1.657 4.247-2.016 6.551-2.016z"/>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m192.591 66.68c0.98-0.653 2.612 0 4.489 2.122 2.039 2.285 2.938 4.08 0.489 5.385-1.877 0.98-2.448-1.958-3.59-3.182-1.795-1.959-3.346-3.02-1.388-4.325z"/>
+ <path fill="#070707" d="m192.631 66.738c0.96-0.649 2.573 0 4.423 2.09 2.009 2.251 2.864 4.02 0.481 5.305-1.837 0.977-2.403-1.929-3.525-3.135-1.768-1.925-3.296-2.965-1.379-4.26z"/>
+ <path fill="#0f0f0f" d="m192.671 66.797c0.939-0.645 2.534 0 4.356 2.059 1.978 2.217 2.792 3.958 0.474 5.225-1.798 0.974-2.357-1.9-3.46-3.087-1.742-1.895-3.247-2.913-1.37-4.197z"/>
+ <path fill="#161616" d="m192.711 66.855c0.919-0.641 2.495 0 4.289 2.027 1.948 2.184 2.721 3.898 0.467 5.146-1.759 0.971-2.313-1.871-3.396-3.041-1.715-1.861-3.197-2.858-1.36-4.132z"/>
+ <path fill="#1e1e1e" d="m192.751 66.914c0.899-0.637 2.457 0 4.223 1.996 1.918 2.149 2.647 3.838 0.46 5.065-1.72 0.968-2.269-1.842-3.331-2.993-1.689-1.83-3.148-2.805-1.352-4.068z"/>
+ <path fill="#262626" d="m192.791 66.973c0.878-0.633 2.418 0 4.155 1.964 1.888 2.116 2.576 3.777 0.453 4.986-1.68 0.965-2.224-1.813-3.267-2.946-1.661-1.798-3.097-2.752-1.341-4.004z"/>
+ <path fill="#2d2d2d" d="m192.831 67.031c0.858-0.629 2.379 0 4.089 1.933 1.857 2.082 2.503 3.717 0.445 4.906-1.641 0.961-2.178-1.784-3.201-2.898-1.636-1.767-3.048-2.7-1.333-3.941z"/>
+ <path fill="#353535" d="m192.87 67.09c0.838-0.625 2.341 0 4.023 1.902 1.827 2.047 2.431 3.656 0.438 4.826-1.601 0.958-2.133-1.755-3.137-2.852-1.608-1.735-2.998-2.646-1.324-3.876z"/>
+ <path fill="#3d3d3d" d="m192.91 67.148c0.818-0.621 2.302 0 3.956 1.87 1.797 2.014 2.359 3.596 0.431 4.746-1.562 0.956-2.088-1.726-3.071-2.804-1.583-1.702-2.95-2.592-1.316-3.812z"/>
+ <path fill="#444" d="m192.95 67.207c0.798-0.617 2.263 0 3.889 1.839 1.768 1.98 2.287 3.535 0.425 4.666-1.523 0.952-2.043-1.697-3.008-2.757-1.556-1.671-2.899-2.539-1.306-3.748z"/>
+ <path fill="#4c4c4c" d="m192.99 67.266c0.777-0.614 2.224 0 3.823 1.807 1.735 1.946 2.214 3.474 0.416 4.586-1.483 0.949-1.998-1.667-2.942-2.709-1.529-1.639-2.85-2.486-1.297-3.684z"/>
+ <path fill="#545454" d="m193.03 67.325c0.757-0.61 2.185 0 3.756 1.775 1.706 1.912 2.143 3.414 0.409 4.506-1.444 0.946-1.953-1.639-2.878-2.663-1.502-1.606-2.799-2.431-1.287-3.618z"/>
+ <path fill="#5b5b5b" d="m193.07 67.383c0.736-0.605 2.146 0 3.688 1.744 1.677 1.878 2.07 3.353 0.402 4.426-1.405 0.943-1.908-1.609-2.813-2.615-1.475-1.575-2.749-2.378-1.277-3.555z"/>
+ <path fill="#636363" d="m193.11 67.442c0.716-0.602 2.106 0 3.622 1.712 1.646 1.844 1.998 3.293 0.395 4.347-1.364 0.94-1.862-1.581-2.748-2.568-1.449-1.543-2.701-2.326-1.269-3.491z"/>
+ <path fill="#6b6b6b" d="m193.15 67.5c0.696-0.598 2.069 0 3.556 1.681 1.615 1.811 1.925 3.232 0.387 4.267-1.325 0.937-1.818-1.552-2.683-2.521-1.423-1.511-2.651-2.272-1.26-3.427z"/>
+ <path fill="#727272" d="m193.19 67.559c0.675-0.594 2.03 0 3.489 1.649 1.585 1.777 1.853 3.172 0.38 4.187-1.287 0.935-1.774-1.522-2.619-2.473-1.396-1.48-2.601-2.219-1.25-3.363z"/>
+ <path fill="#7a7a7a" d="m193.23 67.618c0.654-0.59 1.991 0 3.422 1.618 1.555 1.743 1.781 3.111 0.373 4.107-1.247 0.931-1.729-1.494-2.554-2.426-1.369-1.448-2.551-2.166-1.241-3.299z"/>
+ <path fill="#828282" d="m193.269 67.677c0.635-0.586 1.953 0 3.355 1.586 1.525 1.708 1.709 3.05 0.366 4.026-1.208 0.928-1.684-1.464-2.489-2.378-1.342-1.416-2.501-2.112-1.232-3.234z"/>
+ <path fill="#898989" d="m193.309 67.735c0.614-0.582 1.914 0 3.29 1.555 1.493 1.675 1.636 2.99 0.357 3.947-1.169 0.925-1.639-1.435-2.424-2.332-1.316-1.384-2.452-2.058-1.223-3.17z"/>
+ <path fill="#919191" d="m193.349 67.794c0.595-0.578 1.875 0 3.223 1.523 1.464 1.641 1.564 2.93 0.351 3.867-1.129 0.922-1.594-1.406-2.359-2.284-1.29-1.352-2.403-2.005-1.215-3.106z"/>
+ <path fill="#999" d="m193.389 67.853c0.573-0.574 1.836 0 3.155 1.492 1.435 1.607 1.492 2.869 0.345 3.787-1.091 0.919-1.55-1.377-2.295-2.237-1.263-1.32-2.353-1.953-1.205-3.042z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m165.498 69.906c1.693-0.654 3.012-0.69 5.63 1.036 3.166 2.088 1.705 5.245-0.779 4.601-2.146-0.556-2.417-0.681-4.391-1.086-3.101-0.648-3.641-3.322-0.46-4.551z"/>
+ <path fill="#050505" d="m165.564 70.033c1.658-0.629 2.973-0.656 5.555 1.026 3.066 2.009 1.654 5.012-0.805 4.38-2.131-0.547-2.345-0.656-4.284-1.052-3.055-0.634-3.587-3.173-0.466-4.354z"/>
+ <path fill="#0a0a0a" d="m165.63 70.16c1.623-0.604 2.935-0.622 5.481 1.015 2.965 1.93 1.602 4.779-0.83 4.159-2.119-0.539-2.274-0.63-4.179-1.018-3.009-0.618-3.533-3.022-0.472-4.156z"/>
+ <path fill="#0f0f0f" d="m165.696 70.287c1.587-0.579 2.895-0.587 5.406 1.005 2.864 1.851 1.551 4.546-0.855 3.938-2.105-0.53-2.203-0.605-4.073-0.983-2.963-0.604-3.48-2.873-0.478-3.96z"/>
+ <path fill="#141414" d="m165.761 70.413c1.553-0.553 2.856-0.553 5.331 0.995 2.766 1.772 1.5 4.313-0.88 3.717-2.092-0.521-2.131-0.58-3.967-0.949-2.916-0.588-3.425-2.723-0.484-3.763z"/>
+ <path fill="#191919" d="m165.827 70.54c1.519-0.528 2.818-0.519 5.258 0.984 2.664 1.693 1.448 4.079-0.905 3.497-2.079-0.513-2.06-0.554-3.861-0.915-2.873-0.573-3.373-2.573-0.492-3.566z"/>
+ <path fill="#1e1e1e" d="m165.893 70.667c1.482-0.503 2.778-0.484 5.183 0.974 2.564 1.614 1.397 3.846-0.93 3.276-2.067-0.504-1.989-0.529-3.756-0.88-2.826-0.559-3.319-2.425-0.497-3.37z"/>
+ <path fill="#232323" d="m165.959 70.793c1.447-0.478 2.74-0.45 5.108 0.964 2.464 1.535 1.345 3.613-0.955 3.055-2.053-0.496-1.917-0.503-3.651-0.846-2.779-0.543-3.264-2.274-0.502-3.173z"/>
+ <path fill="#282828" d="m166.025 70.92c1.412-0.453 2.701-0.416 5.034 0.954 2.362 1.456 1.293 3.38-0.981 2.834-2.04-0.487-1.845-0.478-3.545-0.812-2.733-0.528-3.21-2.125-0.508-2.976z"/>
+ <path fill="#2d2d2d" d="m166.09 71.047c1.378-0.428 2.663-0.382 4.96 0.943 2.264 1.377 1.242 3.146-1.006 2.613-2.026-0.478-1.773-0.453-3.438-0.777-2.688-0.513-3.158-1.974-0.516-2.779z"/>
+ <path fill="#333" d="m166.156 71.173c1.343-0.402 2.624-0.347 4.885 0.933 2.163 1.298 1.191 2.914-1.029 2.392-2.015-0.47-1.703-0.428-3.334-0.743-2.642-0.498-3.104-1.824-0.522-2.582z"/>
+ <path fill="#383838" d="m166.222 71.3c1.307-0.377 2.585-0.313 4.81 0.922 2.063 1.219 1.14 2.681-1.055 2.171-2.001-0.461-1.631-0.402-3.229-0.708-2.594-0.483-3.048-1.674-0.526-2.385z"/>
+ <path fill="#3d3d3d" d="m166.288 71.427c1.272-0.352 2.546-0.279 4.736 0.913 1.962 1.14 1.088 2.447-1.081 1.95-1.988-0.452-1.56-0.377-3.122-0.674-2.55-0.469-2.995-1.526-0.533-2.189z"/>
+ <path fill="#424242" d="m166.354 71.554c1.236-0.327 2.507-0.245 4.661 0.902 1.861 1.061 1.037 2.214-1.106 1.729-1.974-0.444-1.488-0.352-3.016-0.64-2.504-0.453-2.942-1.375-0.539-1.991z"/>
+ <path fill="#474747" d="m166.419 71.68c1.203-0.302 2.469-0.21 4.587 0.892 1.762 0.982 0.986 1.98-1.13 1.508-1.962-0.435-1.417-0.326-2.911-0.606-2.458-0.437-2.888-1.224-0.546-1.794z"/>
+ <path fill="#4c4c4c" d="m166.485 71.807c1.167-0.276 2.429-0.176 4.513 0.882 1.66 0.903 0.935 1.748-1.156 1.288-1.948-0.426-1.345-0.301-2.805-0.572-2.412-0.423-2.834-1.076-0.552-1.598z"/>
+ <path fill="#515151" d="m166.551 71.934c1.133-0.251 2.391-0.142 4.438 0.871 1.56 0.824 0.883 1.515-1.181 1.067-1.936-0.417-1.274-0.275-2.699-0.537-2.366-0.408-2.781-0.926-0.558-1.401z"/>
+ <path fill="#565656" d="m166.617 72.061c1.097-0.227 2.351-0.108 4.363 0.861 1.46 0.745 0.831 1.281-1.206 0.846-1.922-0.409-1.202-0.25-2.594-0.503-2.319-0.393-2.726-0.777-0.563-1.204z"/>
+ <path fill="#5b5b5b" d="m166.683 72.187c1.062-0.201 2.312-0.073 4.289 0.851 1.358 0.666 0.778 1.048-1.231 0.625-1.91-0.4-1.131-0.225-2.489-0.469-2.274-0.377-2.672-0.626-0.569-1.007z"/>
+ <path fill="#606060" d="m166.748 72.314c1.027-0.176 2.274-0.04 4.215 0.84 1.26 0.587 0.729 0.815-1.256 0.404-1.896-0.392-1.06-0.2-2.383-0.435-2.228-0.361-2.619-0.475-0.576-0.809z"/>
+ <path fill="#666" d="m166.814 72.44c0.992-0.151 2.234-0.005 4.14 0.83 1.159 0.508 0.677 0.582-1.281 0.183-1.883-0.383-0.987-0.174-2.276-0.4-2.183-0.346-2.566-0.325-0.583-0.613z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#666" d="m159.99 128.249c-9.36 0.36-24.192-25.848-24.552-14.976-0.288 9.216 0.216 9.072 0.216 18 0 5.976-2.736 6.408-8.64 15.408-3.024 4.752-5.4 9.864-7.272 15.048-1.152 3.096-2.232 6.336-3.096 9.504-0.36 1.584-1.008 3.24-1.368 4.824-2.952 10.872-13.464 24.264-15.912 35.136-2.448 10.8-5.328 17.712-4.968 32.185 0.36 14.472 0.504 10.295 4.896 13.896 4.32 3.601 8.784 6.983 15.624 13.032 7.2 6.264 22.177 17.208 24.192 20.592 2.16 3.456 2.088 11.232 0.792 13.752-1.296 2.448-12.6 3.816-12.528 3.816-0.071 0 9.864 13.68 11.809 15.623 1.872 1.873 9.936 10.873 42.768 4.752 18.504-3.455 32.832-13.823 43.2-23.832 13.392-13.031 6.624-16.775 8.352-23.327 2.521-9.433 10.729-12.96 12.601-23.616 0.216-1.512 0.72-2.664 2.088-4.896 2.088-3.168 1.584-9.432 1.584-15.191 0-14.977-1.729-30.24-5.185-41.472-3.168-10.512-8.208-17.856-12.527-27.36-8.641-18.936-8.208-27.432-15.912-39.528-8.784-13.968-4.464-23.256-16.128-22.68-14.546 0.79-26.282 20.734-40.034 21.31z"/>
+ <path fill="#6d6d6d" d="m159.973 129.334c-9.281 0.353-23.746-25.511-24.242-15.179-0.316 8.755 0.1 8.678 0.03 17.247-0.15 5.87-2.953 6.637-8.727 15.481-3.013 4.763-5.273 9.812-6.993 14.877-0.968 3.253-1.56 6.422-2.43 9.526-0.415 1.642-1.497 3.187-2.185 5.042-3.254 10.78-13.545 24.182-15.961 34.877-2.466 10.81-5.37 17.694-4.961 32.141 0.366 14 0.395 10.177 4.773 13.816 4.283 3.616 8.839 7.069 15.662 13.103 7.183 6.248 22.237 17.216 24.243 20.588 2.149 3.444 2.131 11.317 0.844 13.823-1.284 2.439-12.579 3.875-12.508 3.875-0.071 0 9.815 13.566 11.757 15.508 1.87 1.87 9.902 10.809 42.678 4.704 18.524-3.455 33.124-13.753 43.078-23.856 12.789-12.762 6.107-16.773 7.826-23.291 2.513-9.416 11.277-12.961 13.143-23.602 0.216-1.508 0.754-2.654 2.113-4.876 2.096-3.202 1.561-9.447 1.582-15.185 0.067-15.027-1.705-30.234-5.159-41.434-3.171-10.483-8.204-17.817-12.515-27.305-8.624-18.906-8.221-27.415-15.933-39.474-8.586-13.613-4.601-22.583-16.011-21.99-14.374 0.826-26.375 21.016-40.104 21.584z"/>
+ <path fill="#757575" d="m159.955 130.419c-9.201 0.346-23.299-25.175-23.931-15.383-0.344 8.295-0.017 8.284-0.156 16.494-0.301 5.764-3.17 6.867-8.812 15.555-3.002 4.774-5.148 9.76-6.714 14.706-0.784 3.41-0.889 6.508-1.764 9.548-0.471 1.699-1.986 3.133-3.003 5.259-3.554 10.688-13.624 24.1-16.009 34.619-2.483 10.82-5.411 17.678-4.954 32.097 0.373 13.528 0.285 10.058 4.651 13.739 4.244 3.632 8.893 7.154 15.699 13.171 7.167 6.233 22.299 17.224 24.294 20.585 2.142 3.432 2.175 11.404 0.896 13.896-1.271 2.428-12.558 3.932-12.486 3.932-0.071 0 9.768 13.453 11.705 15.392 1.867 1.867 9.867 10.744 42.588 4.655 18.545-3.453 33.415-13.682 42.956-23.879 12.187-12.492 5.591-16.771 7.3-23.258 2.507-9.398 11.826-12.959 13.687-23.586 0.215-1.5 0.788-2.643 2.138-4.854 2.104-3.235 1.538-9.462 1.58-15.178 0.133-15.076-1.681-30.228-5.135-41.394-3.173-10.455-8.199-17.779-12.501-27.25-8.609-18.877-8.234-27.399-15.952-39.42-8.389-13.258-4.739-21.911-15.895-21.301-14.21 0.859-26.474 21.295-40.182 21.855z"/>
+ <path fill="#7c7c7c" d="m159.938 131.504c-9.122 0.338-22.854-24.838-23.622-15.586-0.37 7.833-0.131 7.89-0.341 15.741-0.452 5.657-3.388 7.096-8.899 15.628-2.99 4.785-5.021 9.708-6.433 14.535-0.602 3.566-0.218 6.594-1.099 9.57-0.526 1.756-2.475 3.08-3.82 5.477-3.854 10.596-13.703 24.016-16.057 34.361-2.501 10.829-5.453 17.66-4.948 32.052 0.38 13.059 0.177 9.939 4.529 13.66 4.208 3.648 8.948 7.239 15.739 13.242 7.149 6.217 22.358 17.232 24.345 20.581 2.13 3.42 2.216 11.489 0.946 13.968-1.259 2.417-12.538 3.99-12.466 3.99-0.072 0 9.718 13.34 11.653 15.275 1.865 1.864 9.833 10.681 42.498 4.607 18.565-3.453 33.706-13.609 42.834-23.902 11.583-12.223 5.074-16.771 6.774-23.223 2.499-9.382 12.375-12.959 14.229-23.57 0.215-1.496 0.821-2.633 2.162-4.834 2.111-3.271 1.516-9.478 1.578-15.173 0.199-15.125-1.657-30.221-5.109-41.354-3.177-10.427-8.196-17.741-12.488-27.195-8.594-18.848-8.247-27.383-15.972-39.366-8.192-12.903-4.877-21.239-15.779-20.612-14.041 0.894-26.569 21.576-40.254 22.128z"/>
+ <path fill="#848484" d="m159.921 132.589c-9.043 0.331-22.406-24.502-23.312-15.79-0.398 7.373-0.247 7.496-0.527 14.988-0.602 5.551-3.604 7.326-8.984 15.702-2.98 4.796-4.896 9.656-6.154 14.364-0.417 3.723 0.455 6.679-0.432 9.592-0.582 1.813-2.964 3.026-4.639 5.694-4.153 10.504-13.782 23.936-16.104 34.104-2.519 10.838-5.495 17.643-4.941 32.008 0.387 12.586 0.067 9.819 4.407 13.582 4.171 3.664 9.002 7.324 15.777 13.311 7.132 6.201 22.419 17.24 24.396 20.576 2.12 3.41 2.259 11.578 0.998 14.041-1.247 2.408-12.517 4.049-12.446 4.049-0.07 0 9.67 13.227 11.604 15.16 1.861 1.861 9.798 10.615 42.409 4.558 18.584-3.45 33.996-13.538 42.711-23.926 10.979-11.952 4.557-16.769 6.248-23.187 2.491-9.367 12.924-12.959 14.771-23.557 0.215-1.49 0.856-2.622 2.188-4.813 2.118-3.305 1.491-9.494 1.575-15.166 0.267-15.174-1.635-30.215-5.086-41.314-3.179-10.399-8.19-17.703-12.473-27.141-8.579-18.818-8.262-27.366-15.994-39.312-7.993-12.547-5.013-20.565-15.661-19.922-13.876 0.927-26.669 21.855-40.331 22.399z"/>
+ <path fill="#8c8c8c" d="m159.903 133.674c-8.963 0.323-21.961-24.165-23.001-15.994-0.426 6.912-0.363 7.102-0.713 14.236-0.753 5.445-3.821 7.554-9.071 15.775-2.969 4.807-4.768 9.604-5.875 14.192-0.232 3.881 1.128 6.766 0.234 9.615-0.638 1.87-3.452 2.972-5.455 5.911-4.455 10.413-13.862 23.853-16.153 33.845-2.537 10.849-5.537 17.625-4.935 31.963 0.393 12.115-0.042 9.701 4.285 13.505 4.133 3.68 9.057 7.409 15.814 13.38 7.116 6.188 22.48 17.248 24.447 20.574 2.109 3.398 2.301 11.662 1.049 14.113-1.235 2.396-12.496 4.104-12.425 4.104-0.071 0 9.622 13.114 11.552 15.045 1.86 1.858 9.763 10.552 42.319 4.509 18.604-3.449 34.288-13.467 42.589-23.949 10.377-11.682 4.04-16.766 5.721-23.15 2.486-9.35 13.474-12.959 15.316-23.542 0.214-1.483 0.89-2.611 2.213-4.793 2.126-3.339 1.468-9.507 1.573-15.158 0.333-15.224-1.611-30.208-5.062-41.276-3.181-10.37-8.186-17.664-12.459-27.085-8.563-18.789-8.275-27.35-16.014-39.258-7.796-12.192-5.151-19.893-15.545-19.233-13.707 0.961-26.763 22.134-40.404 22.671z"/>
+ <path fill="#939393" d="m159.886 134.759c-8.885 0.316-21.516-23.829-22.691-16.197-0.454 6.451-0.479 6.708-0.899 13.482-0.903 5.339-4.038 7.784-9.157 15.849-2.957 4.818-4.642 9.552-5.595 14.021-0.05 4.037 1.799 6.852 0.9 9.637-0.693 1.928-3.941 2.919-6.273 6.129-4.756 10.32-13.941 23.77-16.201 33.587-2.555 10.858-5.579 17.608-4.928 31.92 0.399 11.644-0.151 9.581 4.162 13.424 4.096 3.697 9.111 7.494 15.854 13.451 7.099 6.17 22.541 17.256 24.498 20.569 2.1 3.387 2.344 11.75 1.101 14.186-1.223 2.387-12.476 4.163-12.404 4.163-0.071 0 9.573 13.001 11.5 14.929 1.857 1.856 9.729 10.488 42.229 4.461 18.625-3.449 34.579-13.396 42.467-23.973 9.774-11.412 3.523-16.764 5.195-23.115 2.479-9.334 14.022-12.959 15.858-23.527 0.214-1.479 0.924-2.601 2.238-4.772 2.134-3.373 1.445-9.522 1.571-15.151 0.399-15.273-1.587-30.201-5.036-41.237-3.185-10.342-8.184-17.625-12.446-27.03-8.548-18.76-8.288-27.333-16.034-39.204-7.598-11.837-5.289-19.221-15.428-18.544-13.543 0.994-26.863 22.413-40.481 22.942z"/>
+ <path fill="#9b9b9b" d="m159.868 135.844c-8.805 0.308-21.068-23.492-22.381-16.401-0.481 5.991-0.594 6.314-1.085 12.73-1.053 5.232-4.253 8.013-9.243 15.922-2.946 4.829-4.515 9.5-5.314 13.85 0.133 4.194 2.471 6.937 1.565 9.658-0.749 1.986-4.43 2.866-7.091 6.347-5.056 10.229-14.021 23.689-16.249 33.329-2.572 10.868-5.621 17.591-4.921 31.876 0.405 11.172-0.261 9.463 4.04 13.346 4.058 3.713 9.166 7.58 15.892 13.521 7.082 6.155 22.601 17.265 24.548 20.567 2.092 3.373 2.388 11.834 1.152 14.256-1.21 2.377-12.454 4.222-12.383 4.222-0.071 0 9.523 12.888 11.45 14.813 1.854 1.854 9.692 10.424 42.138 4.412 18.645-3.447 34.871-13.324 42.345-23.996 9.171-11.143 3.007-16.762 4.669-23.08 2.472-9.317 14.572-12.959 16.401-23.514 0.214-1.473 0.958-2.588 2.265-4.75 2.142-3.408 1.421-9.539 1.568-15.145 0.466-15.324-1.564-30.196-5.012-41.198-3.187-10.313-8.179-17.587-12.433-26.976-8.533-18.73-8.301-27.316-16.054-39.149-7.401-11.482-5.426-18.548-15.313-17.855-13.373 1.029-26.958 22.694-40.554 23.215z"/>
+ <path fill="#a3a3a3" d="m159.851 136.929c-8.727 0.301-20.622-23.156-22.071-16.604-0.509 5.529-0.71 5.919-1.271 11.976-1.203 5.126-4.47 8.243-9.328 15.996-2.936 4.84-4.39 9.448-5.036 13.679 0.316 4.351 3.143 7.023 2.231 9.68-0.804 2.043-4.919 2.812-7.908 6.563-5.356 10.137-14.101 23.607-16.298 33.072-2.589 10.877-5.661 17.574-4.913 31.832 0.412 10.699-0.37 9.342 3.918 13.268 4.021 3.729 9.221 7.664 15.93 13.59 7.064 6.139 22.661 17.271 24.599 20.563 2.081 3.363 2.43 11.922 1.204 14.33-1.198 2.365-12.434 4.278-12.363 4.278-0.07 0 9.477 12.774 11.399 14.697 1.851 1.851 9.659 10.36 42.048 4.364 18.666-3.447 35.162-13.254 42.223-24.021 8.568-10.873 2.49-16.761 4.144-23.045 2.464-9.301 15.121-12.958 16.943-23.498 0.215-1.467 0.992-2.579 2.29-4.729 2.148-3.441 1.398-9.553 1.566-15.139 0.532-15.373-1.541-30.189-4.987-41.158-3.188-10.285-8.174-17.549-12.419-26.921-8.518-18.701-8.313-27.3-16.073-39.096-7.204-11.126-5.564-17.875-15.196-17.165-13.21 1.064-27.058 22.975-40.632 23.488z"/>
+ <path fill="#aaa" d="m159.834 138.014c-8.646 0.293-20.176-22.819-21.761-16.808-0.536 5.069-0.826 5.526-1.457 11.224-1.354 5.02-4.687 8.472-9.416 16.069-2.924 4.851-4.262 9.396-4.756 13.508 0.501 4.507 3.814 7.109 2.897 9.702-0.858 2.1-5.406 2.759-8.725 6.782-5.657 10.045-14.181 23.524-16.347 32.812-2.606 10.888-5.703 17.557-4.906 31.787 0.418 10.229-0.479 9.225 3.795 13.189 3.984 3.745 9.275 7.749 15.968 13.66 7.048 6.124 22.723 17.279 24.651 20.559 2.07 3.352 2.472 12.008 1.255 14.402-1.186 2.355-12.414 4.337-12.343 4.337-0.071 0 9.428 12.66 11.348 14.581 1.85 1.848 9.624 10.297 41.958 4.314 18.687-3.444 35.453-13.18 42.102-24.043 7.965-10.602 1.973-16.758 3.616-23.01 2.457-9.283 15.67-12.957 17.487-23.482 0.214-1.461 1.026-2.568 2.315-4.709 2.155-3.477 1.375-9.568 1.563-15.131 0.6-15.424-1.518-30.184-4.963-41.119-3.192-10.257-8.17-17.511-12.405-26.866-8.502-18.672-8.328-27.284-16.095-39.042-7.005-10.771-5.701-17.203-15.078-16.476-13.04 1.098-27.152 23.255-40.703 23.76z"/>
+ <path fill="#b2b2b2" d="m159.816 139.099c-8.567 0.286-19.729-22.483-21.45-17.012-0.563 4.608-0.942 5.132-1.643 10.471-1.506 4.914-4.904 8.701-9.502 16.143-2.913 4.862-4.137 9.344-4.477 13.336 0.685 4.665 4.486 7.195 3.564 9.725-0.915 2.157-5.897 2.705-9.543 6.999-5.958 9.953-14.262 23.443-16.396 32.554-2.624 10.898-5.745 17.54-4.9 31.744 0.426 9.757-0.588 9.105 3.674 13.111 3.945 3.761 9.33 7.834 16.006 13.729 7.032 6.109 22.783 17.288 24.702 20.557 2.06 3.338 2.515 12.094 1.306 14.473-1.173 2.346-12.392 4.395-12.321 4.395-0.07 0 9.379 12.549 11.296 14.465 1.847 1.848 9.591 10.234 41.868 4.268 18.706-3.444 35.745-13.11 41.979-24.066 7.361-10.332 1.456-16.757 3.091-22.974 2.45-9.269 16.219-12.958 18.03-23.47 0.213-1.455 1.06-2.557 2.34-4.688 2.164-3.509 1.352-9.583 1.562-15.124 0.665-15.473-1.494-30.177-4.938-41.08-3.195-10.228-8.166-17.472-12.393-26.811-8.486-18.642-8.341-27.267-16.114-38.987-6.809-10.416-5.838-16.531-14.962-15.787-12.873 1.129-27.25 23.531-40.779 24.029z"/>
+ <path fill="#bababa" d="m159.799 140.184c-8.487 0.279-19.282-22.146-21.141-17.215-0.591 4.147-1.057 4.737-1.828 9.717-1.656 4.808-5.121 8.931-9.588 16.217-2.902 4.873-4.01 9.292-4.197 13.165 0.868 4.822 5.158 7.281 4.23 9.747-0.971 2.215-6.385 2.651-10.361 7.216-6.258 9.861-14.339 23.36-16.442 32.297-2.643 10.906-5.787 17.521-4.894 31.699 0.432 9.285-0.697 8.986 3.552 13.032 3.908 3.776 9.384 7.919 16.043 13.799 7.016 6.093 22.845 17.296 24.753 20.552 2.051 3.328 2.559 12.18 1.358 14.547-1.161 2.334-12.372 4.451-12.301 4.451-0.071 0 9.33 12.436 11.245 14.35 1.844 1.844 9.555 10.17 41.777 4.219 18.727-3.443 36.036-13.039 41.857-24.09 6.759-10.063 0.939-16.756 2.565-22.939 2.442-9.25 16.768-12.957 18.572-23.453 0.213-1.451 1.095-2.547 2.365-4.668 2.171-3.543 1.329-9.599 1.56-15.117 0.732-15.522-1.471-30.172-4.913-41.042-3.197-10.2-8.161-17.433-12.379-26.756-8.471-18.612-8.354-27.25-16.135-38.933-6.609-10.061-5.976-15.858-14.845-15.098-12.706 1.165-27.347 23.813-40.853 24.303z"/>
+ <path fill="#c1c1c1" d="m159.781 141.269c-8.408 0.271-18.837-21.81-20.83-17.419-0.619 3.687-1.173 4.344-2.014 8.965-1.808 4.701-5.338 9.16-9.674 16.29-2.892 4.884-3.885 9.24-3.918 12.994 1.052 4.978 5.829 7.367 4.896 9.769-1.026 2.272-6.874 2.598-11.178 7.434-6.56 9.769-14.419 23.277-16.491 32.039-2.66 10.916-5.829 17.504-4.887 31.656 0.438 8.813-0.807 8.867 3.43 12.953 3.87 3.793 9.438 8.004 16.082 13.868 6.997 6.077 22.904 17.304 24.803 20.55 2.041 3.314 2.601 12.266 1.409 14.617-1.149 2.324-12.351 4.51-12.28 4.51-0.07 0 9.282 12.321 11.194 14.233 1.841 1.842 9.521 10.106 41.688 4.17 18.746-3.44 36.326-12.967 41.734-24.112 6.156-9.793 0.423-16.754 2.038-22.904 2.438-9.235 17.318-12.957 19.117-23.438 0.212-1.444 1.128-2.536 2.39-4.647 2.18-3.578 1.306-9.613 1.558-15.11 0.799-15.571-1.447-30.165-4.889-41.002-3.2-10.172-8.156-17.395-12.364-26.701-8.456-18.583-8.367-27.234-16.155-38.88-6.413-9.705-6.114-15.185-14.729-14.408-12.541 1.197-27.445 24.091-40.93 24.573z"/>
+ <path fill="#c9c9c9" d="m159.764 142.354c-8.329 0.264-18.392-21.473-20.521-17.622-0.646 3.225-1.289 3.949-2.2 8.211-1.957 4.596-5.555 9.39-9.761 16.364-2.879 4.895-3.757 9.188-3.638 12.823 1.235 5.135 6.502 7.453 5.562 9.791-1.081 2.329-7.362 2.544-11.995 7.651-6.859 9.677-14.499 23.195-16.54 31.78-2.677 10.927-5.87 17.488-4.879 31.611 0.444 8.344-0.916 8.748 3.307 12.875 3.834 3.81 9.492 8.09 16.121 13.939 6.98 6.061 22.965 17.311 24.854 20.545 2.031 3.303 2.643 12.352 1.461 14.69-1.137 2.313-12.33 4.567-12.26 4.567-0.07 0 9.233 12.209 11.143 14.117 1.839 1.84 9.486 10.043 41.599 4.122 18.767-3.44 36.618-12.896 41.612-24.137 5.554-9.522-0.094-16.751 1.513-22.868 2.43-9.219 17.866-12.957 19.659-23.424 0.213-1.439 1.162-2.525 2.415-4.627 2.188-3.612 1.282-9.629 1.556-15.104 0.865-15.621-1.424-30.158-4.864-40.962-3.202-10.144-8.153-17.357-12.351-26.646-8.441-18.554-8.381-27.218-16.176-38.826-6.216-9.35-6.251-14.513-14.612-13.719-12.374 1.235-27.543 24.375-41.005 24.849z"/>
+ <path fill="#d1d1d1" d="m159.747 143.439c-8.25 0.256-17.944-21.137-20.21-17.826-0.675 2.765-1.406 3.555-2.386 7.459-2.108 4.489-5.772 9.619-9.847 16.437-2.869 4.906-3.631 9.136-3.358 12.652 1.419 5.292 7.174 7.538 6.228 9.812-1.137 2.387-7.852 2.491-12.813 7.869-7.161 9.586-14.579 23.114-16.588 31.522-2.695 10.938-5.912 17.471-4.873 31.568 0.451 7.871-1.025 8.629 3.185 12.797 3.796 3.824 9.547 8.174 16.158 14.008 6.964 6.047 23.026 17.32 24.905 20.541 2.021 3.292 2.686 12.439 1.513 14.764-1.125 2.303-12.31 4.625-12.239 4.625-0.07 0 9.186 12.094 11.092 14.002 1.836 1.836 9.45 9.978 41.509 4.072 18.786-3.439 36.909-12.824 41.49-24.16 4.948-9.252-0.611-16.748 0.985-22.832 2.423-9.203 18.415-12.957 20.203-23.41 0.212-1.434 1.196-2.514 2.44-4.605 2.193-3.646 1.259-9.645 1.553-15.098 0.932-15.67-1.4-30.151-4.84-40.922-3.205-10.115-8.148-17.319-12.336-26.592-8.427-18.524-8.396-27.201-16.197-38.771-6.017-8.995-6.388-13.84-14.495-13.03-12.207 1.266-27.64 24.652-41.079 25.118z"/>
+ <path fill="#d8d8d8" d="m159.729 144.524c-8.17 0.249-17.498-20.8-19.9-18.03-0.702 2.304-1.521 3.162-2.571 6.706-2.259 4.383-5.988 9.848-9.933 16.511-2.858 4.917-3.504 9.084-3.079 12.48 1.604 5.449 7.846 7.625 6.895 9.835-1.193 2.444-8.342 2.438-13.631 8.087-7.461 9.493-14.658 23.031-16.637 31.262-2.712 10.947-5.953 17.455-4.865 31.524 0.458 7.399-1.135 8.511 3.063 12.718 3.758 3.842 9.601 8.26 16.196 14.078 6.946 6.031 23.087 17.328 24.956 20.538 2.011 3.28 2.729 12.524 1.563 14.835-1.112 2.293-12.289 4.684-12.218 4.684-0.071 0 9.136 11.981 11.04 13.886 1.834 1.834 9.417 9.913 41.419 4.024 18.807-3.438 37.2-12.752 41.368-24.184 4.346-8.982-1.128-16.747 0.46-22.798 2.416-9.187 18.964-12.956 20.746-23.394 0.211-1.429 1.229-2.504 2.465-4.586 2.202-3.681 1.236-9.658 1.551-15.091 0.998-15.72-1.377-30.146-4.814-40.884-3.208-10.086-8.145-17.28-12.323-26.536-8.411-18.495-8.408-27.185-16.217-38.717-5.82-8.64-6.526-13.168-14.38-12.341-12.04 1.303-27.736 24.934-41.154 25.393z"/>
+ <path fill="#e0e0e0" d="m159.712 145.609c-8.091 0.241-17.052-20.464-19.59-18.233-0.729 1.843-1.637 2.767-2.757 5.953-2.409 4.276-6.206 10.077-10.02 16.584-2.847 4.928-3.378 9.032-2.8 12.309 1.787 5.606 8.519 7.711 7.561 9.857-1.248 2.502-8.829 2.384-14.448 8.304-7.761 9.402-14.738 22.95-16.684 31.006-2.731 10.955-5.996 17.436-4.859 31.48 0.464 6.928-1.244 8.389 2.939 12.639 3.722 3.857 9.656 8.344 16.234 14.148 6.932 6.014 23.148 17.336 25.008 20.533 2 3.268 2.771 12.611 1.615 14.907-1.1 2.282-12.268 4.741-12.198 4.741-0.069 0 9.089 11.867 10.989 13.77 1.831 1.831 9.382 9.85 41.329 3.977 18.827-3.438 37.492-12.683 41.246-24.207 3.743-8.715-1.646-16.746-0.066-22.762 2.409-9.171 19.514-12.957 21.289-23.381 0.211-1.422 1.265-2.494 2.49-4.564 2.21-3.715 1.213-9.674 1.549-15.084 1.065-15.77-1.354-30.139-4.791-40.844-3.21-10.058-8.14-17.241-12.309-26.481-8.396-18.466-8.421-27.168-16.237-38.664-5.622-8.284-6.663-12.495-14.262-11.651-11.872 1.335-27.833 25.212-41.228 25.663z"/>
+ <path fill="#e8e8e8" d="m159.694 146.694c-8.012 0.234-16.605-20.127-19.279-18.437-0.757 1.383-1.753 2.373-2.943 5.2-2.56 4.171-6.423 10.307-10.105 16.658-2.835 4.939-3.251 8.979-2.52 12.138 1.97 5.763 9.189 7.796 8.226 9.879-1.303 2.559-9.318 2.33-15.265 8.521-8.063 9.31-14.818 22.867-16.733 30.748-2.748 10.967-6.037 17.419-4.853 31.436 0.472 6.457-1.353 8.271 2.818 12.562 3.685 3.873 9.711 8.429 16.273 14.218 6.913 6 23.207 17.344 25.058 20.529 1.991 3.257 2.814 12.697 1.666 14.98-1.087 2.271-12.247 4.799-12.177 4.799-0.07 0 9.04 11.755 10.938 13.654 1.829 1.828 9.349 9.785 41.239 3.926 18.847-3.435 37.783-12.609 41.124-24.229 3.14-8.444-2.161-16.743-0.592-22.728 2.401-9.152 20.062-12.955 21.831-23.364 0.211-1.417 1.298-2.483 2.516-4.544 2.217-3.748 1.19-9.689 1.547-15.076 1.132-15.82-1.331-30.133-4.766-40.806-3.213-10.03-8.136-17.203-12.296-26.427-8.38-18.436-8.435-27.151-16.257-38.609-5.425-7.929-6.802-11.822-14.146-10.962-11.706 1.368-27.931 25.491-41.304 25.934z"/>
+ <path fill="#efefef" d="m159.677 147.779c-7.934 0.226-16.16-19.791-18.97-18.64-0.785 0.921-1.869 1.979-3.13 4.447-2.71 4.064-6.639 10.536-10.19 16.731-2.824 4.95-3.125 8.928-2.24 11.967 2.152 5.919 9.86 7.882 8.892 9.901-1.358 2.616-9.808 2.277-16.083 8.739-8.363 9.218-14.896 22.784-16.781 30.489-2.766 10.977-6.079 17.402-4.846 31.393 0.478 5.984-1.462 8.152 2.696 12.482 3.646 3.889 9.765 8.514 16.311 14.287 6.896 5.983 23.269 17.352 25.109 20.526 1.98 3.245 2.855 12.782 1.718 15.052-1.076 2.262-12.227 4.857-12.156 4.857-0.07 0 8.991 11.641 10.887 13.537 1.826 1.826 9.313 9.723 41.148 3.879 18.868-3.434 38.074-12.538 41.002-24.254 2.537-8.174-2.678-16.741-1.119-22.69 2.396-9.138 20.612-12.957 22.375-23.351 0.212-1.412 1.332-2.473 2.541-4.523 2.226-3.783 1.166-9.704 1.545-15.07 1.197-15.869-1.307-30.125-4.741-40.766-3.215-10.002-8.131-17.165-12.282-26.372-8.365-18.407-8.447-27.135-16.277-38.555-5.228-7.574-6.938-11.15-14.029-10.272-11.541 1.402-28.029 25.771-41.38 26.206z"/>
+ <path fill="#f7f7f7" d="m159.66 148.864c-7.854 0.219-15.714-19.454-18.66-18.844-0.812 0.461-1.983 1.585-3.314 3.694-2.86 3.958-6.856 10.766-10.278 16.805-2.813 4.961-2.998 8.876-1.96 11.796 2.337 6.076 10.533 7.968 9.558 9.923-1.415 2.673-10.296 2.223-16.899 8.956-8.664 9.126-14.978 22.702-16.83 30.23-2.783 10.986-6.121 17.386-4.839 31.349 0.484 5.515-1.571 8.033 2.573 12.403 3.609 3.906 9.82 8.6 16.35 14.357 6.88 5.969 23.329 17.36 25.16 20.523 1.971 3.232 2.898 12.869 1.77 15.124-1.063 2.252-12.206 4.915-12.136 4.915-0.07 0 8.942 11.527 10.835 13.422 1.824 1.822 9.279 9.658 41.059 3.83 18.889-3.434 38.366-12.467 40.881-24.278 1.934-7.903-3.195-16.739-1.646-22.655 2.388-9.121 21.161-12.955 22.918-23.336 0.211-1.404 1.366-2.461 2.566-4.502 2.232-3.816 1.143-9.719 1.542-15.063 1.265-15.92-1.283-30.12-4.717-40.727-3.219-9.974-8.128-17.127-12.269-26.317-8.349-18.378-8.461-27.119-16.298-38.501-5.029-7.219-7.075-10.478-13.912-9.584-11.373 1.438-28.126 26.053-41.454 26.48z"/>
+ <path fill="#fff" d="m159.642 149.949c-7.774 0.211-15.268-19.118-18.35-19.048-0.84 0-2.1 1.191-3.501 2.941-3.011 3.852-7.072 10.995-10.363 16.878-2.803 4.972-2.872 8.824-1.682 11.625 2.521 6.233 11.205 8.054 10.225 9.945-1.471 2.731-10.785 2.17-17.719 9.174-8.964 9.034-15.056 22.621-16.877 29.973-2.801 10.995-6.163 17.368-4.832 31.304 0.49 5.043-1.681 7.914 2.451 12.325 3.571 3.923 9.874 8.685 16.387 14.427 6.863 5.953 23.391 17.368 25.211 20.521 1.962 3.221 2.942 12.954 1.821 15.196-1.051 2.24-12.185 4.972-12.115 4.972-0.069 0 8.895 11.416 10.784 13.307 1.821 1.82 9.244 9.595 40.97 3.781 18.907-3.431 38.656-12.396 40.758-24.302 1.331-7.633-3.712-16.736-2.171-22.619 2.381-9.104 21.71-12.956 23.461-23.321 0.21-1.399 1.399-2.45 2.591-4.481 2.24-3.852 1.12-9.734 1.54-15.057 1.331-15.968-1.26-30.113-4.692-40.688-3.221-9.945-8.123-17.088-12.255-26.262-8.334-18.348-8.474-27.102-16.318-38.447-4.832-6.863-7.213-9.805-13.796-8.894-11.205 1.469-28.222 26.33-41.528 26.75z"/>
+ </g>
+ <path fill="#995900" d="m152.553 88.8575c5.256-0.648 12.456 0.648 15.769 3.096 3.096 2.304 5.256 3.528 8.063 4.464 9.433 3.096 21.816 4.536 21.24 13.032-0.648 10.151-3.6 14.688-12.024 17.351-6.768 2.088-18.863 13.824-28.224 13.824-4.176 0-10.008 0.216-13.392-1.008-3.24-1.152-7.776-6.624-13.104-11.016-5.328-4.32-10.296-8.928-10.439-14.976-0.217-6.407 3.96-8.496 9.863-13.607 3.097-2.736 8.712-7.272 12.601-9.288 3.599-1.799 5.903-1.439 9.647-1.872z"/>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#9e5f00" d="m165.068 78.951c5.225-0.644 12.384 0.645 15.677 3.079 3.078 2.29 5.227 3.51 8.018 4.438 9.375 3.078 21.729 4.529 21.159 12.973-0.641 10.09-3.669 14.581-12.041 17.223-6.723 2.073-18.768 13.589-28.07 13.64-4.21 0.032-9.926 0.234-13.287-0.977-3.215-1.142-7.737-6.608-13.031-10.969-5.291-4.292-10.26-8.774-10.317-14.765-0.153-6.252 3.912-8.411 9.773-13.488 3.071-2.71 8.594-7.303 12.463-9.333 3.563-1.803 5.933-1.392 9.656-1.821z"/>
+ <path fill="#a36400" d="m165.177 79.044c5.195-0.641 12.313 0.64 15.587 3.06 3.06 2.278 5.194 3.494 7.971 4.413 9.317 3.06 21.641 4.522 21.078 12.914-0.634 10.027-3.737 14.474-12.058 17.094-6.678 2.057-18.673 13.352-27.919 13.454-4.241 0.064-9.842 0.252-13.18-0.945-3.19-1.133-7.7-6.592-12.96-10.921-5.254-4.264-10.222-8.622-10.192-14.555-0.093-6.099 3.863-8.329 9.681-13.369 3.048-2.685 8.478-7.335 12.328-9.379 3.526-1.804 5.963-1.338 9.664-1.766z"/>
+ <path fill="#a86a00" d="m165.287 79.138c5.165-0.637 12.241 0.637 15.496 3.043 3.042 2.264 5.165 3.476 7.924 4.387 9.26 3.042 21.556 4.515 20.998 12.854-0.627 9.968-3.805 14.368-12.074 16.967-6.633 2.042-18.576 13.117-27.766 13.27-4.276 0.096-9.759 0.27-13.075-0.914-3.165-1.123-7.661-6.577-12.887-10.874-5.217-4.236-10.187-8.468-10.069-14.345-0.03-5.943 3.815-8.244 9.589-13.249 3.023-2.66 8.36-7.366 12.191-9.424 3.49-1.808 5.993-1.291 9.673-1.715z"/>
+ <path fill="#ad7000" d="m165.396 79.23c5.135-0.633 12.17 0.633 15.404 3.025 3.025 2.251 5.137 3.46 7.88 4.361 9.201 3.025 21.467 4.508 20.917 12.796-0.62 9.905-3.874 14.26-12.093 16.837-6.586 2.027-18.479 12.882-27.611 13.086-4.311 0.127-9.677 0.287-12.971-0.883-3.14-1.113-7.622-6.561-12.814-10.826-5.18-4.208-10.148-8.315-9.945-14.135 0.031-5.789 3.768-8.16 9.497-13.129 2.999-2.635 8.244-7.398 12.055-9.47 3.454-1.808 6.023-1.24 9.681-1.662z"/>
+ <path fill="#b27600" d="m165.506 79.325c5.105-0.63 12.099 0.629 15.314 3.007 3.007 2.237 5.104 3.442 7.832 4.335 9.145 3.007 21.38 4.501 20.837 12.737-0.614 9.844-3.943 14.154-12.109 16.709-6.541 2.011-18.385 12.645-27.46 12.9-4.342 0.16-9.592 0.306-12.862-0.851-3.115-1.103-7.584-6.545-12.743-10.779-5.144-4.18-10.112-8.162-9.821-13.924 0.092-5.634 3.718-8.076 9.405-13.009 2.975-2.609 8.126-7.429 11.919-9.514 3.416-1.812 6.052-1.192 9.688-1.611z"/>
+ <path fill="#b77b00" d="m165.615 79.417c5.075-0.626 12.026 0.626 15.224 2.989 2.989 2.225 5.075 3.425 7.786 4.31 9.087 2.989 21.292 4.494 20.756 12.678-0.606 9.781-4.012 14.046-12.126 16.581-6.496 1.997-18.289 12.41-27.307 12.716-4.376 0.191-9.51 0.323-12.758-0.82-3.09-1.094-7.546-6.53-12.671-10.732-5.106-4.152-10.074-8.008-9.697-13.713 0.155-5.479 3.67-7.992 9.313-12.889 2.951-2.585 8.011-7.461 11.783-9.56 3.38-1.814 6.083-1.143 9.697-1.56z"/>
+ <path fill="#bc8100" d="m165.725 79.511c5.044-0.622 11.954 0.622 15.133 2.972 2.971 2.211 5.045 3.408 7.739 4.284 9.029 2.971 21.205 4.487 20.675 12.619-0.6 9.719-4.079 13.939-12.143 16.451-6.45 1.982-18.192 12.175-27.153 12.532-4.41 0.223-9.428 0.341-12.653-0.789-3.065-1.084-7.507-6.514-12.598-10.684-5.069-4.124-10.038-7.855-9.574-13.504 0.217-5.324 3.622-7.908 9.222-12.77 2.926-2.559 7.894-7.492 11.646-9.605 3.344-1.816 6.113-1.092 9.706-1.506z"/>
+ <path fill="#c18700" d="m165.834 79.604c5.015-0.618 11.883 0.619 15.043 2.954 2.953 2.198 5.015 3.391 7.693 4.259 8.972 2.953 21.118 4.48 20.594 12.559-0.593 9.66-4.147 13.833-12.159 16.324-6.405 1.967-18.098 11.94-27.002 12.347-4.441 0.255-9.343 0.359-12.546-0.757-3.04-1.074-7.469-6.498-12.526-10.637-5.032-4.096-10-7.701-9.45-13.293 0.278-5.169 3.574-7.823 9.13-12.649 2.903-2.534 7.776-7.524 11.511-9.651 3.306-1.821 6.141-1.044 9.712-1.456z"/>
+ <path fill="#c68d00" d="m165.944 79.697c4.984-0.615 11.811 0.614 14.952 2.936 2.935 2.184 4.983 3.374 7.646 4.233 8.915 2.935 21.031 4.473 20.515 12.5-0.586 9.597-4.218 13.726-12.177 16.195-6.36 1.951-18.002 11.703-26.849 12.162-4.476 0.287-9.261 0.377-12.441-0.726-3.015-1.064-7.431-6.482-12.453-10.589-4.995-4.068-9.965-7.549-9.326-13.083 0.34-5.015 3.524-7.741 9.038-12.531 2.878-2.508 7.658-7.555 11.374-9.696 3.269-1.82 6.171-0.992 9.721-1.401z"/>
+ <path fill="#cc9200" d="m166.054 79.791c4.952-0.61 11.738 0.611 14.86 2.918 2.918 2.172 4.954 3.357 7.601 4.207 8.857 2.918 20.942 4.466 20.432 12.442-0.578 9.536-4.285 13.62-12.192 16.066-6.314 1.936-17.906 11.468-26.696 11.978-4.509 0.319-9.178 0.394-12.335-0.696-2.989-1.054-7.393-6.466-12.382-10.541-4.959-4.04-9.928-7.395-9.202-12.873 0.401-4.859 3.477-7.655 8.945-12.411 2.854-2.482 7.542-7.586 11.239-9.741 3.233-1.824 6.201-0.943 9.73-1.349z"/>
+ <path fill="#d19800" d="m166.163 79.883c4.923-0.606 11.668 0.608 14.771 2.901 2.9 2.158 4.924 3.339 7.554 4.181 8.801 2.9 20.855 4.459 20.352 12.383-0.571 9.474-4.353 13.512-12.21 15.938-6.269 1.921-17.81 11.233-26.543 11.793-4.542 0.351-9.094 0.413-12.229-0.664-2.965-1.044-7.354-6.45-12.311-10.494-4.921-4.012-9.89-7.241-9.079-12.662 0.465-4.705 3.431-7.571 8.855-12.29 2.83-2.458 7.425-7.618 11.102-9.787 3.197-1.827 6.231-0.893 9.738-1.299z"/>
+ <path fill="#d69e00" d="m166.273 79.978c4.893-0.603 11.596 0.603 14.679 2.882 2.883 2.145 4.895 3.323 7.507 4.156 8.744 2.882 20.77 4.452 20.272 12.324-0.565 9.412-4.422 13.406-12.228 15.81-6.224 1.905-17.714 10.996-26.39 11.608-4.576 0.383-9.012 0.431-12.124-0.633-2.94-1.034-7.316-6.434-12.237-10.446-4.884-3.984-9.854-7.089-8.955-12.452 0.525-4.551 3.382-7.489 8.764-12.171 2.805-2.432 7.307-7.649 10.965-9.832 3.16-1.829 6.261-0.845 9.747-1.246z"/>
+ <path fill="#dba300" d="m166.382 80.07c4.863-0.599 11.525 0.6 14.59 2.865 2.864 2.131 4.862 3.305 7.461 4.13 8.686 2.864 20.682 4.445 20.19 12.264-0.559 9.352-4.491 13.299-12.244 15.681-6.179 1.89-17.619 10.761-26.237 11.423-4.608 0.415-8.929 0.449-12.018-0.601-2.915-1.024-7.277-6.418-12.166-10.399-4.847-3.956-9.815-6.935-8.831-12.241 0.587-4.396 3.333-7.404 8.671-12.051 2.782-2.407 7.191-7.681 10.83-9.878 3.123-1.829 6.29-0.793 9.754-1.193z"/>
+ <path fill="#e0a900" d="m166.492 80.164c4.832-0.595 11.453 0.596 14.498 2.847 2.847 2.118 4.833 3.289 7.414 4.104 8.629 2.846 20.595 4.438 20.111 12.205-0.553 9.29-4.56 13.193-12.262 15.553-6.134 1.875-17.522 10.526-26.085 11.239-4.642 0.447-8.845 0.467-11.912-0.57-2.89-1.015-7.238-6.402-12.093-10.351-4.81-3.928-9.78-6.782-8.708-12.032 0.649-4.241 3.285-7.32 8.58-11.932 2.757-2.381 7.073-7.712 10.693-9.923 3.088-1.831 6.321-0.743 9.764-1.14z"/>
+ <path fill="#e5af00" d="m166.601 80.257c4.803-0.592 11.382 0.592 14.407 2.829 2.829 2.105 4.804 3.271 7.368 4.079 8.571 2.828 20.507 4.431 20.029 12.146-0.544 9.228-4.627 13.085-12.277 15.423-6.089 1.861-17.427 10.29-25.932 11.055-4.676 0.478-8.763 0.484-11.807-0.539-2.865-1.005-7.2-6.387-12.021-10.304-4.772-3.9-9.742-6.629-8.583-11.821 0.711-4.085 3.236-7.236 8.487-11.812 2.732-2.357 6.957-7.744 10.557-9.968 3.052-1.834 6.351-0.694 9.772-1.088z"/>
+ <path fill="#eab500" d="m166.711 80.351c4.772-0.588 11.31 0.589 14.317 2.811 2.811 2.092 4.771 3.254 7.321 4.054 8.514 2.81 20.42 4.424 19.948 12.087-0.538 9.165-4.695 12.979-12.294 15.295-6.044 1.845-17.332 10.054-25.779 10.869-4.708 0.511-8.68 0.503-11.7-0.507-2.84-0.995-7.163-6.371-11.949-10.257-4.736-3.872-9.706-6.475-8.46-11.61 0.773-3.931 3.188-7.152 8.396-11.692 2.709-2.331 6.839-7.775 10.421-10.013 3.013-1.839 6.38-0.645 9.779-1.037z"/>
+ <path fill="#efba00" d="m166.82 80.443c4.742-0.584 11.238 0.585 14.226 2.794 2.794 2.078 4.743 3.237 7.276 4.027 8.456 2.793 20.332 4.417 19.868 12.029-0.531 9.104-4.766 12.872-12.313 15.167-5.997 1.83-17.234 9.819-25.626 10.685-4.742 0.542-8.596 0.52-11.595-0.476-2.815-0.985-7.124-6.355-11.877-10.209-4.699-3.844-9.668-6.322-8.336-11.4 0.835-3.778 3.14-7.068 8.304-11.573 2.686-2.306 6.724-7.807 10.285-10.059 2.978-1.84 6.411-0.595 9.788-0.985z"/>
+ <path fill="#f4c000" d="m166.93 80.537c4.711-0.58 11.166 0.582 14.135 2.776 2.775 2.066 4.713 3.22 7.229 4.002 8.399 2.775 20.246 4.41 19.787 11.969-0.522 9.043-4.832 12.765-12.328 15.039-5.952 1.815-17.139 9.584-25.473 10.501-4.776 0.574-8.513 0.538-11.49-0.445-2.79-0.976-7.085-6.34-11.804-10.162-4.662-3.816-9.632-6.168-8.213-11.189 0.896-3.623 3.092-6.984 8.213-11.454 2.66-2.281 6.604-7.838 10.147-10.104 2.942-1.844 6.441-0.547 9.797-0.933z"/>
+ <path fill="#f9c600" d="m167.039 80.63c4.683-0.577 11.095 0.577 14.045 2.758 2.758 2.052 4.683 3.203 7.184 3.976 8.341 2.757 20.157 4.403 19.706 11.91-0.518 8.981-4.901 12.659-12.346 14.911-5.906 1.799-17.044 9.347-25.32 10.315-4.809 0.606-8.431 0.556-11.384-0.413-2.765-0.966-7.048-6.324-11.732-10.114-4.625-3.788-9.594-6.016-8.088-10.98 0.958-3.467 3.043-6.9 8.12-11.333 2.637-2.256 6.488-7.87 10.013-10.15 2.902-1.844 6.468-0.495 9.802-0.88z"/>
+ </g>
+ <path fill="#fc0" d="m154.744 90.7245c4.65-0.573 11.022 0.574 13.954 2.74 2.739 2.039 4.651 3.186 7.136 3.951 8.284 2.739 20.071 4.396 19.626 11.851-0.51 8.919-4.97 12.551-12.362 14.781-5.861 1.784-16.947 9.112-25.168 10.131-4.842 0.638-8.347 0.574-11.277-0.382-2.74-0.956-7.01-6.308-11.66-10.067-4.588-3.76-9.559-5.862-7.965-10.769 1.02-3.313 2.995-6.816 8.028-11.213 2.612-2.23 6.371-7.901 9.876-10.195 2.867-1.847 6.499-0.447 9.812-0.828z"/>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fc0" d="m167.982 83.609c1.008 2.088 3.6 2.376 5.328 3.312 1.655 0.936 2.592 1.152 3.239 0.792 1.44-0.792 0.36-3.384-1.079-4.32-1.368-0.935-8.064-1.151-7.488 0.216z"/>
+ <path fill="#f9c600" d="m168.125 83.631c0.982 2.035 3.508 2.316 5.193 3.229 1.614 0.912 2.526 1.123 3.158 0.771 1.402-0.771 0.35-3.298-1.054-4.21-1.332-0.913-7.859-1.123-7.297 0.21z"/>
+ <path fill="#f4c000" d="m168.267 83.653c0.957 1.982 3.418 2.255 5.058 3.144 1.572 0.889 2.461 1.094 3.076 0.752 1.367-0.752 0.342-3.213-1.025-4.101-1.299-0.889-7.656-1.094-7.109 0.205z"/>
+ <path fill="#efba00" d="m168.409 83.674c0.932 1.929 3.327 2.195 4.924 3.06 1.53 0.865 2.395 1.064 2.993 0.732 1.331-0.732 0.333-3.127-0.998-3.992-1.264-0.864-7.451-1.064-6.919 0.2z"/>
+ <path fill="#eab500" d="m168.552 83.696c0.905 1.876 3.234 2.135 4.787 2.977 1.488 0.841 2.329 1.035 2.912 0.711 1.294-0.711 0.323-3.041-0.971-3.882-1.228-0.841-7.246-1.036-6.728 0.194z"/>
+ <path fill="#e5af00" d="m168.694 83.718c0.881 1.823 3.144 2.075 4.653 2.892 1.446 0.818 2.264 1.006 2.83 0.692 1.257-0.692 0.313-2.956-0.943-3.773-1.195-0.818-7.043-1.007-6.54 0.189z"/>
+ <path fill="#e0a900" d="m168.837 83.739c0.855 1.771 3.053 2.015 4.519 2.809 1.403 0.793 2.198 0.977 2.747 0.671 1.221-0.671 0.306-2.87-0.916-3.664-1.161-0.793-6.839-0.976-6.35 0.184z"/>
+ <path fill="#dba300" d="m168.979 83.761c0.829 1.718 2.962 1.955 4.383 2.725 1.363 0.77 2.132 0.948 2.666 0.651 1.184-0.651 0.296-2.784-0.889-3.554-1.125-0.77-6.634-0.948-6.16 0.178z"/>
+ <path fill="#d69e00" d="m169.121 83.782c0.804 1.665 2.871 1.895 4.249 2.641 1.32 0.747 2.066 0.918 2.583 0.631 1.148-0.631 0.287-2.698-0.861-3.444-1.091-0.746-6.43-0.919-5.971 0.172z"/>
+ <path fill="#d19800" d="m169.264 83.804c0.777 1.612 2.778 1.834 4.112 2.557 1.279 0.723 2.001 0.889 2.501 0.611 1.112-0.611 0.278-2.612-0.834-3.335-1.055-0.722-6.224-0.889-5.779 0.167z"/>
+ <path fill="#cc9200" d="m169.406 83.826c0.753 1.559 2.688 1.774 3.979 2.473 1.236 0.699 1.936 0.86 2.42 0.591 1.074-0.591 0.269-2.527-0.808-3.226-1.021-0.699-6.021-0.86-5.591 0.162z"/>
+ <path fill="#c68c00" d="m169.549 83.847c0.728 1.506 2.597 1.714 3.844 2.389 1.194 0.675 1.869 0.831 2.337 0.571 1.039-0.571 0.26-2.441-0.779-3.116-0.988-0.675-5.818-0.831-5.402 0.156z"/>
+ <path fill="#c18700" d="m169.691 83.869c0.702 1.453 2.506 1.654 3.709 2.305 1.152 0.652 1.803 0.802 2.254 0.551 1.002-0.551 0.251-2.355-0.751-3.006-0.953-0.652-5.613-0.802-5.212 0.15z"/>
+ <path fill="#bc8100" d="m169.833 83.89c0.677 1.4 2.415 1.594 3.574 2.221 1.111 0.628 1.738 0.772 2.173 0.531 0.965-0.531 0.241-2.27-0.725-2.897-0.917-0.627-5.408-0.772-5.022 0.145z"/>
+ <path fill="#b77b00" d="m169.976 83.912c0.65 1.347 2.322 1.533 3.438 2.137 1.069 0.604 1.673 0.743 2.091 0.511 0.93-0.511 0.233-2.184-0.696-2.788-0.884-0.603-5.205-0.743-4.833 0.14z"/>
+ <path fill="#b27500" d="m170.118 83.934c0.626 1.294 2.232 1.473 3.304 2.053 1.027 0.581 1.606 0.714 2.009 0.491 0.893-0.491 0.224-2.098-0.669-2.678-0.85-0.58-5.001-0.715-4.644 0.134z"/>
+ <path fill="#ad7000" d="m170.261 83.955c0.6 1.242 2.14 1.413 3.168 1.97 0.984 0.557 1.541 0.685 1.927 0.47 0.855-0.47 0.214-2.012-0.644-2.569-0.812-0.555-4.794-0.684-4.451 0.129z"/>
+ <path fill="#a86a00" d="m170.403 83.977c0.574 1.189 2.05 1.353 3.034 1.886 0.942 0.533 1.475 0.656 1.844 0.45 0.82-0.45 0.205-1.926-0.615-2.459-0.779-0.533-4.591-0.656-4.263 0.123z"/>
+ <path fill="#a36400" d="m170.545 83.998c0.55 1.136 1.959 1.292 2.899 1.802 0.901 0.509 1.41 0.626 1.762 0.43 0.783-0.43 0.197-1.841-0.587-2.35-0.745-0.508-4.387-0.626-4.074 0.118z"/>
+ <path fill="#9e5e00" d="m170.688 84.02c0.522 1.083 1.867 1.232 2.764 1.718 0.859 0.486 1.343 0.597 1.68 0.41 0.746-0.41 0.188-1.755-0.561-2.241-0.709-0.484-4.182-0.597-3.883 0.113z"/>
+ <path fill="#995900" d="m170.83 84.042c0.498 1.03 1.776 1.172 2.629 1.634 0.817 0.462 1.278 0.568 1.599 0.39 0.71-0.39 0.178-1.669-0.533-2.131-0.676-0.461-3.979-0.568-3.695 0.107z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fc0" d="m152.875 86.29c-0.325 0.813 1.952 2.359 3.091 1.301 1.222-1.057 2.686-2.033 3.175-2.359 2.195-1.465 1.383-2.522-2.278-1.871-3.663 0.651-3.663 2.115-3.988 2.929z"/>
+ <path fill="#f9c600" d="m152.934 86.279c-0.318 0.794 1.906 2.304 3.019 1.271 1.193-1.033 2.623-1.986 3.102-2.305 2.145-1.431 1.351-2.463-2.226-1.828-3.578 0.637-3.578 2.067-3.895 2.862z"/>
+ <path fill="#f4c000" d="m152.993 86.269c-0.31 0.775 1.861 2.25 2.948 1.241 1.164-1.008 2.56-1.939 3.026-2.25 2.095-1.397 1.319-2.405-2.173-1.784-3.491 0.62-3.491 2.017-3.801 2.793z"/>
+ <path fill="#efba00" d="m153.051 86.258c-0.302 0.757 1.817 2.195 2.878 1.211 1.136-0.984 2.497-1.892 2.952-2.195 2.044-1.363 1.287-2.347-2.118-1.741-3.409 0.606-3.409 1.968-3.712 2.725z"/>
+ <path fill="#eab500" d="m153.11 86.248c-0.295 0.738 1.771 2.141 2.805 1.181 1.108-0.959 2.437-1.845 2.88-2.141 1.993-1.329 1.255-2.289-2.066-1.698-3.324 0.591-3.324 1.92-3.619 2.658z"/>
+ <path fill="#e5af00" d="m153.169 86.238c-0.287 0.719 1.727 2.086 2.733 1.151 1.08-0.935 2.374-1.798 2.807-2.086 1.942-1.296 1.224-2.23-2.015-1.655s-3.238 1.87-3.525 2.59z"/>
+ <path fill="#e0a900" d="m153.228 86.228c-0.28 0.7 1.681 2.032 2.661 1.121 1.052-0.91 2.312-1.751 2.732-2.031 1.893-1.262 1.191-2.172-1.961-1.611-3.152 0.559-3.152 1.82-3.432 2.521z"/>
+ <path fill="#dba300" d="m153.286 86.217c-0.271 0.681 1.636 1.977 2.591 1.09 1.023-0.886 2.25-1.704 2.659-1.977 1.84-1.228 1.159-2.114-1.909-1.568-3.068 0.547-3.068 1.773-3.341 2.455z"/>
+ <path fill="#d69e00" d="m153.345 86.207c-0.265 0.662 1.591 1.922 2.519 1.061 0.995-0.862 2.188-1.657 2.586-1.922 1.789-1.194 1.127-2.055-1.855-1.525-2.985 0.53-2.985 1.723-3.25 2.386z"/>
+ <path fill="#d19800" d="m153.404 86.197c-0.257 0.643 1.546 1.868 2.447 1.03 0.967-0.837 2.126-1.61 2.512-1.868 1.739-1.16 1.095-1.997-1.803-1.481-2.899 0.516-2.899 1.674-3.156 2.319z"/>
+ <path fill="#cc9200" d="m153.463 86.187c-0.25 0.625 1.5 1.813 2.375 1 0.939-0.813 2.064-1.563 2.439-1.813 1.688-1.126 1.063-1.938-1.75-1.438-2.814 0.5-2.814 1.625-3.064 2.251z"/>
+ <path fill="#c68c00" d="m153.521 86.176c-0.242 0.605 1.456 1.758 2.304 0.97 0.911-0.788 2.002-1.516 2.366-1.758 1.637-1.092 1.031-1.88-1.698-1.395-2.729 0.486-2.729 1.576-2.972 2.183z"/>
+ <path fill="#c18700" d="m153.58 86.166c-0.233 0.587 1.41 1.704 2.233 0.939 0.882-0.763 1.938-1.469 2.292-1.704 1.586-1.058 0.999-1.822-1.646-1.352-2.644 0.472-2.644 1.529-2.879 2.117z"/>
+ <path fill="#bc8100" d="m153.639 86.156c-0.228 0.568 1.364 1.649 2.16 0.91 0.854-0.739 1.878-1.422 2.219-1.649 1.536-1.024 0.967-1.764-1.593-1.308s-2.559 1.477-2.786 2.047z"/>
+ <path fill="#b77b00" d="m153.698 86.146c-0.22 0.549 1.32 1.594 2.089 0.879 0.825-0.715 1.815-1.375 2.146-1.595 1.484-0.99 0.935-1.705-1.54-1.265s-2.475 1.43-2.695 1.981z"/>
+ <path fill="#b27500" d="m153.756 86.135c-0.211 0.53 1.275 1.54 2.019 0.85 0.797-0.69 1.753-1.328 2.072-1.54 1.434-0.957 0.902-1.646-1.487-1.221s-2.391 1.38-2.604 1.911z"/>
+ <path fill="#ad7000" d="m153.815 86.125c-0.204 0.512 1.229 1.486 1.946 0.82 0.769-0.666 1.69-1.281 1.997-1.486 1.385-0.922 0.871-1.588-1.434-1.178s-2.304 1.331-2.509 1.844z"/>
+ <path fill="#a86a00" d="m153.874 86.114c-0.196 0.493 1.185 1.431 1.875 0.79 0.74-0.642 1.628-1.234 1.924-1.431 1.332-0.889 0.84-1.53-1.381-1.135s-2.221 1.283-2.418 1.776z"/>
+ <path fill="#a36400" d="m153.933 86.104c-0.189 0.474 1.139 1.376 1.803 0.759 0.712-0.617 1.566-1.187 1.851-1.376 1.281-0.855 0.808-1.472-1.329-1.092-2.135 0.38-2.135 1.234-2.325 1.709z"/>
+ <path fill="#9e5e00" d="m153.991 86.094c-0.181 0.455 1.095 1.322 1.732 0.729 0.684-0.592 1.504-1.14 1.776-1.321 1.231-0.821 0.775-1.414-1.274-1.048-2.051 0.364-2.051 1.184-2.234 1.64z"/>
+ <path fill="#995900" d="m154.05 86.083c-0.174 0.436 1.05 1.267 1.66 0.699 0.656-0.568 1.442-1.093 1.704-1.267 1.181-0.787 0.743-1.355-1.223-1.005s-1.966 1.136-2.141 1.573z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fc0" d="m156.951 107.887c-0.229 2.858 6.343-4.286 6.743-4.915 0.856-1.543 3.715-5.886 4.172-7.715 0.857-3.2 2.401-5.543 1.429-8.915-0.343-1.086-2.742-1.372-3.829-0.687-3.086 1.829-2.629 4.058-2.972 6.115-1.143 5.831-5.143 11.717-5.543 16.117z"/>
+ <path fill="#ffcc02" d="m157.22 107.441c-0.22 2.787 6.178-4.188 6.566-4.802 0.833-1.506 3.614-5.745 4.056-7.529 0.831-3.122 2.333-5.408 1.382-8.695-0.337-1.058-2.678-1.333-3.735-0.663-3.006 1.788-2.557 3.96-2.889 5.967-1.105 5.685-4.997 11.431-5.38 15.722z"/>
+ <path fill="#ffcc05" d="m157.488 106.995c-0.209 2.715 6.014-4.091 6.392-4.69 0.811-1.469 3.513-5.603 3.941-7.342 0.804-3.043 2.264-5.273 1.331-8.474-0.329-1.031-2.61-1.295-3.64-0.64-2.927 1.747-2.486 3.863-2.806 5.818-1.068 5.541-4.851 11.146-5.218 15.328z"/>
+ <path fill="#ffcc07" d="m157.757 106.548c-0.198 2.645 5.85-3.993 6.217-4.577 0.785-1.431 3.409-5.461 3.824-7.156 0.779-2.964 2.196-5.138 1.282-8.253-0.322-1.003-2.543-1.257-3.545-0.618-2.847 1.706-2.414 3.766-2.722 5.67-1.031 5.398-4.706 10.862-5.056 14.934z"/>
+ <path fill="#ffcd0a" d="m158.026 106.102c-0.189 2.573 5.684-3.896 6.04-4.465 0.762-1.394 3.309-5.32 3.709-6.969 0.753-2.886 2.129-5.004 1.233-8.033-0.315-0.976-2.478-1.219-3.45-0.595-2.768 1.665-2.343 3.668-2.64 5.522-0.993 5.254-4.558 10.577-4.892 14.54z"/>
+ <path fill="#ffcd0c" d="m158.294 105.655c-0.179 2.503 5.52-3.798 5.865-4.351 0.738-1.357 3.207-5.179 3.594-6.783 0.727-2.807 2.061-4.869 1.185-7.813-0.309-0.948-2.411-1.18-3.356-0.572-2.687 1.623-2.271 3.571-2.556 5.374-0.958 5.11-4.414 10.291-4.732 14.145z"/>
+ <path fill="#ffcd0f" d="m158.563 105.209c-0.169 2.431 5.354-3.701 5.688-4.239 0.715-1.319 3.106-5.037 3.479-6.596 0.7-2.728 1.992-4.734 1.135-7.592-0.301-0.92-2.344-1.142-3.261-0.549-2.608 1.583-2.199 3.474-2.473 5.226-0.919 4.965-4.267 10.005-4.568 13.75z"/>
+ <path fill="#ffcd11" d="m158.831 104.762c-0.159 2.361 5.19-3.602 5.515-4.126 0.69-1.282 3.004-4.896 3.361-6.409 0.674-2.649 1.924-4.599 1.087-7.372-0.295-0.893-2.277-1.104-3.167-0.526-2.527 1.541-2.128 3.376-2.389 5.077-0.883 4.822-4.122 9.721-4.407 13.356z"/>
+ <path fill="#ffce14" d="m159.1 104.316c-0.149 2.289 5.024-3.505 5.338-4.014 0.667-1.244 2.901-4.754 3.247-6.223 0.646-2.571 1.854-4.464 1.037-7.151-0.287-0.865-2.211-1.065-3.072-0.504-2.448 1.5-2.056 3.279-2.306 4.929-0.845 4.679-3.976 9.437-4.244 12.963z"/>
+ <path fill="#ffce16" d="m159.369 103.869c-0.139 2.219 4.86-3.407 5.162-3.9 0.643-1.208 2.801-4.613 3.131-6.037 0.622-2.492 1.787-4.329 0.988-6.93-0.28-0.838-2.146-1.027-2.978-0.481-2.368 1.459-1.983 3.182-2.223 4.781-0.807 4.533-3.829 9.151-4.08 12.567z"/>
+ <path fill="#ffce19" d="m159.637 103.423c-0.13 2.147 4.695-3.31 4.986-3.788 0.62-1.17 2.699-4.471 3.016-5.85 0.596-2.414 1.719-4.195 0.939-6.71-0.273-0.81-2.079-0.989-2.883-0.458-2.289 1.418-1.913 3.084-2.139 4.632-0.77 4.391-3.684 8.866-3.919 12.174z"/>
+ <path fill="#ffce1c" d="m159.906 102.977c-0.119 2.076 4.531-3.213 4.811-3.676 0.597-1.133 2.599-4.33 2.899-5.664 0.57-2.335 1.651-4.06 0.891-6.49-0.267-0.782-2.012-0.95-2.787-0.435-2.21 1.377-1.842 2.987-2.057 4.484-0.734 4.247-3.539 8.582-3.757 11.781z"/>
+ <path fill="#ffcf1e" d="m160.174 102.53c-0.108 2.005 4.366-3.115 4.637-3.563 0.571-1.096 2.496-4.189 2.784-5.478 0.543-2.256 1.581-3.925 0.841-6.269-0.26-0.754-1.945-0.912-2.693-0.412-2.129 1.336-1.77 2.889-1.973 4.336-0.697 4.103-3.394 8.297-3.596 11.386z"/>
+ <path fill="#ffcf21" d="m160.443 102.084c-0.099 1.934 4.201-3.018 4.46-3.45 0.548-1.059 2.394-4.047 2.668-5.291 0.517-2.178 1.514-3.79 0.793-6.049-0.253-0.727-1.879-0.874-2.599-0.39-2.051 1.295-1.698 2.792-1.891 4.188-0.658 3.959-3.246 8.012-3.431 10.992z"/>
+ <path fill="#ffcf23" d="m160.712 101.637c-0.089 1.863 4.036-2.919 4.283-3.337 0.526-1.021 2.294-3.905 2.553-5.104 0.491-2.099 1.447-3.655 0.744-5.828-0.246-0.699-1.813-0.835-2.505-0.367-1.969 1.253-1.625 2.694-1.805 4.04-0.623 3.814-3.101 7.726-3.27 10.596z"/>
+ <path fill="#ffcf26" d="m160.98 101.191c-0.079 1.792 3.872-2.822 4.107-3.225 0.502-0.984 2.192-3.764 2.438-4.918 0.464-2.02 1.378-3.52 0.694-5.607-0.238-0.672-1.746-0.797-2.41-0.344-1.89 1.212-1.555 2.597-1.723 3.891-0.583 3.671-2.953 7.442-3.106 10.203z"/>
+ <path fill="#ffd028" d="m161.249 100.744c-0.068 1.721 3.707-2.724 3.933-3.112 0.478-0.947 2.091-3.623 2.321-4.731 0.439-1.942 1.311-3.386 0.646-5.387-0.232-0.645-1.68-0.758-2.316-0.321-1.81 1.171-1.481 2.5-1.639 3.743-0.548 3.527-2.809 7.156-2.945 9.808z"/>
+ <path fill="#ffd02b" d="m161.517 100.298c-0.06 1.65 3.543-2.627 3.757-2.999 0.454-0.91 1.989-3.481 2.206-4.545 0.413-1.863 1.242-3.25 0.597-5.167-0.225-0.617-1.613-0.72-2.221-0.298-1.73 1.13-1.411 2.402-1.557 3.595-0.509 3.383-2.662 6.871-2.782 9.414z"/>
+ <path fill="#ffd02d" d="m161.786 99.852c-0.049 1.579 3.377-2.529 3.581-2.887 0.431-0.872 1.887-3.34 2.091-4.359 0.387-1.784 1.173-3.116 0.547-4.946-0.217-0.589-1.546-0.682-2.126-0.275-1.649 1.089-1.339 2.305-1.472 3.446-0.474 3.24-2.518 6.587-2.621 9.021z"/>
+ <path fill="#ffd030" d="m162.055 99.405c-0.039 1.508 3.212-2.432 3.404-2.773 0.407-0.835 1.786-3.199 1.976-4.172 0.359-1.706 1.104-2.981 0.499-4.726-0.211-0.562-1.481-0.644-2.032-0.253-1.571 1.048-1.268 2.208-1.389 3.298-0.436 3.096-2.372 6.302-2.458 8.626z"/>
+ <path fill="#ffd133" d="m162.323 98.958c-0.029 1.437 3.048-2.334 3.23-2.661 0.383-0.798 1.684-3.057 1.858-3.986 0.334-1.627 1.037-2.846 0.45-4.505-0.204-0.534-1.414-0.605-1.938-0.23-1.49 1.007-1.195 2.11-1.306 3.15-0.397 2.953-2.224 6.018-2.294 8.232z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fc0" d="m179.646 95.994c-3.168 3.456-5.4 6.767-7.2 9-1.872 2.304-6.48 5.04-4.176 7.704 1.943 2.376 9.936-1.944 16.128-6.552 6.12-4.608 15.696-8.711 11.016-13.967-2.448-2.664-8.208-2.088-10.439-0.648-1.729 1.078-2.737 1.655-5.329 4.463z"/>
+ <path fill="#ffcc02" d="m179.782 96.147c-3.118 3.378-5.313 6.628-7.086 8.809-1.841 2.249-6.375 4.945-4.13 7.534 1.893 2.31 9.724-1.943 15.795-6.469 6.001-4.525 15.38-8.574 10.82-13.682-2.387-2.588-8.022-1.998-10.209-0.584-1.692 1.062-2.665 1.67-5.19 4.392z"/>
+ <path fill="#ffcc05" d="m179.919 96.3c-3.068 3.3-5.227 6.488-6.973 8.619-1.81 2.193-6.271 4.85-4.085 7.364 1.843 2.243 9.513-1.943 15.463-6.386 5.882-4.442 15.064-8.437 10.623-13.396-2.323-2.513-7.835-1.907-9.978-0.52-1.655 1.044-2.593 1.684-5.05 4.319z"/>
+ <path fill="#ffcc07" d="m180.055 96.454c-3.02 3.222-5.14 6.347-6.859 8.428-1.78 2.138-6.166 4.754-4.04 7.194 1.793 2.177 9.302-1.942 15.131-6.303 5.762-4.359 14.748-8.299 10.427-13.11-2.261-2.437-7.648-1.817-9.747-0.456-1.619 1.025-2.522 1.698-4.912 4.247z"/>
+ <path fill="#ffcd0a" d="m180.191 96.607c-2.97 3.143-5.052 6.207-6.745 8.237-1.749 2.082-6.063 4.659-3.994 7.023 1.743 2.111 9.09-1.941 14.798-6.219 5.644-4.276 14.433-8.162 10.231-12.824-2.199-2.361-7.463-1.727-9.518-0.392-1.581 1.008-2.45 1.713-4.772 4.175z"/>
+ <path fill="#ffcd0c" d="m180.327 96.761c-2.92 3.065-4.965 6.066-6.631 8.047-1.718 2.027-5.957 4.564-3.949 6.853 1.693 2.044 8.878-1.94 14.466-6.136 5.524-4.194 14.116-8.024 10.034-12.538-2.137-2.286-7.275-1.636-9.285-0.328-1.546 0.988-2.38 1.726-4.635 4.102z"/>
+ <path fill="#ffcd0f" d="m180.464 96.914c-2.871 2.987-4.879 5.926-6.518 7.857-1.688 1.971-5.854 4.468-3.903 6.683 1.643 1.978 8.666-1.94 14.133-6.053 5.404-4.111 13.801-7.887 9.839-12.251-2.075-2.21-7.091-1.546-9.056-0.264-1.509 0.969-2.308 1.738-4.495 4.028z"/>
+ <path fill="#ffcd11" d="m180.6 97.067c-2.821 2.909-4.792 5.786-6.404 7.667-1.657 1.916-5.748 4.373-3.858 6.512 1.593 1.912 8.455-1.938 13.802-5.969 5.284-4.028 13.484-7.75 9.641-11.966-2.012-2.134-6.902-1.456-8.823-0.199-1.474 0.951-2.238 1.752-4.358 3.955z"/>
+ <path fill="#ffce14" d="m180.736 97.221c-2.771 2.83-4.705 5.645-6.29 7.476-1.626 1.86-5.644 4.278-3.813 6.342 1.542 1.845 8.244-1.938 13.47-5.886 5.166-3.945 13.169-7.612 9.444-11.68-1.949-2.059-6.716-1.365-8.592-0.135-1.437 0.933-2.166 1.766-4.219 3.883z"/>
+ <path fill="#ffce16" d="m180.872 97.375c-2.722 2.752-4.617 5.504-6.176 7.286-1.595 1.805-5.539 4.182-3.767 6.172 1.49 1.779 8.031-1.937 13.136-5.803 5.046-3.862 12.853-7.475 9.249-11.394-1.889-1.983-6.53-1.274-8.362-0.071-1.4 0.914-2.095 1.779-4.08 3.81z"/>
+ <path fill="#ffce19" d="m181.009 97.528c-2.673 2.674-4.53 5.364-6.063 7.095-1.564 1.749-5.435 4.087-3.722 6.001 1.44 1.713 7.82-1.936 12.804-5.719 4.927-3.78 12.537-7.338 9.052-11.108-1.825-1.907-6.343-1.185-8.13-0.007-1.364 0.896-2.024 1.793-3.941 3.738z"/>
+ <path fill="#ffce1c" d="m181.145 97.682c-2.623 2.595-4.444 5.225-5.949 6.904-1.534 1.693-5.33 3.992-3.676 5.831 1.39 1.646 7.608-1.935 12.471-5.636 4.808-3.697 12.221-7.2 8.856-10.822-1.764-1.832-6.157-1.094-7.9 0.057-1.327 0.878-1.952 1.807-3.802 3.666z"/>
+ <path fill="#ffcf1e" d="m181.281 97.835c-2.573 2.517-4.357 5.084-5.835 6.714-1.503 1.638-5.226 3.896-3.631 5.661 1.34 1.58 7.396-1.935 12.139-5.553 4.689-3.614 11.905-7.063 8.659-10.536-1.701-1.756-5.97-1.004-7.668 0.121-1.291 0.86-1.881 1.821-3.664 3.593z"/>
+ <path fill="#ffcf21" d="m181.417 97.988c-2.522 2.439-4.27 4.944-5.721 6.524-1.472 1.582-5.121 3.801-3.586 5.491 1.29 1.513 7.186-1.934 11.807-5.47 4.569-3.531 11.589-6.926 8.463-10.25-1.639-1.68-5.783-0.914-7.438 0.186-1.254 0.84-1.809 1.834-3.525 3.519z"/>
+ <path fill="#ffcf23" d="m181.554 98.142c-2.476 2.361-4.185 4.803-5.608 6.333-1.441 1.527-5.017 3.706-3.54 5.32 1.24 1.447 6.974-1.933 11.474-5.386 4.45-3.448 11.273-6.788 8.268-9.964-1.577-1.605-5.599-0.823-7.207 0.25-1.22 0.822-1.74 1.848-3.387 3.447z"/>
+ <path fill="#ffcf26" d="m181.69 98.295c-2.425 2.283-4.098 4.663-5.494 6.143-1.411 1.471-4.912 3.61-3.495 5.15 1.19 1.381 6.763-1.932 11.142-5.303 4.331-3.366 10.957-6.65 8.07-9.679-1.514-1.529-5.411-0.732-6.976 0.313-1.182 0.805-1.667 1.863-3.247 3.376z"/>
+ <path fill="#ffd028" d="m181.826 98.449c-2.375 2.204-4.009 4.522-5.38 5.952-1.38 1.416-4.808 3.515-3.449 4.98 1.14 1.314 6.551-1.932 10.81-5.22 4.211-3.283 10.641-6.513 7.874-9.393-1.452-1.454-5.226-0.642-6.745 0.378-1.147 0.786-1.597 1.876-3.11 3.303z"/>
+ <path fill="#ffd02b" d="m181.962 98.602c-2.324 2.127-3.922 4.382-5.266 5.762-1.349 1.36-4.703 3.42-3.404 4.809 1.089 1.248 6.34-1.93 10.478-5.136 4.092-3.2 10.325-6.376 7.677-9.106-1.389-1.378-5.038-0.552-6.513 0.441-1.111 0.768-1.526 1.89-2.972 3.23z"/>
+ <path fill="#ffd02d" d="m182.099 98.756c-2.276 2.048-3.836 4.241-5.153 5.571-1.318 1.305-4.599 3.324-3.359 4.639 1.039 1.182 6.128-1.93 10.146-5.053 3.973-3.117 10.009-6.238 7.48-8.82-1.328-1.303-4.852-0.462-6.282 0.506-1.074 0.748-1.454 1.903-2.832 3.157z"/>
+ <path fill="#ffd030" d="m182.235 98.909c-2.228 1.97-3.749 4.101-5.039 5.381-1.288 1.249-4.494 3.229-3.313 4.469 0.988 1.115 5.916-1.929 9.813-4.97 3.853-3.034 9.693-6.101 7.285-8.535-1.267-1.227-4.666-0.371-6.052 0.57-1.038 0.731-1.384 1.918-2.694 3.085z"/>
+ <path fill="#ffd133" d="m182.371 99.063c-2.177 1.892-3.662 3.96-4.925 5.19-1.257 1.193-4.39 3.133-3.268 4.298 0.938 1.049 5.704-1.928 9.479-4.886 3.734-2.952 9.377-5.963 7.088-8.249-1.203-1.151-4.479-0.281-5.821 0.634-0.999 0.713-1.31 1.931-2.553 3.013z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fff" d="m186.414 168.569c0.864-2.808 28.872-9.432 33.48-7.272 4.536 2.16 26.279 33.768 22.392 35.496-3.888 1.657-12.24-10.512-24.408-16.128s-32.328-9.216-31.464-12.096z"/>
+ <path fill="#f9f9f9" d="m187.239 168.626c0.848-2.761 28.145-9.076 32.69-6.997 4.476 2.079 25.768 32.897 21.943 34.591-3.824 1.625-11.965-10.346-23.94-15.874-11.976-5.527-31.541-8.89-30.693-11.72z"/>
+ <path fill="#f4f4f4" d="m188.063 168.683c0.832-2.714 27.418-8.72 31.899-6.722 4.417 1.998 25.259 32.026 21.497 33.685-3.76 1.595-11.689-10.18-23.474-15.619-11.782-5.438-30.754-8.565-29.922-11.344z"/>
+ <path fill="#efefef" d="m188.888 168.74c0.814-2.668 26.69-8.364 31.109-6.447 4.357 1.917 24.746 31.155 21.049 32.779-3.695 1.563-11.416-10.014-23.007-15.364-11.59-5.349-29.967-8.239-29.151-10.968z"/>
+ <path fill="#eaeaea" d="m189.712 168.797c0.801-2.621 25.964-8.009 30.32-6.173 4.299 1.837 24.235 30.285 20.603 31.874-3.633 1.532-11.142-9.847-22.54-15.109-11.4-5.261-29.182-7.914-28.383-10.592z"/>
+ <path fill="#e5e5e5" d="m190.537 168.853c0.783-2.573 25.236-7.652 29.53-5.897 4.239 1.756 23.723 29.414 20.155 30.968-3.569 1.501-10.867-9.681-22.074-14.854-11.206-5.172-28.395-7.589-27.611-10.217z"/>
+ <path fill="#e0e0e0" d="m191.361 168.91c0.768-2.527 24.51-7.296 28.74-5.622 4.18 1.675 23.212 28.543 19.708 30.063-3.505 1.469-10.593-9.516-21.607-14.6-11.014-5.083-27.608-7.263-26.841-9.841z"/>
+ <path fill="#dbdbdb" d="m192.186 168.967c0.751-2.48 23.781-6.941 27.95-5.347 4.119 1.593 22.7 27.671 19.26 29.157-3.441 1.438-10.318-9.349-21.141-14.345-10.821-4.994-26.821-6.938-26.069-9.465z"/>
+ <path fill="#d6d6d6" d="m193.01 169.024c0.735-2.433 23.057-6.585 27.16-5.073 4.062 1.513 22.19 26.801 18.813 28.252-3.377 1.407-10.043-9.183-20.673-14.09-10.629-4.906-26.035-6.612-25.3-9.089z"/>
+ <path fill="#d1d1d1" d="m193.835 169.081c0.72-2.387 22.328-6.229 26.37-4.798 4.001 1.432 21.678 25.93 18.365 27.346-3.313 1.376-9.768-9.017-20.206-13.835-10.437-4.817-25.248-6.287-24.529-8.713z"/>
+ <path fill="#ccc" d="m194.659 169.137c0.703-2.339 21.603-5.873 25.58-4.521 3.942 1.351 21.167 25.059 17.918 26.44-3.249 1.345-9.493-8.851-19.739-13.58-10.245-4.729-24.462-5.963-23.759-8.339z"/>
+ <path fill="#c6c6c6" d="m195.484 169.194c0.687-2.292 20.874-5.517 24.79-4.247 3.882 1.27 20.655 24.188 17.47 25.535-3.185 1.314-9.219-8.685-19.271-13.326-10.054-4.639-23.676-5.636-22.989-7.962z"/>
+ <path fill="#c1c1c1" d="m196.308 169.251c0.671-2.246 20.147-5.161 24-3.973 3.822 1.19 20.145 23.318 17.022 24.63-3.121 1.283-8.943-8.519-18.805-13.071-9.859-4.551-22.888-5.311-22.217-7.586z"/>
+ <path fill="#bcbcbc" d="m197.133 169.308c0.654-2.199 19.421-4.805 23.21-3.698 3.764 1.109 19.634 22.447 16.575 23.724-3.057 1.252-8.669-8.353-18.338-12.816-9.668-4.462-22.102-4.985-21.447-7.21z"/>
+ <path fill="#b7b7b7" d="m197.957 169.365c0.64-2.152 18.693-4.45 22.42-3.423 3.705 1.027 19.122 21.575 16.129 22.818-2.993 1.221-8.395-8.186-17.872-12.561-9.476-4.373-21.315-4.66-20.677-6.834z"/>
+ <path fill="#b2b2b2" d="m198.782 169.421c0.622-2.105 17.966-4.093 21.63-3.147 3.646 0.946 18.61 20.704 15.681 21.912-2.93 1.19-8.12-8.02-17.404-12.306-9.284-4.284-20.53-4.335-19.907-6.459z"/>
+ <path fill="#adadad" d="m199.606 169.478c0.606-2.058 17.239-3.737 20.84-2.873 3.586 0.866 18.099 19.834 15.234 21.008-2.866 1.158-7.847-7.855-16.938-12.052-9.091-4.196-19.742-4.009-19.136-6.083z"/>
+ <path fill="#a8a8a8" d="m200.431 169.535c0.59-2.011 16.512-3.382 20.05-2.598 3.525 0.785 17.588 18.963 14.786 20.102-2.803 1.127-7.571-7.688-16.472-11.797-8.898-4.107-18.955-3.684-18.364-5.707z"/>
+ <path fill="#a3a3a3" d="m201.255 169.592c0.574-1.965 15.785-3.026 19.261-2.323 3.467 0.704 17.076 18.092 14.339 19.196-2.738 1.096-7.296-7.522-16.004-11.542-8.707-4.018-18.17-3.358-17.596-5.331z"/>
+ <path fill="#9e9e9e" d="m202.08 169.649c0.559-1.918 15.059-2.67 18.47-2.048 3.407 0.623 16.565 17.221 13.892 18.29-2.674 1.065-7.022-7.356-15.537-11.287-8.515-3.929-17.383-3.033-16.825-4.955z"/>
+ <path fill="#999" d="m202.904 169.705c0.542-1.871 14.331-2.314 17.68-1.773 3.349 0.542 16.055 16.35 13.444 17.385-2.61 1.034-6.747-7.19-15.07-11.032-8.322-3.841-16.596-2.708-16.054-4.58z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fff" d="m151.134 211.625c2.881 0.144 0.145 16.271 0.145 32.903s2.231 22.464 0.144 24.552-5.688-5.399-5.688-22.031c-0.001-16.632 2.519-35.568 5.399-35.424z"/>
+ <path fill="#f9f9f9" d="m151.105 212.016c2.783 0.162 0.109 16.052 0.097 32.419-0.012 16.366 2.188 22.208 0.164 24.237-2.02 2.029-5.561-5.383-5.546-21.752 0.012-16.367 2.502-35.065 5.285-34.904z"/>
+ <path fill="#f4f4f4" d="m151.075 212.407c2.687 0.18 0.076 15.832 0.051 31.934-0.023 16.102 2.143 21.951 0.185 23.924-1.953 1.968-5.435-5.367-5.405-21.473 0.024-16.103 2.484-34.564 5.169-34.385z"/>
+ <path fill="#efefef" d="m151.046 212.797c2.588 0.197 0.041 15.613 0.004 31.449-0.036 15.836 2.098 21.694 0.204 23.609-1.886 1.907-5.308-5.352-5.263-21.195 0.037-15.835 2.467-34.058 5.055-33.863z"/>
+ <path fill="#eaeaea" d="m151.017 213.189c2.49 0.214 0.007 15.392-0.043 30.962-0.05 15.571 2.052 21.439 0.224 23.297-1.818 1.848-5.181-5.334-5.122-20.916 0.049-15.571 2.45-33.557 4.941-33.343z"/>
+ <path fill="#e5e5e5" d="m150.987 213.581c2.394 0.23-0.027 15.17-0.089 30.477s2.007 21.182 0.244 22.982c-1.751 1.787-5.055-5.32-4.98-20.638 0.061-15.305 2.431-33.053 4.825-32.821z"/>
+ <path fill="#e0e0e0" d="m150.958 213.971c2.297 0.248-0.062 14.951-0.136 29.99-0.074 15.041 1.962 20.927 0.264 22.668-1.683 1.728-4.928-5.301-4.839-20.356 0.074-15.04 2.414-32.551 4.711-32.302z"/>
+ <path fill="#dbdbdb" d="m150.928 214.362c2.199 0.266-0.096 14.73-0.182 29.506-0.087 14.775 1.915 20.67 0.282 22.354-1.615 1.667-4.8-5.286-4.696-20.078 0.087-14.776 2.397-32.048 4.596-31.782z"/>
+ <path fill="#d6d6d6" d="m150.899 214.752c2.102 0.283-0.13 14.511-0.229 29.021-0.099 14.511 1.87 20.413 0.303 22.04-1.549 1.607-4.674-5.27-4.556-19.799 0.1-14.51 2.38-31.545 4.482-31.262z"/>
+ <path fill="#d1d1d1" d="m150.87 215.144c2.005 0.301-0.165 14.29-0.274 28.535-0.112 14.245 1.824 20.155 0.321 21.725-1.479 1.548-4.547-5.252-4.413-19.519 0.11-14.245 2.361-31.043 4.366-30.741z"/>
+ <path fill="#ccc" d="m150.84 215.536c1.908 0.317-0.197 14.069-0.32 28.049-0.124 13.979 1.779 19.899 0.342 21.412-1.413 1.486-4.42-5.238-4.272-19.242 0.122-13.979 2.343-30.54 4.25-30.219z"/>
+ <path fill="#c6c6c6" d="m150.811 215.926c1.811 0.334-0.233 13.85-0.368 27.564-0.136 13.713 1.735 19.643 0.362 21.096-1.346 1.428-4.293-5.219-4.131-18.961 0.136-13.712 2.327-30.035 4.137-29.699z"/>
+ <path fill="#c1c1c1" d="m150.781 216.317c1.714 0.354-0.267 13.629-0.414 27.078s1.69 19.387 0.382 20.783c-1.277 1.367-4.166-5.203-3.989-18.682 0.148-13.449 2.308-29.533 4.021-29.179z"/>
+ <path fill="#bcbcbc" d="m150.752 216.708c1.616 0.371-0.301 13.41-0.461 26.594-0.161 13.184 1.646 19.13 0.402 20.469-1.211 1.307-4.04-5.188-3.847-18.402 0.16-13.185 2.29-29.033 3.906-28.661z"/>
+ <path fill="#b7b7b7" d="m150.723 217.099c1.519 0.387-0.336 13.188-0.509 26.106-0.173 12.92 1.601 18.875 0.423 20.156-1.144 1.246-3.913-5.171-3.706-18.123 0.172-12.919 2.273-28.529 3.792-28.139z"/>
+ <path fill="#b2b2b2" d="m150.693 217.491c1.422 0.404-0.37 12.969-0.554 25.621-0.186 12.653 1.555 18.617 0.441 19.842-1.076 1.187-3.786-5.156-3.563-17.846 0.184-12.652 2.255-28.024 3.676-27.617z"/>
+ <path fill="#adadad" d="m150.664 217.881c1.325 0.422-0.404 12.748-0.601 25.136-0.198 12.388 1.51 18.36 0.462 19.528-1.008 1.125-3.66-5.139-3.423-17.566 0.197-12.389 2.238-27.521 3.562-27.098z"/>
+ <path fill="#a8a8a8" d="m150.634 218.272c1.229 0.439-0.438 12.527-0.646 24.65-0.21 12.123 1.464 18.104 0.48 19.213-0.939 1.066-3.531-5.121-3.279-17.285 0.208-12.123 2.219-27.019 3.445-26.578z"/>
+ <path fill="#a3a3a3" d="m150.605 218.663c1.13 0.457-0.474 12.309-0.694 24.166-0.222 11.857 1.419 17.848 0.501 18.899-0.873 1.006-3.405-5.106-3.139-17.009 0.222-11.855 2.202-26.515 3.332-26.056z"/>
+ <path fill="#9e9e9e" d="m150.576 219.054c1.033 0.474-0.507 12.088-0.741 23.68-0.234 11.593 1.374 17.591 0.521 18.585-0.806 0.946-3.279-5.089-2.997-16.729 0.233-11.591 2.184-26.011 3.217-25.536z"/>
+ <path fill="#999" d="m150.546 219.444c0.937 0.492-0.541 11.868-0.787 23.195-0.246 11.326 1.329 17.335 0.541 18.271-0.737 0.885-3.151-5.074-2.855-16.449 0.245-11.328 2.166-25.509 3.101-25.017z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fff" d="m157.434 167.161c1.735 0.192 12.437-2.218 12.822-1.254 0.386 0.772-6.651 2.893-8.966 5.303-0.771 0.771-2.796 2.603-4.049 2.41-0.964-0.096-1.543-2.121-2.989-3.664-3.471-3.47-5.688-3.181-5.013-4.531 0.579-1.06 5.207 1.446 8.195 1.736z"/>
+ <path fill="#fbfbfb" d="m157.479 167.201c1.7 0.188 12.176-2.171 12.554-1.227 0.377 0.755-6.512 2.832-8.778 5.191-0.755 0.755-2.736 2.549-3.964 2.36-0.942-0.094-1.51-2.077-2.926-3.587-3.398-3.397-5.568-3.115-4.907-4.436 0.565-1.038 5.096 1.415 8.021 1.699z"/>
+ <path fill="#f8f8f8" d="m157.525 167.241c1.663 0.184 11.914-2.124 12.283-1.201 0.369 0.739-6.372 2.771-8.589 5.08-0.738 0.739-2.679 2.494-3.879 2.309-0.924-0.092-1.479-2.032-2.863-3.51-3.325-3.324-5.449-3.048-4.803-4.341 0.555-1.015 4.988 1.385 7.851 1.663z"/>
+ <path fill="#f5f5f5" d="m157.57 167.281c1.626 0.18 11.652-2.078 12.014-1.175 0.361 0.723-6.231 2.711-8.4 4.969-0.723 0.722-2.619 2.439-3.793 2.258-0.903-0.09-1.446-1.987-2.802-3.433-3.252-3.251-5.329-2.981-4.695-4.245 0.54-0.993 4.876 1.354 7.676 1.626z"/>
+ <path fill="#f2f2f2" d="m157.615 167.321c1.59 0.176 11.391-2.031 11.745-1.148 0.352 0.706-6.093 2.649-8.212 4.856-0.707 0.707-2.562 2.385-3.709 2.208-0.883-0.088-1.413-1.943-2.738-3.356-3.179-3.178-5.209-2.914-4.591-4.15 0.529-0.971 4.768 1.324 7.505 1.59z"/>
+ <path fill="#efefef" d="m157.66 167.361c1.554 0.172 11.13-1.985 11.475-1.122 0.346 0.69-5.952 2.589-8.022 4.745-0.69 0.69-2.503 2.33-3.624 2.157-0.863-0.086-1.381-1.898-2.675-3.279-3.106-3.105-5.09-2.847-4.486-4.055 0.517-0.948 4.658 1.294 7.332 1.554z"/>
+ <path fill="#ebebeb" d="m157.705 167.401c1.518 0.168 10.868-1.938 11.206-1.096 0.336 0.674-5.813 2.528-7.835 4.634-0.674 0.674-2.444 2.275-3.539 2.106-0.842-0.084-1.348-1.853-2.612-3.202-3.032-3.032-4.97-2.78-4.38-3.959 0.505-0.926 4.549 1.263 7.16 1.517z"/>
+ <path fill="#e8e8e8" d="m157.751 167.441c1.48 0.164 10.606-1.892 10.936-1.069 0.329 0.657-5.673 2.467-7.646 4.522-0.658 0.657-2.385 2.22-3.453 2.055-0.822-0.082-1.315-1.809-2.549-3.124-2.96-2.96-4.851-2.714-4.275-3.865 0.491-0.904 4.438 1.233 6.987 1.481z"/>
+ <path fill="#e5e5e5" d="m157.796 167.481c1.444 0.16 10.346-1.845 10.666-1.043 0.32 0.641-5.532 2.406-7.458 4.41-0.641 0.642-2.325 2.166-3.367 2.005-0.803-0.08-1.284-1.764-2.486-3.047-2.887-2.887-4.732-2.647-4.17-3.769 0.48-0.882 4.329 1.202 6.815 1.444z"/>
+ <path fill="#e2e2e2" d="m157.841 167.521c1.407 0.156 10.083-1.799 10.397-1.017 0.312 0.625-5.394 2.346-7.271 4.299-0.625 0.625-2.267 2.111-3.282 1.954-0.782-0.078-1.251-1.719-2.423-2.97-2.814-2.814-4.612-2.58-4.065-3.674 0.469-0.859 4.221 1.172 6.644 1.408z"/>
+ <path fill="#dfdfdf" d="m157.886 167.56c1.37 0.152 9.821-1.751 10.127-0.99 0.304 0.609-5.254 2.285-7.081 4.188-0.609 0.609-2.208 2.056-3.198 1.903-0.761-0.076-1.218-1.675-2.36-2.893-2.741-2.741-4.492-2.513-3.959-3.579 0.456-0.837 4.111 1.142 6.471 1.371z"/>
+ <path fill="#dbdbdb" d="m157.931 167.6c1.335 0.148 9.561-1.704 9.857-0.963 0.296 0.592-5.114 2.223-6.893 4.076-0.593 0.593-2.149 2.001-3.113 1.853-0.741-0.074-1.186-1.631-2.297-2.817-2.668-2.667-4.373-2.446-3.854-3.483 0.445-0.815 4.003 1.111 6.3 1.334z"/>
+ <path fill="#d8d8d8" d="m157.977 167.64c1.298 0.144 9.299-1.658 9.587-0.937 0.288 0.576-4.974 2.163-6.704 3.964-0.576 0.577-2.091 1.947-3.027 1.803-0.721-0.072-1.153-1.586-2.234-2.74-2.596-2.594-4.253-2.379-3.748-3.388 0.431-0.792 3.891 1.081 6.126 1.298z"/>
+ <path fill="#d5d5d5" d="m158.022 167.68c1.261 0.14 9.037-1.611 9.317-0.911 0.28 0.56-4.834 2.102-6.516 3.853-0.56 0.561-2.032 1.892-2.942 1.752-0.7-0.07-1.12-1.541-2.172-2.663-2.521-2.521-4.133-2.312-3.643-3.292 0.421-0.77 3.784 1.05 5.956 1.261z"/>
+ <path fill="#d2d2d2" d="m158.067 167.72c1.225 0.136 8.775-1.564 9.049-0.884 0.271 0.543-4.695 2.041-6.327 3.741-0.545 0.544-1.974 1.837-2.857 1.701-0.682-0.068-1.09-1.497-2.109-2.585-2.449-2.449-4.014-2.246-3.538-3.198 0.407-0.748 3.673 1.02 5.782 1.225z"/>
+ <path fill="#cfcfcf" d="m158.112 167.76c1.188 0.132 8.515-1.518 8.779-0.858 0.264 0.527-4.555 1.98-6.139 3.63-0.527 0.528-1.915 1.782-2.772 1.65-0.66-0.066-1.057-1.452-2.046-2.508-2.376-2.376-3.895-2.179-3.433-3.103 0.397-0.725 3.565 0.99 5.611 1.189z"/>
+ <path fill="#ccc" d="m158.157 167.8c1.152 0.128 8.253-1.472 8.51-0.832 0.255 0.511-4.415 1.92-5.95 3.518-0.512 0.512-1.855 1.728-2.688 1.6-0.64-0.064-1.023-1.407-1.983-2.431-2.303-2.303-3.773-2.112-3.326-3.007 0.383-0.703 3.454 0.959 5.437 1.152z"/>
+ <path fill="#c8c8c8" d="m158.203 167.84c1.115 0.124 7.991-1.425 8.239-0.805 0.248 0.494-4.274 1.858-5.761 3.406-0.496 0.496-1.798 1.673-2.603 1.549-0.62-0.063-0.992-1.363-1.921-2.354-2.229-2.229-3.655-2.045-3.221-2.912 0.372-0.681 3.346 0.929 5.267 1.116z"/>
+ <path fill="#c5c5c5" d="m158.248 167.88c1.079 0.12 7.73-1.379 7.97-0.779 0.239 0.478-4.135 1.798-5.572 3.295-0.479 0.479-1.739 1.618-2.518 1.498-0.6-0.06-0.959-1.318-1.857-2.277-2.157-2.157-3.535-1.978-3.116-2.816 0.359-0.659 3.235 0.898 5.093 1.079z"/>
+ <path fill="#c2c2c2" d="m158.293 167.92c1.042 0.116 7.469-1.332 7.701-0.753 0.231 0.462-3.995 1.737-5.385 3.184-0.463 0.463-1.68 1.563-2.432 1.447-0.579-0.058-0.927-1.273-1.796-2.2-2.084-2.084-3.415-1.911-3.01-2.721 0.348-0.636 3.127 0.868 4.922 1.043z"/>
+ <path fill="#bfbfbf" d="m158.338 167.959c1.007 0.112 7.207-1.285 7.432-0.726 0.223 0.446-3.855 1.676-5.196 3.072-0.447 0.447-1.621 1.509-2.347 1.397-0.56-0.056-0.895-1.229-1.732-2.123-2.011-2.011-3.296-1.844-2.905-2.626 0.334-0.614 3.016 0.838 4.748 1.006z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m194.253 11.922c-1.222 2.631-3.812 23.214-0.248 20.892 3.594-2.341 13.57-5.312 19.886-7.013 7.003-1.886-17.188-19.463-19.638-13.879z"/>
+ <path fill="#060606" d="m194.485 12.307c-1.21 2.594-3.704 22.255-0.234 20.007 3.491-2.262 13.077-5.1 19.039-6.782 6.59-1.905-16.436-18.609-18.805-13.225z"/>
+ <path fill="#0c0c0c" d="m194.717 12.691c-1.198 2.557-3.595 21.296-0.221 19.124 3.391-2.184 12.586-4.888 18.194-6.551 6.177-1.924-15.684-17.757-17.973-12.573z"/>
+ <path fill="#131313" d="m194.949 13.076c-1.187 2.52-3.487 20.337-0.207 18.239 3.288-2.105 12.093-4.676 17.348-6.321 5.763-1.941-14.933-16.903-17.141-11.918z"/>
+ <path fill="#191919" d="m195.181 13.46c-1.177 2.483-3.379 19.378-0.193 17.355 3.187-2.027 11.6-4.464 16.502-6.09 5.349-1.959-14.182-16.05-16.309-11.265z"/>
+ <path fill="#1f1f1f" d="m195.413 13.845c-1.164 2.446-3.27 18.419-0.18 16.471 3.086-1.949 11.107-4.252 15.657-5.859 4.935-1.978-13.43-15.198-15.477-10.612z"/>
+ <path fill="#262626" d="m195.645 14.229c-1.153 2.409-3.162 17.46-0.166 15.586 2.983-1.87 10.616-4.04 14.811-5.628 4.521-1.995-12.679-14.344-14.645-9.958z"/>
+ <path fill="#2c2c2c" d="m195.878 14.614c-1.142 2.372-3.055 16.501-0.152 14.702 2.882-1.792 10.123-3.828 13.965-5.398 4.107-2.013-11.929-13.49-13.813-9.304z"/>
+ <path fill="#333" d="m196.11 14.999c-1.131 2.335-2.946 15.542-0.14 13.817 2.78-1.713 9.631-3.616 13.119-5.167 3.695-2.031-11.175-12.637-12.979-8.65z"/>
+ <path fill="#393939" d="m196.342 15.383c-1.118 2.299-2.838 14.583-0.126 12.934 2.68-1.636 9.139-3.404 12.274-4.937 3.28-2.049-10.425-11.784-12.148-7.997z"/>
+ <path fill="#3f3f3f" d="m196.574 15.768c-1.108 2.261-2.729 13.624-0.112 12.049 2.577-1.557 8.646-3.192 11.429-4.706 2.865-2.068-9.675-10.931-11.317-7.343z"/>
+ <path fill="#464646" d="m196.806 16.152c-1.097 2.225-2.622 12.665-0.1 11.165 2.477-1.479 8.154-2.98 10.583-4.475 2.453-2.086-8.922-10.078-10.483-6.69z"/>
+ <path fill="#4c4c4c" d="m197.038 16.537c-1.085 2.188-2.513 11.706-0.085 10.28 2.374-1.4 7.661-2.768 9.737-4.244 2.039-2.104-8.171-9.225-9.652-6.036z"/>
+ <path fill="#525252" d="m197.27 16.921c-1.073 2.151-2.405 10.747-0.071 9.396 2.272-1.322 7.168-2.556 8.891-4.013 1.625-2.122-7.42-8.371-8.82-5.383z"/>
+ <path fill="#595959" d="m197.502 17.306c-1.062 2.113-2.297 9.788-0.058 8.512 2.172-1.244 6.677-2.344 8.046-3.783 1.211-2.14-6.669-7.518-7.988-4.729z"/>
+ <path fill="#5f5f5f" d="m197.734 17.69c-1.05 2.077-2.188 8.829-0.044 7.627 2.069-1.165 6.184-2.132 7.2-3.552 0.797-2.157-5.917-6.664-7.156-4.075z"/>
+ <path fill="#666" d="m197.966 18.075c-1.038 2.04-2.079 7.87-0.029 6.743 1.968-1.087 5.69-1.92 6.354-3.321 0.382-2.176-5.167-5.812-6.325-3.422z"/>
+ <path fill="#6c6c6c" d="m198.198 18.459c-1.027 2.003-1.972 6.911-0.017 5.859 1.866-1.008 5.198-1.708 5.509-3.09-0.03-2.194-4.415-4.959-5.492-2.769z"/>
+ <path fill="#727272" d="m198.43 18.844c-1.017 1.966-1.863 5.952-0.003 4.975 1.765-0.93 4.706-1.496 4.662-2.86-0.443-2.212-3.662-4.106-4.659-2.115z"/>
+ <path fill="#797979" d="m198.662 19.228c-1.004 1.929-1.755 4.993 0.011 4.09 1.663-0.852 4.215-1.284 3.817-2.629-0.858-2.23-2.912-3.251-3.828-1.461z"/>
+ <path fill="#7f7f7f" d="m198.894 19.612c-0.993 1.892-1.647 4.034 0.023 3.206 1.563-0.773 3.723-1.072 2.973-2.398-1.272-2.248-2.161-2.399-2.996-0.808z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m143.502 46.386c-0.72 2.16 8.712 5.112 10.801 6.984 2.808 2.52 3.023 7.488 6.336 5.472 2.159-1.296 0.504-4.176-3.456-8.568-5.833-6.481-13.033-5.689-13.681-3.888z"/>
+ <path fill="#050505" d="m143.991 46.582c-0.716 2.073 8.275 4.9 10.336 6.741 2.745 2.457 2.961 7.249 6.146 5.313 2.094-1.254 0.449-4.072-3.343-8.28-5.574-6.203-12.491-5.505-13.139-3.774z"/>
+ <path fill="#0a0a0a" d="m144.479 46.779c-0.71 1.987 7.839 4.688 9.873 6.498 2.682 2.394 2.897 7.009 5.956 5.154 2.028-1.212 0.395-3.968-3.228-7.993-5.319-5.926-11.953-5.321-12.601-3.659z"/>
+ <path fill="#0f0f0f" d="m144.967 46.976c-0.704 1.9 7.403 4.476 9.41 6.254 2.62 2.33 2.835 6.77 5.766 4.995 1.964-1.171 0.342-3.864-3.112-7.706-5.064-5.647-11.415-5.137-12.064-3.543z"/>
+ <path fill="#141414" d="m145.456 47.172c-0.701 1.813 6.966 4.263 8.946 6.011 2.557 2.266 2.772 6.53 5.575 4.835 1.897-1.129 0.287-3.76-2.998-7.418-4.807-5.369-10.874-4.952-11.523-3.428z"/>
+ <path fill="#191919" d="m145.944 47.369c-0.696 1.726 6.53 4.051 8.483 5.768 2.493 2.203 2.71 6.291 5.385 4.676 1.833-1.087 0.231-3.656-2.884-7.13-4.551-5.093-10.335-4.769-10.984-3.314z"/>
+ <path fill="#1e1e1e" d="m146.433 47.565c-0.692 1.64 6.093 3.839 8.019 5.525 2.431 2.14 2.647 6.052 5.194 4.517 1.768-1.046 0.179-3.552-2.77-6.843-4.293-4.814-9.794-4.585-10.443-3.199z"/>
+ <path fill="#232323" d="m146.921 47.762c-0.686 1.553 5.657 3.627 7.558 5.282 2.367 2.076 2.583 5.813 5.003 4.357 1.702-1.003 0.124-3.448-2.654-6.555-4.04-4.537-9.257-4.401-9.907-3.084z"/>
+ <path fill="#282828" d="m147.409 47.959c-0.681 1.466 5.221 3.415 7.094 5.039 2.305 2.013 2.521 5.573 4.813 4.198 1.637-0.962 0.07-3.344-2.54-6.268-3.782-4.26-8.717-4.218-9.367-2.969z"/>
+ <path fill="#2d2d2d" d="m147.898 48.156c-0.677 1.379 4.784 3.203 6.63 4.795 2.242 1.949 2.457 5.333 4.622 4.039 1.572-0.92 0.016-3.24-2.425-5.98-3.526-3.983-8.177-4.034-8.827-2.854z"/>
+ <path fill="#333" d="m148.386 48.353c-0.673 1.292 4.348 2.99 6.167 4.552 2.179 1.886 2.394 5.095 4.432 3.88 1.506-0.878-0.038-3.136-2.312-5.693-3.268-3.705-7.636-3.85-8.287-2.739z"/>
+ <path fill="#383838" d="m148.875 48.549c-0.668 1.206 3.911 2.778 5.703 4.309 2.116 1.823 2.331 4.855 4.242 3.721 1.439-0.836-0.093-3.032-2.197-5.405-3.013-3.428-7.098-3.667-7.748-2.625z"/>
+ <path fill="#3d3d3d" d="m149.363 48.746c-0.662 1.119 3.475 2.566 5.24 4.065 2.053 1.759 2.268 4.616 4.052 3.562 1.375-0.795-0.147-2.928-2.082-5.118-2.757-3.15-6.559-3.483-7.21-2.509z"/>
+ <path fill="#424242" d="m149.851 48.942c-0.657 1.032 3.039 2.354 4.776 3.823 1.99 1.696 2.205 4.376 3.861 3.402 1.31-0.753-0.201-2.824-1.967-4.831-2.5-2.871-6.018-3.298-6.67-2.394z"/>
+ <path fill="#474747" d="m150.34 49.139c-0.652 0.946 2.603 2.142 4.313 3.58 1.927 1.632 2.143 4.137 3.671 3.243 1.244-0.712-0.256-2.72-1.853-4.543-2.245-2.595-5.48-3.115-6.131-2.28z"/>
+ <path fill="#4c4c4c" d="m150.828 49.336c-0.647 0.859 2.166 1.93 3.851 3.336 1.863 1.569 2.079 3.898 3.48 3.084 1.179-0.67-0.31-2.616-1.739-4.255-1.988-2.317-4.94-2.932-5.592-2.165z"/>
+ <path fill="#515151" d="m151.317 49.533c-0.645 0.772 1.729 1.718 3.386 3.093 1.802 1.505 2.018 3.658 3.29 2.925 1.114-0.628-0.364-2.512-1.624-3.968-1.732-2.04-4.4-2.748-5.052-2.05z"/>
+ <path fill="#565656" d="m151.805 49.729c-0.639 0.685 1.293 1.505 2.923 2.85 1.738 1.442 1.954 3.419 3.1 2.766 1.048-0.586-0.418-2.408-1.509-3.681-1.476-1.762-3.862-2.563-4.514-1.935z"/>
+ <path fill="#5b5b5b" d="m152.293 49.926c-0.633 0.598 0.857 1.293 2.46 2.606 1.677 1.379 1.892 3.18 2.91 2.606 0.983-0.544-0.473-2.304-1.395-3.393-1.22-1.483-3.322-2.379-3.975-1.819z"/>
+ <path fill="#606060" d="m152.782 50.123c-0.629 0.512 0.42 1.081 1.996 2.363 1.613 1.315 1.828 2.94 2.719 2.447 0.918-0.502-0.525-2.2-1.28-3.105-0.963-1.207-2.782-2.196-3.435-1.705z"/>
+ <path fill="#666" d="m153.27 50.319c-0.624 0.425-0.017 0.869 1.533 2.12 1.55 1.252 1.765 2.701 2.528 2.288 0.853-0.461-0.581-2.096-1.166-2.818-0.706-0.929-2.242-2.012-2.895-1.59z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m193.47 45.594c-0.072 1.08 2.951 1.728 4.896 2.448 1.944 0.648 5.76 3.24 7.56 5.256 1.801 1.944 5.688 7.704 6.553 6.192 0.863-1.368-2.017-5.328-2.809-6.984s-3.239-5.256-7.128-6.48c-3.384-1.008-9-1.297-9.072-0.432z"/>
+ <path fill="#060606" d="m193.779 45.67c-0.071 1.05 2.869 1.685 4.758 2.387 1.891 0.633 5.598 3.161 7.345 5.122 1.747 1.893 5.535 7.493 6.376 6.027 0.84-1.328-1.936-5.173-2.738-6.795-0.8-1.622-3.175-5.09-6.952-6.301-3.282-0.995-8.718-1.28-8.789-0.44z"/>
+ <path fill="#0c0c0c" d="m194.088 45.747c-0.07 1.021 2.785 1.641 4.62 2.327 1.836 0.618 5.436 3.081 7.131 4.988 1.692 1.842 5.382 7.282 6.198 5.862 0.814-1.288-1.855-5.019-2.67-6.607-0.808-1.587-3.11-4.925-6.776-6.122-3.178-0.983-8.433-1.264-8.503-0.448z"/>
+ <path fill="#131313" d="m194.397 45.824c-0.071 0.991 2.702 1.598 4.481 2.267 1.782 0.603 5.272 3.001 6.916 4.854 1.64 1.791 5.229 7.071 6.022 5.697 0.788-1.248-1.776-4.865-2.603-6.418-0.815-1.554-3.044-4.759-6.599-5.942-3.073-0.973-8.148-1.251-8.217-0.458z"/>
+ <path fill="#191919" d="m194.706 45.9c-0.069 0.961 2.618 1.555 4.345 2.206 1.728 0.588 5.109 2.922 6.7 4.721 1.586 1.74 5.075 6.86 5.846 5.531 0.764-1.207-1.696-4.711-2.532-6.23-0.824-1.519-2.979-4.593-6.424-5.763-2.972-0.959-7.866-1.234-7.935-0.465z"/>
+ <path fill="#1f1f1f" d="m195.015 45.977c-0.07 0.931 2.534 1.511 4.207 2.146 1.672 0.573 4.945 2.843 6.485 4.586 1.531 1.689 4.921 6.649 5.668 5.367 0.738-1.167-1.616-4.557-2.464-6.042-0.832-1.485-2.914-4.428-6.247-5.583-2.868-0.948-7.581-1.219-7.649-0.474z"/>
+ <path fill="#262626" d="m195.324 46.054c-0.069 0.901 2.451 1.468 4.069 2.085 1.618 0.557 4.784 2.763 6.271 4.453 1.479 1.638 4.769 6.438 5.491 5.201 0.714-1.127-1.536-4.402-2.396-5.854-0.839-1.451-2.848-4.263-6.07-5.404-2.765-0.934-7.298-1.202-7.365-0.481z"/>
+ <path fill="#2c2c2c" d="m195.632 46.13c-0.067 0.872 2.369 1.424 3.933 2.025 1.563 0.542 4.621 2.684 6.056 4.318 1.424 1.587 4.615 6.228 5.315 5.036 0.688-1.086-1.456-4.248-2.326-5.665-0.848-1.416-2.783-4.097-5.896-5.224-2.662-0.923-7.015-1.187-7.082-0.49z"/>
+ <path fill="#333" d="m195.941 46.207c-0.068 0.842 2.285 1.381 3.794 1.964 1.51 0.527 4.458 2.605 5.842 4.185 1.37 1.536 4.461 6.016 5.138 4.871 0.662-1.046-1.377-4.093-2.258-5.476-0.855-1.382-2.718-3.932-5.718-5.045-2.56-0.911-6.732-1.172-6.798-0.499z"/>
+ <path fill="#393939" d="m196.25 46.284c-0.066 0.813 2.202 1.338 3.656 1.904 1.456 0.512 4.296 2.525 5.627 4.051 1.317 1.485 4.308 5.805 4.961 4.706 0.638-1.006-1.296-3.939-2.188-5.288-0.863-1.348-2.652-3.766-5.542-4.866-2.457-0.899-6.449-1.157-6.514-0.507z"/>
+ <path fill="#3f3f3f" d="m196.559 46.36c-0.067 0.783 2.118 1.295 3.518 1.844 1.402 0.497 4.133 2.446 5.412 3.917 1.263 1.434 4.155 5.594 4.785 4.541 0.612-0.966-1.217-3.785-2.12-5.1-0.872-1.313-2.587-3.6-5.366-4.687-2.353-0.886-6.165-1.141-6.229-0.515z"/>
+ <path fill="#464646" d="m196.868 46.437c-0.065 0.753 2.035 1.251 3.38 1.783 1.349 0.482 3.972 2.367 5.197 3.783 1.21 1.383 4.002 5.383 4.608 4.375 0.588-0.926-1.137-3.63-2.052-4.911-0.879-1.279-2.521-3.435-5.189-4.507-2.25-0.874-5.881-1.125-5.944-0.523z"/>
+ <path fill="#4c4c4c" d="m197.177 46.514c-0.066 0.723 1.95 1.208 3.241 1.723 1.293 0.467 3.809 2.287 4.983 3.649 1.155 1.332 3.848 5.172 4.431 4.21 0.563-0.885-1.057-3.476-1.982-4.723-0.888-1.245-2.456-3.269-5.014-4.328-2.146-0.862-5.597-1.109-5.659-0.531z"/>
+ <path fill="#525252" d="m197.486 46.591c-0.066 0.693 1.868 1.164 3.104 1.662 1.239 0.452 3.646 2.208 4.769 3.515 1.102 1.281 3.695 4.961 4.254 4.045 0.537-0.845-0.976-3.321-1.913-4.534-0.896-1.21-2.391-3.103-4.838-4.148-2.044-0.851-5.315-1.095-5.376-0.54z"/>
+ <path fill="#595959" d="m197.795 46.667c-0.064 0.664 1.784 1.121 2.968 1.602 1.184 0.437 3.481 2.128 4.552 3.381 1.049 1.23 3.542 4.75 4.078 3.88 0.512-0.805-0.897-3.167-1.846-4.346-0.902-1.176-2.325-2.938-4.66-3.969-1.942-0.838-5.031-1.078-5.092-0.548z"/>
+ <path fill="#5f5f5f" d="m198.104 46.744c-0.065 0.634 1.701 1.078 2.829 1.541 1.13 0.421 3.318 2.049 4.338 3.248 0.994 1.179 3.388 4.539 3.899 3.715 0.487-0.765-0.815-3.013-1.775-4.157-0.911-1.142-2.261-2.772-4.485-3.79-1.837-0.826-4.746-1.064-4.806-0.557z"/>
+ <path fill="#666" d="m198.413 46.821c-0.063 0.604 1.617 1.034 2.691 1.481 1.076 0.406 3.157 1.969 4.123 3.113 0.94 1.128 3.234 4.328 3.724 3.55 0.462-0.725-0.737-2.858-1.707-3.969-0.919-1.108-2.195-2.606-4.309-3.61-1.734-0.814-4.463-1.049-4.522-0.565z"/>
+ <path fill="#6c6c6c" d="m198.721 46.897c-0.063 0.574 1.534 0.991 2.554 1.42 1.021 0.391 2.994 1.89 3.908 2.979 0.887 1.077 3.082 4.117 3.548 3.384 0.436-0.685-0.657-2.704-1.64-3.78-0.927-1.074-2.13-2.44-4.132-3.431-1.631-0.8-4.179-1.031-4.238-0.572z"/>
+ <path fill="#727272" d="m199.03 46.974c-0.063 0.544 1.451 0.948 2.416 1.36 0.967 0.376 2.831 1.811 3.694 2.846 0.833 1.026 2.928 3.906 3.369 3.219 0.411-0.644-0.576-2.549-1.569-3.592-0.936-1.04-2.064-2.275-3.956-3.251-1.528-0.79-3.896-1.017-3.954-0.582z"/>
+ <path fill="#797979" d="m199.339 47.051c-0.062 0.515 1.368 0.904 2.278 1.299 0.913 0.361 2.669 1.731 3.479 2.712 0.779 0.975 2.774 3.695 3.193 3.054 0.386-0.604-0.497-2.396-1.501-3.403-0.942-1.005-1.999-2.11-3.78-3.072-1.424-0.778-3.612-1.002-3.669-0.59z"/>
+ <path fill="#7f7f7f" d="m199.648 47.127c-0.063 0.485 1.284 0.861 2.14 1.239 0.859 0.346 2.506 1.652 3.265 2.578 0.726 0.924 2.621 3.484 3.017 2.889 0.361-0.564-0.417-2.241-1.432-3.215-0.951-0.971-1.935-1.944-3.604-2.893-1.323-0.765-3.33-0.986-3.386-0.598z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#995900" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.017 11.592-31.104 19.152-13.968 9.576-18.792 13.824-23.328 18.359-7.056 7.057-13.752 9.432-24.48 9.432s-15.552-2.231-18.863-5.184c-3.313-2.88-6.984-10.225-6.624-21.168 0.288-10.872 3.744-20.809 5.399-37.729 0.721-7.271 0.648-16.271 0.648-24.264 0-10.08 0.144-18.648 2.304-19.943 3.889-2.448 4.752-2.592 9.36-2.592 4.607 0 6.696 0.287 8.208 1.799 1.439 1.44 0.864 4.752 0.359 9.433-0.432 4.681 1.801 6.192 4.032 8.136 2.232 1.872 4.248 4.248 11.305 4.824 7.056 0.504 9.647-0.648 12.96-2.736 3.312-2.088 7.991-5.832 9.72-7.992 1.656-2.088 5.76-9.287 6.552-9.287 0.719 0 5.472-1.656 8.136 2.232z"/>
+ <path fill="#9e5e00" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.12 11.556-31.26 19.008-13.885 9.371-18.903 13.54-23.521 17.902-6.912 6.74-13.414 9.084-23.915 9.019-10.411-0.047-15.116-2.181-18.414-5.118-3.297-2.867-6.931-9.966-6.613-20.578 0.205-10.851 3.701-20.683 5.256-37.279 0.666-7.379 0.407-16.303 0.335-24.375-0.076-10.068-0.072-18.627 2.084-19.922 3.889-2.444 4.752-2.592 9.36-2.592 4.607 0 6.7 0.291 8.208 1.799 1.491 1.492 0.767 4.887 0.205 9.408-0.63 4.658 1.458 6.486 3.795 8.607 2.34 2.059 4.489 4.471 11.534 5.021 7.232 0.482 10.015-0.832 13.362-3.106 3.303-2.207 7.773-5.903 9.513-8.168 1.641-2.132 5.727-9.386 6.519-9.386 0.719 0 5.472-1.656 8.136 2.232z"/>
+ <path fill="#a36400" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.226 11.52-31.414 18.863-13.803 9.166-19.016 13.256-23.717 17.447-6.768 6.422-13.075 8.733-23.35 8.604-10.094-0.094-14.682-2.131-17.964-5.055-3.283-2.852-6.876-9.705-6.603-19.987 0.122-10.828 3.657-20.556 5.112-36.828 0.612-7.487 0.165-16.336 0.021-24.487-0.15-10.058-0.287-18.605 1.865-19.9 3.889-2.44 4.752-2.592 9.36-2.592 4.607 0 6.703 0.295 8.208 1.799 1.541 1.541 0.67 5.02 0.051 9.383-0.828 4.637 1.116 6.781 3.556 9.078 2.448 2.248 4.731 4.695 11.766 5.221 7.409 0.461 10.383-1.016 13.767-3.477 3.29-2.326 7.552-5.977 9.302-8.346 1.627-2.175 5.695-9.482 6.487-9.482 0.72-0.001 5.473-1.657 8.137 2.231z"/>
+ <path fill="#a86a00" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.329 11.484-31.568 18.72-13.721 8.961-19.127 12.972-23.911 16.989-6.624 6.105-12.737 8.384-22.785 8.189-9.777-0.141-14.245-2.08-17.514-4.99-3.27-2.836-6.822-9.445-6.591-19.396 0.038-10.807 3.613-20.43 4.968-36.378 0.558-7.597-0.076-16.368-0.292-24.599-0.228-10.047-0.504-18.584 1.645-19.879 3.889-2.438 4.752-2.592 9.36-2.592 4.607 0 6.707 0.299 8.208 1.799 1.591 1.593 0.573 5.152-0.104 9.357-1.025 4.615 0.774 7.077 3.319 9.551 2.556 2.434 4.972 4.918 11.995 5.418 7.585 0.439 10.75-1.199 14.17-3.849 3.279-2.444 7.333-6.048 9.093-8.521 1.613-2.219 5.663-9.58 6.455-9.58 0.719 0.001 5.472-1.655 8.136 2.233z"/>
+ <path fill="#ad7000" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.434 11.447-31.724 18.576-13.637 8.756-19.238 12.687-24.106 16.531-6.479 5.789-12.397 8.035-22.219 7.776-9.461-0.188-13.809-2.03-17.063-4.925-3.254-2.823-6.769-9.188-6.581-18.807-0.043-10.785 3.571-20.305 4.824-35.928 0.504-7.705-0.316-16.402-0.604-24.711-0.303-10.037-0.72-18.563 1.425-19.857 3.889-2.434 4.752-2.592 9.36-2.592 4.607 0 6.711 0.303 8.208 1.799 1.642 1.643 0.475 5.285-0.259 9.332-1.225 4.594 0.432 7.373 3.082 10.022 2.664 2.621 5.212 5.142 12.225 5.616 7.762 0.418 11.117-1.383 14.573-4.219 3.269-2.563 7.113-6.121 8.885-8.698 1.598-2.261 5.63-9.677 6.422-9.677 0.719 0.002 5.472-1.654 8.136 2.234z"/>
+ <path fill="#b27500" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.538 11.412-31.879 18.432-13.554 8.551-19.35 12.402-24.3 16.074-6.336 5.473-12.06 7.686-21.654 7.361-9.144-0.233-13.374-1.979-16.613-4.859-3.24-2.809-6.714-8.928-6.57-18.216-0.126-10.765 3.528-20.179 4.68-35.478 0.45-7.813-0.558-16.435-0.918-24.822-0.378-10.026-0.936-18.541 1.206-19.836 3.889-2.43 4.752-2.592 9.36-2.592 4.607 0 6.714 0.305 8.208 1.799 1.691 1.693 0.378 5.418-0.414 9.307-1.422 4.572 0.09 7.668 2.844 10.494 2.772 2.808 5.454 5.363 12.456 5.814 7.938 0.396 11.484-1.566 14.977-4.591 3.258-2.682 6.894-6.192 8.676-8.874 1.584-2.304 5.598-9.773 6.39-9.773 0.718 0 5.471-1.656 8.135 2.232z"/>
+ <path fill="#b77b00" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.643 11.376-32.033 18.288-13.472 8.345-19.461 12.118-24.494 15.616-6.192 5.156-11.723 7.338-21.089 6.949-8.827-0.281-12.938-1.93-16.164-4.795-3.226-2.795-6.66-8.67-6.56-17.627-0.209-10.742 3.485-20.052 4.536-35.027 0.396-7.92-0.799-16.467-1.23-24.934-0.454-10.015-1.152-18.52 0.985-19.814 3.889-2.426 4.752-2.592 9.36-2.592 4.607 0 6.718 0.31 8.208 1.799 1.743 1.744 0.281 5.553-0.569 9.281-1.62 4.551-0.252 7.963 2.607 10.967 2.88 2.994 5.694 5.586 12.686 6.012 8.115 0.373 11.852-1.75 15.379-4.961 3.248-2.801 6.676-6.264 8.469-9.051 1.568-2.348 5.564-9.871 6.356-9.871 0.72 0 5.473-1.656 8.137 2.232z"/>
+ <path fill="#bc8100" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.747 11.34-32.188 18.145-13.389 8.139-19.574 11.834-24.689 15.159-6.048 4.839-11.383 6.988-20.523 6.534-8.51-0.328-12.503-1.879-15.714-4.73-3.211-2.779-6.606-8.41-6.549-17.035-0.292-10.721 3.441-19.926 4.393-34.578 0.342-8.028-1.041-16.498-1.545-25.045-0.529-10.004-1.368-18.498 0.767-19.793 3.889-2.422 4.752-2.592 9.36-2.592 4.607 0 6.721 0.313 8.208 1.799 1.793 1.793 0.184 5.686-0.723 9.256-1.818 4.529-0.595 8.259 2.367 11.438 2.988 3.184 5.938 5.811 12.917 6.211 8.291 0.352 12.22-1.934 15.783-5.332 3.236-2.92 6.454-6.336 8.258-9.227 1.556-2.391 5.533-9.969 6.325-9.969 0.72-0.001 5.473-1.657 8.137 2.231z"/>
+ <path fill="#c18700" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.852 11.304-32.343 18-13.306 7.936-19.685 11.549-24.883 14.703-5.904 4.521-11.045 6.638-19.959 6.119-8.193-0.375-12.067-1.828-15.264-4.666-3.197-2.764-6.553-8.149-6.537-16.444-0.375-10.699 3.397-19.8 4.248-34.128 0.288-8.137-1.282-16.531-1.858-25.156-0.604-9.994-1.584-18.477 0.547-19.771 3.889-2.42 4.752-2.592 9.36-2.592 4.607 0 6.725 0.316 8.208 1.799 1.843 1.845 0.087 5.818-0.878 9.231-2.017 4.507-0.937 8.554 2.131 11.909 3.096 3.369 6.178 6.033 13.146 6.408 8.468 0.33 12.587-2.117 16.187-5.703 3.225-3.038 6.235-6.408 8.049-9.402 1.541-2.436 5.501-10.066 6.293-10.066 0.72-0.001 5.473-1.657 8.137 2.231z"/>
+ <path fill="#c68c00" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-20.955 11.268-32.498 17.855-13.223 7.73-19.796 11.266-25.077 14.246-5.761 4.204-10.706 6.289-19.394 5.707-7.877-0.423-11.631-1.779-14.813-4.602-3.183-2.751-6.498-7.891-6.527-15.855-0.457-10.677 3.355-19.674 4.104-33.678 0.234-8.244-1.521-16.563-2.17-25.268-0.681-9.983-1.8-18.455 0.327-19.75 3.889-2.416 4.752-2.592 9.36-2.592 4.607 0 6.729 0.32 8.208 1.799 1.894 1.895-0.011 5.951-1.033 9.207-2.214 4.484-1.278 8.848 1.895 12.379 3.203 3.558 6.418 6.258 13.377 6.607 8.644 0.309 12.952-2.301 16.589-6.074 3.215-3.156 6.016-6.479 7.841-9.58 1.526-2.477 5.468-10.162 6.26-10.162 0.718 0.001 5.471-1.655 8.135 2.233z"/>
+ <path fill="#cc9200" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.061 11.232-32.652 17.712-13.141 7.524-19.908 10.979-25.272 13.788-5.616 3.888-10.368 5.939-18.828 5.292-7.56-0.468-11.195-1.728-14.363-4.536-3.168-2.736-6.444-7.632-6.517-15.264-0.54-10.656 3.313-19.549 3.96-33.229 0.181-8.352-1.764-16.596-2.483-25.38-0.757-9.972-2.017-18.433 0.107-19.728 3.889-2.412 4.752-2.592 9.36-2.592 4.607 0 6.731 0.323 8.208 1.799 1.944 1.945-0.108 6.084-1.188 9.181-2.412 4.464-1.62 9.144 1.656 12.853 3.313 3.744 6.66 6.479 13.608 6.803 8.819 0.289 13.319-2.483 16.991-6.443 3.204-3.275 5.797-6.553 7.633-9.756 1.512-2.52 5.436-10.26 6.228-10.26 0.719 0 5.472-1.656 8.136 2.232z"/>
+ <path fill="#d19800" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.164 11.195-32.808 17.568-13.057 7.318-20.019 10.695-25.466 13.33-5.473 3.571-10.03 5.592-18.263 4.879-7.243-0.516-10.761-1.678-13.914-4.472-3.153-2.722-6.391-7.372-6.506-14.674-0.623-10.634 3.27-19.422 3.816-32.778 0.126-8.459-2.005-16.627-2.797-25.49-0.832-9.961-2.232-18.412-0.112-19.707 3.889-2.408 4.752-2.592 9.36-2.592 4.607 0 6.736 0.328 8.208 1.799 1.995 1.996-0.205 6.219-1.343 9.156-2.61 4.442-1.962 9.438 1.419 13.323 3.42 3.931 6.9 6.703 13.838 7.002 8.997 0.267 13.687-2.668 17.395-6.815 3.194-3.395 5.577-6.623 7.424-9.932 1.497-2.564 5.403-10.357 6.195-10.357 0.721 0 5.474-1.656 8.138 2.232z"/>
+ <path fill="#d69e00" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.27 11.16-32.962 17.424-12.975 7.114-20.132 10.412-25.661 12.874-5.327 3.254-9.69 5.242-17.697 4.464-6.927-0.563-10.325-1.627-13.464-4.406-3.14-2.707-6.337-7.113-6.494-14.084-0.706-10.611 3.225-19.295 3.672-32.328 0.072-8.567-2.247-16.66-3.111-25.603-0.907-9.95-2.448-18.39-0.331-19.685 3.889-2.404 4.752-2.592 9.36-2.592 4.607 0 6.739 0.332 8.208 1.799 2.045 2.045-0.302 6.352-1.497 9.131-2.808 4.421-2.304 9.734 1.18 13.795 3.528 4.119 7.144 6.927 14.069 7.199 9.173 0.246 14.055-2.851 17.799-7.185 3.182-3.514 5.356-6.696 7.214-10.108 1.483-2.607 5.371-10.455 6.163-10.455 0.719 0 5.472-1.656 8.136 2.232z"/>
+ <path fill="#dba300" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.373 11.124-33.116 17.279-12.893 6.91-20.243 10.127-25.855 12.418-5.184 2.937-9.353 4.892-17.133 4.05-6.609-0.609-9.889-1.577-13.014-4.343-3.125-2.692-6.282-6.854-6.483-13.492-0.789-10.592 3.182-19.17 3.528-31.879 0.018-8.676-2.488-16.692-3.425-25.713-0.982-9.94-2.664-18.369-0.551-19.664 3.889-2.401 4.752-2.592 9.36-2.592 4.607 0 6.743 0.334 8.208 1.799 2.095 2.097-0.399 6.484-1.652 9.105-3.006 4.398-2.646 10.029 0.943 14.268 3.636 4.305 7.384 7.148 14.299 7.397 9.349 0.224 14.422-3.034 18.202-7.558 3.171-3.631 5.137-6.768 7.005-10.284 1.469-2.649 5.339-10.552 6.131-10.552 0.72 0.001 5.473-1.655 8.137 2.233z"/>
+ <path fill="#e0a900" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.478 11.088-33.271 17.136-12.81 6.704-20.354 9.843-26.05 11.96-5.04 2.62-9.015 4.543-16.567 3.637-6.293-0.656-9.453-1.527-12.563-4.277-3.11-2.68-6.229-6.596-6.474-12.903-0.871-10.569 3.141-19.044 3.384-31.428-0.035-8.784-2.728-16.726-3.735-25.826-1.06-9.929-2.88-18.347-0.771-19.642 3.889-2.397 4.752-2.592 9.36-2.592 4.607 0 6.747 0.338 8.208 1.799 2.146 2.146-0.497 6.617-1.808 9.08-3.203 4.377-2.987 10.324 0.706 14.738 3.744 4.493 7.624 7.373 14.529 7.596 9.526 0.203 14.789-3.217 18.605-7.926 3.161-3.752 4.918-6.841 6.797-10.463 1.454-2.693 5.306-10.648 6.098-10.648 0.719-0.001 5.472-1.657 8.136 2.231z"/>
+ <path fill="#e5af00" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.582 11.052-33.427 16.992-12.726 6.498-20.466 9.558-26.244 11.502-4.896 2.304-8.676 4.193-16.002 3.222-5.976-0.702-9.018-1.476-12.114-4.212-3.096-2.664-6.174-6.336-6.462-12.313-0.953-10.547 3.097-18.918 3.24-30.978-0.09-8.892-2.97-16.758-4.05-25.938-1.134-9.918-3.096-18.324-0.99-19.619 3.889-2.395 4.752-2.592 9.36-2.592 4.607 0 6.75 0.342 8.208 1.799 2.196 2.197-0.594 6.75-1.962 9.055-3.402 4.355-3.33 10.619 0.468 15.21 3.852 4.681 7.866 7.597 14.76 7.794 9.702 0.18 15.156-3.402 19.008-8.298 3.15-3.87 4.698-6.912 6.589-10.638 1.439-2.736 5.273-10.746 6.065-10.746 0.72 0 5.473-1.656 8.137 2.232z"/>
+ <path fill="#eab500" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.687 11.016-33.582 16.848-12.643 6.293-20.576 9.274-26.438 11.045-4.752 1.987-8.338 3.846-15.438 2.809-5.658-0.75-8.582-1.426-11.664-4.147-3.08-2.649-6.119-6.077-6.45-11.722-1.037-10.526 3.053-18.792 3.096-30.528-0.144-9-3.211-16.79-4.363-26.049-1.21-9.907-3.312-18.304-1.21-19.599 3.889-2.391 4.752-2.592 9.36-2.592 4.607 0 6.754 0.346 8.208 1.799 2.247 2.248-0.691 6.885-2.117 9.029-3.6 4.336-3.672 10.916 0.231 15.682 3.96 4.867 8.106 7.82 14.989 7.992 9.879 0.158 15.523-3.586 19.411-8.668 3.141-3.99 4.479-6.984 6.38-10.814 1.426-2.78 5.241-10.844 6.033-10.844 0.721-0.001 5.474-1.657 8.138 2.231z"/>
+ <path fill="#efba00" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.791 10.98-33.735 16.703-12.562 6.089-20.69 8.99-26.633 10.589-4.608 1.67-7.999 3.496-14.872 2.394-5.343-0.796-8.147-1.375-11.215-4.082-3.066-2.636-6.065-5.818-6.439-11.132-1.12-10.504 3.009-18.666 2.952-30.077-0.198-9.109-3.453-16.822-4.677-26.162-1.285-9.896-3.528-18.281-1.43-19.576 3.889-2.387 4.752-2.592 9.36-2.592 4.607 0 6.757 0.35 8.208 1.799 2.297 2.297-0.788 7.018-2.271 9.004-3.798 4.314-4.014 11.211-0.008 16.154 4.068 5.055 8.35 8.043 15.221 8.189 10.056 0.137 15.892-3.77 19.815-9.039 3.128-4.107 4.258-7.057 6.17-10.99 1.411-2.824 5.209-10.941 6.001-10.941 0.72-0.001 5.473-1.657 8.137 2.231z"/>
+ <path fill="#f4c000" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.896 10.943-33.891 16.561-12.478 5.883-20.801 8.704-26.827 10.131-4.464 1.353-7.661 3.146-14.307 1.979-5.025-0.843-7.711-1.325-10.765-4.019-3.053-2.621-6.012-5.558-6.429-10.541-1.203-10.482 2.966-18.539 2.809-29.627-0.253-9.217-3.694-16.855-4.99-26.272-1.361-9.886-3.744-18.261-1.649-19.556 3.889-2.383 4.752-2.592 9.36-2.592 4.607 0 6.761 0.353 8.208 1.799 2.347 2.349-0.885 7.15-2.426 8.979-3.996 4.291-4.356 11.505-0.245 16.625 4.176 5.241 8.59 8.265 15.451 8.388 10.23 0.115 16.258-3.953 20.218-9.41 3.117-4.227 4.039-7.129 5.961-11.168 1.396-2.865 5.177-11.037 5.969-11.037 0.72 0 5.473-1.656 8.137 2.232z"/>
+ <path fill="#f9c600" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-21.999 10.908-34.046 16.416-12.395 5.678-20.912 8.421-27.021 9.674-4.32 1.036-7.322 2.797-13.741 1.566-4.709-0.891-7.275-1.275-10.314-3.953-3.037-2.607-5.958-5.299-6.419-9.951-1.284-10.461 2.925-18.414 2.664-29.178-0.306-9.324-3.934-16.887-5.302-26.385-1.437-9.875-3.96-18.238-1.869-19.533 3.889-2.379 4.752-2.592 9.36-2.592 4.607 0 6.765 0.356 8.208 1.799 2.397 2.398-0.983 7.283-2.581 8.955-4.194 4.269-4.698 11.799-0.482 17.096 4.284 5.429 8.83 8.488 15.682 8.586 10.407 0.094 16.625-4.137 20.621-9.781 3.106-4.345 3.819-7.199 5.753-11.344 1.382-2.909 5.144-11.135 5.936-11.135 0.718 0 5.471-1.656 8.135 2.232z"/>
+ <path fill="#fc0" d="m303.631 262.529c4.464 6.479-0.145 14.904 3.096 20.089 5.328 8.496 16.056 17.063 20.16 19.439 2.952 1.8 7.128 3.527 6.983 8.783-0.216 5.977-3.168 7.561-4.823 9.217-3.313 3.313-22.104 10.872-34.2 16.271-12.313 5.473-21.024 8.137-27.217 9.217-4.176 0.72-6.983 2.447-13.176 1.151-4.392-0.937-6.84-1.224-9.864-3.888-3.023-2.592-5.903-5.04-6.407-9.359-1.368-10.441 2.88-18.289 2.52-28.729-0.36-9.432-4.176-16.92-5.616-26.496-1.512-9.864-4.176-18.217-2.088-19.512 3.889-2.377 4.752-2.592 9.36-2.592 4.607 0 6.768 0.359 8.208 1.799 2.448 2.449-1.08 7.416-2.736 8.929-4.392 4.248-5.04 12.096-0.72 17.567 4.392 5.617 9.072 8.713 15.912 8.785 10.584 0.071 16.992-4.32 21.023-10.152 3.097-4.465 3.601-7.272 5.544-11.521 1.368-2.952 5.112-11.231 5.904-11.231 0.72 0.001 5.473-1.655 8.137 2.233z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fc0" d="m236.263 275.762c-0.709-0.258-3.932-15.209-2.191-16.239 3.351-1.997 4.253-2.319 8.377-2.319s6.057 0.322 7.346 1.61c2.126 2.127-1.159 6.702-2.448 7.991-3.738 3.673-10.375 9.215-11.084 8.957z"/>
+ <path fill="#ffcc02" d="m236.492 275.286c-0.77-0.326-4.102-14.719-2.368-15.742 3.344-1.992 4.278-2.211 8.322-2.211 4.124 0 5.976 0.269 7.282 1.602 2.105 2.136-1.119 6.572-2.398 7.852-3.727 3.663-10.081 8.811-10.838 8.499z"/>
+ <path fill="#ffcc05" d="m236.721 274.809c-0.832-0.393-4.273-14.229-2.547-15.247 3.339-1.983 4.306-2.101 8.269-2.101 4.124 0 5.896 0.213 7.217 1.592 2.087 2.146-1.076 6.443-2.346 7.714-3.718 3.653-9.788 8.409-10.593 8.042z"/>
+ <path fill="#ffcc07" d="m236.949 274.333c-0.892-0.461-4.443-13.74-2.722-14.75 3.331-1.979 4.33-1.992 8.213-1.992 4.124 0 5.814 0.158 7.151 1.582 2.068 2.156-1.033 6.316-2.294 7.574-3.707 3.644-9.494 8.007-10.348 7.586z"/>
+ <path fill="#ffcd0a" d="m237.178 273.855c-0.954-0.528-4.614-13.249-2.9-14.254 3.326-1.972 4.355-1.882 8.158-1.882 4.124 0 5.734 0.104 7.089 1.572 2.048 2.166-0.993 6.187-2.243 7.437-3.699 3.634-9.202 7.605-10.104 7.127z"/>
+ <path fill="#ffcd0c" d="m237.407 273.377c-1.015-0.596-4.785-12.758-3.077-13.758 3.319-1.965 4.382-1.771 8.104-1.771 4.124 0 5.653 0.049 7.023 1.563 2.029 2.176-0.951 6.057-2.19 7.299-3.69 3.623-8.91 7.201-9.86 6.667z"/>
+ <path fill="#ffcd0f" d="m237.636 272.901c-1.077-0.662-4.956-12.27-3.256-13.261 3.313-1.959 4.408-1.663 8.05-1.663 4.124 0 5.573-0.006 6.959 1.553 2.01 2.186-0.908 5.93-2.14 7.159-3.679 3.614-8.615 6.8-9.613 6.212z"/>
+ <path fill="#ffcd11" d="m237.864 272.424c-1.137-0.731-5.126-11.779-3.431-12.766 3.306-1.951 4.433-1.553 7.994-1.553 4.123 0 5.492-0.061 6.895 1.543 1.991 2.193-0.867 5.801-2.089 7.021-3.669 3.606-8.321 6.397-9.369 5.755z"/>
+ <path fill="#ffce14" d="m238.093 271.948c-1.197-0.799-5.297-11.289-3.607-12.27 3.299-1.946 4.459-1.443 7.938-1.443 4.124 0 5.412-0.115 6.83 1.533 1.973 2.204-0.824 5.671-2.037 6.883-3.659 3.596-8.028 5.993-9.124 5.297z"/>
+ <path fill="#ffce16" d="m238.322 271.471c-1.26-0.867-5.468-10.801-3.786-11.773 3.293-1.939 4.485-1.334 7.884-1.334 4.124 0 5.332-0.171 6.767 1.524 1.953 2.213-0.783 5.542-1.985 6.743-3.651 3.586-7.736 5.591-8.88 4.84z"/>
+ <path fill="#ffce19" d="m238.551 270.995c-1.32-0.935-5.639-10.312-3.963-11.277 3.286-1.934 4.511-1.225 7.829-1.225 4.124 0 5.252-0.226 6.702 1.514 1.934 2.224-0.741 5.414-1.934 6.605-3.64 3.576-7.442 5.187-8.634 4.383z"/>
+ <path fill="#ffce1c" d="m238.779 270.517c-1.382-1.002-5.809-9.821-4.14-10.781 3.279-1.926 4.535-1.114 7.774-1.114 4.124 0 5.171-0.279 6.637 1.504 1.914 2.233-0.698 5.285-1.882 6.467-3.63 3.566-7.148 4.784-8.389 3.924z"/>
+ <path fill="#ffcf1e" d="m239.008 270.04c-1.442-1.068-5.979-9.33-4.316-10.283 3.272-1.922 4.562-1.006 7.72-1.006 4.124 0 5.09-0.334 6.572 1.494 1.895 2.244-0.657 5.156-1.83 6.328-3.622 3.556-6.857 4.383-8.146 3.467z"/>
+ <path fill="#ffcf21" d="m239.237 269.563c-1.505-1.137-6.151-8.841-4.495-9.788 3.267-1.914 4.588-0.896 7.666-0.896 4.124 0 5.009-0.389 6.508 1.486 1.875 2.252-0.616 5.025-1.778 6.188-3.613 3.548-6.564 3.981-7.901 3.01z"/>
+ <path fill="#ffcf23" d="m239.466 269.086c-1.565-1.205-6.321-8.352-4.672-9.293 3.262-1.906 4.613-0.785 7.61-0.785 4.124 0 4.93-0.444 6.444 1.476 1.855 2.261-0.573 4.897-1.728 6.052-3.601 3.537-6.269 3.575-7.654 2.55z"/>
+ <path fill="#ffcf26" d="m239.694 268.61c-1.627-1.273-6.492-7.861-4.849-8.796 3.255-1.901 4.64-0.677 7.556-0.677 4.124 0 4.849-0.499 6.379 1.466 1.837 2.271-0.531 4.769-1.675 5.912-3.593 3.528-5.977 3.174-7.411 2.095z"/>
+ <path fill="#ffd028" d="m239.923 268.133c-1.688-1.34-6.663-7.373-5.025-8.301 3.248-1.895 4.665-0.566 7.501-0.566 4.124 0 4.768-0.555 6.314 1.456 1.817 2.28-0.489 4.64-1.624 5.774-3.583 3.519-5.684 2.771-7.166 1.637z"/>
+ <path fill="#ffd02b" d="m240.152 267.657c-1.749-1.408-6.834-6.883-5.203-7.805 3.241-1.889 4.69-0.457 7.446-0.457 4.124 0 4.687-0.609 6.25 1.447 1.798 2.289-0.448 4.51-1.573 5.635-3.572 3.509-5.39 2.367-6.92 1.18z"/>
+ <path fill="#ffd02d" d="m240.381 267.178c-1.811-1.475-7.005-6.391-5.381-7.307 3.235-1.881 4.717-0.348 7.393-0.348 4.124 0 4.606-0.664 6.185 1.438 1.779 2.299-0.405 4.381-1.521 5.496-3.564 3.501-5.098 1.965-6.676 0.721z"/>
+ <path fill="#ffd030" d="m240.609 266.702c-1.871-1.543-7.175-5.902-5.557-6.811 3.228-1.875 4.741-0.238 7.336-0.238 4.124 0 4.526-0.719 6.122 1.428 1.759 2.31-0.364 4.252-1.471 5.357-3.552 3.49-4.803 1.562-6.43 0.264z"/>
+ <path fill="#ffd133" d="m240.838 266.225c-1.933-1.611-7.346-5.413-5.734-6.314 3.222-1.869 4.768-0.129 7.281-0.129 4.124 0 4.446-0.773 6.058 1.418 1.74 2.318-0.322 4.123-1.418 5.219-3.545 3.48-4.512 1.16-6.187-0.194z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fc0" d="m302.769 263.374c3.742 5.461-0.062 12.76 2.638 17.117-6.809-6.258-9.938-8.834-19.324 0.367 2.577-3.742 3.129-6.257 4.725-9.814 1.104-2.455 4.354-9.57 5.029-9.57 0.613-0.001 4.724-1.35 6.932 1.9z"/>
+ <path fill="#ffcc02" d="m302.73 263.413c3.655 5.334 0.035 12.601 2.578 16.723-6.668-6.098-9.729-8.666-18.908 0.322 2.421-3.527 3.025-6.094 4.605-9.592 1.122-2.462 4.243-9.302 4.951-9.311 0.628-0.008 4.617-1.318 6.774 1.858z"/>
+ <path fill="#ffcc05" d="m302.691 263.45c3.568 5.209 0.132 12.441 2.517 16.332-6.526-5.938-9.519-8.5-18.492 0.277 2.268-3.314 2.924-5.934 4.488-9.372 1.141-2.468 4.132-9.032 4.873-9.052 0.641-0.013 4.508-1.284 6.614 1.815z"/>
+ <path fill="#ffcc07" d="m302.652 263.487c3.48 5.086 0.229 12.282 2.457 15.939-6.386-5.777-9.311-8.332-18.076 0.232 2.111-3.1 2.819-5.771 4.369-9.15 1.158-2.475 4.021-8.762 4.795-8.791 0.655-0.019 4.399-1.254 6.455 1.77z"/>
+ <path fill="#ffcd0a" d="m302.614 263.524c3.393 4.96 0.323 12.123 2.396 15.549-6.245-5.617-9.102-8.164-17.66 0.188 1.955-2.887 2.716-5.611 4.251-8.93 1.176-2.481 3.91-8.494 4.716-8.533 0.67-0.027 4.291-1.219 6.297 1.726z"/>
+ <path fill="#ffcd0c" d="m302.575 263.562c3.306 4.835 0.419 11.964 2.335 15.155-6.104-5.457-8.891-7.996-17.244 0.143 1.8-2.673 2.613-5.449 4.133-8.707 1.194-2.488 3.8-8.225 4.638-8.273 0.684-0.036 4.182-1.189 6.138 1.682z"/>
+ <path fill="#ffcd0f" d="m302.536 263.599c3.219 4.71 0.517 11.805 2.275 14.765-5.963-5.299-8.683-7.83-16.828 0.098 1.644-2.461 2.51-5.289 4.015-8.486 1.212-2.496 3.688-7.956 4.559-8.016 0.698-0.04 4.075-1.155 5.979 1.639z"/>
+ <path fill="#ffcd11" d="m302.497 263.637c3.131 4.585 0.612 11.645 2.216 14.371-5.822-5.137-8.474-7.661-16.413 0.053 1.489-2.245 2.406-5.125 3.896-8.264 1.229-2.504 3.576-7.688 4.479-7.756 0.714-0.046 3.968-1.123 5.822 1.596z"/>
+ <path fill="#ffce14" d="m302.458 263.674c3.044 4.459 0.708 11.486 2.154 13.979-5.681-4.978-8.263-7.493-15.996 0.009 1.334-2.033 2.303-4.965 3.779-8.043 1.247-2.511 3.464-7.418 4.4-7.498 0.728-0.052 3.859-1.089 5.663 1.553z"/>
+ <path fill="#ffce16" d="m302.42 263.711c2.956 4.336 0.804 11.328 2.094 13.588-5.54-4.817-8.055-7.326-15.58-0.036 1.178-1.819 2.199-4.804 3.659-7.822 1.267-2.517 3.354-7.149 4.323-7.237 0.741-0.061 3.75-1.058 5.504 1.507z"/>
+ <path fill="#ffce19" d="m302.381 263.749c2.868 4.211 0.9 11.168 2.033 13.196-5.398-4.657-7.845-7.159-15.164-0.081 1.022-1.605 2.097-4.642 3.542-7.601 1.283-2.524 3.241-6.88 4.244-6.979 0.755-0.067 3.642-1.026 5.345 1.465z"/>
+ <path fill="#ffce1c" d="m302.342 263.788c2.78 4.084 0.997 11.008 1.973 12.803-5.258-4.498-7.635-6.991-14.748-0.127 0.867-1.391 1.994-4.479 3.424-7.379 1.302-2.531 3.13-6.61 4.166-6.719 0.768-0.074 3.532-0.992 5.185 1.422z"/>
+ <path fill="#ffcf1e" d="m302.302 263.825c2.693 3.959 1.093 10.85 1.913 12.411-5.117-4.338-7.427-6.825-14.333-0.172 0.713-1.177 1.891-4.317 3.307-7.157 1.318-2.537 3.018-6.342 4.086-6.461 0.784-0.08 3.426-0.959 5.027 1.379z"/>
+ <path fill="#ffcf21" d="m302.263 263.862c2.606 3.834 1.188 10.689 1.853 12.02-4.976-4.178-7.217-6.657-13.916-0.217 0.556-0.963 1.786-4.156 3.188-6.936 1.337-2.545 2.906-6.072 4.008-6.202 0.797-0.086 3.318-0.927 4.867 1.335z"/>
+ <path fill="#ffcf23" d="m302.225 263.899c2.519 3.71 1.285 10.531 1.791 11.628-4.835-4.019-7.008-6.489-13.5-0.262 0.4-0.75 1.684-3.994 3.068-6.714 1.356-2.553 2.797-5.805 3.931-5.943 0.813-0.093 3.209-0.895 4.71 1.291z"/>
+ <path fill="#ffcf26" d="m302.186 263.937c2.431 3.584 1.381 10.371 1.73 11.235-4.693-3.857-6.798-6.322-13.084-0.307 0.245-0.535 1.58-3.832 2.951-6.492 1.373-2.56 2.686-5.535 3.852-5.685 0.828-0.1 3.101-0.861 4.551 1.249z"/>
+ <path fill="#ffd028" d="m302.147 263.974c2.344 3.46 1.477 10.213 1.671 10.845-4.553-3.699-6.589-6.156-12.668-0.354 0.089-0.321 1.477-3.67 2.832-6.271 1.392-2.565 2.574-5.267 3.772-5.425 0.842-0.104 2.994-0.828 4.393 1.205z"/>
+ <path fill="#ffd02b" d="m302.108 264.012c2.257 3.334 1.573 10.053 1.61 10.451-4.412-3.537-6.38-5.987-12.253-0.396-0.064-0.109 1.374-3.51 2.716-6.05 1.408-2.573 2.462-4.997 3.693-5.166 0.856-0.112 2.886-0.796 4.234 1.161z"/>
+ <path fill="#ffd02d" d="m302.069 264.049c2.17 3.209 1.67 9.894 1.55 10.061-4.271-3.379-6.17-5.82-11.836-0.441-0.221 0.104 1.271-3.35 2.596-5.83 1.428-2.58 2.352-4.728 3.615-4.906 0.87-0.12 2.777-0.765 4.075 1.116z"/>
+ <path fill="#ffd030" d="m302.03 264.086c2.082 3.084 1.767 9.736 1.49 9.668-4.131-3.219-5.961-5.652-11.42-0.486-0.377 0.318 1.167-3.188 2.478-5.607 1.445-2.586 2.239-4.459 3.536-4.647 0.884-0.127 2.669-0.732 3.916 1.072z"/>
+ <path fill="#ffd133" d="m301.991 264.124c1.995 2.959 1.862 9.576 1.43 9.277-3.989-3.059-5.752-5.486-11.005-0.531-0.532 0.531 1.064-3.027 2.36-5.387 1.463-2.594 2.128-4.189 3.458-4.389 0.899-0.133 2.561-0.699 3.757 1.03z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fc0" d="m305.862 283.481c5.977 7.848 17.064 16.271 21.024 18.576 2.88 1.656 7.056 3.6 6.983 8.783-0.144 5.904-3.168 7.561-4.823 9.217-3.313 3.313-22.177 10.943-34.2 16.271-12.24 5.4-21.097 8.209-27.217 9.217-4.104 0.647-7.056 2.375-13.176 1.151-4.32-0.864-6.912-1.296-9.864-3.888-2.951-2.52-5.976-5.184-6.407-9.359-1.225-10.369 3.672-16.921 8.424-25.921 3.888-7.2 11.735-8.64 16.632-7.991 17.568 2.375 16.416-8.641 21.24-13.465 4.464-4.463 17.208-8.064 21.384-2.591z"/>
+ <path fill="#ffcc02" d="m305.81 283.553c5.962 7.83 17.024 16.234 20.975 18.533 2.873 1.652 7.039 3.592 6.969 8.764-0.145 5.891-3.161 7.542-4.813 9.195-3.304 3.304-22.34 10.992-34.24 16.088-12.259 5.176-20.647 7.873-26.802 8.959-4.077 0.684-7.156 2.394-13.258 1.177-4.304-0.854-6.767-1.231-9.707-3.812-2.939-2.51-5.756-4.961-6.185-9.117-1.211-10.34 3.365-16.657 8.044-25.65 3.89-7.375 11.791-8.434 16.665-7.777 17.514 2.414 16.206-8.959 21.02-13.772 4.452-4.454 17.166-8.047 21.332-2.588z"/>
+ <path fill="#ffcc05" d="m305.76 283.627c5.946 7.812 16.982 16.195 20.925 18.488 2.866 1.648 7.022 3.584 6.951 8.743-0.144 5.876-3.153 7.524-4.801 9.173-3.298 3.297-22.506 11.043-34.28 15.907-12.279 4.949-20.201 7.538-26.389 8.699-4.051 0.721-7.256 2.413-13.341 1.202-4.286-0.846-6.619-1.168-9.55-3.733-2.924-2.501-5.536-4.741-5.959-8.877-1.198-10.312 3.058-16.394 7.664-25.379 3.89-7.552 11.845-8.229 16.698-7.563 17.458 2.453 15.995-9.279 20.797-14.08 4.443-4.443 17.127-8.026 21.285-2.58z"/>
+ <path fill="#ffcc07" d="m305.707 283.702c5.935 7.791 16.943 16.156 20.876 18.444 2.859 1.644 7.007 3.574 6.935 8.722-0.144 5.862-3.146 7.508-4.79 9.151-3.288 3.289-22.668 11.093-34.319 15.726-12.298 4.723-19.753 7.202-25.974 8.44-4.024 0.756-7.357 2.431-13.423 1.226-4.27-0.836-6.473-1.101-9.394-3.654-2.911-2.492-5.317-4.52-5.735-8.635-1.185-10.285 2.75-16.133 7.284-25.109 3.892-7.727 11.9-8.023 16.731-7.35 17.402 2.494 15.785-9.599 20.575-14.389 4.433-4.432 17.087-8.007 21.234-2.572z"/>
+ <path fill="#ffcd0a" d="m305.655 283.774c5.92 7.773 16.904 16.119 20.826 18.4 2.854 1.642 6.99 3.566 6.919 8.703-0.143 5.848-3.138 7.488-4.779 9.129-3.28 3.281-22.832 11.143-34.358 15.543-12.317 4.498-19.307 6.867-25.561 8.182-3.997 0.793-7.457 2.45-13.506 1.251-4.252-0.828-6.325-1.036-9.236-3.577-2.896-2.482-5.096-4.298-5.51-8.393-1.172-10.258 2.443-15.869 6.904-24.84 3.892-7.901 11.955-7.818 16.763-7.135 17.349 2.532 15.576-9.918 20.355-14.697 4.422-4.422 17.046-7.987 21.183-2.566z"/>
+ <path fill="#ffcd0c" d="m305.603 283.847c5.906 7.756 16.864 16.081 20.777 18.358 2.846 1.635 6.973 3.557 6.901 8.68-0.142 5.835-3.131 7.472-4.767 9.107-3.273 3.273-22.997 11.193-34.399 15.361-12.337 4.273-18.858 6.533-25.146 7.924-3.971 0.829-7.557 2.469-13.588 1.276-4.234-0.82-6.179-0.972-9.078-3.5-2.884-2.474-4.878-4.076-5.287-8.152-1.158-10.229 2.137-15.606 6.523-24.569 3.895-8.076 12.01-7.611 16.797-6.92 17.293 2.571 15.366-10.236 20.134-15.004 4.412-4.411 17.006-7.969 21.133-2.561z"/>
+ <path fill="#ffcd0f" d="m305.552 283.92c5.892 7.736 16.824 16.043 20.728 18.313 2.839 1.634 6.957 3.55 6.886 8.66-0.142 5.821-3.124 7.454-4.756 9.086-3.266 3.267-23.161 11.243-34.438 15.179-12.356 4.047-18.411 6.198-24.733 7.666-3.943 0.865-7.656 2.486-13.67 1.301-4.218-0.812-6.032-0.908-8.922-3.422-2.869-2.465-4.656-3.855-5.063-7.91-1.145-10.203 1.83-15.344 6.145-24.299 3.895-8.252 12.064-7.408 16.83-6.707 17.237 2.61 15.154-10.557 19.912-15.313 4.399-4.399 16.963-7.949 21.081-2.554z"/>
+ <path fill="#ffcd11" d="m305.501 283.993c5.877 7.719 16.782 16.004 20.678 18.271 2.833 1.628 6.939 3.54 6.869 8.639-0.143 5.807-3.116 7.436-4.745 9.064-3.257 3.258-23.324 11.293-34.479 14.996-12.375 3.822-17.963 5.863-24.319 7.408-3.917 0.9-7.757 2.504-13.752 1.324-4.2-0.802-5.886-0.842-8.765-3.344-2.856-2.454-4.438-3.633-4.838-7.669-1.132-10.173 1.521-15.081 5.764-24.029 3.896-8.426 12.119-7.2 16.863-6.491 17.183 2.649 14.945-10.875 19.689-15.621 4.391-4.389 16.926-7.93 21.035-2.548z"/>
+ <path fill="#ffce14" d="m305.448 284.066c5.863 7.701 16.743 15.966 20.629 18.228 2.826 1.625 6.924 3.531 6.853 8.619-0.142 5.793-3.108 7.418-4.733 9.043-3.25 3.25-23.489 11.342-34.518 14.813-12.396 3.598-17.517 5.529-23.905 7.148-3.891 0.938-7.857 2.524-13.834 1.351-4.185-0.793-5.74-0.776-8.609-3.267-2.841-2.444-4.217-3.412-4.613-7.426-1.118-10.146 1.216-14.818 5.385-23.76 3.896-8.602 12.174-6.996 16.896-6.277 17.128 2.688 14.735-11.195 19.468-15.929 4.378-4.38 16.883-7.911 20.981-2.543z"/>
+ <path fill="#ffce16" d="m305.396 284.139c5.85 7.682 16.703 15.928 20.579 18.184 2.82 1.62 6.907 3.523 6.837 8.598-0.141 5.779-3.101 7.4-4.722 9.021-3.242 3.242-23.653 11.393-34.559 14.631-12.414 3.372-17.068 5.194-23.491 6.891-3.862 0.975-7.957 2.543-13.917 1.375-4.167-0.783-5.592-0.713-8.451-3.188-2.827-2.437-3.997-3.19-4.389-7.187-1.105-10.117 0.908-14.555 5.004-23.487 3.898-8.776 12.229-6.79 16.928-6.063 17.074 2.728 14.525-11.515 19.248-16.236 4.37-4.371 16.845-7.894 20.933-2.539z"/>
+ <path fill="#ffce19" d="m305.344 284.211c5.836 7.664 16.663 15.891 20.529 18.141 2.813 1.617 6.892 3.516 6.82 8.578-0.14 5.765-3.093 7.382-4.71 8.999-3.235 3.233-23.817 11.442-34.598 14.448-12.434 3.146-16.621 4.859-23.077 6.633-3.837 1.01-8.058 2.561-13.999 1.4-4.15-0.775-5.446-0.648-8.295-3.111-2.814-2.426-3.777-2.969-4.164-6.944-1.094-10.09 0.601-14.293 4.624-23.22 3.898-8.951 12.282-6.584 16.961-5.848 17.019 2.767 14.314-11.834 19.025-16.545 4.361-4.359 16.806-7.872 20.884-2.531z"/>
+ <path fill="#ffce1c" d="m305.292 284.286c5.822 7.646 16.623 15.852 20.481 18.096 2.806 1.613 6.874 3.507 6.804 8.558-0.141 5.751-3.086 7.364-4.699 8.978-3.227 3.227-23.981 11.492-34.638 14.267-12.453 2.921-16.173 4.524-22.663 6.374-3.81 1.046-8.158 2.578-14.082 1.424-4.133-0.766-5.299-0.583-8.137-3.033-2.801-2.416-3.558-2.748-3.94-6.703-1.08-10.062 0.293-14.029 4.244-22.947 3.9-9.127 12.338-6.379 16.994-5.635 16.964 2.805 14.105-12.152 18.805-16.853 4.348-4.351 16.763-7.856 20.831-2.526z"/>
+ <path fill="#ffcf1e" d="m305.241 284.358c5.808 7.627 16.582 15.814 20.432 18.053 2.799 1.609 6.856 3.498 6.787 8.536-0.141 5.738-3.079 7.347-4.688 8.957-3.219 3.218-24.145 11.541-34.677 14.084-12.473 2.695-15.727 4.188-22.25 6.115-3.783 1.083-8.258 2.599-14.163 1.448-4.116-0.756-5.153-0.518-7.981-2.954-2.786-2.408-3.337-2.526-3.716-6.462-1.066-10.034-0.013-13.766 3.864-22.678 3.901-9.303 12.393-6.172 17.027-5.42 16.908 2.844 13.896-12.473 18.583-17.16 4.338-4.337 16.723-7.835 20.782-2.519z"/>
+ <path fill="#ffcf21" d="m305.189 284.431c5.793 7.608 16.542 15.776 20.382 18.009 2.792 1.605 6.84 3.49 6.771 8.516-0.14 5.725-3.071 7.33-4.677 8.936-3.211 3.211-24.309 11.591-34.717 13.902-12.491 2.47-15.278 3.854-21.836 5.856-3.756 1.119-8.357 2.616-14.246 1.474-4.099-0.748-5.006-0.453-7.823-2.877-2.772-2.398-3.118-2.306-3.492-6.22-1.053-10.007-0.319-13.505 3.484-22.407 3.903-9.479 12.448-5.969 17.062-5.207 16.853 2.883 13.684-12.791 18.36-17.469 4.328-4.328 16.683-7.819 20.732-2.513z"/>
+ <path fill="#ffcf23" d="m305.137 284.504c5.778 7.59 16.503 15.736 20.332 17.965 2.786 1.602 6.825 3.482 6.755 8.496-0.139 5.709-3.064 7.311-4.665 8.912-3.204 3.203-24.474 11.642-34.759 13.721-12.51 2.244-14.829 3.52-21.421 5.598-3.729 1.155-8.457 2.635-14.327 1.499-4.082-0.739-4.86-0.389-7.667-2.8-2.76-2.389-2.897-2.083-3.268-5.979-1.04-9.979-0.627-13.24 3.104-22.138 3.903-9.653 12.503-5.762 17.093-4.991 16.799 2.922 13.475-13.111 18.141-17.777 4.318-4.316 16.643-7.799 20.682-2.506z"/>
+ <path fill="#ffcf26" d="m305.086 284.579c5.765 7.57 16.463 15.697 20.282 17.92 2.779 1.599 6.809 3.474 6.738 8.476-0.139 5.696-3.056 7.293-4.654 8.892-3.194 3.194-24.637 11.69-34.797 13.536-12.529 2.021-14.382 3.185-21.007 5.341-3.703 1.191-8.559 2.652-14.411 1.523-4.065-0.73-4.713-0.324-7.509-2.723-2.745-2.379-2.679-1.861-3.043-5.735-1.027-9.952-0.936-12.979 2.724-21.868 3.905-9.828 12.557-5.557 17.126-4.777 16.744 2.961 13.265-13.431 17.919-18.084 4.307-4.309 16.602-7.783 20.632-2.501z"/>
+ <path fill="#ffd028" d="m305.033 284.651c5.752 7.553 16.423 15.66 20.234 17.878 2.771 1.593 6.791 3.464 6.722 8.454-0.139 5.682-3.049 7.275-4.643 8.869-3.188 3.188-24.801 11.74-34.838 13.355-12.548 1.793-13.935 2.85-20.593 5.082-3.676 1.228-8.658 2.67-14.493 1.547-4.048-0.721-4.565-0.258-7.353-2.644-2.731-2.37-2.457-1.64-2.818-5.495-1.014-9.923-1.242-12.716 2.345-21.597 3.905-10.004 12.611-5.351 17.158-4.563 16.689 3 13.055-13.75 17.698-18.393 4.297-4.295 16.562-7.761 20.581-2.493z"/>
+ <path fill="#ffd02b" d="m304.982 284.724c5.737 7.534 16.382 15.622 20.184 17.834 2.766 1.59 6.774 3.456 6.705 8.433-0.138 5.67-3.041 7.26-4.631 8.85-3.18 3.179-24.966 11.789-34.877 13.172-12.568 1.568-13.487 2.515-20.179 4.824-3.65 1.263-8.759 2.688-14.575 1.572-4.031-0.713-4.42-0.195-7.196-2.566-2.718-2.361-2.238-1.42-2.594-5.254-1.001-9.896-1.549-12.453 1.964-21.328 3.907-10.178 12.666-5.145 17.192-4.348 16.634 3.039 12.844-14.068 17.476-18.701 4.285-4.286 16.521-7.743 20.531-2.488z"/>
+ <path fill="#ffd02d" d="m304.93 284.797c5.723 7.516 16.342 15.584 20.135 17.789 2.758 1.588 6.758 3.449 6.688 8.414-0.138 5.654-3.034 7.24-4.619 8.826-3.173 3.172-25.13 11.84-34.918 12.99-12.587 1.344-13.039 2.18-19.766 4.564-3.622 1.301-8.856 2.709-14.657 1.599-4.014-0.704-4.272-0.13-7.039-2.489-2.702-2.352-2.018-1.197-2.369-5.012-0.987-9.868-1.855-12.189 1.584-21.057 3.908-10.354 12.722-4.94 17.226-4.135 16.578 3.078 12.634-14.389 17.254-19.009 4.275-4.273 16.481-7.721 20.481-2.48z"/>
+ <path fill="#ffd030" d="m304.879 284.87c5.709 7.498 16.302 15.547 20.085 17.748 2.752 1.582 6.741 3.438 6.673 8.391-0.139 5.642-3.027 7.224-4.609 8.806-3.164 3.164-25.293 11.89-34.956 12.808-12.606 1.119-12.592 1.844-19.352 4.308-3.596 1.336-8.958 2.726-14.739 1.622-3.997-0.695-4.127-0.065-6.882-2.411-2.69-2.343-1.799-0.976-2.146-4.771-0.974-9.84-2.163-11.928 1.204-20.787 3.91-10.529 12.777-4.734 17.258-3.92 16.524 3.117 12.424-14.707 17.034-19.316 4.263-4.267 16.439-7.706 20.43-2.478z"/>
+ <path fill="#ffd133" d="m304.826 284.943c5.695 7.479 16.263 15.509 20.036 17.703 2.745 1.579 6.726 3.431 6.656 8.372-0.137 5.627-3.02 7.205-4.597 8.783-3.157 3.156-25.458 11.939-34.997 12.625-12.626 0.893-12.145 1.51-18.938 4.049-3.569 1.373-9.058 2.745-14.822 1.646-3.979-0.686-3.979 0-6.725-2.332-2.676-2.334-1.578-0.756-1.921-4.529-0.961-9.813-2.471-11.665 0.824-20.516 3.91-10.705 12.83-4.529 17.29-3.707 16.47 3.156 12.215-15.027 16.813-19.625 4.255-4.253 16.401-7.684 20.381-2.469z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#995900" d="m52.494 273.618c-6.479 4.68-22.896 4.248-27.072 9.719-4.104 5.473 0.145 13.393 0.072 28.08 0 6.265-1.08 11.017-1.8 14.832-1.008 4.824-1.656 8.209 0.36 11.664 3.672 6.121 9.575 7.633 43.344 14.688 18.072 3.744 35.136 13.464 46.584 14.399 11.448 0.865 13.896-2.951 20.88-9.144 6.912-6.192 9.144-4.248 8.928-17.856-0.216-13.535-8.928-17.567-18.792-33.191s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-8.208 13.248-14.688 17.929z"/>
+ <path fill="#9e5e00" d="m52.598 273.905c-6.397 4.702-22.475 3.788-27.062 9.512-4.154 5.414 0.228 13.276 0.098 27.955-0.025 6.23-1.152 10.881-1.937 14.877-1.037 4.871-1.678 8.201 0.349 11.619 3.787 6.162 9.695 7.123 43.456 14.168 18.061 3.737 34.541 13.307 46.343 14.112 11.186 0.792 13.564-2.829 20.463-8.96 6.896-6.195 9.024-4.277 8.858-17.406-0.075-13.521-8.305-17.349-18.169-32.973s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-8.153 13.479-14.583 18.216z"/>
+ <path fill="#a36400" d="m52.703 274.193c-6.314 4.724-22.054 3.327-27.051 9.304-4.204 5.356 0.31 13.16 0.123 27.828-0.051 6.198-1.225 10.748-2.074 14.924-1.065 4.918-1.699 8.194 0.339 11.571 3.902 6.206 9.813 6.617 43.567 13.651 18.05 3.73 33.948 13.146 46.101 13.824 10.923 0.72 13.234-2.707 20.045-8.777 6.885-6.199 8.907-4.305 8.792-16.956 0.064-13.507-7.683-17.129-17.547-32.753s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-8.1 13.709-14.479 18.504z"/>
+ <path fill="#a86a00" d="m52.807 274.481c-6.231 4.745-21.633 2.866-27.04 9.094-4.255 5.299 0.393 13.047 0.148 27.702-0.076 6.167-1.297 10.616-2.211 14.972-1.095 4.965-1.721 8.188 0.328 11.524 4.018 6.25 9.932 6.108 43.679 13.134 18.039 3.721 33.354 12.988 45.86 13.535 10.659 0.648 12.902-2.585 19.627-8.593 6.869-6.203 8.788-4.335 8.723-16.507 0.205-13.492-7.06-16.909-16.924-32.533s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-8.045 13.94-14.374 18.792z"/>
+ <path fill="#ad7000" d="m52.912 274.769c-6.149 4.767-21.211 2.405-27.029 8.885-4.306 5.242 0.476 12.931 0.173 27.576-0.101 6.136-1.368 10.483-2.347 15.019-1.123 5.012-1.742 8.181 0.316 11.478 4.133 6.293 10.052 5.603 43.791 12.614 18.028 3.716 32.76 12.83 45.619 13.248 10.396 0.576 12.57-2.463 19.21-8.409 6.854-6.206 8.668-4.363 8.653-16.056 0.347-13.479-6.437-16.69-16.301-32.314s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.991 14.169-14.269 19.079z"/>
+ <path fill="#b27500" d="m53.016 275.057c-6.066 4.787-20.79 1.943-27.019 8.676-4.355 5.184 0.559 12.816 0.198 27.45-0.126 6.103-1.44 10.351-2.484 15.065-1.151 5.059-1.764 8.172 0.307 11.431 4.248 6.336 10.17 5.094 43.901 12.096 18.019 3.708 32.166 12.672 45.378 12.96 10.135 0.504 12.24-2.34 18.792-8.227 6.841-6.209 8.551-4.391 8.586-15.605 0.486-13.464-5.813-16.47-15.678-32.094s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.937 14.399-14.165 19.368z"/>
+ <path fill="#b77b00" d="m53.121 275.344c-5.983 4.811-20.369 1.484-27.008 8.469-4.406 5.126 0.641 12.7 0.224 27.324-0.151 6.068-1.512 10.216-2.621 15.111-1.181 5.105-1.785 8.166 0.295 11.385 4.363 6.379 10.289 4.586 44.014 11.576 18.008 3.701 31.572 12.515 45.137 12.672 9.872 0.433 11.909-2.217 18.375-8.041 6.825-6.215 8.431-4.422 8.518-15.156 0.627-13.45-5.191-16.251-15.056-31.875s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.884 14.631-14.062 19.655z"/>
+ <path fill="#bc8100" d="m53.225 275.633c-5.9 4.832-19.948 1.023-26.997 8.259-4.457 5.069 0.724 12.585 0.249 27.198-0.177 6.037-1.584 10.082-2.758 15.158-1.21 5.152-1.808 8.158 0.284 11.338 4.479 6.422 10.407 4.078 44.125 11.059 17.997 3.693 30.979 12.355 44.896 12.384 9.608 0.36 11.578-2.095 17.957-7.858 6.812-6.217 8.313-4.449 8.45-14.707 0.766-13.435-4.569-16.03-14.434-31.654s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.829 14.859-13.956 19.943z"/>
+ <path fill="#c18700" d="m53.329 275.92c-5.817 4.854-19.526 0.563-26.985 8.051-4.507 5.011 0.807 12.47 0.273 27.072-0.201 6.004-1.655 9.949-2.894 15.205-1.239 5.199-1.829 8.151 0.273 11.291 4.594 6.465 10.526 3.57 44.236 10.541 17.985 3.686 30.384 12.196 44.655 12.096 9.345 0.287 11.245-1.973 17.539-7.676 6.797-6.221 8.193-4.479 8.381-14.256 0.906-13.42-3.946-15.812-13.811-31.436s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.774 15.094-13.851 20.232z"/>
+ <path fill="#c68c00" d="m53.433 276.209c-5.734 4.875-19.104 0.101-26.975 7.84-4.558 4.955 0.89 12.355 0.299 26.947-0.227 5.973-1.728 9.816-3.031 15.252-1.267 5.246-1.851 8.145 0.263 11.244 4.709 6.508 10.646 3.063 44.349 10.022 17.975 3.679 29.79 12.038 44.413 11.808 9.083 0.217 10.915-1.851 17.122-7.492 6.782-6.224 8.074-4.506 8.313-13.806 1.048-13.405-3.323-15.592-13.188-31.216s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.722 15.322-13.749 20.521z"/>
+ <path fill="#cc9200" d="m53.538 276.497c-5.651 4.896-18.684-0.359-26.964 7.633-4.607 4.896 0.972 12.24 0.324 26.82-0.252 5.939-1.8 9.684-3.168 15.299-1.296 5.293-1.872 8.137 0.252 11.196 4.824 6.552 10.764 2.556 44.46 9.505 17.964 3.672 29.196 11.879 44.172 11.52 8.82 0.144 10.584-1.729 16.704-7.309 6.768-6.228 7.956-4.535 8.244-13.355 1.188-13.393-2.7-15.372-12.564-30.996s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.668 15.551-13.644 20.807z"/>
+ <path fill="#d19800" d="m53.642 276.786c-5.569 4.918-18.263-0.82-26.953 7.424-4.658 4.838 1.055 12.123 0.35 26.693-0.277 5.907-1.872 9.551-3.305 15.346-1.325 5.34-1.894 8.129 0.241 11.15 4.938 6.596 10.883 2.048 44.571 8.984 17.953 3.666 28.602 11.723 43.931 11.232 8.558 0.072 10.253-1.605 16.287-7.123 6.753-6.232 7.837-4.566 8.175-12.906 1.329-13.379-2.077-15.153-11.941-30.777s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.614 15.783-13.54 21.097z"/>
+ <path fill="#d69e00" d="m53.747 277.073c-5.486 4.94-17.842-1.281-26.942 7.215-4.709 4.781 1.138 12.01 0.374 26.568-0.302 5.875-1.943 9.417-3.441 15.393-1.354 5.387-1.915 8.123 0.23 11.104 5.055 6.639 11.002 1.541 44.684 8.467 17.942 3.658 28.008 11.563 43.688 10.944 8.296 0 9.923-1.483 15.869-6.941 6.74-6.235 7.72-4.593 8.108-12.456 1.468-13.363-1.455-14.933-11.319-30.557s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.56 16.012-13.435 21.383z"/>
+ <path fill="#dba300" d="m53.851 277.361c-5.403 4.962-17.421-1.741-26.932 7.007-4.759 4.723 1.221 11.893 0.399 26.441-0.327 5.843-2.016 9.283-3.578 15.439-1.383 5.434-1.937 8.115 0.22 11.057 5.169 6.682 11.12 1.033 44.795 7.949 17.932 3.649 27.414 11.404 43.448 10.656 8.031-0.072 9.59-1.361 15.451-6.758 6.725-6.238 7.6-4.623 8.039-12.006 1.609-13.35-0.832-14.714-10.696-30.338s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.504 16.245-13.33 21.673z"/>
+ <path fill="#e0a900" d="m53.956 277.649c-5.321 4.982-16.999-2.203-26.921 6.797-4.81 4.666 1.303 11.779 0.425 26.316-0.353 5.811-2.088 9.15-3.715 15.486-1.411 5.48-1.959 8.108 0.208 11.01 5.285 6.725 11.239 0.525 44.907 7.431 17.92 3.644 26.819 11.246 43.207 10.368 7.769-0.145 9.259-1.239 15.034-6.574 6.71-6.242 7.479-4.65 7.97-11.557 1.75-13.334-0.209-14.493-10.073-30.117s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.452 16.474-13.226 21.96z"/>
+ <path fill="#e5af00" d="m54.06 277.937c-5.238 5.004-16.578-2.664-26.91 6.588-4.86 4.608 1.386 11.664 0.45 26.19-0.378 5.777-2.16 9.018-3.853 15.533-1.439 5.526-1.979 8.101 0.198 10.963 5.4 6.768 11.358 0.018 45.018 6.912 17.91 3.635 26.227 11.088 42.967 10.08 7.506-0.217 8.928-1.117 14.615-6.391 6.696-6.246 7.362-4.68 7.902-11.105 1.89-13.32 0.414-14.274-9.45-29.898s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.397 16.704-13.121 22.248z"/>
+ <path fill="#eab500" d="m54.165 278.225c-5.155 5.025-16.157-3.124-26.899 6.38-4.91 4.55 1.469 11.548 0.476 26.063-0.403 5.746-2.232 8.885-3.989 15.58-1.469 5.573-2.002 8.094 0.188 10.916 5.515 6.812 11.477-0.49 45.129 6.394 17.899 3.629 25.633 10.93 42.725 9.792 7.243-0.288 8.598-0.993 14.199-6.206 6.681-6.25 7.243-4.709 7.833-10.656 2.031-13.306 1.037-14.055-8.827-29.679s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.346 16.935-13.019 22.536z"/>
+ <path fill="#efba00" d="m54.269 278.513c-5.073 5.048-15.736-3.585-26.889 6.171-4.961 4.492 1.552 11.434 0.5 25.938-0.428 5.713-2.304 8.752-4.125 15.627-1.498 5.621-2.023 8.086 0.176 10.869 5.631 6.854 11.596-0.996 45.241 5.875 17.889 3.623 25.038 10.771 42.483 9.504 6.981-0.359 8.266-0.871 13.781-6.022 6.668-6.253 7.125-4.737 7.766-10.206 2.17-13.291 1.659-13.835-8.205-29.459s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.289 17.164-12.912 22.823z"/>
+ <path fill="#f4c000" d="m54.374 278.801c-4.99 5.068-15.314-4.047-26.878 5.962-5.012 4.435 1.634 11.317 0.525 25.812-0.453 5.682-2.376 8.618-4.263 15.674-1.526 5.668-2.045 8.08 0.166 10.822 5.745 6.898 11.714-1.505 45.353 5.357 17.878 3.613 24.444 10.613 42.243 9.216 6.717-0.433 7.934-0.749 13.362-5.839 6.653-6.258 7.007-4.768 7.697-9.756 2.312-13.277 2.282-13.616-7.582-29.24s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.235 17.395-12.807 23.112z"/>
+ <path fill="#f9c600" d="m54.477 279.088c-4.906 5.092-14.893-4.506-26.866 5.754-5.062 4.377 1.717 11.203 0.551 25.686-0.479 5.648-2.448 8.485-4.399 15.721-1.555 5.715-2.066 8.072 0.155 10.775 5.86 6.941 11.833-2.012 45.464 4.839 17.867 3.607 23.851 10.454 42.001 8.929 6.455-0.504 7.604-0.627 12.946-5.656 6.638-6.26 6.886-4.795 7.628-9.307 2.452-13.262 2.905-13.396-6.959-29.02s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.182 17.626-12.705 23.399z"/>
+ <path fill="#fc0" d="m54.582 279.377c-4.823 5.111-14.472-4.969-26.855 5.543-5.112 4.32 1.8 11.088 0.576 25.561-0.504 5.616-2.521 8.352-4.536 15.768-1.584 5.76-2.088 8.064 0.144 10.729 5.977 6.984 11.952-2.52 45.576 4.32 17.856 3.6 23.256 10.295 41.76 8.64 6.192-0.576 7.272-0.504 12.528-5.472 6.624-6.264 6.768-4.824 7.56-8.856 2.592-13.248 3.528-13.176-6.336-28.8s-11.448-18.504-18-28.872c-6.552-10.224-19.512-28.8-26.928-29.017-5.904-0.144-9.216 3.024-12.888 6.769s-7.129 17.855-12.601 23.687z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fc0" d="m57.701 285.002c-4.278 4.539-18.28-1.36-24.892 3.631-4.732 3.5 2.398 7.908 1.426 20.873-0.389 4.926-3.824 5.834-2.398 12.64 1.103 5.121 2.27 4.991 4.214 7.325 5.315 6.223 4.084 1.686 34.355 7.777 16.011 3.242 20.938 9.271 37.596 7.779 5.575-0.518 6.612-0.453 11.279-4.926 5.964-5.575 2.917-4.408 3.565-7.973 2.269-11.863 0.453-13.938-8.428-28.004-8.88-14.066-8.102-14.844-13.936-24.113-5.834-9.141-13.872-25.799-20.549-25.93-5.25-0.129-8.297 2.723-11.603 6.094-3.305 3.372-5.768 19.643-10.629 24.827z"/>
+ <path fill="#ffcc02" d="m57.995 285.094c-4.461 4.701-18.604-1.196-25.06 3.705-4.701 3.514 2.578 8.05 1.608 20.68-0.4 4.896-3.733 5.877-2.458 12.634 0.986 5.093 2.357 4.938 4.334 7.201 5.686 6.131 4.673 1.826 34.119 7.743 15.918 3.211 20.815 9.215 37.372 7.732 5.541-0.513 6.479-0.463 11.155-4.849 5.865-5.407 2.858-4.287 3.412-8.058 2.066-11.787 0.442-13.981-8.188-27.649-8.83-13.983-8.143-14.698-13.941-23.913-5.8-9.085-13.701-25.805-20.338-25.934-5.219-0.129-8.248 2.705-11.533 6.057-3.287 3.352-5.644 19.505-10.482 24.651z"/>
+ <path fill="#ffcc05" d="m58.289 285.185c-4.644 4.866-18.93-1.031-25.229 3.78-4.669 3.527 2.759 8.191 1.792 20.488-0.413 4.866-3.642 5.918-2.519 12.625 0.872 5.066 2.447 4.887 4.455 7.078 6.057 6.04 5.263 1.969 33.883 7.709 15.825 3.18 20.693 9.159 37.148 7.686 5.508-0.506 6.345-0.471 11.03-4.77 5.768-5.24 2.803-4.168 3.26-8.142 1.865-11.716 0.432-14.027-7.949-27.298-8.78-13.899-8.183-14.553-13.948-23.713-5.764-9.03-13.529-25.811-20.126-25.938-5.188-0.129-8.198 2.688-11.465 6.021-3.266 3.331-5.517 19.368-10.332 24.474z"/>
+ <path fill="#ffcc07" d="m58.584 285.276c-4.827 5.03-19.255-0.865-25.397 3.855-4.639 3.541 2.938 8.332 1.975 20.295-0.425 4.838-3.551 5.961-2.578 12.619 0.757 5.039 2.536 4.834 4.575 6.955 6.428 5.949 5.852 2.108 33.646 7.674 15.733 3.147 20.571 9.103 36.925 7.639 5.475-0.501 6.211-0.48 10.905-4.693 5.67-5.072 2.745-4.045 3.107-8.224 1.663-11.642 0.42-14.073-7.711-26.946-8.73-13.814-8.223-14.406-13.952-23.511-5.729-8.976-13.358-25.817-19.916-25.944-5.156-0.127-8.148 2.674-11.396 5.984s-5.391 19.229-10.183 24.297z"/>
+ <path fill="#ffcd0a" d="m58.878 285.368c-5.01 5.193-19.579-0.701-25.565 3.93-4.607 3.555 3.117 8.475 2.157 20.102-0.437 4.809-3.459 6.004-2.639 12.613 0.643 5.011 2.626 4.781 4.695 6.83 6.799 5.857 6.442 2.25 33.411 7.64 15.641 3.118 20.45 9.048 36.701 7.593 5.44-0.494 6.076-0.488 10.781-4.615 5.57-4.904 2.688-3.926 2.954-8.308 1.462-11.569 0.409-14.118-7.472-26.593-8.68-13.732-8.263-14.262-13.958-23.311-5.695-8.922-13.188-25.824-19.705-25.951-5.125-0.127-8.1 2.658-11.326 5.949-3.227 3.289-5.266 19.091-10.034 24.121z"/>
+ <path fill="#ffcd0c" d="m59.173 285.458c-5.193 5.358-19.905-0.535-25.734 4.008-4.577 3.566 3.297 8.613 2.34 19.908-0.449 4.778-3.368 6.045-2.698 12.605 0.526 4.982 2.715 4.729 4.815 6.707 7.17 5.766 7.031 2.391 33.175 7.604 15.549 3.088 20.328 8.994 36.477 7.547 5.408-0.488 5.943-0.498 10.657-4.537 5.472-4.737 2.63-3.805 2.802-8.393 1.261-11.494 0.398-14.162-7.232-26.24-8.63-13.647-8.304-14.117-13.964-23.109-5.66-8.867-13.017-25.829-19.494-25.956-5.094-0.126-8.05 2.642-11.257 5.912-3.21 3.27-5.142 18.955-9.887 23.944z"/>
+ <path fill="#ffcd0f" d="m59.467 285.547c-5.376 5.523-20.229-0.369-25.903 4.084-4.545 3.58 3.478 8.756 2.523 19.715-0.461 4.75-3.277 6.088-2.758 12.599 0.411 4.955 2.804 4.677 4.936 6.584 7.541 5.675 7.621 2.532 32.938 7.569 15.456 3.056 20.206 8.938 36.253 7.5 5.375-0.482 5.811-0.506 10.533-4.459 5.374-4.57 2.572-3.686 2.649-8.477 1.058-11.42 0.387-14.209-6.995-25.888-8.58-13.563-8.344-13.972-13.969-22.909-5.626-8.813-12.846-25.834-19.283-25.961-5.063-0.125-8 2.625-11.188 5.877-3.188 3.251-5.015 18.818-9.736 23.766z"/>
+ <path fill="#ffcd11" d="m59.76 285.639c-5.559 5.688-20.555-0.205-26.071 4.158-4.515 3.594 3.657 8.896 2.706 19.521-0.473 4.721-3.186 6.131-2.818 12.594 0.297 4.926 2.894 4.623 5.057 6.459 7.911 5.584 8.21 2.674 32.703 7.535 15.362 3.025 20.084 8.883 36.027 7.453 5.342-0.477 5.677-0.515 10.409-4.381 5.275-4.402 2.516-3.564 2.497-8.561 0.855-11.348 0.375-14.254-6.756-25.535-8.53-13.479-8.385-13.825-13.976-22.709-5.59-8.758-12.673-25.842-19.071-25.965-5.031-0.125-7.951 2.608-11.119 5.838-3.168 3.232-4.888 18.684-9.588 23.593z"/>
+ <path fill="#ffce14" d="m60.055 285.73c-5.742 5.851-20.88-0.04-26.24 4.233-4.483 3.607 3.837 9.039 2.889 19.33-0.485 4.69-3.095 6.172-2.878 12.584 0.182 4.9 2.982 4.572 5.177 6.338 8.282 5.492 8.8 2.814 32.467 7.498 15.271 2.996 19.962 8.828 35.804 7.408 5.309-0.472 5.543-0.523 10.285-4.304 5.177-4.235 2.458-3.444 2.344-8.644 0.654-11.273 0.364-14.299-6.518-25.184-8.479-13.395-8.425-13.679-13.98-22.507-5.556-8.704-12.502-25.849-18.86-25.972-5-0.123-7.901 2.593-11.05 5.803s-4.765 18.547-9.44 23.417z"/>
+ <path fill="#ffce16" d="m60.349 285.821c-5.925 6.016-21.205 0.125-26.408 4.309-4.453 3.621 4.017 9.18 3.07 19.137-0.496 4.662-3.002 6.215-2.938 12.579 0.066 4.872 3.072 4.518 5.298 6.213 8.653 5.401 9.389 2.956 32.23 7.464 15.178 2.964 19.84 8.771 35.58 7.361 5.275-0.465 5.409-0.532 10.159-4.225 5.079-4.068 2.401-3.324 2.192-8.729 0.452-11.2 0.354-14.346-6.279-24.831-8.429-13.312-8.464-13.534-13.985-22.306-5.521-8.65-12.331-25.854-18.649-25.978-4.969-0.123-7.853 2.577-10.98 5.767-3.129 3.19-4.637 18.409-9.29 23.239z"/>
+ <path fill="#ffce19" d="m60.643 285.911c-6.107 6.18-21.529 0.291-26.577 4.385-4.421 3.635 4.197 9.322 3.254 18.945-0.508 4.631-2.911 6.256-2.997 12.571-0.049 4.845 3.161 4.465 5.418 6.089 9.023 5.309 9.979 3.098 31.994 7.43 15.085 2.934 19.718 8.717 35.355 7.314 5.242-0.459 5.276-0.541 10.036-4.148 4.98-3.899 2.344-3.203 2.039-8.811 0.25-11.127 0.342-14.391-6.04-24.479-8.38-13.228-8.505-13.389-13.991-22.105-5.486-8.596-12.16-25.859-18.438-25.982-4.938-0.123-7.803 2.561-10.911 5.73-3.109 3.17-4.513 18.272-9.142 23.061z"/>
+ <path fill="#ffce1c" d="m60.938 286.002c-6.291 6.344-21.855 0.455-26.746 4.459-4.391 3.648 4.377 9.463 3.437 18.752-0.521 4.603-2.82 6.299-3.058 12.564-0.163 4.817 3.251 4.413 5.539 5.965 9.395 5.219 10.567 3.24 31.758 7.396 14.993 2.903 19.597 8.661 35.132 7.269 5.209-0.453 5.143-0.55 9.912-4.07 4.882-3.732 2.286-3.082 1.887-8.895 0.048-11.053 0.329-14.436-5.802-24.126-8.33-13.144-8.545-13.243-13.997-21.905-5.451-8.541-11.988-25.865-18.228-25.988-4.906-0.121-7.753 2.545-10.843 5.695-3.088 3.149-4.385 18.132-8.991 22.884z"/>
+ <path fill="#ffcf1e" d="m61.232 286.092c-6.473 6.51-22.18 0.621-26.914 4.535-4.359 3.662 4.557 9.604 3.619 18.559-0.532 4.574-2.729 6.342-3.117 12.559-0.278 4.789 3.34 4.36 5.659 5.842 9.766 5.127 11.157 3.381 31.521 7.359 14.9 2.872 19.475 8.607 34.908 7.223 5.176-0.447 5.008-0.559 9.787-3.992 4.784-3.565 2.229-2.963 1.735-8.979-0.155-10.98 0.317-14.482-5.564-23.773-8.279-13.061-8.585-13.098-14.002-21.705-5.416-8.485-11.817-25.871-18.017-25.992-4.875-0.121-7.704 2.527-10.773 5.658-3.068 3.128-4.26 17.995-8.842 22.706z"/>
+ <path fill="#ffcf21" d="m61.526 286.184c-6.655 6.673-22.505 0.785-27.082 4.611-4.328 3.674 4.736 9.744 3.802 18.365-0.544 4.543-2.638 6.383-3.178 12.551-0.394 4.761 3.43 4.308 5.78 5.718 10.136 5.036 11.746 3.522 31.285 7.325 14.808 2.841 19.353 8.551 34.685 7.176 5.142-0.441 4.875-0.567 9.663-3.914 4.685-3.398 2.171-2.842 1.582-9.063-0.357-10.906 0.307-14.527-5.325-23.422-8.23-12.976-8.625-12.951-14.008-21.503-5.382-8.432-11.646-25.878-17.806-25.999-4.844-0.119-7.654 2.512-10.704 5.622-3.049 3.109-4.134 17.859-8.694 22.533z"/>
+ <path fill="#ffcf23" d="m61.821 286.275c-6.839 6.837-22.83 0.95-27.251 4.687-4.298 3.688 4.916 9.886 3.984 18.172-0.557 4.515-2.546 6.426-3.237 12.545-0.509 4.732 3.519 4.255 5.9 5.594 10.507 4.945 12.336 3.663 31.05 7.29 14.715 2.812 19.23 8.496 34.46 7.129 5.108-0.435 4.74-0.575 9.538-3.835 4.587-3.23 2.113-2.723 1.43-9.146-0.559-10.834 0.296-14.572-5.086-23.069-8.18-12.892-8.666-12.806-14.014-21.302-5.347-8.377-11.476-25.885-17.595-26.004-4.813-0.119-7.605 2.496-10.635 5.584-3.029 3.088-4.008 17.722-8.544 22.355z"/>
+ <path fill="#ffcf26" d="m62.115 286.366c-7.021 7.002-23.154 1.115-27.42 4.762-4.266 3.701 5.096 10.027 4.168 17.979-0.568 4.486-2.455 6.469-3.297 12.538-0.624 4.706 3.607 4.203 6.021 5.472 10.878 4.852 12.925 3.803 30.813 7.254 14.622 2.78 19.108 8.441 34.236 7.084 5.075-0.43 4.606-0.584 9.413-3.759 4.489-3.063 2.058-2.601 1.277-9.229-0.761-10.76 0.284-14.618-4.848-22.717-8.129-12.809-8.706-12.66-14.019-21.102-5.313-8.322-11.304-25.891-17.384-26.01-4.781-0.118-7.556 2.48-10.566 5.55-3.008 3.068-3.881 17.584-8.394 22.178z"/>
+ <path fill="#ffd028" d="m62.409 286.456c-7.204 7.166-23.479 1.281-27.588 4.838-4.235 3.715 5.275 10.168 4.351 17.787-0.58 4.455-2.364 6.51-3.357 12.53-0.738 4.679 3.697 4.149 6.142 5.347 11.249 4.763 13.515 3.946 30.577 7.221 14.529 2.748 18.986 8.386 34.012 7.037 5.043-0.424 4.474-0.594 9.289-3.68 4.392-2.897 2-2.481 1.125-9.314-0.963-10.686 0.273-14.664-4.608-22.364-8.079-12.726-8.746-12.517-14.024-20.901-5.277-8.269-11.133-25.896-17.173-26.015-4.75-0.116-7.506 2.464-10.497 5.513-2.992 3.047-3.759 17.447-8.249 22.001z"/>
+ <path fill="#ffd02b" d="m62.704 286.547c-7.388 7.33-23.805 1.445-27.757 4.912-4.204 3.729 5.455 10.311 4.533 17.594-0.593 4.427-2.272 6.553-3.417 12.523-0.854 4.651 3.786 4.098 6.262 5.225 11.619 4.671 14.104 4.087 30.341 7.185 14.438 2.72 18.864 8.33 33.787 6.991 5.01-0.418 4.341-0.604 9.166-3.604 4.292-2.729 1.942-2.359 0.972-9.396-1.163-10.613 0.263-14.709-4.369-22.012-8.03-12.641-8.787-12.371-14.03-20.7-5.243-8.214-10.962-25.903-16.962-26.021-4.719-0.117-7.457 2.447-10.428 5.477-2.972 3.029-3.632 17.313-8.098 21.826z"/>
+ <path fill="#ffd02d" d="m62.998 286.638c-7.57 7.493-24.13 1.61-27.925 4.987-4.174 3.742 5.635 10.451 4.716 17.4-0.604 4.398-2.182 6.596-3.478 12.518-0.969 4.623 3.875 4.045 6.382 5.101 11.991 4.579 14.694 4.228 30.105 7.149 14.345 2.688 18.743 8.275 33.563 6.944 4.977-0.411 4.207-0.61 9.042-3.524 4.193-2.562 1.884-2.239 0.819-9.481-1.366-10.538 0.251-14.754-4.133-21.659-7.979-12.557-8.825-12.224-14.034-20.498-5.208-8.16-10.791-25.91-16.751-26.027-4.688-0.114-7.407 2.433-10.358 5.441-2.951 3.01-3.505 17.174-7.948 21.649z"/>
+ <path fill="#ffd030" d="m63.292 286.729c-7.753 7.658-24.454 1.775-28.094 5.063-4.142 3.756 5.815 10.594 4.899 17.209-0.616 4.367-2.09 6.638-3.537 12.51-1.084 4.596 3.964 3.992 6.502 4.977 12.362 4.488 15.283 4.369 29.869 7.115 14.252 2.656 18.621 8.22 33.34 6.898 4.943-0.406 4.073-0.621 8.917-3.447 4.095-2.395 1.828-2.119 0.667-9.565-1.568-10.465 0.239-14.8-3.894-21.307-7.929-12.474-8.866-12.078-14.04-20.298-5.173-8.105-10.619-25.916-16.54-26.031-4.656-0.115-7.357 2.415-10.289 5.404-2.932 2.988-3.38 17.036-7.8 21.472z"/>
+ <path fill="#ffd133" d="m63.587 286.82c-7.937 7.822-24.78 1.94-28.263 5.138-4.111 3.77 5.995 10.734 5.081 17.016-0.627 4.339-1.998 6.68-3.597 12.504-1.199 4.568 4.054 3.939 6.623 4.854 12.732 4.396 15.873 4.51 29.633 7.08 14.16 2.625 18.499 8.164 33.116 6.852 4.909-0.4 3.939-0.629 8.793-3.369 3.997-2.227 1.77-1.999 0.514-9.648-1.771-10.393 0.228-14.846-3.654-20.955-7.88-12.39-8.907-11.934-14.046-20.098-5.139-8.051-10.448-25.922-16.329-26.037-4.625-0.113-7.309 2.398-10.221 5.368s-3.254 16.897-7.65 21.295z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m88.782 218.681c-0.936 2.088-1.728 20.017 2.952 27 4.68 6.911 3.312 10.872-1.872 5.616-5.4-5.112-8.928-12.816-9-18.145 0-3.096 2.376-15.84 3.313-17.208 1.007-1.44 5.327 1.153 4.607 2.737z"/>
+ <path fill="#030303" d="m88.692 219.032c-0.903 2.34-1.656 19.698 3.01 26.668 4.665 6.901 3.186 10.49-1.84 5.356-5.23-4.997-8.607-12.47-8.741-17.787-0.043-3.114 2.236-15.419 3.133-16.791 0.961-1.394 5.126 0.989 4.438 2.554z"/>
+ <path fill="#070707" d="m88.602 219.379c-0.871 2.593-1.584 19.383 3.067 26.338 4.65 6.891 3.06 10.109-1.808 5.098-5.062-4.882-8.287-12.125-8.481-17.432-0.087-3.131 2.095-14.998 2.952-16.373 0.915-1.345 4.926 0.828 4.27 2.369z"/>
+ <path fill="#0b0b0b" d="m88.512 219.729c-0.839 2.844-1.513 19.066 3.124 26.006 4.638 6.881 2.935 9.729-1.774 4.84-4.893-4.768-7.967-11.779-8.223-17.076-0.129-3.149 1.955-14.576 2.772-15.955 0.868-1.299 4.724 0.665 4.101 2.185z"/>
+ <path fill="#0f0f0f" d="m88.422 220.079c-0.806 3.096-1.439 18.748 3.183 25.674 4.623 6.869 2.809 9.347-1.742 4.58-4.724-4.651-7.646-11.434-7.963-16.719-0.173-3.168 1.814-14.154 2.592-15.537 0.819-1.252 4.52 0.504 3.93 2.002z"/>
+ <path fill="#131313" d="m88.332 220.426c-0.773 3.349-1.368 18.433 3.24 25.345 4.608 6.858 2.682 8.964-1.71 4.319-4.554-4.535-7.326-11.088-7.704-16.361-0.216-3.186 1.674-13.734 2.412-15.12 0.774-1.206 4.32 0.343 3.762 1.817z"/>
+ <path fill="#161616" d="m88.242 220.777c-0.741 3.601-1.296 18.115 3.298 25.013 4.594 6.848 2.556 8.582-1.678 4.061-4.385-4.421-7.006-10.742-7.444-16.006-0.26-3.203 1.533-13.313 2.231-14.702 0.728-1.16 4.118 0.18 3.593 1.634z"/>
+ <path fill="#1a1a1a" d="m88.152 221.125c-0.709 3.853-1.224 17.799 3.355 24.682 4.579 6.837 2.43 8.201-1.646 3.802-4.216-4.306-6.686-10.397-7.186-15.649-0.303-3.221 1.394-12.892 2.052-14.285 0.682-1.112 3.918 0.018 3.425 1.45z"/>
+ <path fill="#1e1e1e" d="m88.062 221.475c-0.677 4.104-1.152 17.482 3.413 24.35 4.564 6.826 2.304 7.82-1.613 3.543-4.046-4.191-6.365-10.051-6.927-15.293-0.345-3.24 1.253-12.47 1.872-13.867 0.634-1.066 3.716-0.144 3.255 1.267z"/>
+ <path fill="#222" d="m87.972 221.825c-0.645 4.355-1.08 17.164 3.47 24.018 4.551 6.816 2.179 7.439-1.58 3.285-3.877-4.076-6.044-9.707-6.667-14.938-0.389-3.258 1.112-12.049 1.691-13.449 0.587-1.019 3.514-0.306 3.086 1.084z"/>
+ <path fill="#262626" d="m87.883 222.172c-0.612 4.609-1.009 16.849 3.527 23.688 4.536 6.804 2.052 7.056-1.548 3.024-3.708-3.961-5.724-9.36-6.408-14.58-0.432-3.276 0.973-11.629 1.513-13.032 0.54-0.971 3.311-0.467 2.916 0.9z"/>
+ <path fill="#2a2a2a" d="m87.792 222.523c-0.579 4.86-0.936 16.531 3.586 23.356 4.521 6.793 1.926 6.675-1.516 2.765-3.539-3.845-5.403-9.015-6.148-14.224-0.476-3.293 0.831-11.207 1.332-12.614 0.493-0.925 3.11-0.63 2.746 0.717z"/>
+ <path fill="#2d2d2d" d="m87.702 222.872c-0.547 5.112-0.864 16.215 3.644 23.025 4.507 6.783 1.8 6.293-1.483 2.506-3.369-3.73-5.083-8.669-5.89-13.867-0.519-3.312 0.691-10.785 1.152-12.197 0.446-0.878 2.908-0.792 2.577 0.533z"/>
+ <path fill="#313131" d="m87.612 223.221c-0.515 5.363-0.792 15.898 3.701 22.693 4.492 6.772 1.674 5.912-1.451 2.248-3.2-3.615-4.763-8.324-5.63-13.512-0.563-3.33 0.551-10.363 0.972-11.779 0.399-0.831 2.707-0.953 2.408 0.35z"/>
+ <path fill="#353535" d="m87.522 223.57c-0.482 5.616-0.72 15.581 3.758 22.363 4.479 6.761 1.549 5.53-1.418 1.987-3.031-3.5-4.442-7.978-5.371-13.154-0.604-3.348 0.41-9.943 0.792-11.361 0.352-0.785 2.506-1.115 2.239 0.165z"/>
+ <path fill="#393939" d="m87.432 223.918c-0.45 5.869-0.648 15.265 3.815 22.033 4.464 6.75 1.422 5.147-1.386 1.728-2.862-3.384-4.122-7.632-5.112-12.798-0.647-3.366 0.271-9.522 0.612-10.944 0.307-0.737 2.305-1.278 2.071-0.019z"/>
+ <path fill="#3d3d3d" d="m87.343 224.269c-0.418 6.12-0.576 14.946 3.873 21.7 4.45 6.74 1.296 4.767-1.354 1.469-2.692-3.27-3.802-7.286-4.853-12.441-0.691-3.383 0.129-9.101 0.432-10.527 0.26-0.691 2.103-1.44 1.902-0.201z"/>
+ <path fill="#414141" d="m87.252 224.618c-0.385 6.373-0.504 14.631 3.932 21.369 4.436 6.729 1.17 4.385-1.321 1.211-2.523-3.154-3.481-6.941-4.594-12.086-0.734-3.402-0.011-8.68 0.252-10.109 0.212-0.644 1.901-1.602 1.731-0.385z"/>
+ <path fill="#444" d="m87.162 224.967c-0.353 6.623-0.432 14.314 3.989 21.037 4.421 6.719 1.044 4.004-1.289 0.951-2.354-3.039-3.161-6.595-4.334-11.729-0.778-3.42-0.151-8.258 0.071-9.691 0.166-0.597 1.7-1.763 1.563-0.568z"/>
+ <path fill="#484848" d="m87.072 225.316c-0.32 6.876-0.36 13.997 4.047 20.707 4.406 6.707 0.918 3.622-1.257 0.692-2.186-2.924-2.841-6.25-4.075-11.373-0.82-3.438-0.292-7.838-0.108-9.273 0.119-0.551 1.498-1.926 1.393-0.753z"/>
+ <path fill="#4c4c4c" d="m86.982 225.665c-0.288 7.129-0.288 13.68 4.104 20.377 4.393 6.695 0.792 3.24-1.224 0.432s-2.52-5.904-3.816-11.016c-0.863-3.457-0.432-7.416-0.287-8.856 0.071-0.505 1.295-2.089 1.223-0.937z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m88.782 218.681c4.32-9.433 6.696-19.584 12.888-29.448 6.12-9.792 3.672-13.608-0.863-8.64-4.536 4.968-9.504 15.48-9.504 15.48s-5.832 9.216-7.128 19.872c-0.217 1.8 3.887 4.248 4.607 2.736z"/>
+ <path fill="#020202" d="m88.968 218.071c4.279-9.5 6.615-19.246 12.586-28.802 5.901-9.488 3.608-13.279-0.764-8.472-4.403 4.847-9.302 15.236-9.368 15.381 0 0-5.637 8.993-6.877 19.239-0.209 1.771 3.72 4.145 4.423 2.654z"/>
+ <path fill="#050505" d="m89.152 217.459c4.239-9.566 6.535-18.908 12.284-28.155 5.683-9.183 3.547-12.95-0.663-8.303-4.27 4.725-9.099 14.991-9.231 15.282 0 0-5.442 8.766-6.627 18.605-0.201 1.741 3.554 4.042 4.237 2.571z"/>
+ <path fill="#070707" d="m89.338 216.849c4.199-9.634 6.454-18.572 11.981-27.51 5.465-8.878 3.484-12.621-0.563-8.135-4.137 4.605-8.896 14.748-9.096 15.184 0 0-5.247 8.542-6.376 17.972-0.192 1.713 3.387 3.937 4.054 2.489z"/>
+ <path fill="#0a0a0a" d="m89.522 216.239c4.159-9.701 6.375-18.234 11.68-26.865 5.247-8.573 3.422-12.292-0.461-7.966-4.005 4.483-8.693 14.503-8.96 15.085 0 0-5.053 8.317-6.126 17.337-0.186 1.684 3.219 3.837 3.867 2.409z"/>
+ <path fill="#0c0c0c" d="m89.708 215.627c4.118-9.769 6.294-17.897 11.376-26.218 5.029-8.268 3.36-11.962-0.359-7.797-3.871 4.361-8.49 14.259-8.823 14.985 0 0-4.858 8.093-5.876 16.706-0.179 1.653 3.051 3.731 3.682 2.324z"/>
+ <path fill="#0f0f0f" d="m89.893 215.017c4.077-9.836 6.213-17.56 11.073-25.573 4.812-7.963 3.298-11.633-0.259-7.629-3.738 4.241-8.288 14.015-8.688 14.887 0 0-4.663 7.868-5.625 16.072-0.169 1.624 2.886 3.628 3.499 2.243z"/>
+ <path fill="#111" d="m90.078 214.407c4.037-9.904 6.133-17.224 10.772-24.928 4.593-7.658 3.234-11.304-0.159-7.46-3.605 4.119-8.085 13.771-8.551 14.788 0 0-4.47 7.645-5.375 15.439-0.162 1.594 2.718 3.524 3.313 2.161z"/>
+ <path fill="#141414" d="m90.263 213.795c3.997-9.971 6.052-16.885 10.47-24.281 4.374-7.353 3.172-10.975-0.059-7.291-3.472 3.998-7.882 13.526-8.415 14.689 0 0-4.273 7.418-5.124 14.805-0.154 1.565 2.551 3.421 3.128 2.078z"/>
+ <path fill="#161616" d="m90.449 213.184c3.956-10.037 5.972-16.548 10.167-23.635 4.156-7.048 3.11-10.645 0.043-7.123-3.34 3.877-7.68 13.283-8.279 14.591 0 0-4.08 7.194-4.874 14.172-0.147 1.535 2.383 3.317 2.943 1.995z"/>
+ <path fill="#191919" d="m90.634 212.573c3.916-10.105 5.892-16.209 9.865-22.989 3.938-6.743 3.047-10.316 0.144-6.954-3.207 3.755-7.477 13.038-8.143 14.491 0 0-3.886 6.969-4.624 13.54-0.139 1.506 2.217 3.213 2.758 1.912z"/>
+ <path fill="#1c1c1c" d="m90.819 211.963c3.875-10.174 5.811-15.874 9.563-22.344 3.721-6.438 2.984-9.987 0.244-6.785-3.073 3.634-7.274 12.793-8.007 14.392 0 0-3.69 6.745-4.373 12.905-0.131 1.477 2.049 3.112 2.573 1.832z"/>
+ <path fill="#1e1e1e" d="m91.004 211.352c3.836-10.24 5.73-15.536 9.262-21.698 3.501-6.133 2.922-9.658 0.344-6.617-2.94 3.513-7.071 12.55-7.87 14.293 0 0-3.496 6.521-4.123 12.272-0.124 1.447 1.881 3.009 2.387 1.75z"/>
+ <path fill="#212121" d="m91.189 210.741c3.795-10.307 5.649-15.198 8.958-21.052 3.284-5.828 2.859-9.328 0.445-6.448-2.808 3.392-6.868 12.305-7.734 14.195 0 0-3.301 6.295-3.872 11.639-0.115 1.418 1.715 2.904 2.203 1.666z"/>
+ <path fill="#232323" d="m91.375 210.129c3.754-10.373 5.569-14.86 8.655-20.405 3.066-5.523 2.797-8.999 0.547-6.279-2.675 3.27-6.666 12.061-7.599 14.097 0 0-3.106 6.07-3.622 11.004-0.107 1.388 1.548 2.801 2.019 1.583z"/>
+ <path fill="#262626" d="m91.559 209.519c3.714-10.44 5.489-14.523 8.354-19.76 2.847-5.218 2.735-8.67 0.647-6.111-2.542 3.149-6.463 11.817-7.462 13.997 0 0-2.912 5.848-3.372 10.373-0.099 1.357 1.381 2.697 1.833 1.501z"/>
+ <path fill="#282828" d="m91.745 208.909c3.674-10.509 5.408-14.187 8.051-19.115 2.629-4.913 2.672-8.341 0.748-5.942-2.409 3.028-6.261 11.573-7.326 13.898 0 0-2.717 5.621-3.121 9.738-0.092 1.33 1.213 2.595 1.648 1.421z"/>
+ <path fill="#2b2b2b" d="m91.929 208.297c3.634-10.575 5.328-13.848 7.75-18.468 2.41-4.608 2.609-8.011 0.848-5.773-2.275 2.906-6.058 11.328-7.189 13.799 0 0-2.522 5.397-2.871 9.106-0.084 1.299 1.046 2.491 1.462 1.336z"/>
+ <path fill="#2d2d2d" d="m92.115 207.687c3.593-10.643 5.247-13.512 7.447-17.823 2.191-4.303 2.547-7.682 0.949-5.605-2.144 2.786-5.855 11.085-7.055 13.701 0 0-2.327 5.172-2.62 8.473-0.076 1.269 0.881 2.386 1.279 1.254z"/>
+ <path fill="#303030" d="m92.301 207.077c3.552-10.71 5.167-13.175 7.145-17.178 1.974-3.998 2.484-7.353 1.05-5.436-2.011 2.664-5.652 10.84-6.918 13.602 0 0-2.133 4.947-2.37 7.839-0.07 1.24 0.712 2.283 1.093 1.173z"/>
+ <path fill="#333" d="m92.485 206.465c3.513-10.778 5.087-12.837 6.843-16.531s2.422-7.024 1.15-5.268c-1.877 2.543-5.449 10.596-6.781 13.502 0 0-1.938 4.724-2.12 7.207-0.061 1.211 0.545 2.18 0.908 1.09z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m273.03 225.592c2.088-5.903 1.872-20.951-3.456-30.671-1.872-3.528-3.672-7.632-4.752-7.848-1.152-0.216-3.24 2.088-3.024 2.448 0.288 0.576 10.009 14.256 7.992 32.832-0.144 1.513 2.736 4.608 3.24 3.239z"/>
+ <path fill="#030303" d="m272.936 224.831c2.025-5.719 1.753-20.379-3.344-29.663-1.815-3.407-3.535-7.374-4.595-7.59-1.122-0.214-3.125 2.022-2.925 2.367 0.258 0.562 9.605 13.778 7.729 31.753-0.129 1.487 2.647 4.456 3.135 3.133z"/>
+ <path fill="#070707" d="m272.841 224.068c1.967-5.532 1.637-19.808-3.228-28.654-1.762-3.286-3.401-7.115-4.44-7.332-1.091-0.211-3.009 1.956-2.824 2.287 0.227 0.548 9.201 13.299 7.466 30.672-0.118 1.463 2.555 4.305 3.026 3.027z"/>
+ <path fill="#0b0b0b" d="m272.747 223.305c1.904-5.348 1.518-19.235-3.115-27.645-1.706-3.165-3.265-6.856-4.282-7.073-1.062-0.21-2.896 1.889-2.727 2.206 0.197 0.534 8.799 12.821 7.203 29.592-0.103 1.44 2.466 4.153 2.921 2.92z"/>
+ <path fill="#0f0f0f" d="m272.652 222.542c1.843-5.162 1.398-18.662-3.001-26.635-1.65-3.044-3.13-6.598-4.127-6.815-1.03-0.207-2.779 1.823-2.626 2.126 0.167 0.52 8.396 12.341 6.94 28.511-0.09 1.416 2.376 4.001 2.814 2.813z"/>
+ <path fill="#131313" d="m272.558 221.779c1.78-4.976 1.279-18.09-2.889-25.626-1.595-2.923-2.994-6.34-3.97-6.557-1-0.205-2.664 1.756-2.527 2.045 0.137 0.506 7.993 11.861 6.679 27.432-0.078 1.392 2.285 3.849 2.707 2.706z"/>
+ <path fill="#161616" d="m272.463 221.016c1.721-4.79 1.163-17.518-2.773-24.617-1.539-2.802-2.858-6.081-3.814-6.299-0.969-0.203-2.548 1.691-2.427 1.965 0.106 0.492 7.59 11.383 6.415 26.352-0.065 1.369 2.194 3.697 2.599 2.599z"/>
+ <path fill="#1a1a1a" d="m272.369 220.254c1.659-4.605 1.044-16.946-2.66-23.609-1.483-2.681-2.723-5.822-3.658-6.04-0.938-0.201-2.434 1.624-2.326 1.884 0.074 0.478 7.185 10.904 6.15 25.271-0.051 1.344 2.106 3.546 2.494 2.494z"/>
+ <path fill="#1e1e1e" d="m272.274 219.491c1.598-4.42 0.926-16.373-2.546-22.6-1.43-2.56-2.587-5.564-3.501-5.782-0.908-0.198-2.319 1.558-2.229 1.804 0.044 0.464 6.782 10.425 5.889 24.19-0.037 1.321 2.016 3.396 2.387 2.388z"/>
+ <path fill="#222" d="m272.18 218.728c1.535-4.233 0.807-15.802-2.434-21.59-1.373-2.439-2.452-5.306-3.345-5.524-0.877-0.197-2.203 1.491-2.128 1.723 0.014 0.45 6.379 9.947 5.625 23.111-0.023 1.297 1.927 3.242 2.282 2.28z"/>
+ <path fill="#262626" d="m272.085 217.965c1.476-4.049 0.69-15.229-2.318-20.582-1.317-2.317-2.316-5.046-3.188-5.265-0.847-0.194-2.088 1.425-2.029 1.642-0.016 0.436 5.977 9.468 5.362 22.032-0.011 1.272 1.835 3.089 2.173 2.173z"/>
+ <path fill="#2a2a2a" d="m271.991 217.202c1.413-3.861 0.571-14.656-2.206-19.572-1.262-2.196-2.18-4.788-3.032-5.007-0.815-0.191-1.973 1.36-1.929 1.562-0.047 0.422 5.573 8.988 5.099 20.951 0.003 1.247 1.746 2.939 2.068 2.066z"/>
+ <path fill="#2d2d2d" d="m271.896 216.439c1.353-3.677 0.453-14.084-2.091-18.563-1.207-2.076-2.045-4.529-2.876-4.749-0.786-0.19-1.858 1.293-1.83 1.481-0.077 0.408 5.17 8.51 4.836 19.87 0.017 1.226 1.656 2.789 1.961 1.961z"/>
+ <path fill="#313131" d="m271.802 215.676c1.29-3.491 0.334-13.512-1.979-17.553-1.151-1.956-1.909-4.272-2.72-4.493-0.755-0.187-1.742 1.227-1.73 1.401-0.106 0.394 4.768 8.031 4.573 18.791 0.031 1.202 1.567 2.637 1.856 1.854z"/>
+ <path fill="#353535" d="m271.707 214.915c1.23-3.307 0.217-12.94-1.864-16.545-1.096-1.834-1.773-4.014-2.563-4.234-0.725-0.185-1.627 1.16-1.631 1.32-0.138 0.38 4.364 7.552 4.311 17.71 0.042 1.176 1.475 2.485 1.747 1.749z"/>
+ <path fill="#393939" d="m271.613 214.151c1.168-3.119 0.098-12.367-1.751-15.535-1.04-1.714-1.638-3.755-2.407-3.976-0.693-0.183-1.512 1.094-1.531 1.24-0.168 0.366 3.962 7.074 4.049 16.63 0.055 1.153 1.384 2.332 1.64 1.641z"/>
+ <path fill="#3d3d3d" d="m271.518 213.388c1.106-2.935-0.021-11.796-1.638-14.527-0.983-1.592-1.502-3.496-2.25-3.716-0.664-0.181-1.396 1.028-1.432 1.159-0.198 0.352 3.558 6.594 3.785 15.549 0.07 1.13 1.296 2.183 1.535 1.535z"/>
+ <path fill="#414141" d="m271.424 212.625c1.047-2.75-0.139-11.223-1.522-13.518-0.93-1.471-1.367-3.238-2.094-3.459-0.635-0.178-1.282 0.962-1.333 1.079-0.229 0.338 3.153 6.114 3.521 14.47 0.083 1.106 1.205 2.03 1.428 1.428z"/>
+ <path fill="#444" d="m271.329 211.862c0.985-2.563-0.256-10.65-1.409-12.508-0.874-1.35-1.23-2.979-1.938-3.2-0.604-0.177-1.166 0.895-1.233 0.998-0.259 0.324 2.751 5.636 3.259 13.39 0.096 1.082 1.115 1.876 1.321 1.32z"/>
+ <path fill="#484848" d="m271.235 211.099c0.923-2.377-0.375-10.077-1.296-11.499-0.818-1.229-1.097-2.72-1.781-2.942-0.573-0.174-1.052 0.829-1.134 0.918-0.289 0.309 2.348 5.156 2.996 12.309 0.11 1.058 1.025 1.726 1.215 1.214z"/>
+ <path fill="#4c4c4c" d="m271.14 210.336c0.861-2.192-0.493-9.506-1.183-10.49-0.763-1.107-0.96-2.463-1.625-2.684-0.542-0.172-0.936 0.762-1.034 0.836-0.319 0.297 1.945 4.68 2.733 11.229 0.124 1.035 0.936 1.576 1.109 1.109z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m264.822 187.073c-10.224-13.968-23.472-18.504-22.104-14.112 0 0 10.152 5.76 19.08 16.56 1.728 2.088 4.608-0.288 3.024-2.448z"/>
+ <path fill="#030303" d="m264.372 186.687c-9.924-13.495-22.894-17.912-21.539-13.7 0.018 0.012 9.901 5.614 18.609 16.071 1.678 2.016 4.467-0.285 2.93-2.371z"/>
+ <path fill="#070707" d="m263.922 186.3c-9.624-13.022-22.316-17.319-20.975-13.287 0.036 0.023 9.652 5.467 18.139 15.582 1.628 1.943 4.325-0.283 2.836-2.295z"/>
+ <path fill="#0b0b0b" d="m263.472 185.913c-9.324-12.549-21.739-16.726-20.41-12.874 0.053 0.034 9.4 5.32 17.668 15.093 1.578 1.871 4.183-0.28 2.742-2.219z"/>
+ <path fill="#0f0f0f" d="m263.022 185.527c-9.024-12.077-21.162-16.134-19.847-12.462 0.071 0.045 9.151 5.174 17.198 14.604 1.529 1.798 4.043-0.278 2.649-2.142z"/>
+ <path fill="#131313" d="m262.571 185.14c-8.723-11.603-20.583-15.541-19.28-12.049 0.088 0.056 8.901 5.027 16.728 14.114 1.477 1.726 3.899-0.275 2.552-2.065z"/>
+ <path fill="#161616" d="m262.121 184.753c-8.423-11.13-20.006-14.948-18.716-11.636 0.106 0.067 8.651 4.88 16.257 13.625 1.428 1.654 3.759-0.272 2.459-1.989z"/>
+ <path fill="#1a1a1a" d="m261.671 184.367c-8.124-10.658-19.428-14.356-18.15-11.224 0.124 0.078 8.399 4.734 15.785 13.136 1.378 1.581 3.617-0.27 2.365-1.912z"/>
+ <path fill="#1e1e1e" d="m261.221 183.98c-7.824-10.185-18.851-13.763-17.588-10.811 0.143 0.089 8.151 4.587 15.315 12.647 1.33 1.508 3.477-0.267 2.273-1.836z"/>
+ <path fill="#222" d="m260.771 183.593c-7.524-9.711-18.273-13.17-17.022-10.398 0.159 0.1 7.9 4.44 14.844 12.158 1.279 1.436 3.335-0.265 2.178-1.76z"/>
+ <path fill="#262626" d="m260.321 183.206c-7.224-9.238-17.695-12.578-16.458-9.985 0.177 0.111 7.65 4.293 14.374 11.668 1.229 1.364 3.193-0.262 2.084-1.683z"/>
+ <path fill="#2a2a2a" d="m259.871 182.82c-6.924-8.766-17.118-11.986-15.893-9.573 0.193 0.122 7.398 4.147 13.902 11.179 1.18 1.291 3.053-0.259 1.991-1.606z"/>
+ <path fill="#2d2d2d" d="m259.42 182.433c-6.623-8.293-16.539-11.393-15.328-9.16 0.214 0.133 7.15 4 13.434 10.69 1.128 1.219 2.909-0.257 1.894-1.53z"/>
+ <path fill="#313131" d="m258.97 182.046c-6.323-7.819-15.963-10.8-14.764-8.747 0.23 0.144 6.899 3.853 12.962 10.201 1.08 1.146 2.769-0.254 1.802-1.454z"/>
+ <path fill="#353535" d="m258.52 181.66c-6.023-7.347-15.384-10.208-14.199-8.335 0.248 0.155 6.649 3.707 12.492 9.712 1.028 1.073 2.627-0.252 1.707-1.377z"/>
+ <path fill="#393939" d="m258.07 181.273c-5.723-6.874-14.807-9.615-13.634-7.922 0.265 0.166 6.398 3.56 12.021 9.222 0.978 1.002 2.485-0.249 1.613-1.3z"/>
+ <path fill="#3d3d3d" d="m257.62 180.886c-5.423-6.401-14.229-9.021-13.07-7.509 0.283 0.177 6.149 3.413 11.552 8.733 0.927 0.929 2.343-0.246 1.518-1.224z"/>
+ <path fill="#414141" d="m257.17 180.5c-5.124-5.928-13.65-8.43-12.505-7.097 0.301 0.188 5.898 3.267 11.079 8.244 0.879 0.856 2.203-0.244 1.426-1.147z"/>
+ <path fill="#444" d="m256.719 180.113c-4.823-5.455-13.073-7.837-11.94-6.684 0.319 0.199 5.649 3.12 10.609 7.755 0.829 0.784 2.061-0.241 1.331-1.071z"/>
+ <path fill="#484848" d="m256.269 179.726c-4.523-4.982-12.495-7.244-11.375-6.271 0.336 0.21 5.397 2.973 10.138 7.266 0.779 0.711 1.92-0.239 1.237-0.995z"/>
+ <path fill="#4c4c4c" d="m255.819 179.339c-4.223-4.509-11.918-6.652-10.812-5.859 0.354 0.222 5.148 2.827 9.668 6.777 0.73 0.639 1.779-0.236 1.144-0.918z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m273.03 225.592c0.144 6.265-5.76 22.248-7.992 21.673-2.52-0.576 0.504-5.257 2.809-13.177 0.936-3.312 1.655-11.447 1.943-11.735 0.936-0.936 3.24 1.728 3.24 3.239z"/>
+ <path fill="#050505" d="m272.846 226.116c0.103 6.082-5.638 21.594-7.797 21.018-2.422-0.567 0.548-5.146 2.814-12.928 0.899-3.178 1.595-10.947 1.887-11.25 0.914-0.927 3.147 1.527 3.096 3.16z"/>
+ <path fill="#0a0a0a" d="m272.662 226.637c0.063 5.902-5.514 20.939-7.601 20.363-2.323-0.558 0.592-5.035 2.82-12.681 0.861-3.041 1.534-10.446 1.83-10.761 0.89-0.917 3.054 1.327 2.951 3.079z"/>
+ <path fill="#0f0f0f" d="m272.478 227.159c0.021 5.721-5.392 20.284-7.405 19.711-2.224-0.55 0.635-4.927 2.827-12.434 0.824-2.907 1.473-9.945 1.773-10.273 0.866-0.911 2.959 1.125 2.805 2.996z"/>
+ <path fill="#141414" d="m272.294 227.68c-0.02 5.539-5.268 19.63-7.21 19.057-2.125-0.541 0.68-4.816 2.835-12.186 0.786-2.771 1.412-9.445 1.717-9.786 0.84-0.901 2.863 0.924 2.658 2.915z"/>
+ <path fill="#191919" d="m272.11 228.202c-0.063 5.359-5.146 18.977-7.014 18.403-2.027-0.532 0.722-4.706 2.841-11.938 0.748-2.637 1.351-8.944 1.659-9.299 0.818-0.893 2.77 0.722 2.514 2.834z"/>
+ <path fill="#1e1e1e" d="m271.926 228.723c-0.103 5.179-5.021 18.322-6.818 17.75-1.928-0.523 0.766-4.596 2.848-11.691 0.711-2.5 1.29-8.443 1.603-8.811 0.792-0.885 2.674 0.522 2.367 2.752z"/>
+ <path fill="#232323" d="m271.742 229.245c-0.144 4.998-4.9 17.668-6.623 17.096-1.83-0.514 0.811-4.485 2.854-11.442 0.673-2.366 1.229-7.944 1.545-8.325 0.771-0.876 2.583 0.32 2.224 2.671z"/>
+ <path fill="#282828" d="m271.558 229.766c-0.186 4.816-4.777 17.013-6.428 16.443-1.731-0.506 0.854-4.377 2.86-11.196 0.636-2.231 1.168-7.443 1.488-7.837 0.749-0.866 2.49 0.119 2.08 2.59z"/>
+ <path fill="#2d2d2d" d="m271.374 230.288c-0.226 4.637-4.653 16.359-6.231 15.789-1.632-0.496 0.896-4.266 2.866-10.947 0.6-2.098 1.107-6.943 1.433-7.351 0.722-0.859 2.393-0.081 1.932 2.509z"/>
+ <path fill="#333" d="m271.19 230.809c-0.268 4.456-4.531 15.705-6.036 15.136-1.534-0.489 0.94-4.155 2.873-10.7 0.561-1.961 1.046-6.443 1.375-6.863 0.7-0.848 2.3-0.282 1.788 2.427z"/>
+ <path fill="#383838" d="m271.006 231.331c-0.308 4.275-4.407 15.051-5.841 14.482-1.435-0.48 0.984-4.046 2.88-10.453 0.524-1.826 0.985-5.941 1.318-6.375 0.676-0.84 2.206-0.483 1.643 2.346z"/>
+ <path fill="#3d3d3d" d="m270.822 231.853c-0.35 4.093-4.285 14.396-5.645 13.828-1.338-0.472 1.027-3.937 2.886-10.206 0.485-1.691 0.924-5.441 1.261-5.887 0.653-0.832 2.113-0.684 1.498 2.265z"/>
+ <path fill="#424242" d="m270.638 232.374c-0.392 3.914-4.162 13.742-5.45 13.176-1.238-0.463 1.072-3.826 2.893-9.959 0.449-1.555 0.863-4.94 1.204-5.399 0.629-0.824 2.019-0.886 1.353 2.182z"/>
+ <path fill="#474747" d="m270.454 232.896c-0.432 3.731-4.039 13.087-5.254 12.521-1.14-0.453 1.115-3.715 2.9-9.711 0.411-1.42 0.802-4.439 1.146-4.912 0.606-0.815 1.925-1.086 1.208 2.102z"/>
+ <path fill="#4c4c4c" d="m270.27 233.417c-0.474 3.553-3.916 12.434-5.06 11.869-1.041-0.445 1.159-3.606 2.907-9.463 0.373-1.287 0.741-3.941 1.089-4.427 0.583-0.806 1.832-1.287 1.064 2.021z"/>
+ <path fill="#515151" d="m270.086 233.939c-0.514 3.37-3.793 11.778-4.862 11.214-0.942-0.436 1.201-3.496 2.913-9.216 0.336-1.151 0.68-3.438 1.032-3.938 0.558-0.797 1.736-1.489 0.917 1.94z"/>
+ <path fill="#565656" d="m269.902 234.461c-0.555 3.188-3.671 11.123-4.667 10.56-0.844-0.429 1.246-3.386 2.919-8.968 0.298-1.016 0.619-2.939 0.976-3.451 0.535-0.788 1.643-1.689 0.772 1.859z"/>
+ <path fill="#5b5b5b" d="m269.718 234.982c-0.597 3.009-3.548 10.47-4.472 9.907-0.745-0.42 1.29-3.275 2.926-8.721 0.262-0.881 0.559-2.438 0.919-2.963 0.511-0.781 1.549-1.89 0.627 1.777z"/>
+ <path fill="#606060" d="m269.534 235.504c-0.638 2.828-3.425 9.814-4.276 9.252-0.646-0.409 1.333-3.166 2.933-8.473 0.224-0.746 0.497-1.938 0.862-2.476 0.487-0.769 1.454-2.09 0.481 1.697z"/>
+ <path fill="#666" d="m269.35 236.025c-0.68 2.647-3.303 9.161-4.081 8.599-0.548-0.4 1.377-3.056 2.938-8.225 0.187-0.611 0.437-1.438 0.806-1.988 0.464-0.763 1.361-2.293 0.337 1.614z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m251.07 187.865c-1.537 1.622-2.903 9.991 0.938 12.893 3.844 2.818 10.59-2.391 10.59-5.379-0.086-6.746-9.991-9.222-11.528-7.514z"/>
+ <path fill="#010101" d="m251.207 188.006c-1.559 1.611-2.876 9.823 0.857 12.667 3.731 2.764 10.349-2.273 10.384-5.279-0.047-6.576-9.681-9.083-11.241-7.388z"/>
+ <path fill="#030303" d="m251.344 188.146c-1.582 1.601-2.85 9.653 0.774 12.438 3.62 2.709 10.109-2.154 10.178-5.177-0.007-6.404-9.37-8.943-10.952-7.261z"/>
+ <path fill="#050505" d="m251.481 188.287c-1.604 1.589-2.823 9.484 0.691 12.211 3.511 2.653 9.869-2.037 9.975-5.078 0.031-6.232-9.061-8.802-10.666-7.133z"/>
+ <path fill="#070707" d="m251.617 188.427c-1.626 1.579-2.795 9.316 0.611 11.984 3.397 2.6 9.629-1.918 9.768-4.976 0.071-6.062-8.751-8.664-10.379-7.008z"/>
+ <path fill="#090909" d="m251.754 188.567c-1.648 1.568-2.768 9.146 0.529 11.758 3.287 2.543 9.389-1.802 9.563-4.875 0.109-5.892-8.441-8.525-10.092-6.883z"/>
+ <path fill="#0b0b0b" d="m251.891 188.708c-1.671 1.557-2.741 8.978 0.445 11.529 3.177 2.489 9.15-1.683 9.358-4.774 0.15-5.72-8.13-8.385-9.803-6.755z"/>
+ <path fill="#0d0d0d" d="m252.028 188.848c-1.694 1.546-2.715 8.809 0.364 11.302 3.064 2.435 8.908-1.565 9.152-4.673 0.189-5.549-7.82-8.245-9.516-6.629z"/>
+ <path fill="#0f0f0f" d="m252.165 188.989c-1.716 1.535-2.688 8.64 0.282 11.074 2.953 2.38 8.669-1.447 8.948-4.572 0.226-5.378-7.512-8.106-9.23-6.502z"/>
+ <path fill="#111" d="m252.301 189.129c-1.737 1.524-2.659 8.471 0.2 10.847 2.844 2.325 8.431-1.33 8.743-4.471 0.266-5.207-7.201-7.966-8.943-6.376z"/>
+ <path fill="#131313" d="m252.438 189.269c-1.76 1.514-2.633 8.304 0.118 10.619 2.73 2.271 8.189-1.212 8.538-4.369 0.305-5.036-6.892-7.827-8.656-6.25z"/>
+ <path fill="#151515" d="m252.575 189.41c-1.783 1.503-2.606 8.133 0.036 10.391 2.62 2.216 7.949-1.094 8.332-4.268 0.344-4.865-6.581-7.687-8.368-6.123z"/>
+ <path fill="#161616" d="m252.712 189.55c-1.805 1.492-2.58 7.965-0.046 10.164 2.508 2.162 7.709-0.975 8.127-4.167 0.383-4.694-6.272-7.548-8.081-5.997z"/>
+ <path fill="#181818" d="m252.849 189.691c-1.828 1.481-2.554 7.796-0.129 9.937 2.397 2.105 7.47-0.857 7.922-4.066 0.423-4.524-5.961-7.409-7.793-5.871z"/>
+ <path fill="#1a1a1a" d="m252.985 189.831c-1.85 1.47-2.525 7.626-0.21 9.708 2.286 2.053 7.229-0.74 7.717-3.964 0.461-4.352-5.652-7.269-7.507-5.744z"/>
+ <path fill="#1c1c1c" d="m253.122 189.971c-1.872 1.46-2.499 7.459-0.292 9.482 2.175 1.996 6.989-0.623 7.511-3.865 0.501-4.18-5.341-7.128-7.219-5.617z"/>
+ <path fill="#1e1e1e" d="m253.259 190.112c-1.895 1.448-2.472 7.289-0.375 9.254 2.064 1.942 6.75-0.504 7.308-3.763 0.539-4.01-5.033-6.99-6.933-5.491z"/>
+ <path fill="#202020" d="m253.396 190.252c-1.917 1.438-2.445 7.122-0.457 9.027 1.953 1.888 6.51-0.386 7.102-3.662 0.578-3.839-4.722-6.85-6.645-5.365z"/>
+ <path fill="#222" d="m253.533 190.393c-1.939 1.426-2.418 6.951-0.539 8.799 1.841 1.832 6.271-0.268 6.896-3.561 0.618-3.668-4.412-6.711-6.357-5.238z"/>
+ <path fill="#242424" d="m253.669 190.533c-1.961 1.416-2.391 6.783-0.621 8.572 1.731 1.776 6.03-0.149 6.692-3.46 0.657-3.497-4.102-6.571-6.071-5.112z"/>
+ <path fill="#262626" d="m253.806 190.673c-1.984 1.405-2.364 6.615-0.703 8.344 1.619 1.724 5.79-0.032 6.485-3.358 0.697-3.326-3.791-6.432-5.782-4.986z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m250.71 256.698c1.513 1.512 2.809-2.232 4.32-3.457 1.512-1.224 3.96-3.888 8.856-3.888s4.535-0.144 4.319-2.017c-0.144-1.799-1.584-1.655-5.903-1.008-4.32 0.576-7.2 2.809-8.929 4.824-1.654 1.945-3.527 4.681-2.663 5.546z"/>
+ <path fill="#050505" d="m251.043 256.331c1.459 1.449 2.703-2.121 4.205-3.308 1.501-1.187 3.931-3.731 8.64-3.731 4.71-0.002 4.415-0.129 4.209-1.94-0.139-1.743-1.543-1.593-5.749-0.979-4.207 0.543-7.029 2.703-8.712 4.648-1.616 1.877-3.438 4.474-2.593 5.31z"/>
+ <path fill="#0a0a0a" d="m251.376 255.961c1.406 1.389 2.6-2.008 4.089-3.156 1.491-1.148 3.901-3.576 8.425-3.577 4.521-0.001 4.293-0.11 4.096-1.862-0.132-1.688-1.501-1.531-5.594-0.951-4.093 0.51-6.857 2.596-8.495 4.471-1.575 1.812-3.348 4.271-2.521 5.075z"/>
+ <path fill="#0f0f0f" d="m251.709 255.594c1.354 1.326 2.494-1.895 3.975-3.008 1.479-1.111 3.871-3.42 8.207-3.422s4.171-0.094 3.984-1.785c-0.126-1.631-1.46-1.467-5.438-0.924-3.979 0.479-6.687 2.492-8.28 4.297-1.533 1.747-3.257 4.066-2.448 4.842z"/>
+ <path fill="#141414" d="m252.042 255.226c1.302 1.265 2.39-1.783 3.858-2.856 1.47-1.076 3.842-3.266 7.991-3.268s4.05-0.077 3.873-1.709c-0.119-1.575-1.419-1.404-5.284-0.895-3.865 0.444-6.515 2.385-8.063 4.121-1.49 1.678-3.165 3.861-2.375 4.607z"/>
+ <path fill="#191919" d="m252.374 254.859c1.249 1.202 2.285-1.671 3.744-2.708 1.458-1.037 3.813-3.109 7.774-3.111 3.963-0.004 3.929-0.061 3.761-1.633-0.113-1.519-1.377-1.341-5.128-0.867-3.751 0.414-6.344 2.279-7.847 3.945-1.449 1.613-3.075 3.656-2.304 4.374z"/>
+ <path fill="#1e1e1e" d="m252.707 254.491c1.196 1.141 2.182-1.559 3.628-2.558 1.448-1 3.783-2.954 7.56-2.957 3.775-0.003 3.806-0.043 3.648-1.556-0.106-1.461-1.336-1.277-4.974-0.838-3.637 0.379-6.172 2.174-7.63 3.77-1.408 1.546-2.984 3.451-2.232 4.139z"/>
+ <path fill="#232323" d="m253.04 254.124c1.144 1.078 2.076-1.447 3.514-2.41 1.437-0.961 3.753-2.797 7.342-2.799 3.589-0.004 3.685-0.027 3.537-1.48-0.101-1.405-1.294-1.215-4.818-0.811-3.524 0.349-6.001 2.068-7.415 3.594-1.367 1.48-2.894 3.245-2.16 3.906z"/>
+ <path fill="#282828" d="m253.373 253.754c1.09 1.018 1.972-1.334 3.398-2.258 1.426-0.926 3.723-2.642 7.125-2.646 3.402-0.005 3.563-0.011 3.426-1.403-0.095-1.349-1.253-1.15-4.664-0.781-3.409 0.314-5.829 1.961-7.198 3.418-1.325 1.415-2.803 3.041-2.087 3.67z"/>
+ <path fill="#2d2d2d" d="m253.706 253.388c1.038 0.954 1.866-1.222 3.282-2.11 1.416-0.887 3.694-2.486 6.909-2.49 3.217-0.004 3.441 0.008 3.313-1.326-0.088-1.293-1.212-1.088-4.508-0.754-3.296 0.283-5.658 1.857-6.981 3.244-1.284 1.345-2.712 2.836-2.015 3.436z"/>
+ <path fill="#333" d="m254.039 253.02c0.985 0.893 1.762-1.109 3.167-1.96s3.664-2.33 6.693-2.335c3.029-0.006 3.32 0.023 3.202-1.249-0.082-1.237-1.171-1.024-4.354-0.726-3.182 0.25-5.485 1.75-6.765 3.066-1.243 1.282-2.622 2.634-1.943 3.204z"/>
+ <path fill="#383838" d="m254.372 252.652c0.933 0.831 1.657-0.998 3.051-1.81 1.396-0.813 3.636-2.174 6.478-2.18 2.843-0.006 3.199 0.039 3.09-1.172-0.076-1.181-1.129-0.963-4.198-0.697-3.068 0.217-5.314 1.644-6.55 2.891-1.202 1.214-2.531 2.427-1.871 2.968z"/>
+ <path fill="#3d3d3d" d="m254.704 252.284c0.88 0.771 1.553-0.885 2.938-1.66 1.383-0.775 3.604-2.019 6.26-2.024 2.656-0.007 3.078 0.058 2.979-1.095-0.069-1.125-1.088-0.899-4.044-0.67-2.954 0.185-5.144 1.539-6.333 2.715-1.16 1.148-2.441 2.222-1.8 2.734z"/>
+ <path fill="#424242" d="m255.037 251.917c0.827 0.707 1.448-0.773 2.821-1.51 1.373-0.738 3.576-1.863 6.044-1.871 2.47-0.007 2.956 0.074 2.867-1.018-0.063-1.068-1.046-0.836-3.889-0.641-2.841 0.151-4.973 1.433-6.116 2.539-1.119 1.083-2.35 2.018-1.727 2.501z"/>
+ <path fill="#474747" d="m255.37 251.549c0.774 0.645 1.344-0.661 2.706-1.362 1.362-0.7 3.545-1.706 5.828-1.713 2.283-0.009 2.834 0.09 2.754-0.942-0.057-1.012-1.005-0.773-3.733-0.613-2.727 0.119-4.801 1.328-5.899 2.365-1.078 1.013-2.26 1.81-1.656 2.265z"/>
+ <path fill="#4c4c4c" d="m255.703 251.181c0.721 0.584 1.239-0.55 2.59-1.212 1.353-0.663 3.517-1.551 5.612-1.559 2.096-0.009 2.713 0.107 2.643-0.865-0.051-0.955-0.964-0.709-3.577-0.584-2.613 0.086-4.631 1.222-5.686 2.188-1.035 0.949-2.168 1.607-1.582 2.032z"/>
+ <path fill="#515151" d="m256.036 250.813c0.669 0.521 1.134-0.436 2.476-1.063 1.341-0.625 3.485-1.395 5.395-1.402 1.91-0.01 2.591 0.124 2.531-0.789-0.044-0.898-0.922-0.646-3.423-0.557-2.499 0.055-4.458 1.117-5.469 2.014-0.994 0.882-2.077 1.402-1.51 1.797z"/>
+ <path fill="#565656" d="m256.369 250.446c0.616 0.459 1.029-0.324 2.36-0.912 1.33-0.589 3.456-1.24 5.178-1.248 1.723-0.01 2.47 0.141 2.42-0.713-0.039-0.842-0.881-0.582-3.268-0.527-2.387 0.021-4.287 1.01-5.252 1.836-0.953 0.816-1.987 1.199-1.438 1.564z"/>
+ <path fill="#5b5b5b" d="m256.701 250.079c0.564 0.397 0.926-0.213 2.245-0.764s3.427-1.084 4.963-1.093c1.536-0.011 2.348 0.157 2.307-0.636-0.031-0.785-0.839-0.52-3.112-0.5-2.271-0.01-4.116 0.906-5.035 1.662-0.913 0.751-1.898 0.993-1.368 1.331z"/>
+ <path fill="#606060" d="m257.034 249.709c0.511 0.336 0.821-0.1 2.13-0.612 1.309-0.515 3.397-0.929 4.746-0.938 1.351-0.01 2.227 0.176 2.196-0.558-0.026-0.729-0.799-0.457-2.958-0.472-2.158-0.043-3.945 0.799-4.82 1.486-0.87 0.682-1.805 0.788-1.294 1.094z"/>
+ <path fill="#666" d="m257.367 249.342c0.458 0.273 0.716 0.012 2.014-0.465 1.299-0.476 3.368-0.771 4.53-0.781 1.163-0.012 2.105 0.191 2.084-0.482-0.02-0.673-0.757-0.393-2.803-0.443-2.044-0.076-3.773 0.693-4.604 1.311-0.828 0.616-1.714 0.582-1.221 0.86z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m270.222 247.265c0 2.304 4.68 3.096 9.144 3.743 4.392 0.648 7.92 1.513 8.136 6.121 0.216 4.535-0.936 7.775 1.08 7.416 4.32-0.793 5.904-5.473 5.832-7.633 0-2.16-3.168-6.047-8.855-8.207-4.177-1.584-7.2-2.305-10.872-2.449-4.897-0.214-4.465 1.009-4.465 1.009z"/>
+ <path fill="#030303" d="m270.348 247.304c0.012 2.239 4.643 2.97 9.049 3.639 4.352 0.675 7.75 1.511 8.11 6.002 0.345 4.415-0.854 7.515 1.136 7.188 4.145-0.739 5.688-5.2 5.607-7.332-0.015-2.151-3.118-5.91-8.733-8.038-4.129-1.563-7.11-2.298-10.74-2.452-4.754-0.217-4.438 0.952-4.429 0.993z"/>
+ <path fill="#070707" d="m270.474 247.342c0.023 2.174 4.605 2.845 8.953 3.533 4.312 0.701 7.58 1.508 8.087 5.885 0.471 4.295-0.771 7.252 1.189 6.963 3.97-0.689 5.472-4.93 5.384-7.033-0.028-2.145-3.068-5.773-8.61-7.87-4.082-1.543-7.022-2.291-10.609-2.456-4.612-0.218-4.412 0.896-4.394 0.978z"/>
+ <path fill="#0b0b0b" d="m270.6 247.379c0.035 2.109 4.568 2.721 8.857 3.431 4.271 0.728 7.411 1.506 8.063 5.767 0.599 4.174-0.689 6.988 1.245 6.735 3.795-0.638 5.257-4.66 5.159-6.733-0.044-2.137-3.019-5.636-8.488-7.701-4.035-1.522-6.933-2.285-10.478-2.459-4.469-0.219-4.384 0.837-4.358 0.96z"/>
+ <path fill="#0f0f0f" d="m270.726 247.418c0.047 2.043 4.531 2.595 8.762 3.324 4.232 0.754 7.242 1.504 8.039 5.649 0.725 4.052-0.608 6.728 1.299 6.509 3.621-0.586 5.041-4.389 4.937-6.436-0.06-2.127-2.971-5.496-8.367-7.53-3.988-1.502-6.842-2.278-10.346-2.464-4.328-0.22-4.359 0.784-4.324 0.948z"/>
+ <path fill="#131313" d="m270.852 247.458c0.059 1.978 4.495 2.469 8.667 3.219 4.19 0.781 7.071 1.502 8.014 5.531 0.854 3.932-0.526 6.465 1.354 6.283 3.445-0.535 4.824-4.119 4.712-6.137-0.074-2.119-2.92-5.359-8.245-7.361-3.941-1.482-6.753-2.271-10.213-2.469-4.186-0.221-4.333 0.726-4.289 0.934z"/>
+ <path fill="#161616" d="m270.978 247.495c0.07 1.914 4.458 2.344 8.57 3.115 4.151 0.807 6.903 1.5 7.99 5.413 0.98 3.812-0.443 6.202 1.409 6.056 3.271-0.482 4.609-3.848 4.488-5.836-0.088-2.111-2.871-5.222-8.123-7.193-3.894-1.461-6.663-2.264-10.081-2.471-4.043-0.223-4.306 0.67-4.253 0.916z"/>
+ <path fill="#1a1a1a" d="m271.104 247.534c0.082 1.848 4.422 2.219 8.476 3.01 4.111 0.832 6.734 1.498 7.965 5.295 1.108 3.69-0.36 5.94 1.464 5.83 3.097-0.433 4.394-3.578 4.265-5.537-0.104-2.104-2.821-5.084-8-7.023-3.848-1.441-6.575-2.26-9.949-2.477-3.905-0.223-4.283 0.613-4.221 0.902z"/>
+ <path fill="#1e1e1e" d="m271.23 247.573c0.094 1.781 4.385 2.092 8.381 2.904 4.07 0.859 6.563 1.495 7.939 5.178 1.236 3.568-0.278 5.678 1.52 5.602 2.921-0.381 4.177-3.305 4.04-5.237-0.118-2.097-2.771-4.946-7.878-6.854-3.8-1.42-6.485-2.252-9.817-2.479-3.762-0.226-4.256 0.556-4.185 0.886z"/>
+ <path fill="#222" d="m271.356 247.61c0.105 1.717 4.348 1.969 8.285 2.801 4.029 0.885 6.395 1.493 7.916 5.059 1.362 3.449-0.197 5.416 1.573 5.377 2.746-0.329 3.962-3.036 3.816-4.939-0.133-2.087-2.722-4.808-7.756-6.684-3.753-1.4-6.396-2.247-9.686-2.484-3.618-0.227-4.227 0.499-4.148 0.87z"/>
+ <path fill="#262626" d="m271.482 247.648c0.118 1.651 4.311 1.843 8.189 2.694 3.99 0.914 6.226 1.492 7.892 4.943 1.491 3.328-0.115 5.152 1.629 5.149 2.571-0.278 3.746-2.765 3.592-4.64-0.146-2.08-2.672-4.672-7.634-6.516-3.705-1.38-6.306-2.24-9.553-2.488-3.478-0.225-4.203 0.446-4.115 0.858z"/>
+ <path fill="#2a2a2a" d="m271.608 247.687c0.129 1.587 4.273 1.716 8.094 2.59 3.95 0.938 6.056 1.489 7.867 4.825 1.618 3.206-0.033 4.891 1.684 4.922 2.396-0.227 3.53-2.494 3.368-4.34-0.162-2.072-2.623-4.534-7.512-6.348-3.658-1.358-6.216-2.232-9.421-2.492-3.336-0.226-4.177 0.39-4.08 0.843z"/>
+ <path fill="#2d2d2d" d="m271.734 247.725c0.141 1.521 4.237 1.591 7.999 2.484 3.909 0.967 5.886 1.488 7.842 4.707 1.746 3.086 0.05 4.629 1.739 4.697 2.221-0.176 3.313-2.225 3.144-4.041-0.177-2.064-2.572-4.396-7.389-6.178-3.612-1.339-6.128-2.227-9.29-2.497-3.194-0.227-4.151 0.334-4.045 0.828z"/>
+ <path fill="#313131" d="m271.86 247.763c0.153 1.456 4.2 1.466 7.903 2.381 3.869 0.991 5.717 1.485 7.817 4.589 1.873 2.965 0.132 4.365 1.794 4.469 2.046-0.123 3.099-1.953 2.92-3.742-0.191-2.055-2.523-4.258-7.267-6.008-3.565-1.318-6.038-2.22-9.158-2.5-3.051-0.229-4.124 0.276-4.009 0.811z"/>
+ <path fill="#353535" d="m271.986 247.801c0.165 1.392 4.163 1.341 7.808 2.275 3.829 1.018 5.547 1.483 7.794 4.473 2 2.843 0.213 4.103 1.848 4.242 1.873-0.074 2.883-1.683 2.696-3.443-0.206-2.047-2.474-4.12-7.145-5.838-3.517-1.299-5.948-2.215-9.026-2.506-2.91-0.229-4.099 0.221-3.975 0.797z"/>
+ <path fill="#393939" d="m272.112 247.84c0.176 1.326 4.126 1.215 7.712 2.17 3.789 1.045 5.378 1.482 7.771 4.354 2.127 2.723 0.295 3.842 1.901 4.016 1.698-0.021 2.667-1.412 2.474-3.144-0.222-2.038-2.426-3.981-7.023-5.669-3.47-1.277-5.859-2.208-8.894-2.509-2.769-0.231-4.074 0.164-3.941 0.782z"/>
+ <path fill="#3d3d3d" d="m272.238 247.877c0.188 1.262 4.089 1.09 7.617 2.066 3.749 1.071 5.208 1.479 7.745 4.236 2.255 2.602 0.377 3.578 1.957 3.789 1.522 0.029 2.451-1.141 2.249-2.844-0.236-2.033-2.375-3.846-6.901-5.5-3.423-1.258-5.769-2.203-8.762-2.514-2.626-0.231-4.046 0.109-3.905 0.767z"/>
+ <path fill="#414141" d="m272.364 247.917c0.2 1.195 4.052 0.965 7.522 1.961 3.708 1.098 5.037 1.477 7.72 4.119 2.383 2.479 0.46 3.315 2.012 3.562 1.348 0.081 2.236-0.87 2.025-2.545-0.251-2.022-2.325-3.707-6.778-5.331-3.377-1.237-5.681-2.195-8.631-2.518-2.485-0.233-4.02 0.05-3.87 0.752z"/>
+ <path fill="#444" d="m272.49 247.956c0.212 1.129 4.015 0.838 7.426 1.855 3.668 1.124 4.869 1.475 7.696 4 2.51 2.359 0.542 3.055 2.067 3.336 1.173 0.133 2.02-0.6 1.801-2.246-0.266-2.016-2.276-3.568-6.656-5.16-3.329-1.218-5.591-2.189-8.499-2.521-2.343-0.235-3.994-0.007-3.835 0.736z"/>
+ <path fill="#484848" d="m272.616 247.993c0.223 1.065 3.979 0.715 7.331 1.752 3.628 1.15 4.699 1.473 7.671 3.883 2.638 2.238 0.624 2.791 2.122 3.108 0.998 0.185 1.804-0.329 1.577-1.946-0.28-2.008-2.227-3.432-6.534-4.992-3.282-1.196-5.501-2.182-8.367-2.525-2.201-0.235-3.968-0.064-3.8 0.72z"/>
+ <path fill="#4c4c4c" d="m272.742 248.032c0.235 1 3.941 0.588 7.235 1.645 3.588 1.178 4.529 1.472 7.646 3.766 2.766 2.118 0.706 2.529 2.177 2.883 0.823 0.235 1.589-0.059 1.354-1.648-0.295-1.998-2.177-3.293-6.412-4.822-3.235-1.176-5.412-2.176-8.235-2.529-2.059-0.239-3.942-0.119-3.765 0.705z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#4c4c4c" d="m287.565 252.854c1.646 1 1.353 2.059 2.412 2.766 0.528 0.352 1.412 0.352 0.882-1-0.706-1.588-1.294-2.472-4.941-3.943-2.353-0.941-1.882 0.059 1.647 2.177z"/>
+ <path fill="#505050" d="m287.609 252.868c1.605 0.975 1.32 2.008 2.353 2.696 0.517 0.343 1.377 0.343 0.86-0.976-0.689-1.549-1.262-2.41-4.82-3.846-2.295-0.917-1.836 0.059 1.607 2.126z"/>
+ <path fill="#545454" d="m287.652 252.879c1.567 0.951 1.287 1.957 2.294 2.629 0.503 0.334 1.343 0.334 0.839-0.951-0.671-1.51-1.23-2.35-4.699-3.749-2.238-0.893-1.79 0.058 1.566 2.071z"/>
+ <path fill="#575757" d="m287.696 252.891c1.526 0.926 1.254 1.908 2.235 2.563 0.489 0.325 1.308 0.325 0.816-0.928-0.653-1.471-1.199-2.289-4.578-3.652-2.179-0.872-1.743 0.055 1.527 2.017z"/>
+ <path fill="#5b5b5b" d="m287.74 252.903c1.485 0.902 1.22 1.857 2.175 2.494 0.479 0.318 1.274 0.318 0.796-0.902-0.637-1.432-1.167-2.229-4.457-3.556-2.122-0.849-1.697 0.054 1.486 1.964z"/>
+ <path fill="#5f5f5f" d="m287.783 252.915c1.446 0.879 1.188 1.808 2.117 2.426 0.464 0.31 1.239 0.31 0.773-0.877-0.619-1.394-1.136-2.168-4.336-3.459-2.064-0.826-1.65 0.052 1.446 1.91z"/>
+ <path fill="#636363" d="m287.827 252.926c1.405 0.854 1.154 1.758 2.057 2.359 0.452 0.301 1.205 0.301 0.754-0.853-0.603-1.354-1.104-2.108-4.216-3.363-2.007-0.801-1.605 0.053 1.405 1.857z"/>
+ <path fill="#676767" d="m287.871 252.94c1.364 0.828 1.121 1.705 1.998 2.29 0.438 0.292 1.17 0.292 0.731-0.828-0.585-1.315-1.072-2.047-4.095-3.267-1.948-0.779-1.558 0.05 1.366 1.805z"/>
+ <path fill="#6b6b6b" d="m287.914 252.952c1.325 0.805 1.088 1.655 1.94 2.223 0.425 0.283 1.135 0.283 0.709-0.803-0.568-1.277-1.041-1.988-3.974-3.17-1.891-0.757-1.512 0.047 1.325 1.75z"/>
+ <path fill="#6e6e6e" d="m287.958 252.963c1.284 0.779 1.056 1.605 1.88 2.156 0.413 0.274 1.102 0.274 0.688-0.779-0.551-1.238-1.009-1.926-3.853-3.073-1.833-0.733-1.466 0.046 1.285 1.696z"/>
+ <path fill="#727272" d="m288.002 252.976c1.243 0.755 1.021 1.554 1.821 2.087 0.399 0.266 1.066 0.266 0.666-0.755-0.533-1.199-0.977-1.865-3.731-2.976-1.776-0.71-1.421 0.045 1.244 1.644z"/>
+ <path fill="#767676" d="m288.045 252.989c1.203 0.73 0.989 1.504 1.763 2.02 0.387 0.257 1.031 0.257 0.645-0.731-0.516-1.159-0.946-1.805-3.61-2.879-1.72-0.688-1.376 0.042 1.202 1.59z"/>
+ <path fill="#7a7a7a" d="m288.089 253c1.163 0.705 0.955 1.453 1.703 1.951 0.373 0.25 0.997 0.25 0.623-0.705-0.499-1.121-0.914-1.744-3.489-2.783-1.661-0.664-1.329 0.041 1.163 1.537z"/>
+ <path fill="#7e7e7e" d="m288.133 253.012c1.122 0.682 0.923 1.404 1.644 1.885 0.361 0.24 0.962 0.24 0.602-0.682-0.481-1.082-0.882-1.684-3.367-2.687-1.605-0.64-1.285 0.041 1.121 1.484z"/>
+ <path fill="#828282" d="m288.176 253.025c1.082 0.657 0.89 1.353 1.586 1.815 0.348 0.232 0.927 0.232 0.578-0.656-0.463-1.043-0.85-1.623-3.245-2.59-1.547-0.618-1.237 0.039 1.081 1.431z"/>
+ <path fill="#858585" d="m288.22 253.038c1.042 0.631 0.855 1.301 1.524 1.748 0.335 0.223 0.894 0.223 0.559-0.633-0.446-1.005-0.818-1.563-3.125-2.492-1.488-0.596-1.19 0.037 1.042 1.377z"/>
+ <path fill="#898989" d="m288.264 253.049c1.001 0.607 0.821 1.252 1.466 1.681 0.322 0.214 0.857 0.214 0.536-0.608-0.43-0.965-0.786-1.502-3.004-2.396-1.43-0.573-1.144 0.034 1.002 1.323z"/>
+ <path fill="#8d8d8d" d="m288.307 253.061c0.961 0.584 0.79 1.201 1.407 1.613 0.309 0.205 0.823 0.205 0.515-0.584-0.412-0.926-0.755-1.441-2.883-2.299-1.373-0.548-1.098 0.034 0.961 1.27z"/>
+ <path fill="#919191" d="m288.351 253.073c0.921 0.559 0.756 1.151 1.348 1.547 0.296 0.196 0.789 0.196 0.493-0.56-0.395-0.888-0.723-1.381-2.762-2.204-1.315-0.525-1.052 0.033 0.921 1.217z"/>
+ <path fill="#959595" d="m288.395 253.084c0.88 0.535 0.723 1.102 1.289 1.479 0.282 0.189 0.754 0.189 0.471-0.534-0.377-0.849-0.691-1.321-2.641-2.106-1.257-0.505-1.006 0.031 0.881 1.161z"/>
+ <path fill="#999" d="m288.438 253.097c0.84 0.51 0.689 1.05 1.229 1.409 0.271 0.181 0.721 0.181 0.45-0.51-0.36-0.81-0.66-1.26-2.52-2.01-1.199-0.48-0.959 0.031 0.841 1.111z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path d="m222.275 107.427c-0.738 0.902 0.574 8.365 5.412 13.285 4.839 4.838 7.791 4.838 9.759 2.706 3.771-4.018 0.738-7.791-1.558-10.415-2.297-2.624-5.248-1.722-7.955-4.346-2.706-2.624-4.592-2.46-5.658-1.23z"/>
+ <path fill="#050505" d="m222.345 107.494c-0.732 0.895 0.569 8.3 5.369 13.182 4.803 4.801 7.731 4.801 9.685 2.685 3.742-3.987 0.731-7.73-1.546-10.334-2.278-2.604-5.208-1.709-7.894-4.312-2.684-2.604-4.556-2.441-5.614-1.221z"/>
+ <path fill="#0a0a0a" d="m222.416 107.561c-0.727 0.888 0.565 8.235 5.328 13.079 4.763 4.763 7.67 4.763 9.607 2.664 3.713-3.956 0.727-7.67-1.534-10.253-2.26-2.584-5.166-1.696-7.831-4.279-2.664-2.583-4.521-2.422-5.57-1.211z"/>
+ <path fill="#0f0f0f" d="m222.486 107.628c-0.721 0.881 0.561 8.17 5.286 12.976 4.726 4.725 7.608 4.725 9.532 2.642 3.684-3.924 0.72-7.609-1.522-10.172-2.243-2.563-5.126-1.682-7.77-4.244-2.643-2.563-4.485-2.403-5.526-1.202z"/>
+ <path fill="#141414" d="m222.556 107.695c-0.716 0.874 0.557 8.105 5.243 12.872 4.689 4.688 7.55 4.688 9.456 2.622 3.655-3.893 0.716-7.549-1.51-10.091-2.224-2.543-5.085-1.669-7.707-4.211-2.621-2.543-4.449-2.384-5.482-1.192z"/>
+ <path fill="#191919" d="m222.627 107.762c-0.71 0.867 0.552 8.04 5.202 12.769 4.65 4.65 7.488 4.65 9.379 2.601 3.626-3.862 0.71-7.489-1.497-10.011s-5.044-1.655-7.646-4.177-4.414-2.364-5.438-1.182z"/>
+ <path fill="#1e1e1e" d="m222.697 107.829c-0.704 0.86 0.547 7.975 5.16 12.666 4.613 4.612 7.428 4.612 9.304 2.579 3.596-3.83 0.703-7.427-1.486-9.929-2.188-2.502-5.003-1.642-7.584-4.143-2.579-2.502-4.378-2.346-5.394-1.173z"/>
+ <path fill="#232323" d="m222.767 107.896c-0.697 0.853 0.543 7.91 5.117 12.562 4.576 4.575 7.367 4.575 9.229 2.559 3.567-3.8 0.698-7.367-1.473-9.848-2.171-2.482-4.963-1.629-7.522-4.11s-4.343-2.326-5.351-1.163z"/>
+ <path fill="#282828" d="m222.838 107.963c-0.691 0.846 0.538 7.845 5.076 12.459 4.537 4.537 7.307 4.537 9.152 2.538 3.537-3.769 0.691-7.307-1.461-9.768-2.154-2.461-4.922-1.615-7.461-4.076-2.538-2.461-4.307-2.307-5.306-1.153z"/>
+ <path fill="#2d2d2d" d="m222.908 108.03c-0.686 0.839 0.534 7.78 5.034 12.355 4.5 4.499 7.246 4.499 9.076 2.516 3.509-3.737 0.686-7.246-1.449-9.686-2.135-2.441-4.881-1.602-7.399-4.042-2.516-2.44-4.271-2.287-5.262-1.143z"/>
+ <path fill="#333" d="m222.978 108.096c-0.681 0.832 0.529 7.715 4.992 12.253 4.463 4.462 7.186 4.462 9.001 2.496 3.479-3.706 0.68-7.186-1.438-9.605-2.118-2.42-4.841-1.588-7.337-4.008s-4.235-2.27-5.218-1.136z"/>
+ <path fill="#383838" d="m223.048 108.163c-0.674 0.825 0.526 7.65 4.95 12.15 4.425 4.425 7.125 4.425 8.925 2.475 3.45-3.675 0.676-7.125-1.425-9.525-2.099-2.4-4.8-1.575-7.274-3.975-2.475-2.399-4.201-2.25-5.176-1.125z"/>
+ <path fill="#3d3d3d" d="m223.119 108.23c-0.669 0.818 0.521 7.585 4.908 12.047 4.387 4.387 7.063 4.387 8.849 2.453 3.42-3.643 0.669-7.064-1.413-9.443-2.082-2.38-4.759-1.562-7.214-3.941-2.453-2.38-4.164-2.231-5.13-1.116z"/>
+ <path fill="#424242" d="m223.189 108.297c-0.663 0.811 0.516 7.52 4.866 11.944 4.35 4.349 7.004 4.349 8.772 2.432 3.392-3.612 0.663-7.004-1.4-9.363-2.064-2.359-4.719-1.548-7.151-3.907-2.433-2.359-4.129-2.212-5.087-1.106z"/>
+ <path fill="#474747" d="m223.259 108.364c-0.656 0.804 0.513 7.455 4.824 11.84 4.313 4.312 6.944 4.312 8.697 2.412 3.362-3.582 0.658-6.944-1.388-9.282-2.046-2.339-4.679-1.535-7.09-3.874-2.411-2.338-4.093-2.192-5.043-1.096z"/>
+ <path fill="#4c4c4c" d="m223.33 108.431c-0.651 0.797 0.507 7.39 4.782 11.737 4.274 4.274 6.882 4.274 8.621 2.39 3.332-3.55 0.651-6.883-1.377-9.201s-4.636-1.521-7.028-3.839c-2.39-2.318-4.057-2.174-4.998-1.087z"/>
+ <path fill="#515151" d="m223.4 108.498c-0.646 0.79 0.503 7.325 4.74 11.634 4.236 4.236 6.821 4.236 8.545 2.369 3.304-3.519 0.646-6.822-1.364-9.12s-4.596-1.508-6.966-3.806c-2.369-2.298-4.022-2.154-4.955-1.077z"/>
+ <path fill="#565656" d="m223.47 108.565c-0.641 0.783 0.499 7.26 4.697 11.53 4.199 4.199 6.763 4.199 8.471 2.349 3.273-3.488 0.64-6.762-1.353-9.039-1.993-2.278-4.556-1.495-6.905-3.773-2.347-2.277-3.985-2.135-4.91-1.067z"/>
+ <path fill="#5b5b5b" d="m223.541 108.632c-0.635 0.776 0.493 7.195 4.656 11.427 4.161 4.161 6.701 4.161 8.393 2.327 3.245-3.456 0.636-6.701-1.34-8.958-1.975-2.257-4.514-1.48-6.843-3.738-2.327-2.257-3.95-2.116-4.866-1.058z"/>
+ <path fill="#606060" d="m223.611 108.699c-0.629 0.769 0.489 7.13 4.614 11.324 4.124 4.123 6.64 4.123 8.317 2.306 3.215-3.425 0.628-6.641-1.328-8.877-1.957-2.237-4.474-1.468-6.78-3.705-2.306-2.236-3.915-2.097-4.823-1.048z"/>
+ <path fill="#666" d="m223.681 108.765c-0.623 0.762 0.484 7.065 4.571 11.221 4.086 4.086 6.58 4.086 8.242 2.285 3.187-3.394 0.623-6.58-1.315-8.796-1.939-2.217-4.434-1.455-6.72-3.671-2.284-2.216-3.878-2.078-4.778-1.039z"/>
+ </g>
+ <g transform="translate(-12.4048,10.0005)">
+ <path fill="#fc0" d="m137.79 109.277c1.978 1.366 2.031 1.607 4.948 3.514 4.64 3.768 12.885 4.616 16.922 4.75 9.233 1.467 25.738-7.161 32.273-11.111 3.291-2.463 9.38-7.551 11.659-7.637 1.405 1.485-0.66 1.792-3.587 3.775-3.906 2.779-7.25 5.156-13.172 8.515-6.338 3.316-16.078 8.794-28.548 8.054-6.542-0.959-6.566-1.024-10.606-3.086-2.4-1.732-7.901-4.608-9.889-6.774z"/>
+ <linearGradient id="al" x1="129.342" gradientUnits="userSpaceOnUse" x2="195.598" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.305" y2="259.305">
+ <stop stop-color="#FAC700" offset="0"/>
+ <stop stop-color="#F7C400" offset=".415"/>
+ <stop stop-color="#F7C400" offset="1"/>
+ </linearGradient>
+ <path fill="url(#al)" d="m137.742 109.259c1.926 1.274 2.165 1.643 5.083 3.554 4.616 3.734 12.716 4.616 16.796 4.763 9.365 1.452 26.05-7.294 32.356-11.159 3.357-2.506 9.344-7.498 11.595-7.604 1.365 1.472-0.728 1.768-3.688 3.814-3.889 2.753-7.119 5.065-12.972 8.383-6.29 3.291-16.078 8.795-28.536 8.104-6.561-0.945-6.851-1.07-10.758-3.079-2.468-1.755-7.876-4.587-9.876-6.776z"/>
+ <linearGradient id="am" x1="129.293" gradientUnits="userSpaceOnUse" x2="195.554" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.311" y2="259.311">
+ <stop stop-color="#F6C200" offset="0"/>
+ <stop stop-color="#EFBC00" offset=".415"/>
+ <stop stop-color="#EFBC00" offset="1"/>
+ </linearGradient>
+ <path fill="url(#am)" d="m137.693 109.24c1.876 1.183 2.3 1.68 5.218 3.595 4.593 3.7 12.548 4.616 16.67 4.776 9.498 1.437 26.364-7.428 32.44-11.207 3.425-2.55 9.308-7.444 11.528-7.57 1.326 1.457-0.795 1.743-3.788 3.854-3.87 2.725-6.99 4.973-12.771 8.25-6.243 3.266-16.078 8.796-28.525 8.154-6.579-0.931-7.134-1.117-10.908-3.073-2.536-1.779-7.852-4.567-9.864-6.779z"/>
+ <linearGradient id="an" x1="129.245" gradientUnits="userSpaceOnUse" x2="195.51" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.317" y2="259.317">
+ <stop stop-color="#F1BD00" offset="0"/>
+ <stop stop-color="#E8B500" offset=".415"/>
+ <stop stop-color="#E8B500" offset="1"/>
+ </linearGradient>
+ <path fill="url(#an)" d="m137.645 109.222c1.825 1.091 2.434 1.715 5.352 3.635 4.569 3.665 12.38 4.615 16.544 4.789 9.631 1.422 26.677-7.562 32.524-11.255 3.491-2.594 9.271-7.392 11.463-7.538 1.287 1.442-0.861 1.718-3.889 3.893-3.853 2.699-6.86 4.882-12.57 8.119-6.195 3.241-16.078 8.797-28.513 8.203-6.6-0.916-7.418-1.163-11.061-3.066-2.603-1.801-7.826-4.546-9.85-6.78z"/>
+ <linearGradient id="ao" x1="129.196" gradientUnits="userSpaceOnUse" x2="195.465" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.322" y2="259.322">
+ <stop stop-color="#EDB800" offset="0"/>
+ <stop stop-color="#E0AD00" offset=".415"/>
+ <stop stop-color="#E0AD00" offset="1"/>
+ </linearGradient>
+ <path fill="url(#ao)" d="m137.596 109.203c1.774 1 2.568 1.752 5.487 3.676 4.545 3.631 12.211 4.615 16.418 4.801 9.764 1.408 26.989-7.695 32.608-11.302 3.557-2.637 9.236-7.338 11.396-7.505 1.247 1.427-0.928 1.693-3.99 3.932-3.833 2.672-6.729 4.791-12.369 7.986-6.148 3.217-16.078 8.799-28.501 8.254-6.619-0.902-7.702-1.21-11.21-3.059-2.672-1.825-7.803-4.525-9.839-6.783z"/>
+ <linearGradient id="ap" x1="129.148" gradientUnits="userSpaceOnUse" x2="195.422" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.327" y2="259.327">
+ <stop stop-color="#E9B300" offset="0"/>
+ <stop stop-color="#D8A500" offset=".415"/>
+ <stop stop-color="#D8A500" offset="1"/>
+ </linearGradient>
+ <path fill="url(#ap)" d="m137.548 109.184c1.724 0.909 2.703 1.788 5.622 3.717 4.522 3.597 12.043 4.615 16.292 4.814 9.896 1.393 27.303-7.829 32.692-11.35 3.624-2.681 9.2-7.286 11.331-7.472 1.208 1.412-0.995 1.668-4.092 3.972-3.814 2.644-6.6 4.698-12.168 7.853-6.101 3.192-16.077 8.8-28.489 8.303-6.638-0.887-7.986-1.256-11.361-3.052-2.741-1.848-7.779-4.504-9.827-6.785z"/>
+ <linearGradient id="aq" x1="129.099" gradientUnits="userSpaceOnUse" x2="195.379" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.332" y2="259.332">
+ <stop stop-color="#E4AE00" offset="0"/>
+ <stop stop-color="#D19E00" offset=".415"/>
+ <stop stop-color="#D19E00" offset="1"/>
+ </linearGradient>
+ <path fill="url(#aq)" d="m137.499 109.166c1.673 0.817 2.838 1.824 5.757 3.757 4.499 3.562 11.875 4.614 16.166 4.827 10.029 1.378 27.615-7.963 32.776-11.398 3.691-2.725 9.164-7.232 11.265-7.439 1.169 1.397-1.061 1.644-4.191 4.01-3.796 2.618-6.469 4.608-11.968 7.722-6.053 3.167-16.077 8.801-28.478 8.353-6.657-0.873-8.27-1.303-11.512-3.046-2.809-1.87-7.755-4.483-9.815-6.786z"/>
+ <linearGradient id="ar" x1="129.051" gradientUnits="userSpaceOnUse" x2="195.338" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.336" y2="259.336">
+ <stop stop-color="#E0A900" offset="0"/>
+ <stop stop-color="#C99600" offset=".415"/>
+ <stop stop-color="#C99600" offset="1"/>
+ </linearGradient>
+ <path fill="url(#ar)" d="m137.451 109.147c1.622 0.726 2.972 1.86 5.892 3.798 4.475 3.528 11.705 4.614 16.04 4.84 10.161 1.362 27.929-8.097 32.859-11.447 3.757-2.767 9.128-7.178 11.2-7.405 1.13 1.382-1.128 1.619-4.294 4.049-3.777 2.592-6.339 4.517-11.767 7.589-6.005 3.143-16.077 8.803-28.466 8.404-6.676-0.859-8.553-1.35-11.663-3.039-2.876-1.894-7.729-4.462-9.801-6.789z"/>
+ <linearGradient id="w" x1="129.003" gradientUnits="userSpaceOnUse" x2="195.296" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.34" y2="259.34">
+ <stop stop-color="#DCA400" offset="0"/>
+ <stop stop-color="#C18E00" offset=".415"/>
+ <stop stop-color="#C18E00" offset="1"/>
+ </linearGradient>
+ <path fill="url(#w)" d="m137.403 109.129c1.571 0.634 3.105 1.896 6.026 3.838 4.45 3.494 11.536 4.614 15.913 4.852 10.295 1.348 28.241-8.23 32.943-11.494 3.824-2.811 9.092-7.125 11.134-7.373 1.092 1.367-1.194 1.594-4.394 4.088-3.759 2.565-6.209 4.425-11.566 7.457-5.957 3.118-16.077 8.804-28.454 8.453-6.694-0.844-8.837-1.396-11.813-3.032-2.945-1.916-7.706-4.44-9.789-6.789z"/>
+ <linearGradient id="x" x1="128.954" gradientUnits="userSpaceOnUse" x2="195.255" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.343" y2="259.343">
+ <stop stop-color="#D79F00" offset="0"/>
+ <stop stop-color="#BA8700" offset=".415"/>
+ <stop stop-color="#BA8700" offset="1"/>
+ </linearGradient>
+ <path fill="url(#x)" d="m137.354 109.11c1.521 0.543 3.241 1.932 6.161 3.879 4.428 3.459 11.368 4.613 15.788 4.865 10.427 1.333 28.554-8.364 33.026-11.542 3.892-2.855 9.057-7.073 11.068-7.339 1.052 1.353-1.261 1.569-4.495 4.127-3.74 2.538-6.078 4.334-11.365 7.325-5.91 3.093-16.077 8.805-28.441 8.503-6.716-0.83-9.121-1.443-11.966-3.026-3.012-1.939-7.68-4.42-9.776-6.792z"/>
+ <linearGradient id="y" x1="128.906" gradientUnits="userSpaceOnUse" x2="195.216" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.348" y2="259.348">
+ <stop stop-color="#D39B00" offset="0"/>
+ <stop stop-color="#B27F00" offset=".415"/>
+ <stop stop-color="#B27F00" offset="1"/>
+ </linearGradient>
+ <path fill="url(#y)" d="m137.306 109.091c1.47 0.451 3.375 1.969 6.296 3.919 4.403 3.426 11.2 4.614 15.662 4.879 10.559 1.318 28.865-8.498 33.109-11.59 3.958-2.899 9.021-7.02 11.003-7.306 1.013 1.337-1.328 1.544-4.596 4.167-3.722 2.511-5.949 4.242-11.165 7.192-5.862 3.068-16.077 8.807-28.43 8.553-6.734-0.816-9.405-1.489-12.116-3.019-3.08-1.963-7.656-4.4-9.763-6.795z"/>
+ <linearGradient id="z" x1="128.857" gradientUnits="userSpaceOnUse" x2="195.176" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.35" y2="259.35">
+ <stop stop-color="#CF9600" offset="0"/>
+ <stop stop-color="#a70" offset=".415"/>
+ <stop stop-color="#a70" offset="1"/>
+ </linearGradient>
+ <path fill="url(#z)" d="m137.257 109.073c1.421 0.359 3.511 2.005 6.432 3.959 4.38 3.392 11.032 4.614 15.536 4.892 10.691 1.303 29.179-8.631 33.193-11.638 4.024-2.942 8.984-6.967 10.938-7.274 0.973 1.323-1.396 1.521-4.697 4.206-3.703 2.484-5.818 4.151-10.964 7.06-5.814 3.043-16.077 8.808-28.418 8.603-6.753-0.802-9.689-1.535-12.268-3.012-3.149-1.986-7.632-4.378-9.752-6.796z"/>
+ <linearGradient id="aa" x1="128.809" gradientUnits="userSpaceOnUse" x2="195.137" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.354" y2="259.354">
+ <stop stop-color="#CA9100" offset="0"/>
+ <stop stop-color="#A37000" offset=".415"/>
+ <stop stop-color="#A37000" offset="1"/>
+ </linearGradient>
+ <path fill="url(#aa)" d="m137.209 109.054c1.368 0.268 3.645 2.041 6.565 4 4.356 3.357 10.864 4.613 15.41 4.904 10.824 1.289 29.493-8.764 33.277-11.685 4.092-2.986 8.948-6.914 10.871-7.241 0.935 1.308-1.461 1.496-4.797 4.245-3.685 2.457-5.688 4.06-10.763 6.928-5.768 3.018-16.077 8.809-28.407 8.653-6.771-0.788-9.972-1.582-12.418-3.006-3.216-2.008-7.607-4.357-9.738-6.798z"/>
+ <linearGradient id="ab" x1="128.76" gradientUnits="userSpaceOnUse" x2="195.099" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.356" y2="259.356">
+ <stop stop-color="#C68C00" offset="0"/>
+ <stop stop-color="#9B6800" offset=".415"/>
+ <stop stop-color="#9B6800" offset="1"/>
+ </linearGradient>
+ <path fill="url(#ab)" d="m137.16 109.036c1.318 0.176 3.779 2.077 6.701 4.04 4.333 3.323 10.695 4.613 15.284 4.917 10.957 1.274 29.805-8.898 33.36-11.733 4.158-3.03 8.912-6.86 10.807-7.208 0.894 1.292-1.528 1.471-4.899 4.284-3.666 2.43-5.558 3.968-10.562 6.796-5.72 2.993-16.077 8.81-28.396 8.702-6.791-0.773-10.256-1.628-12.568-2.999-3.285-2.031-7.583-4.336-9.727-6.799z"/>
+ <linearGradient id="ac" x1="128.712" gradientUnits="userSpaceOnUse" x2="195.062" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.358" y2="259.358">
+ <stop stop-color="#C28700" offset="0"/>
+ <stop stop-color="#936000" offset=".415"/>
+ <stop stop-color="#936000" offset="1"/>
+ </linearGradient>
+ <path fill="url(#ac)" d="m137.112 109.017c1.267 0.085 3.912 2.113 6.835 4.081 4.31 3.289 10.527 4.613 15.158 4.93 11.09 1.258 30.118-9.032 33.445-11.782 4.225-3.072 8.877-6.807 10.739-7.174 0.855 1.278-1.595 1.446-5 4.323-3.647 2.404-5.427 3.877-10.36 6.663-5.673 2.969-16.077 8.812-28.384 8.753-6.811-0.759-10.54-1.675-12.719-2.992-3.353-2.055-7.559-4.315-9.714-6.802z"/>
+ <linearGradient id="ad" x1="128.663" gradientUnits="userSpaceOnUse" x2="195.025" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.36" y2="259.36">
+ <stop stop-color="#BD8200" offset="0"/>
+ <stop stop-color="#8C5900" offset=".415"/>
+ <stop stop-color="#8C5900" offset="1"/>
+ </linearGradient>
+ <path fill="url(#ad)" d="m137.063 108.998c1.217-0.006 4.047 2.15 6.97 4.122 4.286 3.254 10.359 4.612 15.032 4.943 11.223 1.243 30.431-9.166 33.53-11.83 4.289-3.116 8.84-6.753 10.673-7.141 0.815 1.263-1.661 1.421-5.101 4.362-3.63 2.377-5.298 3.785-10.161 6.531-5.624 2.944-16.075 8.813-28.37 8.802-6.83-0.744-10.824-1.721-12.87-2.985-3.422-2.077-7.535-4.294-9.703-6.804z"/>
+ <linearGradient id="ae" x1="128.615" gradientUnits="userSpaceOnUse" x2="194.989" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.362" y2="259.362">
+ <stop stop-color="#B97D00" offset="0"/>
+ <stop stop-color="#845100" offset=".415"/>
+ <stop stop-color="#845100" offset="1"/>
+ </linearGradient>
+ <path fill="url(#ae)" d="m137.015 108.98c1.166-0.098 4.181 2.185 7.104 4.162 4.262 3.22 10.19 4.612 14.906 4.955 11.354 1.229 30.743-9.299 33.613-11.877 4.356-3.16 8.804-6.7 10.607-7.108 0.776 1.248-1.728 1.397-5.202 4.401-3.61 2.35-5.167 3.694-9.96 6.399-5.576 2.919-16.076 8.814-28.358 8.852-6.85-0.73-11.108-1.768-13.021-2.979-3.489-2.1-7.51-4.273-9.689-6.805z"/>
+ <linearGradient id="af" x1="128.567" gradientUnits="userSpaceOnUse" x2="194.954" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.364" y2="259.364">
+ <stop stop-color="#B57800" offset="0"/>
+ <stop stop-color="#7C4900" offset=".415"/>
+ <stop stop-color="#7C4900" offset="1"/>
+ </linearGradient>
+ <path fill="url(#af)" d="m136.967 108.961c1.115-0.189 4.315 2.222 7.239 4.203 4.239 3.186 10.021 4.612 14.78 4.968 11.488 1.214 31.057-9.433 33.697-11.925 4.424-3.203 8.768-6.647 10.542-7.075 0.736 1.233-1.795 1.372-5.303 4.44-3.593 2.323-5.038 3.603-9.759 6.266-5.529 2.895-16.077 8.816-28.348 8.903-6.868-0.716-11.392-1.815-13.172-2.972-3.557-2.124-7.485-4.252-9.676-6.808z"/>
+ <linearGradient id="ah" x1="128.518" gradientUnits="userSpaceOnUse" x2="194.918" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.366" y2="259.366">
+ <stop stop-color="#B07300" offset="0"/>
+ <stop stop-color="#754200" offset=".415"/>
+ <stop stop-color="#754200" offset="1"/>
+ </linearGradient>
+ <path fill="url(#ah)" d="m136.918 108.943c1.064-0.281 4.45 2.257 7.374 4.243 4.216 3.151 9.854 4.611 14.654 4.981 11.621 1.199 31.37-9.567 33.781-11.973 4.49-3.247 8.731-6.594 10.476-7.042 0.698 1.218-1.861 1.347-5.403 4.479-3.573 2.296-4.906 3.511-9.558 6.134-5.48 2.87-16.076 8.817-28.336 8.952-6.887-0.701-11.675-1.861-13.323-2.965-3.626-2.146-7.462-4.231-9.665-6.809z"/>
+ <linearGradient id="ai" x1="128.47" gradientUnits="userSpaceOnUse" x2="194.886" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.367" y2="259.367">
+ <stop stop-color="#AC6E00" offset="0"/>
+ <stop stop-color="#6D3A00" offset=".415"/>
+ <stop stop-color="#6D3A00" offset="1"/>
+ </linearGradient>
+ <path fill="url(#ai)" d="m136.87 108.924c1.013-0.372 4.584 2.294 7.509 4.284 4.191 3.117 9.685 4.611 14.528 4.994 11.753 1.184 31.682-9.701 33.864-12.021 4.557-3.291 8.695-6.542 10.411-7.009 0.657 1.203-1.929 1.322-5.504 4.518-3.557 2.269-4.777 3.42-9.358 6.002-5.434 2.845-16.076 8.818-28.324 9.002-6.907-0.687-11.959-1.908-13.474-2.959-3.694-2.169-7.437-4.21-9.652-6.811z"/>
+ <linearGradient id="aj" x1="128.421" gradientUnits="userSpaceOnUse" x2="194.85" gradientTransform="matrix(1,0,0,-1,8.3999,368.3)" y1="259.369" y2="259.369">
+ <stop stop-color="#A86A00" offset="0"/>
+ <stop stop-color="#663200" offset=".415"/>
+ <stop stop-color="#663200" offset="1"/>
+ </linearGradient>
+ <path fill="url(#aj)" d="m136.821 108.905c0.963-0.464 4.719 2.33 7.644 4.324 4.168 3.083 9.517 4.611 14.402 5.007 11.886 1.169 31.995-9.834 33.948-12.069 4.624-3.334 8.66-6.487 10.345-6.976 0.619 1.188-1.995 1.298-5.604 4.558-3.537 2.242-4.647 3.328-9.157 5.869-5.386 2.82-16.076 8.82-28.313 9.052-6.926-0.673-12.243-1.954-13.625-2.952-3.762-2.192-7.413-4.189-9.64-6.813z"/>
+ </g>
+ </g>
+</svg>
diff --git a/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg b/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg
new file mode 100644
index 00000000..9afea859
--- /dev/null
+++ b/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg
@@ -0,0 +1,248 @@
+<ns0:svg height="592.78998" id="svg2275" version="1.0" width="958.69" ns1:docbase="C:\Users\Adam\Desktop" ns1:docname="Blank_US_Map_with_borders.svg" ns1:version="0.32" ns2:output_extension="org.inkscape.output.svg.inkscape" ns2:version="0.46" xmlns:ns0="http://www.w3.org/2000/svg" xmlns:ns1="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:ns2="http://www.inkscape.org/namespaces/inkscape">
+ <ns0:metadata id="metadata2625">
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <ns4:Work rdf:about="" xmlns:ns4="http://creativecommons.org/ns#">
+ <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format>
+ <ns5:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" xmlns:ns5="http://purl.org/dc/elements/1.1/" />
+ </ns4:Work>
+ </rdf:RDF>
+ </ns0:metadata>
+ <ns0:defs id="defs2623">
+ <ns2:perspective id="perspective226" ns1:type="inkscape:persp3d" ns2:persp3d-origin="479.345 : 197.59666 : 1" ns2:vp_x="0 : 296.39499 : 1" ns2:vp_y="0 : 1000 : 0" ns2:vp_z="958.69 : 296.39499 : 1" />
+ </ns0:defs>
+ <ns1:namedview bordercolor="#666666" borderopacity="1.0" gridtolerance="10.0" guidetolerance="10.0" id="base" objecttolerance="10.0" pagecolor="#ffffff" showgrid="false" ns2:current-layer="svg2275" ns2:cx="479.345" ns2:cy="299.1307" ns2:pageopacity="0.0" ns2:pageshadow="2" ns2:window-height="820" ns2:window-width="1440" ns2:window-x="-8" ns2:window-y="-8" ns2:zoom="0.99941554" />
+ <ns0:path d="M 798.49579,591.98217 L 799.73403,593.07468 L 802.54072,590.88965 L 807.98899,586.51962 L 811.78627,582.48573 L 814.3453,575.59451 L 815.3359,573.82969 L 815.501,570.30004 L 814.75805,570.80427 L 813.76746,573.74564 L 812.28156,578.53588 L 808.97958,583.99844 L 804.52191,588.36847 L 801.05483,590.38542 L 798.49579,591.98217 z M 784.71002,597.19259 L 787.18651,596.52028 L 788.5073,596.26817 L 789.99319,593.83102 L 792.38713,592.15024 L 793.70792,592.65448 L 795.44146,592.99063 L 795.8542,594.08315 L 792.30458,595.34374 L 788.012,596.85644 L 785.61807,598.11703 L 784.71002,597.19259 z M 657.3254,482.07418 L 660.96585,481.47149 L 667.07449,479.28647 L 673.18314,478.78224 L 677.6408,478.10993 L 685.40042,479.95879 L 693.65535,483.99266 L 695.30633,485.50536 L 698.2781,486.6819 L 699.92909,488.69884 L 700.25929,491.55616 L 703.56126,490.21154 L 707.52362,490.21154 L 711.15578,488.1946 L 714.95305,484.49689 L 718.08992,484.66497 L 718.58522,483.48842 L 717.75972,482.47995 L 717.92482,480.46302 L 722.05228,479.62263 L 724.69386,479.62263 L 727.66563,481.13533 L 731.95819,482.64803 L 734.43467,486.51382 L 737.24134,487.52229 L 738.39703,491.05193 L 741.8641,492.73271 L 743.51508,495.42195 L 745.49627,496.09427 L 750.77942,497.43889 L 752.1002,500.63237 L 755.23708,504.49816 L 755.23708,514.41476 L 753.75119,519.28902 L 754.08139,522.14634 L 755.40217,527.18868 L 757.21826,531.39063 L 758.04375,530.8864 L 759.52964,526.18021 L 756.88806,525.17175 L 756.55786,524.49943 L 758.20885,523.82712 L 762.83161,524.83559 L 762.9967,526.51637 L 759.69473,532.23102 L 757.54845,534.75219 L 761.18062,538.61798 L 763.8222,541.81146 L 766.79397,547.35803 L 769.76574,551.3919 L 771.91202,556.60232 L 773.7281,556.93847 L 775.37909,554.75346 L 777.19517,555.93001 L 779.83675,560.13195 L 780.49714,563.82967 L 783.63401,568.36777 L 784.4595,567.02315 L 788.42187,567.3593 L 792.05403,569.7124 L 795.5211,575.09089 L 796.34659,578.62053 L 796.67679,581.64593 L 797.83248,582.6544 L 799.15327,583.15863 L 801.62975,582.15016 L 803.11563,580.46938 L 807.078,580.3013 L 810.21487,578.7886 L 813.02154,575.42704 L 812.52624,573.41011 L 812.19605,570.88894 L 812.85644,568.87201 L 812.52624,566.85507 L 815.00272,565.51045 L 815.33292,561.98081 L 814.67252,560.13195 L 814.17723,547.69419 L 812.85644,539.79453 L 808.23368,531.22255 L 804.60152,525.17175 L 801.95994,519.62517 L 798.98817,516.59977 L 796.0164,508.86819 L 796.84189,507.52356 L 797.99758,506.17894 L 796.34659,503.15354 L 792.21913,499.28775 L 787.26618,493.5731 L 783.46891,487.01806 L 778.02066,477.26954 L 774.21165,467.14054 L 771.56179,458.12552" id="FL_Gulf" style="fill:#cccccc;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 772.07835,458.61245 L 774.21165,467.14054 L 778.02066,477.26954 L 783.46891,487.01806 L 787.26618,493.5731 L 792.21913,499.28775 L 796.34659,503.15354 L 797.99758,506.17894 L 796.84189,507.52356 L 796.0164,508.86819 L 798.98817,516.59977 L 801.95994,519.62517 L 804.60152,525.17175 L 808.23368,531.22255 L 812.85644,539.79453 L 814.17723,547.69419 L 814.67252,560.13195 L 815.33292,561.98081 L 815.00272,565.51045 L 812.52624,566.85507 L 812.85644,568.87201 L 812.19605,570.88894 L 812.52624,573.41011 L 813.02154,575.42704 L 810.21487,578.7886 L 807.078,580.3013 L 803.11563,580.46938 L 801.62975,582.15016 L 799.15327,583.15863 L 797.83248,582.6544 L 796.67679,581.64593 L 796.34659,578.62053 L 795.5211,575.09089 L 792.05403,569.7124 L 788.42187,567.3593 L 784.4595,567.02315 L 783.63401,568.36777 L 780.49714,563.82967 L 779.83675,560.13195 L 777.19517,555.93001 L 775.37909,554.75346 L 773.7281,556.93847 L 771.91202,556.60232 L 769.76574,551.3919 L 766.79397,547.35803 L 763.8222,541.81146 L 761.18062,538.61798 L 757.54845,534.75219 L 759.69473,532.23102 L 762.9967,526.51637 L 762.83161,524.83559 L 758.20885,523.82712 L 756.55786,524.49943 L 756.88806,525.17175 L 759.52964,526.18021 L 758.04375,530.8864 L 757.21826,531.39063 L 755.40217,527.18868 L 754.08139,522.14634 L 753.75119,519.28902 L 755.23708,514.41476 L 755.23708,504.49816 L 752.1002,500.63237 L 750.77942,497.43889 L 745.49627,496.09427 L 743.51508,495.42195 L 741.8641,492.73271 L 738.39703,491.05193 L 737.24134,487.52229 L 734.43467,486.51382 L 731.95819,482.64803 L 727.66563,481.13533 L 724.69386,479.62263 L 722.05228,479.62263 L 717.92482,480.46302 L 717.75972,482.47995 L 718.58522,483.48842 L 718.08992,484.66497 L 714.95305,484.49689 L 711.15578,488.1946 L 707.52362,490.21154 L 703.56126,490.21154 L 700.25929,491.55616 L 699.92909,488.69884 L 698.2781,486.6819 L 695.30633,485.50536 L 693.65535,483.99266 L 685.40042,479.95879 L 677.6408,478.10993 L 673.18314,478.78224 L 667.07449,479.28647 L 660.96585,481.47149 L 657.41261,482.10878 L 657.16963,473.73948 L 654.52806,471.72255 L 652.71197,469.87369 L 653.04217,466.6802 L 663.44338,465.33558 L 689.52898,462.31017 L 696.46313,461.63786 L 702.73688,461.46977 L 705.37846,465.50365 L 706.86434,467.01635 L 714.95419,467.18443 L 726.00404,466.51212 L 747.97393,465.1675 L 753.53546,464.46636 L 758.21005,464.49519 L 758.37515,467.52059 L 761.01673,468.36098 L 761.34692,463.82287 L 759.69594,459.11668 L 760.85163,457.4359 L 766.79518,458.27629 L 772.07835,458.61245 z M 784.71002,597.19259 L 787.18651,596.52028 L 788.5073,596.26817 L 789.99319,593.83102 L 792.38713,592.15024 L 793.70792,592.65448 L 795.44146,592.99063 L 795.8542,594.08315 L 792.30458,595.34374 L 788.012,596.85644 L 785.61807,598.11703 L 784.71002,597.19259 z M 798.49579,591.98217 L 799.73403,593.07468 L 802.54072,590.88965 L 807.98899,586.51962 L 811.78627,582.48573 L 814.3453,575.59451 L 815.3359,573.82969 L 815.501,570.30004 L 814.75805,570.80427 L 813.76746,573.74564 L 812.28156,578.53588 L 808.97958,583.99844 L 804.52191,588.36847 L 801.05483,590.38542 L 798.49579,591.98217 z " id="FL" style="fill:#501616" />
+ <ns0:path d="M 777.85557,425.66962 L 776.04071,426.6776 L 773.39913,425.33297 L 772.73874,423.14795 L 771.41795,419.45024 L 769.10656,417.26521 L 766.46499,416.5929 L 764.814,411.55057 L 762.00732,405.33167 L 757.71476,403.31473 L 755.56847,401.29779 L 754.24768,398.60854 L 752.1014,396.5916 L 749.79002,395.24697 L 747.47863,392.22157 L 744.34176,389.86848 L 739.71899,388.01961 L 739.2237,386.50691 L 736.74722,383.48151 L 736.25192,381.9688 L 732.78485,376.5903 L 729.31778,376.75838 L 725.19031,374.2372 L 723.86952,372.89258 L 723.53932,371.04372 L 724.36481,369.02679 L 726.67619,368.01831 L 726.346,365.8333 L 732.61975,363.14405 L 741.86527,358.43786 L 749.29471,357.59747 L 766.13479,357.09323 L 768.44617,359.11017 L 770.09715,362.47174 L 774.55482,361.9675 L 787.43252,360.45479 L 790.4043,361.29519 L 803.282,369.19486 L 813.60504,377.63896 L 808.06859,383.31398 L 805.42701,389.70094 L 804.93171,396.25598 L 803.28073,397.09637 L 802.12504,399.95369 L 799.64856,400.62601 L 797.50228,404.32372 L 794.69561,407.18104 L 792.38423,410.71068 L 790.73325,411.55107 L 787.10108,415.08071 L 784.12931,415.24879 L 785.1199,418.61034 L 780.00185,424.32499 L 777.85557,425.66962 z " id="SC" style="fill:#e9afaf" />
+ <ns0:path d="M 704.71806,368.52255 L 699.7651,369.36294 L 691.17997,370.53949 L 682.42974,371.46391 L 682.42974,373.73297 L 682.59484,375.91799 L 683.25523,379.44763 L 686.72231,387.68346 L 689.19879,397.93623 L 690.68467,404.3232 L 692.33566,409.36554 L 693.82155,416.5929 L 695.96784,423.14795 L 698.60941,426.6776 L 699.10471,430.20724 L 701.0859,431.04763 L 701.251,433.23265 L 699.4349,438.27499 L 698.93961,441.63656 L 698.77451,443.65349 L 700.4255,448.19161 L 700.7557,453.73818 L 699.9302,456.25936 L 700.5906,457.09975 L 702.07649,457.94014 L 702.73688,461.46977 L 705.37846,465.50365 L 706.86434,467.01635 L 714.95419,467.18443 L 726.00404,466.51212 L 747.97393,465.1675 L 753.53546,464.46636 L 758.21005,464.49519 L 758.37515,467.52059 L 761.01673,468.36098 L 761.34692,463.82287 L 759.69594,459.11668 L 760.85163,457.4359 L 766.79518,458.27629 L 771.87844,458.60669 L 771.08653,452.05785 L 773.39791,441.63702 L 774.88379,437.26699 L 774.3885,434.57775 L 778.51596,427.3504 L 777.90454,425.66937 L 776.04071,426.6776 L 773.39913,425.33297 L 772.73874,423.14795 L 771.41795,419.45024 L 769.10656,417.26521 L 766.46499,416.5929 L 764.814,411.55057 L 762.00732,405.33167 L 757.71476,403.31473 L 755.56847,401.29779 L 754.24768,398.60854 L 752.1014,396.5916 L 749.79002,395.24697 L 747.47863,392.22157 L 744.34176,389.86848 L 739.71899,388.01961 L 739.2237,386.50691 L 736.74722,383.48151 L 736.25192,381.9688 L 732.78485,376.5903 L 729.31778,376.75838 L 725.19031,374.2372 L 723.86952,372.89258 L 723.53932,371.04372 L 724.36481,369.02679 L 726.67619,368.01831 L 726.51109,365.64481 L 724.69501,366.16945 L 718.75146,367.17792 L 711.65221,368.01831 L 704.71806,368.52255 z " id="GA" style="fill:#d35f5f" />
+ <ns0:path d="M 639.33795,481.63956 L 637.68799,465.83981 L 634.88131,446.34274 L 635.04641,431.71994 L 635.8719,399.44893 L 635.7068,382.13688 L 635.87539,375.46299 L 643.79664,375.07759 L 672.19362,372.38834 L 682.58068,371.46391 L 682.42974,373.73297 L 682.59484,375.91799 L 683.25523,379.44763 L 686.72231,387.68346 L 689.19879,397.93623 L 690.68467,404.3232 L 692.33566,409.36554 L 693.82155,416.5929 L 695.96784,423.14795 L 698.60941,426.6776 L 699.10471,430.20724 L 701.0859,431.04763 L 701.251,433.23265 L 699.4349,438.27499 L 698.93961,441.63656 L 698.77451,443.65349 L 700.4255,448.19161 L 700.7557,453.73818 L 699.9302,456.25936 L 700.5906,457.09975 L 702.07649,457.94014 L 702.90198,461.63786 L 696.46313,461.63786 L 689.52898,462.31017 L 663.44338,465.33558 L 653.04217,466.6802 L 652.71197,469.87369 L 654.52806,471.72255 L 657.16963,473.73948 L 657.76284,481.98993 L 651.05994,484.66497 L 648.25327,484.32881 L 651.05994,482.31188 L 651.05994,481.30341 L 647.92307,475.08453 L 645.61169,474.41221 L 644.12581,478.95032 L 642.80502,481.80764 L 642.14462,481.63956 L 639.33795,481.63956 z " id="AL" style="fill:#e9afaf" />
+ <ns0:path d="M 850.23842,306.65958 L 851.98478,311.54471 L 855.61694,318.26782 L 858.09342,320.78899 L 858.75382,323.14208 L 856.27734,323.31016 L 857.10283,323.98247 L 856.77263,328.3525 L 854.13106,329.69712 L 853.47066,331.88214 L 852.14988,334.90754 L 848.35261,336.58832 L 845.87614,336.25216 L 844.39025,336.08408 L 842.73926,334.73946 L 843.06946,336.08408 L 843.06946,337.09255 L 845.05064,337.09255 L 845.87614,338.43717 L 843.89495,344.99221 L 848.18751,344.99221 L 848.84791,346.67299 L 851.15929,344.3199 L 852.48007,343.81567 L 850.49889,347.51338 L 847.36202,352.55572 L 846.04123,352.55572 L 844.88554,352.05149 L 842.07887,352.7238 L 836.79572,355.24497 L 830.19178,360.79154 L 826.72471,365.6658 L 824.74353,372.38892 L 824.24824,374.91008 L 819.46038,375.41432 L 813.43993,377.723 L 803.282,369.19486 L 790.4043,361.29519 L 787.43252,360.45479 L 774.55482,361.9675 L 770.09715,362.47174 L 768.44617,359.11017 L 766.13479,357.09323 L 749.29471,357.59747 L 741.86527,358.43786 L 732.61975,363.14405 L 726.346,365.8333 L 724.69501,366.16945 L 718.75146,367.17792 L 711.65221,368.01831 L 704.71806,368.52255 L 705.04826,363.4802 L 706.86434,361.9675 L 709.67103,361.29519 L 710.33142,357.42939 L 714.62399,354.57206 L 718.58636,353.05935 L 722.87893,349.36164 L 727.33659,347.17662 L 727.99698,343.98313 L 731.95935,339.94926 L 732.61975,339.78119 C 732.61975,339.78119 732.61975,340.95773 733.44524,340.95773 C 734.27073,340.95773 735.42643,341.29389 735.42643,341.29389 L 737.73781,337.59616 L 739.88409,336.92385 L 742.19547,337.26001 L 743.84646,333.56229 L 746.81824,330.87303 L 747.31353,328.68802 L 747.31353,324.57011 L 751.9363,325.32646 L 759.22415,323.98183 L 775.38031,321.96489 L 792.88078,319.27565 L 813.92151,315.35219 L 833.49506,311.37597 L 845.21707,308.35056 L 850.23842,306.65958 z M 854.21672,340.95692 L 856.85831,338.3517 L 860.07773,335.66244 L 861.64617,334.99013 L 861.81127,332.88915 L 861.15088,326.50217 L 859.66499,324.06503 L 859.00459,322.13213 L 859.74753,321.88001 L 862.55422,327.59468 L 862.96697,332.21684 L 862.80187,335.74649 L 859.33479,337.34323 L 856.44555,339.86441 L 855.28987,341.125 L 854.21672,340.95692 z " id="NC" style="fill:#c83737" />
+ <ns0:path d="M 712.3126,329.69649 L 659.31592,334.90691 L 643.2212,336.75577 L 638.50172,337.28883 L 634.55111,337.26001 L 634.55111,341.29389 L 625.96598,341.79812 L 618.86673,342.47043 L 607.53473,342.52544 L 607.26436,348.59252 L 605.08072,355.11718 L 604.06449,358.25292 L 602.68706,362.80789 L 602.35687,365.49714 L 598.22939,367.85023 L 599.71528,371.54796 L 598.72469,376.08606 L 597.15628,377.85089 L 605.49374,377.76685 L 630.09345,375.74991 L 635.54175,375.58184 L 643.79664,375.07759 L 672.19362,372.38834 L 682.58068,371.54796 L 691.17997,370.53949 L 699.7651,369.36294 L 704.71806,368.52255 L 705.04826,363.4802 L 706.86434,361.9675 L 709.67103,361.29519 L 710.33142,357.42939 L 714.62399,354.57206 L 718.58636,353.05935 L 722.87893,349.36164 L 727.33659,347.17662 L 727.99698,343.98313 L 731.95935,339.94926 L 732.61975,339.78119 C 732.61975,339.78119 732.61975,340.95773 733.44524,340.95773 C 734.27073,340.95773 735.42643,341.29389 735.42643,341.29389 L 737.73781,337.59616 L 739.88409,336.92385 L 742.19547,337.26001 L 743.84646,333.56229 L 746.81824,330.87303 L 747.31353,328.68802 L 747.49366,324.59981 L 745.16725,324.65414 L 742.69078,326.67109 L 734.60093,326.83916 L 722.3505,328.8153 L 712.3126,329.69649 z " id="TN" style="fill:#de8787" />
+ <ns0:path d="M 893.09433,183.30123 L 892.6011,178.92994 L 891.77561,174.39182 L 890.04208,168.25697 L 895.90308,166.66023 L 897.55407,167.83677 L 901.02115,172.37489 L 903.99187,176.99768 L 901.01902,178.59507 L 899.69824,178.42699 L 898.54255,180.27585 L 896.06607,182.29279 L 893.09433,183.30123 z " id="RI" style="fill:#f4d7d7" />
+ <ns0:path d="M 893.58963,183.30123 L 892.6011,178.92994 L 891.77561,174.39182 L 890.12463,168.17293 L 884.84146,169.34947 L 862.55312,174.30778 L 863.21351,177.75339 L 864.6994,185.31692 L 864.6994,193.72083 L 863.54371,196.07393 L 865.41508,198.26677 L 870.47581,194.73055 L 874.10797,191.36899 L 876.08916,189.18398 L 876.91465,189.85629 L 879.72132,188.34359 L 885.00447,187.16705 L 893.58963,183.30123 z " id="CT" style="fill:#de8787" />
+ <ns0:path d="M 919.55232,177.09192 L 921.77043,176.37882 L 922.23741,174.59609 L 923.28809,174.71493 L 924.33877,177.09192 L 923.0546,177.56732 L 919.08535,177.68617 L 919.55232,177.09192 z M 909.97943,177.92387 L 912.31427,175.19033 L 913.94868,175.19033 L 915.81656,176.73537 L 913.36497,177.80501 L 911.14686,178.87466 L 909.97943,177.92387 z M 874.44023,155.06282 L 892.27091,150.69278 L 894.5823,150.02047 L 896.72858,146.65891 L 900.54482,144.92957 L 903.4955,149.51759 L 901.01902,154.89608 L 900.68883,156.40879 L 902.67001,159.09803 L 903.8257,158.25764 L 905.64178,158.25764 L 907.95316,160.94689 L 911.91552,167.16577 L 915.54769,167.67001 L 917.85907,166.66154 L 919.67515,164.81268 L 918.84966,161.95536 L 916.70338,160.27458 L 915.21749,161.11497 L 914.2269,159.77034 L 914.7222,159.26611 L 916.86848,159.09803 L 918.68456,159.93842 L 920.66574,162.45959 L 921.65633,165.48499 L 921.98653,168.00616 L 917.69397,169.51886 L 913.73161,171.5358 L 909.76924,176.24198 L 907.78806,177.75468 L 907.78806,176.74621 L 910.26454,175.23351 L 910.75983,173.38466 L 909.93434,170.19118 L 906.96257,171.70388 L 906.13708,173.21658 L 906.63237,175.56967 L 903.82678,177.08172 L 901.02115,172.37489 L 897.55407,167.83677 L 895.90308,166.66023 L 890.04208,168.25697 L 884.84146,169.34947 L 862.55312,174.30778 L 861.56253,168.34101 L 862.22292,157.33189 L 867.50608,156.40745 L 874.44023,155.06282" id="MA" style="fill:#c83737" />
+ <ns0:path d="M 943.28423,76.73985 L 945.26541,78.924863 L 947.57679,82.790656 L 947.57679,84.807591 L 945.43051,89.68185 L 943.44933,90.354162 L 939.98226,93.547643 L 935.02931,99.262292 C 935.02931,99.262292 934.36891,99.262292 933.70852,99.262292 C 933.04813,99.262292 932.71793,97.077279 932.71793,97.077279 L 930.90185,97.245357 L 929.91126,98.758058 L 927.43478,100.27076 L 926.44419,101.78346 L 928.09517,103.29616 L 927.59988,103.96847 L 927.10458,106.8258 L 925.1234,106.65772 L 925.1234,104.97694 L 924.7932,103.63232 L 923.30732,103.96847 L 921.49123,100.60692 L 919.34495,101.95154 L 920.66574,103.46424 L 920.99594,104.64079 L 920.17045,105.98541 L 920.50064,109.17889 L 920.66574,110.85967 L 919.01476,113.54892 L 916.04298,114.05315 L 915.71279,117.07855 L 910.26454,120.27203 L 908.94375,120.77627 L 907.29277,119.26356 L 904.15589,122.96128 L 905.14649,126.32284 L 903.6606,127.66746 L 903.4955,132.20556 L 901.88477,140.12915 L 899.37016,138.9273 L 898.87486,135.73381 L 894.91249,134.55727 L 894.5823,131.69993 L 887.15284,107.32858 L 882.28553,91.967581 L 884.77927,91.608771 L 886.32526,92.034941 L 886.32526,89.345695 L 887.15075,83.631045 L 889.79233,78.756786 L 891.27821,74.554837 L 889.29703,72.033669 L 889.29703,65.814786 L 890.12252,64.806318 L 890.94802,61.948993 L 890.78292,60.436292 L 890.61782,55.393954 L 892.4339,50.351617 L 895.40568,41.107331 L 897.55196,36.737305 L 898.87274,36.737305 L 900.19353,36.905383 L 900.19353,38.081928 L 901.51432,40.435019 L 904.32099,41.107331 L 905.14649,40.266941 L 905.14649,39.258474 L 909.27395,36.233071 L 911.09003,34.384214 L 912.57592,34.552292 L 918.68456,37.073461 L 920.66574,38.081928 L 929.91126,69.176344 L 936.0199,69.176344 L 936.84539,71.193279 L 937.01049,76.235617 L 939.98226,78.588708 L 940.80775,78.588708 L 940.97285,78.084474 L 940.47756,76.907928 L 943.28423,76.73985 z M 921.90732,108.08415 L 923.47577,106.48741 L 924.87911,107.57992 L 925.45696,110.1011 L 923.72342,111.02553 L 921.90732,108.08415 z M 928.75894,101.94929 L 930.57502,103.88219 C 930.57502,103.88219 931.89582,103.96623 931.89582,103.63007 C 931.89582,103.29391 932.14346,101.52909 932.14346,101.52909 L 933.05151,100.6887 L 932.22602,98.839833 L 930.16228,99.596189 L 928.75894,101.94929 z " id="ME" style="fill:#f4d7d7" />
+ <ns0:path d="M 900.54588,144.88986 L 900.85393,143.29871 L 901.96733,139.87704 L 899.37016,138.9273 L 898.87486,135.73381 L 894.91249,134.55727 L 894.5823,131.69993 L 887.15284,107.32858 L 882.45357,92.208279 L 881.5374,92.203019 L 880.87701,93.883799 L 880.21662,93.379565 L 879.22603,92.371097 L 877.74014,94.388032 L 876.76354,100.09176 L 877.08182,105.98396 L 879.063,108.84129 L 879.063,113.04325 L 875.26572,117.2452 L 872.62415,118.42176 L 872.62415,119.5983 L 873.77984,121.44716 L 873.77984,130.35531 L 872.95434,139.93577 L 872.78925,144.97812 L 873.77984,146.32275 L 873.61474,151.02894 L 873.11944,152.8778 L 874.60533,154.97877 L 892.27091,150.69278 L 894.5823,150.02047 L 896.72858,146.65891 L 900.54588,144.88986 z " id="NH" style="fill:#f4d7d7" />
+ <ns0:path d="M 862.38802,157.584 L 861.56253,151.70126 L 858.42565,140.27193 L 857.76525,139.93577 L 854.79347,138.59115 L 855.61896,135.56574 L 854.79347,133.38072 L 852.1519,128.67453 L 853.14249,124.64065 L 852.31699,119.26214 L 849.84051,112.53901 L 849.0178,107.42109 L 876.75058,99.933872 L 877.08182,105.98396 L 879.063,108.84129 L 879.063,113.04325 L 875.26572,117.2452 L 872.62415,118.42176 L 872.62415,119.5983 L 873.77984,121.44716 L 873.77984,130.35531 L 872.95434,139.93577 L 872.78925,144.97812 L 873.77984,146.32275 L 873.61474,151.02894 L 873.11944,152.8778 L 874.60533,154.97877 L 867.50608,156.40745 L 862.38802,157.584 z " id="VT" style="fill:#f4d7d7" />
+ <ns0:path d="M 846.20833,194.22506 L 845.05264,193.21659 L 842.41105,193.04851 L 840.09968,191.03158 L 837.62319,185.485 L 834.55471,184.51732 L 832.17493,182.29151 L 813.18856,186.49346 L 769.27227,195.56969 L 760.19184,197.08239 L 759.43798,189.88537 L 762.17121,188.00743 L 763.492,186.83089 L 764.48259,185.15011 L 766.29867,183.97356 L 768.27985,182.12471 L 768.77515,180.44393 L 770.92143,177.5866 L 772.07712,176.57814 L 771.91202,175.56967 L 770.59123,172.37619 L 768.77515,172.20811 L 766.79397,165.82115 L 769.76574,163.97229 L 774.2234,162.45959 L 778.35086,161.11497 L 781.65283,160.61073 L 788.09167,160.44266 L 790.07285,161.78728 L 791.72384,161.95536 L 793.87012,160.61073 L 796.51169,159.43419 L 801.79484,158.92995 L 803.94112,157.0811 L 805.75721,153.71954 L 807.40819,151.7026 L 809.55447,151.7026 L 811.53565,150.52606 L 811.70075,148.17297 L 810.21487,145.98795 L 809.88467,144.47525 L 811.04036,142.29024 L 811.04036,140.77754 L 809.22428,140.77754 L 807.40819,139.93715 L 806.5827,138.7606 L 806.4176,136.07136 L 812.36115,130.35671 L 813.02154,129.51632 L 814.50743,126.49092 L 817.4792,121.78473 L 820.28587,117.91894 L 822.43215,115.39777 L 824.89861,113.49969 L 828.0455,112.20429 L 833.65885,110.85967 L 836.96082,111.02775 L 841.58358,109.51505 L 849.30966,107.36166 L 849.84051,112.53901 L 852.31699,119.26214 L 853.14249,124.64065 L 852.1519,128.67453 L 854.79347,133.38072 L 855.61896,135.56574 L 854.79347,138.59115 L 857.76525,139.93577 L 858.42565,140.27193 L 861.56253,151.70126 L 862.05782,157.07976 L 861.56253,168.34101 L 862.38802,174.05567 L 863.21351,177.75339 L 864.6994,185.31692 L 864.6994,193.72083 L 863.54371,196.07393 L 865.42216,198.14582 L 865.19266,199.77289 L 863.21147,201.62175 L 863.54167,202.96637 L 864.86246,202.63021 L 866.34835,201.28559 L 868.65972,198.59634 L 869.81541,197.92403 L 871.4664,198.59634 L 873.77778,198.76442 L 881.8676,194.73055 L 884.83937,191.87323 L 886.16016,190.36053 L 890.45272,192.0413 L 886.98565,195.73902 L 883.02329,198.76442 L 875.75896,204.31099 L 873.11738,205.31946 L 867.17384,207.3364 L 863.04638,208.51294 L 861.49899,207.95886 L 860.90212,204.47784 L 861.39742,201.62051 L 861.23232,199.4355 L 858.59075,198.25894 L 853.96798,197.25047 L 850.0056,196.07393 L 846.20833,194.22506 z " id="NY" style="fill:#280b0b" />
+ <ns0:path d="M 846.20833,194.22506 L 844.06205,196.74624 L 844.06205,199.93973 L 842.08086,203.13321 L 841.91576,204.814 L 843.23656,206.15862 L 843.07146,208.6798 L 840.76007,209.85635 L 841.58556,212.71367 L 841.75066,213.89023 L 844.55734,214.22639 L 845.54794,216.91563 L 849.18011,219.43681 L 851.65659,221.11759 L 851.65659,221.95798 L 848.35462,225.15147 L 846.70362,227.50456 L 845.21774,230.3619 L 842.90636,231.70652 L 841.66812,232.46288 L 841.42046,233.72347 L 840.79828,236.43369 L 841.91377,238.76697 L 845.21574,241.79237 L 850.1687,244.14546 L 854.29616,244.81777 L 854.46126,246.33047 L 853.63576,247.33894 L 853.96596,250.19627 L 854.79145,250.19627 L 856.93773,247.6751 L 857.76322,242.63276 L 860.5699,238.43081 L 863.70677,231.70769 L 864.86246,225.99305 L 864.20207,224.8165 L 864.03697,215.06798 L 862.38598,211.53834 L 861.23029,212.37873 L 858.42362,212.71489 L 857.92832,212.21066 L 859.08401,211.20219 L 861.23029,209.18525 L 861.29469,208.048 L 860.90212,204.47784 L 861.39742,201.62051 L 861.23232,199.4355 L 858.59075,198.25894 L 853.96798,197.25047 L 850.0056,196.07393 L 846.20833,194.22506 z " id="NJ" style="fill:#a02c2c" />
+ <ns0:path d="M 841.75066,232.37883 L 842.90636,231.70652 L 845.21774,230.3619 L 846.70362,227.50456 L 848.35462,225.15147 L 851.65659,221.95798 L 851.65659,221.11759 L 849.18011,219.43681 L 845.54794,216.91563 L 844.55734,214.22639 L 841.75066,213.89023 L 841.58556,212.71367 L 840.76007,209.85635 L 843.07146,208.6798 L 843.23656,206.15862 L 841.91576,204.814 L 842.08086,203.13321 L 844.06205,199.93973 L 844.06205,196.74624 L 846.45598,194.22507 L 845.05264,193.21659 L 842.41105,193.04851 L 840.09968,191.03158 L 837.62319,185.485 L 834.55471,184.51732 L 832.17493,182.29151 L 813.18856,186.49346 L 769.27227,195.56969 L 760.19184,197.08239 L 759.68563,189.71729 L 754.08139,195.57094 L 752.7606,196.07518 L 748.46894,199.20351 L 751.4416,219.10066 L 753.16482,230.27806 L 756.81257,250.30417 L 761.54207,249.5232 L 773.73965,247.96108 L 812.47286,239.9916 L 827.66544,237.0562 L 836.14231,235.36944 L 837.45809,234.05962 L 839.60438,232.37883 L 841.75066,232.37883 z " id="PA" style="fill:#782121" />
+ <ns0:path d="M 840.59298,235.90964 L 841.42046,233.72347 L 841.66812,232.37883 L 839.60438,232.37883 L 837.45809,234.05962 L 835.9722,235.57232 L 837.45809,239.94236 L 839.76948,245.8251 L 841.91576,255.9098 L 843.56675,262.46486 L 848.68482,262.29678 L 854.95755,261.03674 L 852.64517,253.38975 L 851.65458,253.89398 L 848.02242,251.37281 L 846.20633,246.49855 L 844.22515,242.80084 L 841.91377,241.79237 L 839.76749,238.09466 L 840.59298,235.90964 z " id="DE" style="fill:#f4d7d7" />
+ <ns0:path d="M 854.95655,260.95325 L 848.68482,262.29678 L 843.56675,262.46486 L 841.91576,255.9098 L 839.76948,245.8251 L 837.45809,239.94236 L 836.14231,235.36944 L 827.66544,237.0562 L 812.47286,239.9916 L 774.22495,247.84224 L 775.38031,253.05285 L 776.37091,258.93558 L 776.7011,258.59942 L 778.84739,256.07825 L 781.15877,252.88476 L 783.63525,252.71668 L 785.12115,251.20398 L 786.93723,248.51473 L 788.25802,249.18705 L 791.22979,248.85089 L 793.87137,246.66588 L 795.92094,245.15492 L 797.80542,244.65068 L 799.48473,245.82549 L 802.45651,247.33819 L 804.43769,249.18705 L 805.67593,250.78379 L 809.88595,252.5486 L 809.88595,255.57402 L 815.49931,256.91864 L 817.48049,258.26326 L 818.47108,256.24633 L 820.78247,257.92711 L 819.29657,261.28868 L 818.96637,264.146 L 817.15029,266.83525 L 817.15029,269.02027 L 817.81068,270.86913 L 822.98233,272.27864 L 827.38511,272.21447 L 830.52198,273.22294 L 832.66826,273.5591 L 833.65885,271.37408 L 832.17296,269.18907 L 832.17296,267.34021 L 829.69649,265.1552 L 827.55021,259.44055 L 828.87099,253.89398 L 828.70589,251.70897 L 827.38511,250.36434 C 827.38511,250.36434 828.87099,248.68356 828.87099,248.01125 C 828.87099,247.33894 829.36629,245.82624 829.36629,245.82624 L 831.34747,244.48162 L 833.32865,242.80084 L 833.82395,243.8093 L 832.33806,245.49008 L 831.01727,249.35588 L 831.34747,250.53242 L 833.16355,250.86858 L 833.65885,256.58323 L 831.51257,257.59169 L 831.84277,261.28941 L 832.33806,261.12133 L 833.49375,259.1044 L 835.14473,260.95325 L 833.49375,262.29788 L 833.16355,265.82751 L 835.80513,269.35715 L 839.76749,269.86138 L 841.41848,269.02099 L 844.72045,274.39949 L 846.53653,274.90372 L 846.53653,278.60143 L 844.22515,283.64377 L 843.72986,290.87112 L 845.21574,294.40076 L 846.70163,294.56884 L 848.68281,290.19881 L 849.5083,286.5011 L 849.6734,279.10567 L 852.81027,274.06333 L 854.95655,266.83598 L 854.95655,260.95325 z M 838.20212,271.12031 L 839.3578,273.72552 L 839.5229,275.57439 L 840.67859,277.50729 C 840.67859,277.50729 841.58664,276.58285 841.58664,276.2467 C 841.58664,275.91054 840.8437,273.05321 840.8437,273.05321 L 840.10075,270.61606 L 838.20212,271.12031 z " id="MD" style="fill:#d35f5f" />
+ <ns0:path d="M 822.59725,272.21447 L 827.38511,272.21447 L 830.52198,273.22294 L 832.66826,273.5591 L 833.65885,271.37408 L 832.17296,269.18907 L 832.17296,267.34021 L 829.69649,265.1552 L 827.55021,259.44055 L 828.87099,253.89398 L 828.70589,251.70897 L 827.38511,250.36434 C 827.38511,250.36434 828.87099,248.68356 828.87099,248.01125 C 828.87099,247.33894 829.36629,245.82624 829.36629,245.82624 L 831.34747,244.48162 L 833.32865,242.80084 L 833.82395,243.8093 L 832.33806,245.49008 L 831.01727,249.35588 L 831.34747,250.53242 L 833.16355,250.86858 L 833.65885,256.58323 L 831.51257,257.59169 L 831.84277,261.28941 L 832.33806,261.12133 L 833.49375,259.1044 L 835.14473,260.95325 L 833.49375,262.29788 L 833.16355,265.82751 L 835.80513,269.35715 L 839.76749,269.86138 L 841.41848,269.02099 L 844.72045,274.39949 L 846.53653,274.90372 L 846.53653,278.60143 L 844.22515,283.64377 L 843.72986,290.87112 L 845.21574,294.40076 L 846.70163,294.56884 L 848.68281,290.19881 L 849.5083,286.5011 L 849.6734,279.10567 L 852.81027,274.06333 L 854.95655,266.83598 L 854.95655,260.95325 M 838.20212,271.12031 L 839.3578,273.72552 L 839.5229,275.57439 L 840.67859,277.50729 C 840.67859,277.50729 841.58664,276.58285 841.58664,276.2467 C 841.58664,275.91054 840.8437,273.05321 840.8437,273.05321 L 840.10075,270.61606 L 838.20212,271.12031 z " id="MD_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 774.24466,247.91204 L 775.38031,253.05285 L 776.37091,258.93558 L 776.7011,258.59942 L 778.84739,256.07825 L 781.15877,252.88476 L 783.63525,252.71668 L 785.12115,251.20398 L 786.93723,248.51473 L 788.25802,249.18705 L 791.22979,248.85089 L 793.87137,246.66588 L 795.92094,245.15492 L 797.80542,244.65068 L 799.48473,245.82549 L 802.45651,247.33819 L 804.43769,249.18705 L 805.84103,250.53167 L 804.7679,255.7421 L 798.98943,252.5486 L 794.36667,250.69975 L 794.20157,256.24633 L 793.70628,258.43134 L 792.05529,261.28868 L 791.39489,262.96946 L 788.25802,265.49063 L 787.76272,267.84373 L 784.29564,268.17988 L 783.96545,271.37336 L 782.80976,277.08802 L 780.16818,277.08802 L 778.84739,276.24763 L 777.1964,273.3903 L 775.38031,273.55838 L 775.05012,278.09649 L 772.90384,284.9877 L 767.78577,296.24894 L 768.61127,297.59356 L 768.44617,300.45089 L 766.29989,302.46782 L 764.814,302.13167 L 761.51202,304.65285 L 758.87045,303.64438 L 757.05436,308.51864 C 757.05436,308.51864 753.25709,309.35903 752.59669,309.52711 C 751.9363,309.69518 750.12022,308.18248 750.12022,308.18248 L 747.64373,310.53557 L 745.00215,311.2079 L 742.03037,310.36749 L 740.70958,309.02287 L 738.47074,305.8795 L 735.26132,303.81246 L 732.61975,300.95512 L 729.64798,297.08933 L 728.98758,294.73623 L 726.346,293.22353 L 725.5205,291.54275 L 725.27285,286.0802 L 727.50168,285.99616 L 729.48288,285.15577 L 729.64798,282.29845 L 731.29896,280.78574 L 731.46406,275.57532 L 732.45465,271.54144 L 733.77544,270.86913 L 735.09623,272.04567 L 735.59153,273.89453 L 737.40761,272.88606 L 737.90291,271.20529 L 736.74722,269.35643 L 736.74722,266.83525 L 737.73781,265.49063 L 740.04919,261.96099 L 741.36998,260.44829 L 743.51627,260.95252 L 745.82765,259.27173 L 748.96452,255.7421 L 751.27591,251.70821 L 751.6061,245.82549 L 752.1014,240.61506 L 752.1014,235.74079 L 750.94571,232.54731 L 751.9363,231.0346 L 753.24707,229.68997 L 756.81257,250.30417 L 761.54207,249.5232 L 774.24466,247.91204 z " id="WV" style="fill:#f4d7d7" />
+ <ns0:path d="M 738.61165,306.09768 L 735.42643,309.35903 L 731.13386,313.05675 L 726.51109,318.60333 L 724.69501,320.45219 L 724.69501,322.6372 L 720.73264,324.82222 L 714.95419,328.35186 L 712.28688,329.81359 L 659.31592,334.90691 L 643.2212,336.75577 L 638.50172,337.28883 L 634.55111,337.26001 L 634.55111,341.29389 L 625.96598,341.79812 L 618.86673,342.47043 L 608.21432,342.68419 L 609.24067,341.39196 L 611.46709,339.55999 L 613.56845,338.3715 L 613.80194,335.04372 L 614.73588,333.14213 L 613.09488,330.50244 L 613.91377,328.51994 L 616.22516,326.67109 L 618.37144,325.99877 L 621.17811,327.3434 L 624.81029,328.68802 L 625.96598,328.35186 L 626.13108,325.99877 L 624.81029,323.47759 L 625.14049,321.1245 L 627.12167,319.6118 L 629.76325,318.93949 L 631.41424,318.26718 L 630.58875,316.41831 L 629.92835,314.40138 L 631.08404,313.56099 L 632.15718,310.11537 L 635.2115,308.35056 L 641.15506,307.34209 L 644.78724,306.83786 L 646.27312,308.85479 L 648.08921,309.69518 L 649.90529,306.33362 L 652.87707,304.82092 L 654.85825,306.5017 L 655.68375,307.67825 L 657.83004,307.17401 L 657.66493,303.64438 L 660.63671,301.96359 L 661.7924,301.1232 L 662.94809,302.80398 L 667.73595,302.80398 L 668.56145,300.61896 L 668.23125,298.26587 L 671.20302,294.56815 L 675.99089,290.53428 L 676.48618,285.82809 L 679.29287,285.49193 L 683.25523,283.64307 L 686.06192,281.62613 L 685.73171,279.60919 L 684.24582,278.09649 L 684.82367,275.82744 L 689.03369,275.57532 L 691.51017,274.73493 L 694.48195,276.41571 L 696.13293,280.95382 L 702.07649,281.28997 L 703.89257,283.13884 L 706.03885,283.30692 L 708.51534,281.79422 L 711.65221,282.29845 L 712.973,283.81115 L 715.77968,281.12189 L 717.59577,279.77727 L 719.24675,279.77727 L 719.90714,282.63461 L 721.72324,283.64307 L 725.3554,285.82809 L 725.5205,291.54275 L 726.346,293.22353 L 728.98758,294.73623 L 729.64798,297.08933 L 732.61975,300.95512 L 735.26132,303.81246 L 738.61165,306.09768 z " id="KY" style="fill:#e9afaf" />
+ <ns0:path d="M 748.46982,198.97029 L 741.20371,203.30253 L 737.24134,205.65562 L 733.77427,209.52141 L 729.64681,213.55528 L 726.34484,214.39567 L 723.37307,214.8999 L 717.75972,217.58915 L 715.61344,217.75723 L 712.14638,214.56375 L 706.86322,215.23606 L 704.22165,213.72336 L 701.78994,212.3189 L 696.79333,213.05024 L 686.39211,214.73102 L 678.46737,215.99161 L 679.78816,231.20268 L 681.60425,245.48932 L 684.24582,269.86066 L 684.82367,275.82744 L 689.03369,275.57532 L 691.51017,274.73493 L 694.48195,276.41571 L 696.13293,280.95382 L 702.07649,281.28997 L 703.89257,283.13884 L 706.03885,283.30692 L 708.51534,281.79422 L 711.65221,282.29845 L 712.973,283.81115 L 715.77968,281.12189 L 717.59577,279.77727 L 719.24675,279.77727 L 719.90714,282.63461 L 721.72324,283.64307 L 725.27285,286.0802 L 727.50168,285.99616 L 729.48288,285.15577 L 729.64798,282.29845 L 731.29896,280.78574 L 731.46406,275.57532 L 732.45465,271.54144 L 733.77544,270.86913 L 735.09623,272.04567 L 735.59153,273.89453 L 737.40761,272.88606 L 737.90291,271.20529 L 736.74722,269.35643 L 736.74722,266.83525 L 737.73781,265.49063 L 740.04919,261.96099 L 741.36998,260.44829 L 743.51627,260.95252 L 745.82765,259.27173 L 748.96452,255.7421 L 751.27591,251.70821 L 751.6061,245.82549 L 752.1014,240.61506 L 752.1014,235.74079 L 750.94571,232.54731 L 751.9363,231.0346 L 753.33994,230.27806 L 751.4416,219.10066 L 748.46982,198.97029 z " id="OH" style="fill:#c83737" />
+ <ns0:path d="M 594.42414,81.655837 L 596.29202,79.516552 L 598.51013,78.684606 L 603.99703,74.643722 L 606.33188,74.049474 L 606.79885,74.524879 L 601.54544,79.873103 L 598.1599,81.893534 L 596.05854,82.844333 L 594.42414,81.655837 z M 682.43117,115.05941 L 683.09156,117.66462 L 686.39354,117.8327 L 687.71434,116.57211 C 687.71434,116.57211 687.63178,115.05941 687.30159,114.89133 C 686.97139,114.72326 685.6506,112.95843 685.6506,112.95843 L 683.42177,113.21054 L 681.77077,113.37862 L 681.44058,114.55518 L 682.43117,115.05941 z M 713.13697,180.61201 L 709.835,172.04003 L 707.52362,162.62767 L 705.04714,159.26611 L 702.40557,157.41725 L 700.75458,158.5938 L 696.79222,160.44266 L 694.81104,165.65307 L 692.00436,169.51886 L 690.84867,170.19118 L 689.36279,169.51886 C 689.36279,169.51886 686.72121,168.00616 686.88631,167.33385 C 687.05141,166.66154 687.38161,162.12343 687.38161,162.12343 L 690.84867,160.77881 L 691.67417,157.24918 L 692.33456,154.55993 L 694.81104,152.87915 L 694.48084,142.45832 L 692.82985,140.10523 L 691.50907,139.26484 L 690.68357,137.07982 L 691.50907,136.23943 L 693.16005,136.57559 L 693.32515,134.89481 L 690.84867,132.54172 L 689.52789,129.85247 L 686.88631,129.85247 L 682.26355,128.33977 L 676.6502,124.81014 L 673.84353,124.81014 L 673.18314,125.48245 L 672.19255,124.97821 L 669.05568,122.62512 L 666.0839,124.47398 L 663.11213,126.82707 L 663.44233,130.52479 L 664.43292,130.86094 L 666.5792,131.36518 L 667.07449,132.20556 L 664.43292,133.04595 L 661.79134,133.38211 L 660.30546,135.23097 L 659.97526,137.41598 L 660.30546,139.09676 L 660.63565,144.81141 L 657.00349,146.99642 L 656.34309,146.82834 L 656.34309,142.45832 L 657.66388,139.93715 L 658.32427,137.41598 L 657.49878,136.57559 L 655.5176,137.41598 L 654.52701,141.78601 L 651.72034,142.96255 L 649.90425,144.97949 L 649.73915,145.98795 L 650.39955,146.82834 L 649.73915,149.51759 L 647.42778,150.02182 L 647.42778,151.19837 L 648.25327,153.71954 L 647.09758,160.1065 L 645.44659,164.30845 L 646.10699,169.18271 L 646.60228,170.35925 L 645.77679,172.88042 L 645.44659,173.72081 L 645.1164,176.57814 L 648.74856,182.79702 L 651.72034,189.52014 L 653.20622,194.56247 L 652.38073,199.43673 L 651.39014,205.65562 L 648.91366,211.03411 L 648.58347,213.89143 L 645.43483,217.12572 L 644.67586,217.92491 L 649.40999,217.75643 L 671.86343,215.40333 L 678.13717,214.73102 L 678.46737,215.99161 L 686.39211,214.73102 L 696.79333,213.05024 L 702.12014,212.57103 L 700.75458,211.37027 L 700.91968,209.85756 L 703.06596,205.99177 L 705.10906,204.18493 L 704.88204,198.9325 L 706.51299,197.27212 L 707.62681,196.91556 L 707.85382,193.21785 L 709.42225,190.0664 L 710.49539,190.69668 L 710.66049,191.36899 L 711.48598,191.53707 L 713.46716,190.5286 L 713.13697,180.61201 z M 578.8376,112.43927 L 580.72799,111.3639 L 583.53467,110.52351 L 587.16683,108.17042 L 587.16683,107.16195 L 587.82723,106.48964 L 593.93587,105.48118 L 596.41235,103.46424 L 600.87001,101.27923 L 601.03511,99.934604 L 603.01629,96.909201 L 604.83237,96.068811 L 606.15316,94.219954 L 608.46454,91.866863 L 612.9222,89.345695 L 617.71005,88.841461 L 618.86574,90.018006 L 618.53554,91.026474 L 614.73828,92.034941 L 613.25239,95.228422 L 610.94101,96.068811 L 610.44572,98.58998 L 607.96924,101.95154 L 607.63904,104.64079 L 608.46454,105.14502 L 609.45513,103.96847 L 613.08729,100.94307 L 614.40808,102.28769 L 616.71946,102.28769 L 620.02143,103.29616 L 621.50732,104.47271 L 622.9932,107.66619 L 625.79988,110.52351 L 629.76224,110.35543 L 631.24813,109.34697 L 632.89911,110.69159 L 634.5501,111.19582 L 635.87088,110.35543 L 637.02657,110.35543 L 638.67756,109.34697 L 642.80502,105.64925 L 646.27209,104.47271 L 653.04112,104.13655 L 657.66388,102.11962 L 660.30546,100.77499 L 661.79134,100.94307 L 661.79134,106.8258 L 662.28664,107.16195 L 665.25841,108.00234 L 667.23959,107.49811 L 673.51333,105.81733 L 674.66902,104.64079 L 676.15491,105.14502 L 676.15491,112.37237 L 679.45688,115.56585 L 680.77767,116.23816 L 682.09845,117.24663 L 680.77767,117.58279 L 679.95217,117.24663 L 676.15491,116.7424 L 674.00863,117.41471 L 671.69725,117.24663 L 668.39528,118.75933 L 666.5792,118.75933 L 660.63565,117.41471 L 655.3525,117.58279 L 653.37132,120.27203 L 646.27209,120.94434 L 643.79561,121.78473 L 642.63992,124.97821 L 641.31913,126.15476 L 640.82384,125.98668 L 639.33795,124.3059 L 634.71519,126.82707 L 634.0548,126.82707 L 632.89911,125.14629 L 632.07362,125.31437 L 630.09244,129.85247 L 629.10185,134.05442 L 625.27363,142.51293 L 623.6083,141.31962 L 622.20739,139.89342 L 620.57299,129.197 L 616.83724,128.00851 L 615.43633,125.63153 L 602.59467,122.77914 L 600.02634,121.59066 L 591.62089,119.21367 L 583.21544,118.02518 L 578.8376,112.43927 z " id="MI" style="fill:#c83737" />
+ <ns0:path d="M 363.20447,145.98954 L 351.44763,144.98362 L 318.67723,141.55738 L 302.09981,139.41809 L 273.14771,135.13952 L 252.83454,132.04945 L 251.38528,143.66906 L 247.4644,168.89268 L 242.09425,200.50655 L 240.53069,211.44019 L 238.82546,223.80098 L 245.48808,224.7662 L 262.73518,227.14318 L 271.75391,228.36645 L 292.76047,230.93187 L 330.81813,235.21041 L 355.80134,237.34972 L 360.23755,191.23625 L 361.87193,164.85174 L 363.20447,145.98954 z " id="WY" style="fill:#f4d7d7" />
+ <ns0:path d="M 365.51098,123.96764 L 366.33647,111.87386 L 368.64299,85.935913 L 370.0439,70.247815 L 371.33142,55.452236 L 338.69364,52.032396 L 308.81082,48.334682 L 278.92799,44.132734 L 245.9083,38.586162 L 227.08707,35.056526 L 193.6675,27.848581 L 189.09322,50.043547 L 192.59549,57.887585 L 191.19458,62.64155 L 193.06246,67.395514 L 196.33125,68.821708 L 200.067,79.518133 L 203.80276,83.321298 L 204.26973,84.509794 L 207.772,85.698291 L 208.23897,87.837565 L 201.00095,106.14034 L 201.00095,108.75502 L 203.56928,112.08279 L 204.50321,112.08279 L 209.40639,108.99272 L 210.10685,107.80422 L 211.74124,108.51732 L 211.50775,113.98438 L 214.30957,127.05779 L 217.34487,129.67247 L 218.2788,130.38556 L 220.14668,132.76254 L 219.67972,136.32802 L 220.38017,139.89349 L 221.5476,140.84429 L 223.88244,138.4673 L 226.68426,138.4673 L 229.95305,140.13119 L 232.52138,139.1804 L 236.7241,139.1804 L 240.45985,140.84429 L 243.26167,140.36889 L 243.72864,137.27881 L 246.76394,136.56572 L 248.16485,137.99191 L 248.63182,141.31968 L 251.26853,143.90677 L 252.83454,132.04945 L 273.14771,135.13952 L 302.09981,139.41809 L 318.67723,141.55738 L 351.44763,144.98362 L 363.16317,146.23449 L 364.89307,129.69427 L 365.51098,123.96764 z " id="MT" style="fill:#f4d7d7" />
+ <ns0:path d="M 144.08485,180.96023 L 148.93381,161.76187 L 153.37002,143.34026 L 154.77093,138.94284 L 157.33926,132.76269 L 156.0551,130.38571 L 153.48676,130.50455 L 152.66957,129.43491 L 153.13654,128.24642 L 153.48676,125.0375 L 158.03971,119.33273 L 159.90759,118.85734 L 161.07501,117.66885 L 161.65873,114.34107 L 162.59266,113.62798 L 166.5619,107.56668 L 170.53114,103.05041 L 170.76463,99.128389 L 167.26235,96.394856 L 165.91982,91.819167 L 166.32842,81.776422 L 170.06418,64.662142 L 174.61712,43.031605 L 178.46962,29.00742 L 179.24775,25.053259 L 193.6675,27.848581 L 189.09322,50.043547 L 192.59549,57.887585 L 191.19458,62.64155 L 193.06246,67.395514 L 196.33125,68.821708 L 200.067,79.518133 L 203.80276,83.321298 L 204.26973,84.509794 L 207.772,85.698291 L 208.23897,87.837565 L 201.00095,106.14034 L 201.00095,108.75502 L 203.56928,112.08279 L 204.50321,112.08279 L 209.40639,108.99272 L 210.10685,107.80422 L 211.74124,108.51732 L 211.50775,113.98438 L 214.30957,127.05779 L 217.34487,129.67247 L 218.2788,130.38556 L 220.14668,132.76254 L 219.67972,136.32802 L 220.38017,139.89349 L 221.5476,140.84429 L 223.88244,138.4673 L 226.68426,138.4673 L 229.95305,140.13119 L 232.52138,139.1804 L 236.7241,139.1804 L 240.45985,140.84429 L 243.26167,140.36889 L 243.72864,137.27881 L 246.76394,136.56572 L 248.16485,137.99191 L 248.63182,141.31968 L 251.31689,143.45897 L 247.4644,168.89268 L 242.21095,200.38777 L 237.30774,199.55589 L 228.78555,198.12969 L 218.27875,196.22811 L 206.02081,194.08882 L 193.0624,191.65242 L 184.89044,189.57255 L 175.43432,187.67098 L 165.51122,185.65054 L 144.08485,180.96023 z " id="ID" style="fill:#f4d7d7" />
+ <ns0:path d="M 95.99889,2.9536428 L 100.45655,4.4663441 L 110.36246,7.3236687 L 119.11268,9.3406038 L 139.58489,15.223331 L 163.02887,21.106058 L 179.49525,24.969183 L 178.46962,29.00742 L 174.61712,43.031605 L 170.06418,64.662142 L 166.32842,81.776422 L 166.13328,91.861195 L 151.85237,88.313121 L 136.44238,84.628799 L 120.68217,84.747642 L 120.21521,83.321459 L 114.61157,85.460744 L 110.05862,84.866496 L 107.60703,83.202605 L 106.32286,83.915707 L 101.53644,83.678 L 99.785303,82.251817 L 94.415149,80.112532 L 93.597952,80.231386 L 89.161743,78.686338 L 87.177124,80.587926 L 80.873036,80.231386 L 74.802439,75.952816 L 75.50289,75.12087 L 75.736374,67.039124 L 73.401527,62.998262 L 69.198802,62.404014 L 68.498351,59.789335 L 66.094359,59.304248 L 64.13488,57.747045 L 62.318797,58.755513 L 60.007419,55.73011 L 60.337616,52.704708 L 63.14429,52.368552 L 64.795274,48.166604 L 62.153699,46.990058 L 62.318797,43.124266 L 66.776456,42.451954 L 63.969782,39.59463 L 62.483896,32.199201 L 63.14429,29.173799 L 63.14429,20.93798 L 61.328206,17.576422 L 63.639585,7.8279025 L 65.785865,8.3321363 L 68.262342,11.357539 L 71.069016,14.046786 L 74.370985,16.063721 L 78.993743,18.248734 L 82.130616,18.921045 L 85.102388,20.433747 L 88.569459,21.442214 L 90.880838,21.274136 L 90.880838,18.752967 L 92.201625,17.576422 L 94.347905,16.231799 L 94.678102,17.408344 L 95.008299,19.257201 L 92.696921,19.761435 L 92.366724,21.946448 L 94.182807,23.459149 L 95.338496,25.980318 L 95.99889,27.997253 L 97.484776,27.829175 L 97.649875,26.484552 L 96.659284,25.139928 L 96.163989,21.77837 L 96.989481,19.929513 L 96.329087,18.416812 L 96.329087,16.063721 L 98.14517,12.366006 L 96.989481,9.6767597 L 94.513004,4.634422 L 94.843201,3.7940324 L 95.99889,2.9536428 z M 86.341086,9.169955 L 88.404826,9.001877 L 88.900121,10.430545 L 90.468562,8.7497548 L 92.862495,8.7497548 L 93.687987,10.3465 L 92.119546,12.111324 L 92.779951,12.951724 L 92.037002,15.052704 L 90.63366,15.472893 C 90.63366,15.472893 89.725613,15.556938 89.725613,15.220782 C 89.725613,14.884626 91.21151,12.531524 91.21151,12.531524 L 89.477971,11.943246 L 89.147774,13.455958 L 88.404826,14.12827 L 86.836382,11.775168 L 86.341086,9.169955 z " id="WA" style="fill:#d35f5f" />
+ <ns0:path d="M 224.65378,521.59843 L 226.52879,518.16091 L 228.7163,517.84841 L 229.0288,518.62966 L 226.99754,521.59843 L 224.65378,521.59843 z M 234.49758,518.00466 L 240.43511,520.50467 L 242.46637,520.19217 L 244.02887,516.44215 L 243.40387,513.16089 L 239.34135,512.69214 L 235.43508,514.41089 L 234.49758,518.00466 z M 264.18522,527.69221 L 267.77898,533.00473 L 270.12274,532.69223 L 271.2165,532.22348 L 272.62275,533.47348 L 276.21652,533.31723 L 277.15403,531.91098 L 274.34151,530.19222 L 272.4665,526.59845 L 270.43524,523.16094 L 264.81022,525.97345 L 264.18522,527.69221 z M 283.71656,536.286 L 284.96656,534.41099 L 289.49783,535.34849 L 290.12284,534.87974 L 296.06036,535.50474 L 295.74786,536.75475 L 293.24785,538.161 L 289.02908,537.8485 L 283.71656,536.286 z M 288.87283,541.28602 L 290.74784,545.03604 L 293.7166,543.94228 L 294.0291,542.37977 L 292.4666,540.34851 L 288.87283,540.03601 L 288.87283,541.28602 z M 295.59161,540.19226 L 297.77912,537.37975 L 302.31039,539.72351 L 306.52916,540.81727 L 310.74793,543.47353 L 310.74793,545.34854 L 307.31042,547.0673 L 302.62289,548.0048 L 300.27913,546.59854 L 295.59161,540.19226 z M 311.68544,555.19233 L 313.24794,553.94233 L 316.52921,555.50484 L 323.87299,558.94235 L 327.15426,560.97361 L 328.71676,563.31737 L 330.59177,567.53614 L 334.49804,570.03615 L 334.18554,571.28616 L 330.43552,574.41117 L 326.373,575.81743 L 324.96675,575.19243 L 321.99798,576.91118 L 319.65422,580.0362 L 317.46671,582.84871 L 315.74795,582.69246 L 312.31044,580.19245 L 311.99794,575.81743 L 312.62294,573.47367 L 311.06043,568.00489 L 309.02917,566.28613 L 308.87292,563.78612 L 311.06043,562.84862 L 313.09169,559.87986 L 313.56044,558.94235 L 311.99794,557.22359 L 311.68544,555.19233 z " id="HI_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1pt" />
+ <ns0:path d="M 224.65378,521.59843 L 226.52879,518.16091 L 228.7163,517.84841 L 229.0288,518.62966 L 226.99754,521.59843 L 224.65378,521.59843 z M 234.49758,518.00466 L 240.43511,520.50467 L 242.46637,520.19217 L 244.02887,516.44215 L 243.40387,513.16089 L 239.34135,512.69214 L 235.43508,514.41089 L 234.49758,518.00466 z M 264.18522,527.69221 L 267.77898,533.00473 L 270.12274,532.69223 L 271.2165,532.22348 L 272.62275,533.47348 L 276.21652,533.31723 L 277.15403,531.91098 L 274.34151,530.19222 L 272.4665,526.59845 L 270.43524,523.16094 L 264.81022,525.97345 L 264.18522,527.69221 z M 283.71656,536.286 L 284.96656,534.41099 L 289.49783,535.34849 L 290.12284,534.87974 L 296.06036,535.50474 L 295.74786,536.75475 L 293.24785,538.161 L 289.02908,537.8485 L 283.71656,536.286 z M 288.87283,541.28602 L 290.74784,545.03604 L 293.7166,543.94228 L 294.0291,542.37977 L 292.4666,540.34851 L 288.87283,540.03601 L 288.87283,541.28602 z M 295.59161,540.19226 L 297.77912,537.37975 L 302.31039,539.72351 L 306.52916,540.81727 L 310.74793,543.47353 L 310.74793,545.34854 L 307.31042,547.0673 L 302.62289,548.0048 L 300.27913,546.59854 L 295.59161,540.19226 z M 311.68544,555.19233 L 313.24794,553.94233 L 316.52921,555.50484 L 323.87299,558.94235 L 327.15426,560.97361 L 328.71676,563.31737 L 330.59177,567.53614 L 334.49804,570.03615 L 334.18554,571.28616 L 330.43552,574.41117 L 326.373,575.81743 L 324.96675,575.19243 L 321.99798,576.91118 L 319.65422,580.0362 L 317.46671,582.84871 L 315.74795,582.69246 L 312.31044,580.19245 L 311.99794,575.81743 L 312.62294,573.47367 L 311.06043,568.00489 L 309.02917,566.28613 L 308.87292,563.78612 L 311.06043,562.84862 L 313.09169,559.87986 L 313.56044,558.94235 L 311.99794,557.22359 L 311.68544,555.19233 z " id="HI" style="fill:#e9afaf" />
+ <ns0:path d="M 365.08234,342.9472 L 388.2557,344.07626 L 420.00963,345.26475 L 418.60872,369.98537 L 418.14175,388.52584 L 418.37523,390.18973 L 422.81144,393.9929 L 424.9128,395.18139 L 425.61327,394.94369 L 426.31372,392.80441 L 427.71463,394.70599 L 429.81599,394.70599 L 429.81599,393.2798 L 432.6178,394.70599 L 432.15084,398.74687 L 436.35356,398.98457 L 438.92189,400.17306 L 443.12462,400.88615 L 445.69295,402.78774 L 448.02779,400.64846 L 451.53007,401.36155 L 454.0984,404.92703 L 455.03233,404.92703 L 455.03233,407.30401 L 457.36718,408.0171 L 459.70203,405.64012 L 461.5699,406.35321 L 464.13823,406.35321 L 465.07218,408.9679 L 469.97536,410.86948 L 471.37627,410.15638 L 473.24415,405.87781 L 474.41156,405.87781 L 475.57899,408.0171 L 479.78172,408.73019 L 483.51747,410.15638 L 486.55277,411.10718 L 488.42065,410.15638 L 489.1211,407.54171 L 493.55731,407.54171 L 495.65868,408.49249 L 498.46049,406.35321 L 499.62792,406.35321 L 500.32837,408.0171 L 504.53109,408.0171 L 506.16549,405.87781 L 508.03337,406.35321 L 510.13473,408.9679 L 513.40351,410.86948 L 516.6723,411.82028 L 519.47412,413.12761 L 521.80896,415.14805 L 524.84426,413.72186 L 527.64608,414.91035 L 528.33194,426.45357 L 528.34653,436.5409 L 529.04699,446.28652 L 529.74745,450.3274 L 532.31578,454.60597 L 533.24971,459.83533 L 537.68592,465.54009 L 537.9194,468.86787 L 538.61987,469.58096 L 537.9194,478.3758 L 534.88411,483.60516 L 536.5185,485.74444 L 535.81804,488.35912 L 535.11759,495.96547 L 533.71668,499.29324 L 534.00598,503.01936 L 527.40119,504.83432 L 517.33018,509.5405 L 516.33959,511.55743 L 513.69802,513.57437 L 511.55174,515.08707 L 510.23095,515.92746 L 504.4525,521.47403 L 501.64583,523.65904 L 496.19758,527.0206 L 490.41913,529.54177 L 483.98029,533.07141 L 482.16421,534.58411 L 476.22066,538.28182 L 472.7536,538.95414 L 468.79123,544.66878 L 464.66377,545.00494 L 463.67318,547.02188 L 465.98456,549.03881 L 464.49867,554.75346 L 463.17788,559.45964 L 462.0222,563.49351 L 461.1967,568.19969 L 462.0222,570.72086 L 463.83828,577.94821 L 464.82887,584.33517 L 466.64495,587.1925 L 465.65436,588.7052 L 462.51749,590.72214 L 456.73904,586.68827 L 451.1257,585.51172 L 449.80491,586.01595 L 446.50294,585.34364 L 442.21038,582.15016 L 436.92723,580.97362 L 429.1676,577.44398 L 427.02132,573.41011 L 425.70053,566.68699 L 422.39856,564.67006 L 421.73817,562.31697 L 422.39856,561.64466 L 422.72876,558.11502 L 421.40797,557.44271 L 420.74758,556.43424 L 422.06837,551.89614 L 420.41738,549.54304 L 417.11541,548.19842 L 413.64834,543.66032 L 410.01618,536.76912 L 405.72362,534.07988 L 405.88872,532.06294 L 400.44047,519.28902 L 399.61497,514.91899 L 397.79889,512.90206 L 397.63379,511.38936 L 391.52515,505.84279 L 388.88357,502.6493 L 388.88357,501.47276 L 386.242,499.28775 L 379.30786,498.1112 L 371.71333,497.43889 L 368.57646,495.0858 L 363.9537,496.93466 L 360.32154,498.44736 L 358.01016,501.80891 L 357.01957,505.67471 L 352.56191,512.06167 L 350.08543,514.58284 L 347.44386,513.57437 L 345.62777,512.39782 L 343.64659,511.72551 L 339.68423,509.37242 L 339.68423,508.70011 L 337.86815,506.68317 L 332.585,504.49816 L 324.99047,496.43042 L 322.67909,491.55616 L 322.67909,483.15227 L 319.37712,476.42915 L 318.88182,473.57182 L 317.23084,472.56336 L 316.07515,470.37834 L 310.9571,468.19333 L 309.63631,466.51255 L 302.37198,458.27673 L 301.05119,454.91517 L 296.26332,452.56207 L 294.77744,448.02394 L 292.13584,444.99855 L 290.15467,444.49434 L 289.49163,439.63101 L 297.66367,440.34413 L 327.31615,443.19648 L 356.96871,444.86037 L 359.30356,420.13975 L 363.27277,362.37936 L 364.90719,342.88781 L 366.3081,342.91754 M 467.38967,586.18345 L 466.81183,578.788 L 464.00514,571.30851 L 463.42729,563.99709 L 464.99573,555.42509 L 468.38027,548.28176 L 471.92989,542.65112 L 475.14933,538.95339 L 475.80972,539.20552 L 470.9393,546.09673 L 466.48163,552.90391 L 464.41788,559.79513 L 464.08769,565.17365 L 464.99573,571.56063 L 467.63732,579.04012 L 468.13261,584.41863 L 468.29771,585.93134 L 467.38967,586.18345 z " id="TX" style="fill:#280b0b" />
+ <ns0:path d="M 140.74058,399.1133 L 144.96457,398.27159 L 146.48222,396.01346 L 147.06593,393.16108 L 143.33019,392.56684 L 142.86321,391.73489 L 143.33019,389.95216 L 143.44692,383.89086 L 145.54828,383.17775 L 148.46685,380.32538 L 149.05056,375.21486 L 150.56821,371.4117 L 152.55282,369.39125 L 156.0551,367.60852 L 157.68948,366.18234 L 157.80623,363.80536 L 156.75555,363.21111 L 155.93835,362.14147 L 154.65419,356.08016 L 151.85237,350.96965 L 152.44317,347.57226 L 149.86775,344.19525 L 135.04148,320.54427 L 115.19528,290.35661 L 91.963567,255.17727 L 79.318752,235.85757 L 80.989783,229.03046 L 88.111072,202.05171 L 96.399776,169.3682 L 82.624178,165.56503 L 68.848582,161.99955 L 56.006928,157.72098 L 48.301935,155.58169 L 36.627704,152.49162 L 29.426941,149.98419 L 27.813217,154.89608 L 27.648119,162.62767 L 22.364968,174.89736 L 19.228097,177.5866 L 18.8979,178.76315 L 17.081817,179.60354 L 15.595931,183.97356 L 14.770438,187.33512 L 17.577112,191.70515 L 19.228097,196.07518 L 20.383786,199.77289 L 20.053589,206.49601 L 18.237506,209.68949 L 17.577112,215.74029 L 16.586521,219.60608 L 18.402605,223.63995 L 21.209279,228.34614 L 23.520657,233.38847 L 24.841445,237.59042 L 24.511248,240.95198 L 24.181051,241.45621 L 24.181051,243.64123 L 29.959497,250.19627 L 29.464202,252.71743 L 28.803808,255.07053 L 28.143414,257.08746 L 28.308513,265.65943 L 30.454793,269.52523 L 32.435974,272.21447 L 35.242648,272.71871 L 36.233239,275.57603 L 35.07755,279.27375 L 32.93127,280.95453 L 31.775581,280.95453 L 30.950088,284.9884 L 31.445384,288.0138 L 34.747353,292.5519 L 36.398338,298.09847 L 37.884224,302.97273 L 39.205012,306.16621 L 42.672079,312.21702 L 44.157966,314.90627 L 44.653261,317.93167 L 46.304246,318.94014 L 46.304246,321.4613 L 45.478753,323.47824 L 43.66267,330.87367 L 43.167375,332.8906 L 45.643852,335.74793 L 49.936412,336.25216 L 54.559169,338.10102 L 58.521532,340.28603 L 61.493305,340.28603 L 64.465077,343.47951 L 67.106653,348.52185 L 68.262342,350.87494 L 72.224705,353.05995 L 77.177659,353.90034 L 78.663546,356.08536 L 79.32394,359.44692 L 77.838053,360.11923 L 78.16825,361.12769 L 81.470222,361.96808 L 84.276896,362.13616 L 87.248668,367.01042 L 91.211035,371.38045 L 92.036527,373.73354 L 94.678102,378.10356 L 95.008299,381.46512 L 95.008299,391.21364 L 95.503595,393.0625 L 105.7397,394.5752 L 125.88171,397.43253 L 140.74058,399.1133 z M 50.26694,346.75563 L 51.587732,348.35237 L 51.422633,349.697 L 48.120652,349.61296 L 47.542806,348.35237 L 46.88241,346.83966 L 50.26694,346.75563 z M 52.248128,346.75563 L 53.48637,346.08332 L 57.118549,348.26833 L 60.255432,349.52892 L 59.347387,350.20124 L 54.724613,349.94912 L 53.073623,348.26833 L 52.248128,346.75563 z M 73.380807,367.34524 L 75.19689,369.78238 L 76.022393,370.79086 L 77.590834,371.37912 L 78.168673,369.86642 L 77.178082,368.01756 L 74.453952,365.91658 L 73.380807,366.08465 L 73.380807,367.34524 z M 71.89491,376.33744 L 73.711004,379.61497 L 74.949248,381.63192 L 73.463351,381.88403 L 72.142563,380.62344 C 72.142563,380.62344 71.399615,379.11074 71.399615,378.69054 C 71.399615,378.27035 71.399615,376.42148 71.399615,376.42148 L 71.89491,376.33744 z " id="CA" style="fill:#280b0b" />
+ <ns0:path d="M 141.11208,399.22238 L 138.4292,401.4664 L 138.099,402.9791 L 138.5943,403.98756 L 157.91082,415.08071 L 170.2932,422.98037 L 185.31716,431.8885 L 202.4874,442.30933 L 215.03489,444.8305 L 242.33612,448.47703 L 244.42909,434.63935 L 248.26156,406.64864 L 255.37454,351.33455 L 259.72234,319.30647 L 233.45531,315.31482 L 205.67064,310.56085 L 171.53056,303.99238 L 168.54652,322.80241 L 168.07955,323.2778 L 166.32842,326.01134 L 163.76009,325.89248 L 162.47592,323.04011 L 159.67411,322.68356 L 158.74017,321.49507 L 157.80623,321.49507 L 156.87229,322.08932 L 154.88767,323.15896 L 154.77093,330.40875 L 154.53744,332.19149 L 153.95374,345.26489 L 152.43609,347.52302 L 151.85237,350.96965 L 154.65419,356.08016 L 155.93835,362.14147 L 156.75555,363.21111 L 157.80623,363.80536 L 157.68948,366.18234 L 156.0551,367.60852 L 152.55282,369.39125 L 150.56821,371.4117 L 149.05056,375.21486 L 148.46685,380.32538 L 145.54828,383.17775 L 143.44692,383.89086 L 143.33019,389.95216 L 142.86321,391.73489 L 143.33019,392.56684 L 147.06593,393.16108 L 146.48222,396.01346 L 144.96457,398.27159 L 141.11208,399.22238 z " id="AZ" style="fill:#de8787" />
+ <ns0:path d="M 144.08485,180.96023 L 165.51122,185.65054 L 175.43432,187.67098 L 184.89044,189.57255 L 193.12078,191.77126 L 191.89498,197.53545 L 188.27597,215.71936 L 184.42347,236.99336 L 182.43886,246.26359 L 180.22075,260.40663 L 176.83522,277.63975 L 173.56644,293.44668 L 171.55619,304.36486 L 168.54652,322.80241 L 168.07955,323.2778 L 166.32842,326.01134 L 163.76009,325.89248 L 162.47592,323.04011 L 159.67411,322.68356 L 158.74017,321.49507 L 157.80623,321.49507 L 156.87229,322.08932 L 154.88767,323.15896 L 154.77093,330.40875 L 154.53744,332.19149 L 153.95374,345.26489 L 152.43963,347.54764 L 149.86775,344.19525 L 135.04148,320.54427 L 115.19528,290.35661 L 91.963567,255.17727 L 79.318752,235.85757 L 80.989783,229.03046 L 88.111072,202.05171 L 96.166292,169.45944 L 130.48854,177.92533 L 144.49761,181.0154" id="NV" style="fill:#e9afaf" />
+ <ns0:path d="M 259.6056,319.59339 L 233.45531,315.31482 L 205.67064,310.56085 L 171.45228,304.13508 L 173.56644,293.44668 L 176.83522,277.63975 L 180.22075,260.40663 L 182.43886,246.26359 L 184.42347,236.99336 L 188.27597,215.71936 L 191.89498,197.53545 L 193.03322,191.74155 L 206.02081,194.08882 L 218.27875,196.22811 L 228.78555,198.12969 L 237.30774,199.55589 L 242.21095,200.38777 L 240.53069,211.44019 L 238.82546,223.80098 L 245.48808,224.7662 L 262.73518,227.14318 L 272.10412,228.36648 L 268.94498,251.37398 L 265.6762,274.66841 L 261.84373,303.76606 L 260.30605,315.31482 L 259.6056,319.59339 z " id="UT" style="fill:#e9afaf" />
+ <ns0:path d="M 384.05299,331.95365 L 388.2557,263.49654 L 389.8901,240.2021 L 355.80134,237.34972 L 330.81813,235.21041 L 292.76047,230.93187 L 271.63008,228.31722 L 268.94498,251.37398 L 265.6762,274.66841 L 261.84373,303.76606 L 260.30605,315.31482 L 259.72234,319.35569 L 294.86179,323.63426 L 332.58567,328.23704 L 366.04543,330.52751 L 372.84567,331.2406 L 384.5199,331.83485" id="CO" style="fill:#e9afaf" />
+ <ns0:path d="M 290.31977,444.66242 L 289.49163,439.63101 L 297.66367,440.34413 L 327.31615,443.19648 L 356.96871,444.86037 L 359.30356,420.13975 L 363.27277,362.37936 L 364.90719,342.88781 L 366.3081,342.91754 L 366.29351,330.73549 L 332.58567,328.23704 L 294.86179,323.63426 L 259.66398,319.35569 L 255.37454,351.33455 L 248.26156,406.64864 L 244.42909,434.63935 L 242.33612,448.47703 L 258.12559,450.54515 L 259.44637,440.12432 L 276.45152,442.81356 L 290.31977,444.66242 z " id="NM" style="fill:#e9afaf" />
+ <ns0:path d="M 144.38087,180.54003 L 148.93381,161.76187 L 153.37002,143.34026 L 154.77093,138.94284 L 157.33926,132.76269 L 156.0551,130.38571 L 153.48676,130.50455 L 152.66957,129.43491 L 153.13654,128.24642 L 153.48676,125.0375 L 158.03971,119.33273 L 159.90759,118.85734 L 161.07501,117.66885 L 161.65873,114.34107 L 162.59266,113.62798 L 166.5619,107.56668 L 170.53114,103.05041 L 170.76463,99.128389 L 167.26235,96.394856 L 166.19165,92.03947 L 151.85237,88.313121 L 136.44238,84.628799 L 120.68217,84.747642 L 120.21521,83.321459 L 114.61157,85.460744 L 110.05862,84.866496 L 107.60703,83.202605 L 106.32286,83.915707 L 101.53644,83.678 L 99.785303,82.251817 L 94.415149,80.112532 L 93.597952,80.231386 L 89.161743,78.686338 L 87.177124,80.587926 L 80.873036,80.231386 L 74.802439,75.952816 L 75.50289,75.12087 L 75.736374,67.039124 L 73.401527,62.998262 L 69.198802,62.404014 L 68.498351,59.789335 L 66.094359,59.304248 L 60.172517,61.44476 L 57.861139,68.167876 L 54.559169,78.588708 L 51.2572,85.311824 L 46.139147,99.934604 L 39.535209,114.05315 L 31.280285,127.16323 L 29.299104,130.18863 L 28.473611,139.09676 L 27.152823,145.31564 L 29.426941,149.98419 L 36.627704,152.49162 L 48.301935,155.58169 L 56.006928,157.72098 L 68.848582,161.99955 L 82.624178,165.56503 L 96.399776,169.60589 M 144.08485,180.96023 L 96.166292,169.45944 L 130.48854,177.92533 L 144.49761,181.0154" id="OR" style="fill:#e9afaf" />
+ <ns0:path d="M 482.58353,129.91009 L 481.88308,121.11525 L 480.0152,113.50891 L 478.14732,99.484714 L 477.68036,89.263683 L 475.81248,85.698205 L 474.17808,80.468846 L 474.17808,69.772421 L 474.87853,65.731548 L 472.88209,60.014242 L 442.87077,59.427825 L 423.88445,58.755513 L 396.8083,57.410889 L 371.33142,55.452236 L 370.0439,70.247815 L 368.64299,85.935913 L 366.33647,111.87386 L 365.67607,124.49947 L 423.04492,128.24621 L 482.58353,129.91009 z " id="ND" style="fill:#f4d7d7" />
+ <ns0:path d="M 484.10703,208.42015 L 483.13305,206.29529 L 481.4161,203.35887 L 483.28398,198.8426 L 484.6849,192.90014 L 481.88308,190.76086 L 481.4161,187.90848 L 482.35005,185.29379 L 484.21793,185.29379 L 484.6849,178.16284 L 484.45141,146.54897 L 483.98444,143.4589 L 479.78172,139.89342 L 478.61429,137.99183 L 478.61429,136.32794 L 480.71565,134.66406 L 482.11657,133.23787 L 482.4668,129.91009 L 423.04492,128.24621 L 365.67608,124.20534 L 364.89307,129.69427 L 363.24575,146.19243 L 361.87193,164.85174 L 360.23755,191.5928 L 376.1145,192.66245 L 396.66115,193.85093 L 414.87297,195.03943 L 439.15538,196.22791 L 450.12916,195.75252 L 452.23052,198.1295 L 457.1337,201.21959 L 458.30112,202.17037 L 462.73733,200.74418 L 466.70658,200.26879 L 469.50839,200.03109 L 471.37627,201.45728 L 476.51293,203.12117 L 479.54822,204.78505 L 480.0152,206.44894 L 480.94914,208.58823 L 482.81702,208.58823 L 484.10703,208.42015 z " id="SD" style="fill:#f4d7d7" />
+ <ns0:path d="M 496.12564,252.80011 L 497.52656,255.41479 L 497.29307,257.79177 L 499.8614,261.83265 L 503.13018,266.11122 L 496.82609,266.11122 L 451.76325,265.63581 L 410.43676,264.20963 L 388.13897,263.37769 L 389.8901,240.2021 L 355.80134,237.34972 L 360.23755,191.5928 L 376.1145,192.66245 L 396.66115,193.85093 L 414.87297,195.03943 L 439.15538,196.22791 L 450.12916,195.75252 L 452.23052,198.1295 L 457.1337,201.21959 L 458.30112,202.17037 L 462.73733,200.74418 L 466.70658,200.26879 L 469.50839,200.03109 L 471.37627,201.45728 L 476.51293,203.12117 L 479.54822,204.78505 L 480.0152,206.44894 L 480.94914,208.58823 L 482.81702,208.58823 L 484.45141,208.46938 L 485.61883,214.05529 L 488.42065,221.89933 L 489.35459,226.891 L 491.68943,230.69417 L 492.38988,236.16123 L 494.02428,240.4398 L 494.25776,247.33306 L 496.23653,253.05224" id="NE" style="fill:#f4d7d7" />
+ <ns0:path d="M 580.12177,205.73586 L 580.18014,207.63744 L 582.51499,208.35053 L 583.44892,209.53902 L 583.91589,211.44061 L 587.88513,215.00608 L 588.58559,217.38307 L 587.88513,220.94855 L 586.01725,224.75171 L 585.3168,227.36639 L 582.98196,229.26798 L 581.11408,229.98108 L 575.74393,231.40726 L 575.04347,233.30885 L 574.34302,235.44814 L 575.04347,236.87433 L 576.91135,238.53822 L 576.67787,242.81678 L 574.80999,244.48067 L 574.10954,246.14456 L 574.10954,248.99694 L 572.24166,249.47233 L 570.60726,250.66083 L 570.37378,252.08702 L 570.60726,254.22631 L 568.85613,256.06846 L 565.4706,252.56242 L 564.30317,250.18543 L 556.3647,250.89852 L 546.32485,251.37392 L 520.40805,252.32472 L 506.63246,252.56242 L 497.05959,252.80011 L 495.9476,252.92617 L 494.25776,247.33306 L 494.02428,240.4398 L 492.38988,236.16123 L 491.68943,230.69417 L 489.35459,226.891 L 488.42065,221.89933 L 485.61883,214.05529 L 484.45141,208.46938 L 483.0505,206.21125 L 481.4161,203.35887 L 483.28398,198.8426 L 484.6849,192.90014 L 481.88308,190.76086 L 481.4161,187.90848 L 482.35005,185.29379 L 484.10119,185.29379 L 495.89216,185.29379 L 546.55834,184.5807 L 565.00364,183.86761 L 569.20636,183.74877 L 569.90681,187.19538 L 572.24166,188.85927 L 572.47514,190.28546 L 570.37378,193.85093 L 570.60726,197.17871 L 573.1756,201.21959 L 575.74393,202.40807 L 578.77923,202.88347 L 580.12177,205.73586 z " id="IA" style="fill:#e9afaf" />
+ <ns0:path d="M 639.20393,481.84625 L 638.01716,483.15227 L 632.73401,483.15227 L 631.24813,482.31188 L 629.10185,481.97572 L 622.16771,483.99266 L 620.35163,483.15227 L 617.71005,487.52229 L 616.58406,488.3312 L 615.43633,485.74444 L 614.2689,481.70357 L 610.76664,478.3758 L 611.93406,470.53175 L 611.23361,469.58096 L 609.36573,469.81866 L 600.96028,470.53175 L 576.2109,471.24485 L 575.74393,469.58096 L 576.44438,461.26152 L 579.94666,454.84366 L 585.3168,445.33573 L 584.38287,443.19645 L 585.55029,443.19645 L 586.25075,439.86868 L 583.91589,437.96709 L 584.14938,436.0655 L 582.04802,431.31154 L 581.75616,425.75534 L 583.15706,422.99209 L 582.74847,418.47583 L 581.34756,415.38574 L 582.74847,413.95956 L 581.34756,411.82028 L 581.81454,409.91869 L 582.74847,403.50083 L 585.78377,400.64846 L 585.08332,398.50917 L 588.81908,393.0421 L 591.62089,392.09132 L 591.62089,389.47664 L 590.92044,388.05044 L 593.72225,382.58339 L 596.52407,381.39489 L 596.63379,377.84737 L 605.49374,377.76685 L 630.09345,375.74991 L 635.69855,375.51222 L 635.7068,382.13688 L 635.8719,399.44893 L 635.04641,431.71994 L 634.88131,446.34274 L 637.68799,465.83981 L 639.20393,481.84625 z " id="MS" style="fill:#e9afaf" />
+ <ns0:path d="M 632.23973,310.19942 L 632.07463,306.16555 L 632.56993,301.45935 L 634.88131,298.43395 L 636.6974,294.40007 L 639.33898,290.03004 L 638.84368,283.97923 L 637.0276,281.12189 L 636.6974,277.76034 L 637.52289,272.04567 L 637.0276,264.81831 L 635.7068,248.17858 L 634.38601,232.21115 L 633.3949,220.02589 L 636.53128,220.95071 L 638.01716,221.95917 L 639.17285,221.62302 L 641.31913,219.60608 L 644.20888,217.92491 L 649.40999,217.75643 L 671.86343,215.40333 L 678.13717,214.73102 L 678.30227,215.90756 L 679.78816,231.20268 L 681.60425,245.48932 L 684.24582,269.86066 L 684.74112,275.7434 L 684.24582,278.09649 L 685.73171,279.60919 L 686.06192,281.62613 L 683.25523,283.64307 L 679.29287,285.49193 L 676.48618,285.82809 L 675.99089,290.53428 L 671.20302,294.56815 L 668.23125,298.26587 L 668.56145,300.61896 L 667.73595,302.80398 L 662.94809,302.80398 L 661.7924,301.1232 L 660.63671,301.96359 L 657.66493,303.64438 L 657.83004,307.17401 L 655.68375,307.67825 L 654.85825,306.5017 L 652.87707,304.82092 L 649.90529,306.33362 L 648.08921,309.69518 L 646.27312,308.85479 L 644.78724,306.83786 L 641.15506,307.34209 L 635.2115,308.35056 L 632.23973,310.19942 z " id="IN" style="fill:#de8787" />
+ <ns0:path d="M 632.07463,310.03134 L 632.07463,306.16555 L 632.56993,301.45935 L 634.88131,298.43395 L 636.6974,294.40007 L 639.33898,290.03004 L 638.84368,283.97923 L 637.0276,281.12189 L 636.6974,277.76034 L 637.52289,272.04567 L 637.0276,264.81831 L 635.7068,248.17858 L 634.38601,232.21115 L 633.56001,220.10992 L 632.23872,219.26993 L 631.41322,216.58068 L 630.09244,212.71489 L 628.44145,210.86603 L 626.95557,208.17679 L 626.71703,202.46993 L 616.60376,203.83427 L 588.81909,205.617 L 579.94666,205.17133 L 580.18014,207.63744 L 582.51499,208.35053 L 583.44892,209.53902 L 583.91589,211.44061 L 587.88513,215.00608 L 588.58559,217.38307 L 587.88513,220.94855 L 586.01725,224.75171 L 585.3168,227.36639 L 582.98196,229.26798 L 581.11408,229.98108 L 575.74393,231.40726 L 575.04347,233.30885 L 574.34302,235.44814 L 575.04347,236.87433 L 576.91135,238.53822 L 576.67787,242.81678 L 574.80999,244.48067 L 574.10954,246.14456 L 574.10954,248.99694 L 572.24166,249.47233 L 570.60726,250.66083 L 570.37378,252.08702 L 570.60726,254.22631 L 568.85613,255.59306 L 567.80545,258.50488 L 568.27242,262.30804 L 570.60726,269.91439 L 578.07878,277.75843 L 583.68241,281.56161 L 583.44892,286.07787 L 584.38287,287.50407 L 590.92044,287.97946 L 593.72225,289.40566 L 593.0218,293.20882 L 590.68696,299.38898 L 589.98649,302.71676 L 592.32134,306.75762 L 598.85891,312.22469 L 603.52862,312.93778 L 605.62997,318.16715 L 607.73133,321.49492 L 606.7974,324.58499 L 608.43179,328.86356 L 610.29967,331.00285 L 613.32836,330.65102 L 613.91377,328.51994 L 616.22516,326.67109 L 618.37144,325.99877 L 621.17811,327.3434 L 624.81029,328.68802 L 625.96598,328.35186 L 626.13108,325.99877 L 624.81029,323.47759 L 625.14049,321.1245 L 627.12167,319.6118 L 629.76325,318.93949 L 631.41424,318.26718 L 630.58875,316.41831 L 629.92835,314.40138 L 631.08404,313.56099 L 632.07463,310.03134 z " id="IL" style="fill:#782121" />
+ <ns0:path d="M 482.35005,129.91009 L 481.88308,121.11525 L 480.0152,113.50891 L 478.14732,99.484714 L 477.68036,89.263683 L 475.81248,85.698205 L 474.17808,80.468846 L 474.17808,69.772421 L 474.87853,65.731548 L 473.01887,60.063466 L 503.79211,60.100136 L 504.1223,51.528162 L 504.7827,51.360084 L 507.09408,51.864318 L 509.07526,52.704708 L 509.90075,58.419357 L 511.38664,64.806318 L 513.03762,66.487097 L 517.99058,66.487097 L 518.32077,67.999799 L 524.75961,68.335954 L 524.75961,70.520967 L 529.71257,70.520967 L 530.04276,69.176344 L 531.19845,67.999799 L 533.50983,67.327487 L 534.83062,68.335954 L 537.80239,68.335954 L 541.76476,71.025201 L 547.21301,73.54637 L 549.68948,74.050604 L 550.18478,73.042136 L 551.67066,72.537902 L 552.16596,75.563305 L 554.80753,76.907928 L 555.30283,76.403695 L 556.62362,76.571773 L 556.62362,78.756786 L 559.26519,79.765253 L 562.40206,79.765253 L 564.05305,78.924863 L 567.35502,75.563305 L 569.99659,75.059071 L 570.82209,76.907928 L 571.31738,78.252552 L 572.30797,78.252552 L 573.29856,77.412162 L 582.37898,77.076006 L 584.19506,80.269487 L 584.85546,80.269487 L 585.58425,79.142165 L 590.11857,78.756786 L 589.49346,81.126733 L 585.47097,83.036786 L 576.02857,87.25913 L 571.15228,89.345695 L 568.01541,92.034941 L 565.53894,95.732656 L 563.22756,99.766526 L 561.41147,100.60692 L 556.78872,105.81733 L 555.46793,105.98541 L 552.00086,109.17889 L 552.70347,109.74365 L 549.82713,112.55812 L 549.59365,115.4105 L 549.59365,124.20534 L 548.42622,125.86923 L 543.05607,129.91009 L 540.72123,136.09025 L 541.18819,136.32794 L 543.75652,138.46723 L 544.45698,141.79501 L 542.5891,145.12278 L 542.5891,149.16365 L 543.05607,156.0569 L 546.09137,159.14699 L 549.59365,159.14699 L 551.46153,162.47476 L 554.96379,162.95015 L 558.93303,168.89261 L 566.17105,173.17118 L 568.27242,176.02356 L 569.20636,183.86761 L 565.00364,183.86761 L 546.55834,184.5807 L 495.89216,185.29379 L 484.10119,185.29379 L 484.6849,178.16284 L 484.45141,146.54897 L 483.98444,143.4589 L 479.78172,139.89342 L 478.61429,137.99183 L 478.61429,136.32794 L 480.71565,134.66406 L 482.11657,133.23787 L 482.35005,129.91009 z " id="MN" style="fill:#d35f5f" />
+ <ns0:path d="M 626.6436,202.64577 L 626.79047,198.26019 L 625.13948,193.55401 L 624.47909,187.16705 L 623.3234,184.64588 L 624.31399,181.4524 L 625.13948,178.42699 L 626.62537,175.73775 L 625.96497,172.20811 L 625.30458,168.5104 L 625.79988,166.66154 L 627.78106,164.14037 L 627.94616,161.28305 L 627.12066,159.93842 L 627.78106,157.24918 L 628.27635,153.88762 L 631.08303,148.00489 L 634.0548,140.94562 L 634.2199,138.59253 L 633.8897,137.58406 L 633.06421,138.08829 L 628.77165,144.64333 L 625.96497,148.84528 L 623.98379,150.69414 L 623.1583,153.04723 L 621.67241,153.88762 L 620.51673,155.90455 L 619.03084,155.5684 L 618.86574,153.71954 L 620.18653,151.19837 L 622.33281,146.32411 L 624.14889,144.64333 L 625.27363,142.26081 L 623.6083,141.31962 L 622.20739,139.89342 L 620.57299,129.197 L 616.83724,128.00851 L 615.43633,125.63153 L 602.59467,122.77914 L 600.02634,121.59066 L 591.62089,119.21367 L 583.21544,118.02518 L 578.9573,112.40581 L 578.41662,113.71699 L 577.26093,113.54892 L 576.60053,112.37237 L 573.79386,111.53198 L 572.63817,111.70006 L 570.82209,112.70853 L 569.8315,112.03621 L 570.49189,110.01928 L 572.47307,106.8258 L 573.62876,105.64925 L 571.64758,104.13655 L 569.5013,104.97694 L 566.52953,106.99388 L 558.935,110.35543 L 555.96322,111.02775 L 552.99145,110.52351 L 551.98885,109.6104 L 549.82713,112.55812 L 549.59365,115.4105 L 549.59365,124.20534 L 548.42622,125.86923 L 543.05607,129.91009 L 540.72123,136.09025 L 541.18819,136.32794 L 543.75652,138.46723 L 544.45698,141.79501 L 542.5891,145.12278 L 542.5891,149.16365 L 543.05607,156.0569 L 546.09137,159.14699 L 549.59365,159.14699 L 551.46153,162.47476 L 554.96379,162.95015 L 558.93303,168.89261 L 566.17105,173.17118 L 568.27242,176.02356 L 569.20636,183.74877 L 569.90681,187.19538 L 572.24166,188.85927 L 572.47514,190.28546 L 570.37378,193.85093 L 570.60726,197.17871 L 573.1756,201.21959 L 575.74393,202.40807 L 578.77923,202.88347 L 580.03422,205.49817 L 589.40281,205.49815 L 616.60376,203.83427 L 626.6436,202.64577 z " id="WI" style="fill:#de8787" />
+ <ns0:path d="M 568.73938,255.89021 L 565.4706,252.56242 L 564.30317,250.18543 L 556.3647,250.89852 L 546.32485,251.37392 L 520.40805,252.32472 L 506.63246,252.56242 L 498.57727,252.68126 L 496.24239,252.80011 L 497.52656,255.41479 L 497.29307,257.79177 L 499.8614,261.83265 L 503.01343,266.11122 L 506.16549,268.96359 L 508.50034,269.20129 L 509.90124,270.15209 L 509.90124,273.24216 L 508.03337,274.90605 L 507.56639,277.28304 L 509.66775,280.84852 L 512.23609,283.93859 L 514.80442,285.84018 L 516.20533,297.96278 L 515.50487,334.68718 L 515.73836,339.55999 L 516.20533,346.69094 L 540.02077,346.21554 L 563.83621,345.50245 L 585.08332,344.55165 L 596.29058,344.07626 L 598.15846,347.16634 L 597.69149,349.54332 L 594.4227,352.3957 L 593.72225,355.48577 L 600.02634,355.96118 L 605.163,355.24808 L 607.26436,348.59252 L 607.11843,342.73921 L 610.06619,341.22388 L 611.46709,339.55999 L 613.56845,338.3715 L 613.80194,335.04372 L 614.73588,333.14213 L 613.33497,330.61659 L 610.29967,331.00285 L 608.43179,328.86356 L 606.7974,324.58499 L 607.73133,321.49492 L 605.62997,318.16715 L 603.52862,312.93778 L 598.85891,312.22469 L 592.32134,306.75762 L 589.98649,302.71676 L 590.68696,299.38898 L 593.0218,293.20882 L 593.72225,289.40566 L 590.92044,287.97946 L 584.38287,287.50407 L 583.44892,286.07787 L 583.68241,281.56161 L 578.07878,277.75843 L 570.60726,269.91439 L 568.27242,262.30804 L 567.80545,258.50488 L 568.73938,255.89021 z " id="MO" style="fill:#de8787" />
+ <ns0:path d="M 604.99844,354.98628 L 600.02634,355.96118 L 593.72225,355.48577 L 594.4227,352.3957 L 597.69149,349.54332 L 598.15846,347.16634 L 596.29058,344.07626 L 585.08332,344.55165 L 563.83621,345.50245 L 540.02077,346.21554 L 516.20533,346.69094 L 517.83972,353.82189 L 517.83971,362.37904 L 519.24063,373.78867 L 519.47412,413.12761 L 521.80896,415.14805 L 524.84426,413.72186 L 527.64608,414.91035 L 528.31735,426.58728 L 550.99455,426.55757 L 570.60726,425.60677 L 581.63941,425.75534 L 583.15706,422.99209 L 582.74847,418.47583 L 581.34756,415.38574 L 582.74847,413.95956 L 581.34756,411.82028 L 581.81454,409.91869 L 582.74847,403.50083 L 585.78377,400.64846 L 585.08332,398.50917 L 588.81908,393.0421 L 591.62089,392.09132 L 591.62089,389.47664 L 590.92044,388.05044 L 593.72225,382.58339 L 596.52407,381.39489 L 596.22104,377.59524 L 598.72469,376.42223 L 599.71528,371.54796 L 598.22939,367.85023 L 602.35687,365.49714 L 602.68706,362.80789 L 604.06449,358.25292 L 604.99844,354.98628 z " id="AR" style="fill:#e9afaf" />
+ <ns0:path d="M 383.76113,331.71594 L 372.84567,331.2406 L 366.27891,330.73549 L 366.54158,330.94347 L 365.98708,342.94722 L 388.2557,344.07626 L 420.00963,345.26475 L 418.60872,369.98537 L 418.14175,388.52584 L 418.37523,390.18973 L 422.81144,393.9929 L 424.9128,395.18139 L 425.61327,394.94369 L 426.31372,392.80441 L 427.71463,394.70599 L 429.81599,394.70599 L 429.81599,393.2798 L 432.6178,394.70599 L 432.15084,398.74687 L 436.35356,398.98457 L 438.92189,400.17306 L 443.12462,400.88615 L 445.69295,402.78774 L 448.02779,400.64846 L 451.53007,401.36155 L 454.0984,404.92703 L 455.03233,404.92703 L 455.03233,407.30401 L 457.36718,408.0171 L 459.70203,405.64012 L 461.5699,406.35321 L 464.13823,406.35321 L 465.07218,408.9679 L 469.97536,410.86948 L 471.37627,410.15638 L 473.24415,405.87781 L 474.41156,405.87781 L 475.57899,408.0171 L 479.78172,408.73019 L 483.51747,410.15638 L 486.55277,411.10718 L 488.42065,410.15638 L 489.1211,407.54171 L 493.55731,407.54171 L 495.65868,408.49249 L 498.46049,406.35321 L 499.62792,406.35321 L 500.32837,408.0171 L 504.53109,408.0171 L 506.16549,405.87781 L 508.03337,406.35321 L 510.13473,408.9679 L 513.40351,410.86948 L 516.6723,411.82028 L 519.47412,413.48417 L 519.24063,373.78867 L 517.83971,362.37904 L 517.83972,353.82189 L 516.20533,346.69094 L 515.73836,339.55999 L 515.50487,334.92488 L 501.9627,335.75681 L 454.56566,335.28143 L 408.56892,333.14212 L 383.76113,331.71594 z " id="OK" style="fill:#e9afaf" />
+ <ns0:path d="M 515.50487,335.04372 L 501.9627,335.75681 L 454.56566,335.28143 L 408.56892,333.14212 L 383.90706,331.83479 L 388.13897,263.37769 L 410.43676,264.20963 L 451.76325,265.63581 L 496.82609,266.11122 L 503.01343,266.11122 L 506.16549,268.96359 L 508.50034,269.20129 L 509.90124,270.15209 L 509.90124,273.24216 L 508.03337,274.90605 L 507.56639,277.28304 L 509.66775,280.84852 L 512.23609,283.93859 L 514.80442,285.84018 L 516.20533,297.96278 L 515.50487,335.04372 z " id="KS" style="fill:#e9afaf" />
+ <ns0:path d="M 616.71945,488.11058 L 615.43633,485.74444 L 614.2689,481.70357 L 610.76664,478.3758 L 611.93406,470.53175 L 611.23361,469.58096 L 609.36573,469.81866 L 600.96028,470.53175 L 576.2109,471.24485 L 575.74393,469.58096 L 576.44438,461.26152 L 579.94666,454.84366 L 585.3168,445.33573 L 584.38287,443.19645 L 585.55029,443.19645 L 586.25075,439.86868 L 583.91589,437.96709 L 584.14938,436.0655 L 582.04802,431.31154 L 581.69779,425.60677 L 570.60726,425.60677 L 550.99455,426.55757 L 528.31735,426.58728 L 528.34653,436.5409 L 529.04699,446.28652 L 529.74745,450.3274 L 532.31578,454.60597 L 533.24971,459.83533 L 537.68592,465.54009 L 537.9194,468.86787 L 538.61987,469.58096 L 537.9194,478.3758 L 534.88411,483.60516 L 536.5185,485.74444 L 535.81804,488.35912 L 535.11759,495.96547 L 533.71668,499.29324 L 533.84174,503.05325 L 538.62788,501.47276 L 546.88281,501.1366 L 557.44911,504.83432 L 564.05305,506.01086 L 567.85031,504.49816 L 571.15228,505.67471 L 574.45425,506.68317 L 575.27974,504.49816 L 571.97778,503.32162 L 569.3362,503.82585 L 566.52953,502.14507 C 566.52953,502.14507 566.69462,500.80045 567.35502,500.63237 C 568.01541,500.46429 570.49189,499.6239 570.49189,499.6239 L 572.30797,501.1366 L 574.12406,500.12814 L 577.42603,500.80045 L 578.91191,503.32162 L 579.24211,505.67471 L 583.86487,506.01086 L 585.68095,507.85972 L 584.85546,509.5405 L 583.53467,510.38089 L 585.18565,512.06167 L 593.77077,515.75938 L 597.40294,514.41476 L 598.39353,511.89359 L 601.03511,511.22128 L 602.85119,509.70858 L 604.17198,510.71704 L 604.99747,513.74245 L 602.68609,514.58284 L 603.34648,515.25515 L 606.81355,513.91053 L 609.12493,510.38089 L 609.95042,509.87666 L 607.80414,509.5405 L 608.62964,507.85972 L 608.46454,506.34702 L 610.61082,505.84279 L 611.76651,504.49816 L 612.4269,505.33855 C 612.4269,505.33855 612.2618,508.53203 613.08729,508.53203 C 613.91279,508.53203 617.37985,509.20434 617.37985,509.20434 L 621.50732,511.22128 L 622.49791,512.73398 L 625.46968,512.73398 L 626.62537,513.74245 L 628.93675,510.54897 L 628.93675,509.03627 L 627.61596,509.03627 L 624.14889,506.17894 L 618.20535,505.33855 L 614.90338,502.98546 L 616.05907,500.12814 L 618.37045,500.46429 L 618.53554,499.79198 L 616.71946,498.78351 L 616.71946,498.27928 L 620.02143,498.27928 L 621.83751,495.0858 L 620.51673,493.06886 L 620.18653,490.21154 L 618.70064,490.37962 L 616.71946,492.56463 L 616.05907,495.25388 L 612.9222,494.58156 L 611.93161,492.73271 L 613.74769,490.71577 L 615.81141,488.86693 L 616.71945,488.11058 z " id="LA" style="fill:#de8787" />
+ <ns0:path d="M 817.62464,258.28441 L 818.55858,256.38283 L 820.84673,258.09426 L 819.7727,260.66139 L 818.09161,259.04505 L 817.62464,258.28441 z " id="path6656" style="fill:#0000ff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <ns0:g id="g8180" style="fill:#cccccc">
+ <ns0:path d="M 738.7284,305.32516 L 740.70958,309.02287 L 742.03037,310.36749 L 745.00215,311.2079 L 747.64373,310.53557 L 750.12022,308.18248 C 750.12022,308.18248 751.9363,309.69518 752.59669,309.52711 C 753.25709,309.35903 757.05436,308.51864 757.05436,308.51864 L 758.87045,303.64438 L 761.51202,304.65285 L 764.814,302.13167 L 766.29989,302.46782 L 768.44617,300.45089 L 768.61127,297.59356 L 767.78577,296.24894 L 772.90384,284.9877 L 775.05012,278.09649 L 775.38031,273.55838 L 777.1964,273.3903 L 778.84739,276.24763 L 780.16818,277.08802 L 782.80976,277.08802 L 783.96545,271.37336 L 784.29564,268.17988 L 787.76272,267.84373 L 788.25802,265.49063 L 791.39489,262.96946 L 792.05529,261.28868 L 793.70628,258.43134 L 794.20157,256.24633 L 794.36667,250.69975 L 798.98943,252.5486 L 804.7679,255.7421 L 805.59338,250.44764 L 809.88595,252.5486 L 809.88595,255.57402 L 815.49931,256.91864 L 817.48049,258.26326 L 818.47108,256.24633 L 820.78247,257.92711 L 819.29657,261.28868 L 818.96637,264.146 L 817.15029,266.83525 L 817.15029,269.02027 L 817.81068,270.86913 L 822.98233,272.27864 L 824.90863,273.89525 L 830.19178,274.23141 L 832.83336,276.5845 L 836.13533,277.25681 L 837.45611,278.60143 L 836.96082,283.30762 L 837.95141,284.31608 L 838.11651,286.66917 L 839.43729,288.85419 L 839.2722,290.70305 L 835.97023,289.5265 L 835.97023,290.53497 L 837.95141,292.21575 L 837.95141,293.39229 L 839.43729,294.56884 L 840.75808,296.24962 L 840.92318,298.60271 L 838.6118,300.11541 L 838.942,300.61964 L 841.58358,300.11541 L 844.88554,299.4431 L 846.04123,299.27502 L 850.20356,306.58097 L 845.21707,308.35056 L 833.49506,311.37597 L 813.92151,315.35219 L 792.88078,319.27565 L 775.38031,321.96489 L 759.22415,323.98183 L 751.9363,325.32646 L 747.32856,324.68385 L 745.16725,324.65414 L 742.69078,326.67109 L 734.60093,326.83916 L 722.3505,328.8153 L 712.13922,329.78475 L 714.95419,328.35186 L 720.73264,324.82222 L 724.69501,322.6372 L 724.69501,320.45219 L 726.51109,318.60333 L 731.13386,313.05675 L 735.42643,309.35903 L 738.7284,305.32516 z " id="VA" style="fill:#d35f5f" />
+ <ns0:path d="M 845.69487,293.77543 L 844.35758,290.76684 L 844.75381,282.95122 L 847.26331,278.51396 L 847.13123,274.21116 L 853.07478,271.9253 L 852.41438,274.21116 L 849.24449,278.3795 L 848.92256,286.54808 L 848.17135,289.94326 L 846.33876,293.85948 L 845.69487,293.77543 z " id="path3106" style="fill:#d35f5f" ns1:nodetypes="cccccccccccc" />
+ </ns0:g>
+ <ns0:path d="M 467.38967,586.18345 L 466.81183,578.788 L 464.00514,571.30851 L 463.42729,563.99709 L 464.99573,555.42509 L 468.38027,548.28176 L 471.92989,542.65112 L 475.14933,538.95339 L 475.80972,539.20552 L 470.9393,546.09673 L 466.48163,552.90391 L 464.41788,559.79513 L 464.08769,565.17365 L 464.99573,571.56063 L 467.63732,579.04012 L 468.13261,584.41863 L 468.29771,585.93134 L 467.38967,586.18345 z M 465.65436,588.7052 L 466.64495,587.1925 L 464.82887,584.33517 L 463.83828,577.94821 L 462.0222,570.72086 L 461.1967,568.19969 L 462.0222,563.49351 L 463.17788,559.45964 L 464.49867,554.75346 L 465.98456,549.03881 L 463.67318,547.02188 L 464.66377,545.00494 L 468.79123,544.66878 L 472.7536,538.95414 L 476.22066,538.28182 L 482.16421,534.58411 L 483.98029,533.07141 L 490.41913,529.54177 L 496.19758,527.0206 L 501.64583,523.65904 L 504.4525,521.47403 L 510.23095,515.92746 L 511.55174,515.08707 L 513.69802,513.57437 L 516.33959,511.55743 L 517.33018,509.5405 L 527.40119,504.83432 L 534.17024,502.98546" id="TX_Gulf" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 900.52373,145.31564 L 900.85393,143.29871 L 902.00961,139.60099" id="NH_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 919.55232,177.09192 L 921.77043,176.37882 L 922.23741,174.59609 L 923.28809,174.71493 L 924.33877,177.09192 L 923.0546,177.56732 L 919.08535,177.68617 L 919.55232,177.09192 z M 909.97943,177.92387 L 912.31427,175.19033 L 913.94868,175.19033 L 915.81656,176.73537 L 913.36497,177.80501 L 911.14686,178.87466 L 909.97943,177.92387 z M 903.66061,177.08236 L 906.63237,175.56967 L 906.13708,173.21658 L 906.96257,171.70388 L 909.93434,170.19118 L 910.75983,173.38466 L 910.26454,175.23351 L 907.78806,176.74621 L 907.78806,177.75468 L 909.76924,176.24198 L 913.73161,171.5358 L 917.69397,169.51886 L 921.98653,168.00616 L 921.65633,165.48499 L 920.66574,162.45959 L 918.68456,159.93842 L 916.86848,159.09803 L 914.7222,159.26611 L 914.2269,159.77034 L 915.21749,161.11497 L 916.70338,160.27458 L 918.84966,161.95536 L 919.67515,164.81268 L 917.85907,166.66154 L 915.54769,167.67001 L 911.91552,167.16577 L 907.95316,160.94689 L 905.64178,158.25764 L 903.8257,158.25764 L 902.67001,159.09803 L 900.68883,156.40879 L 901.01902,154.89608 L 903.4955,149.51759 L 900.35862,144.81139" id="MA_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 771.67854,458.60093 L 771.08653,452.05785 L 773.39791,441.63702 L 774.88379,437.26699 L 774.3885,434.57775 L 778.51596,427.3504 L 777.85557,425.66962" id="GA_Atlantic" style="fill:#6666e6;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 777.85557,425.66962 L 780.00185,424.32499 L 785.1199,418.61034 L 784.12931,415.24879 L 787.10108,415.08071 L 790.73325,411.55107 L 792.38423,410.71068 L 794.69561,407.18104 L 797.50228,404.32372 L 799.64856,400.62601 L 802.12504,399.95369 L 803.28073,397.09637 L 804.93171,396.25598 L 805.42701,389.70094 L 808.06859,383.31398 L 813.51684,377.43125" id="SC_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 813.18663,377.59933 L 819.46038,375.41432 L 824.24824,374.91008 L 824.74353,372.38892 L 826.72471,365.6658 L 830.19178,360.79154 L 836.79572,355.24497 L 842.07887,352.7238 L 844.88554,352.05149 L 846.04123,352.55572 L 847.36202,352.55572 L 850.49889,347.51338 L 852.48007,343.81567 L 851.15929,344.3199 L 848.84791,346.67299 L 848.18751,344.99221 L 843.89495,344.99221 L 845.87614,338.43717 L 845.05064,337.09255 L 843.06946,337.09255 L 843.06946,336.08408 L 842.73926,334.73946 L 844.39025,336.08408 L 845.87614,336.25216 L 848.35261,336.58832 L 852.14988,334.90754 L 853.47066,331.88214 L 854.13106,329.69712 L 856.77263,328.3525 L 857.10283,323.98247 L 856.27734,323.31016 L 858.75382,323.14208 L 858.09342,320.78899 L 855.61694,318.26782 L 851.98478,311.54471 L 850.1687,306.50237 M 854.21672,340.95692 L 856.85831,338.3517 L 860.07773,335.66244 L 861.64617,334.99013 L 861.81127,332.88915 L 861.15088,326.50217 L 859.66499,324.06503 L 859.00459,322.13213 L 859.74753,321.88001 L 862.55422,327.59468 L 862.96697,332.21684 L 862.80187,335.74649 L 859.33479,337.34323 L 856.44555,339.86441 L 855.28987,341.125 L 854.21672,340.95692 z " id="NC_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 850.1687,306.50237 L 846.04123,299.27502 L 844.88554,299.4431 L 841.58358,300.11541 L 838.942,300.61964 L 838.6118,300.11541 L 840.92318,298.60271 L 840.75808,296.24962 L 839.43729,294.56884 L 837.95141,293.39229 L 837.95141,292.21575 L 835.97023,290.53497 L 835.97023,289.5265 L 839.2722,290.70305 L 839.43729,288.85419 L 838.11651,286.66917 L 837.95141,284.31608 L 836.96082,283.30762 L 837.45611,278.60143 L 836.13533,277.25681 L 832.83336,276.5845 L 830.19178,274.23141 L 824.90863,273.89525 L 822.59725,272.21447" id="VA_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 893.09433,183.30123 L 896.06607,182.29279 L 898.54255,180.27585 L 899.69824,178.42699 L 901.01902,178.59507 L 903.99082,176.91428" id="RI_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 865.2752,198.17615 L 870.47581,194.73055 L 874.10797,191.36899 L 876.08916,189.18398 L 876.91465,189.85629 L 879.72132,188.34359 L 885.00447,187.16705 L 893.58963,183.30123" id="CT_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 840.75808,236.41389 L 841.91377,238.76697 L 845.21574,241.79237 L 850.1687,244.14546 L 854.29616,244.81777 L 854.46126,246.33047 L 853.63576,247.33894 L 853.96596,250.19627 L 854.79145,250.19627 L 856.93773,247.6751 L 857.76322,242.63276 L 860.5699,238.43081 L 863.70677,231.70769 L 864.86246,225.99305 L 864.20207,224.8165 L 864.03697,215.06798 L 862.38598,211.53834 L 861.23029,212.37873 L 858.42362,212.71489 L 857.92832,212.21066 L 859.08401,211.20219 L 861.23029,209.18525 L 861.06519,207.84063" id="NJ_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 861.06519,207.84063 L 863.04638,208.51294 L 867.17384,207.3364 L 873.11738,205.31946 L 875.75896,204.31099 L 883.02329,198.76442 L 886.98565,195.73902 L 890.45272,192.0413 L 886.16016,190.36053 L 884.83937,191.87323 L 881.8676,194.73055 L 873.77778,198.76442 L 871.4664,198.59634 L 869.81541,197.92403 L 868.65972,198.59634 L 866.34835,201.28559 L 864.86246,202.63021 L 863.54167,202.96637 L 863.21147,201.62175 L 865.19266,199.77289 L 865.52285,197.75595" id="NY_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 854.95655,260.95325 L 852.64517,253.38975 L 851.65458,253.89398 L 848.02242,251.37281 L 846.20633,246.49855 L 844.22515,242.80084 L 841.91377,241.79237 L 839.76749,238.09466 L 840.59298,235.90964" id="DE_Atlantic" style="fill:#cccccc;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 29.464207,149.85375 L 27.813223,154.89608 L 27.648124,162.62767 L 22.364973,174.89736 L 19.228102,177.5866 L 18.897905,178.76315 L 17.081822,179.60354 L 15.595936,183.97356 L 14.770444,187.33512 L 17.577118,191.70515 L 19.228102,196.07518 L 20.383791,199.77289 L 20.053595,206.49601 L 18.237511,209.68949 L 17.577118,215.74029 L 16.586527,219.60608 L 18.40261,223.63995 L 21.209284,228.34614 L 23.520662,233.38847 L 24.84145,237.59042 L 24.511253,240.95198 L 24.181056,241.45621 L 24.181056,243.64123 L 29.959503,250.19627 L 29.464207,252.71743 L 28.803813,255.07053 L 28.14342,257.08746 L 28.308518,265.65943 L 30.454798,269.52523 L 32.43598,272.21447 L 35.242654,272.71871 L 36.233244,275.57603 L 35.077555,279.27375 L 32.931275,280.95453 L 31.775586,280.95453 L 30.950093,284.9884 L 31.445389,288.0138 L 34.747358,292.5519 L 36.398343,298.09847 L 37.884229,302.97273 L 39.205017,306.16621 L 42.672085,312.21702 L 44.157971,314.90627 L 44.653266,317.93167 L 46.304251,318.94014 L 46.304251,321.4613 L 45.478759,323.47824 L 43.662676,330.87367 L 43.16738,332.8906 L 45.643857,335.74793 L 49.936417,336.25216 L 54.559175,338.10102 L 58.521538,340.28603 L 61.49331,340.28603 L 64.465083,343.47951 L 67.106658,348.52185 L 68.262347,350.87494 L 72.224711,353.05995 L 77.177665,353.90034 L 78.663551,356.08536 L 79.323945,359.44692 L 77.838059,360.11923 L 78.168256,361.12769 L 81.470225,361.96808 L 84.276899,362.13616 L 87.248671,367.01042 L 91.211035,371.38045 L 92.036527,373.73354 L 94.678102,378.10356 L 95.008299,381.46512 L 95.008299,391.21364 L 95.503595,393.0625 M 50.266945,346.75563 L 51.587737,348.35237 L 51.422639,349.697 L 48.120658,349.61296 L 47.542811,348.35237 L 46.882415,346.83966 L 50.266945,346.75563 z M 52.248133,346.75563 L 53.486376,346.08332 L 57.118555,348.26833 L 60.255437,349.52892 L 59.347393,350.20124 L 54.724619,349.94912 L 53.073629,348.26833 L 52.248133,346.75563 z M 73.380812,367.34524 L 75.196895,369.78238 L 76.022398,370.79086 L 77.590839,371.37912 L 78.168678,369.86642 L 77.178087,368.01756 L 74.453957,365.91658 L 73.380812,366.08465 L 73.380812,367.34524 z M 71.894915,376.33744 L 73.711009,379.61497 L 74.949253,381.63192 L 73.463356,381.88403 L 72.142568,380.62344 C 72.142568,380.62344 71.39962,379.11074 71.39962,378.69054 C 71.39962,378.27035 71.39962,376.42148 71.39962,376.42148 L 71.894915,376.33744 z " id="CA_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 95.99889,2.9536428 L 94.843201,3.7940324 L 94.513004,4.634422 L 96.989481,9.6767597 L 98.14517,12.366006 L 96.329087,16.063721 L 96.329087,18.416812 L 96.989481,19.929513 L 96.163989,21.77837 L 96.659284,25.139928 L 97.649875,26.484552 L 97.484776,27.829175 L 95.99889,27.997253 L 95.338496,25.980318 L 94.182807,23.459149 L 92.366724,21.946448 L 92.696921,19.761435 L 95.008299,19.257201 L 94.678102,17.408344 L 94.347905,16.231799 L 92.201625,17.576422 L 90.880838,18.752967 L 90.880838,21.274136 L 88.569459,21.442214 L 85.102391,20.433747 L 82.130619,18.921045 L 78.993748,18.248734 L 74.370991,16.063721 L 71.069021,14.046786 L 68.262347,11.357539 L 65.78587,8.3321363 L 63.63959,7.8279025 L 61.328212,17.576422 L 63.144295,20.93798 L 63.144295,29.173799 L 62.483901,32.199201 L 63.969787,39.59463 L 66.776461,42.451954 L 62.318803,43.124266 L 62.153704,46.990058 L 64.79528,48.166604 L 63.144295,52.368552 L 60.337621,52.704708 L 60.007424,55.73011 L 62.318803,58.755513 L 64.134886,57.747045 L 66.446264,59.427825 M 86.341089,9.169955 L 88.404826,9.001877 L 88.900121,10.430545 L 90.468562,8.7497548 L 92.862495,8.7497548 L 93.687987,10.3465 L 92.119546,12.111324 L 92.779951,12.951724 L 92.037002,15.052704 L 90.63366,15.472893 C 90.63366,15.472893 89.725613,15.556938 89.725613,15.220782 C 89.725613,14.884626 91.21151,12.531524 91.21151,12.531524 L 89.477971,11.943246 L 89.147774,13.455958 L 88.404826,14.12827 L 86.836385,11.775168 L 86.341089,9.169955 z " id="WA_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 66.446264,59.427825 L 60.172522,61.44476 L 57.861144,68.167876 L 54.559175,78.588708 L 51.257205,85.311824 L 46.139153,99.934604 L 39.535214,114.05315 L 31.28029,127.16323 L 29.299109,130.18863 L 28.473616,139.09676 L 27.152829,145.31564 L 29.464207,149.85375" id="OR_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 639.33795,481.63956 L 642.14462,481.63956 L 642.80502,481.80764 L 644.12581,478.95032 L 645.61169,474.41221 L 647.92307,475.08453 L 651.05994,481.30341 L 651.05994,482.31188 L 648.25327,484.32881 L 651.05994,484.66497 L 658.02586,481.83647" id="AL_Gulf" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 616.38926,488.36268 L 617.71005,487.52229 L 620.35163,483.15227 L 622.16771,483.99266 L 629.10185,481.97572 L 631.24813,482.31188 L 632.73401,483.15227 L 638.01716,483.15227 L 639.33795,481.63956" id="MS_Gulf" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:path d="M 533.67494,503.06949 L 538.62788,501.47276 L 546.88281,501.1366 L 557.44911,504.83432 L 564.05305,506.01086 L 567.85031,504.49816 L 571.15228,505.67471 L 574.45425,506.68317 L 575.27974,504.49816 L 571.97778,503.32162 L 569.3362,503.82585 L 566.52953,502.14507 C 566.52953,502.14507 566.69462,500.80045 567.35502,500.63237 C 568.01541,500.46429 570.49189,499.6239 570.49189,499.6239 L 572.30797,501.1366 L 574.12406,500.12814 L 577.42603,500.80045 L 578.91191,503.32162 L 579.24211,505.67471 L 583.86487,506.01086 L 585.68095,507.85972 L 584.85546,509.5405 L 583.53467,510.38089 L 585.18565,512.06167 L 593.77077,515.75938 L 597.40294,514.41476 L 598.39353,511.89359 L 601.03511,511.22128 L 602.85119,509.70858 L 604.17198,510.71704 L 604.99747,513.74245 L 602.68609,514.58284 L 603.34648,515.25515 L 606.81355,513.91053 L 609.12493,510.38089 L 609.95042,509.87666 L 607.80414,509.5405 L 608.62964,507.85972 L 608.46454,506.34702 L 610.61082,505.84279 L 611.76651,504.49816 L 612.4269,505.33855 C 612.4269,505.33855 612.2618,508.53203 613.08729,508.53203 C 613.91279,508.53203 617.37985,509.20434 617.37985,509.20434 L 621.50732,511.22128 L 622.49791,512.73398 L 625.46968,512.73398 L 626.62537,513.74245 L 628.93675,510.54897 L 628.93675,509.03627 L 627.61596,509.03627 L 624.14889,506.17894 L 618.20535,505.33855 L 614.90338,502.98546 L 616.05907,500.12814 L 618.37045,500.46429 L 618.53554,499.79198 L 616.71946,498.78351 L 616.71946,498.27928 L 620.02143,498.27928 L 621.83751,495.0858 L 620.51673,493.06886 L 620.18653,490.21154 L 618.70064,490.37962 L 616.71946,492.56463 L 616.05907,495.25388 L 612.9222,494.58156 L 611.93161,492.73271 L 613.74769,490.71577 L 616.38926,488.36268 L 617.2973,487.77441" id="LA_Gulf" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" />
+ <ns0:g id="Great_Lakes_borders" style="fill:none;stroke:#80b0f0" transform="matrix(1.0566302,0,0,1.0756987,-45.325399,-166.80506)">
+ <ns0:path d="M 652.1875,357.8125 L 649.84375,359.21875 L 647.8125,361.09375 L 646.71875,361.40625 L 645.3125,360.46875 L 642.18749,359.53125" id="IN_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" />
+ <ns0:path d="M 712.53906,338.43751 L 712.8125,334.6875 L 714.29687,331.75782 L 715.50782,330.39062 M 717.85156,323.16407 L 714.6875,315 L 712.5,306.25 L 710.15625,303.125 L 707.65625,301.40625 L 706.09375,302.5 L 702.34375,304.21875 L 700.46875,309.0625 L 697.8125,312.65625 L 696.71875,313.28125 L 695.3125,312.65625 C 695.3125,312.65625 692.8125,311.25 692.96875,310.625 C 693.125,310 693.4375,305.78125 693.4375,305.78125 L 696.71875,304.53125 L 697.5,301.25 L 698.125,298.75 L 700.46875,297.1875 L 700.15625,287.5 L 698.59375,285.3125 L 697.34375,284.53125 L 696.5625,282.5 L 697.34375,281.71875 L 698.90625,282.03125 L 699.0625,280.46875 L 696.71875,278.28125 L 695.46875,275.78125 L 692.96875,275.78125 L 688.59375,274.375 L 683.28125,271.09375 L 680.625,271.09375 L 680,271.71875 L 679.0625,271.25 L 676.09375,269.0625 L 673.28125,270.78125 L 670.46875,272.96875 L 670.78125,276.40625 L 671.71875,276.71875 L 673.75,277.1875 L 674.21875,277.96875 L 671.71875,278.75 L 669.21875,279.0625 L 667.8125,280.78125 L 667.5,282.8125 L 667.8125,284.375 L 668.125,289.6875 L 664.6875,291.71875 L 664.0625,291.5625 L 664.0625,287.5 L 665.3125,285.15625 L 665.9375,282.8125 L 665.15625,282.03125 L 663.28125,282.8125 L 662.34375,286.875 L 659.6875,287.96875 L 657.96875,289.84375 L 657.8125,290.78125 L 658.4375,291.5625 L 657.8125,294.0625 L 655.625,294.53125 L 655.625,295.625 L 656.40625,297.96875 L 655.3125,303.90625 L 653.75,307.8125 L 654.375,312.34375 L 654.84375,313.4375 L 654.0625,315.78125 L 653.75,316.5625 L 653.4375,319.21875 L 656.875,325 L 659.6875,331.25 L 661.09375,335.9375 L 660.3125,340.46875 L 659.375,346.25 L 657.03125,351.25 L 656.71875,353.90625 L 654.84375,356.25 L 652.1875,357.8125 M 605.4621,230.97629 L 607.22987,228.98755 L 609.3291,228.21415 L 614.52193,224.45763 L 616.73164,223.9052 L 617.17359,224.34715 L 612.20173,229.31901 L 608.99764,231.19726 L 607.0089,232.08115 L 605.4621,230.97629 z M 634.68749,287.50003 L 638.28125,279.6875 L 639.21875,275.78125 L 641.09375,271.5625 L 641.875,271.40625 L 642.96875,272.96875 L 643.59375,272.96875 L 647.96875,270.625 L 649.375,272.1875 L 649.84375,272.34375 L 651.09375,271.25 L 652.1875,268.28125 L 654.53125,267.5 L 661.25,266.875 L 663.125,264.375 L 668.125,264.21875 L 673.75,265.46875 L 675.46875,265.46875 L 678.59375,264.0625 L 680.78125,264.21875 L 682.8125,263.59375 L 686.40625,264.0625 L 687.1875,264.375 L 688.4375,264.0625 L 687.1875,263.125 L 685.9375,262.5 L 682.8125,259.53125 L 682.8125,252.8125 L 681.40625,252.34375 L 680.3125,253.4375 L 674.375,255 L 672.5,255.46875 L 669.6875,254.6875 L 669.21875,254.375 L 669.21875,248.90625 L 667.8125,248.75 L 665.3125,250 L 660.9375,251.875 L 654.53125,252.1875 L 651.25,253.28125 L 647.34375,256.71875 L 645.78125,257.65625 L 644.6875,257.65625 L 643.4375,258.4375 L 641.875,257.96875 L 640.3125,256.71875 L 638.90625,257.65625 L 635.15625,257.8125 L 632.5,255.15625 L 631.09375,252.1875 L 629.6875,251.09375 L 626.5625,250.15625 L 624.375,250.15625 L 623.125,248.90625 L 619.6875,251.71875 L 618.75,252.8125 L 617.96875,252.34375 L 618.28125,249.84375 L 620.625,246.71875 L 621.09375,244.375 L 623.28125,243.59375 L 624.6875,240.625 L 628.28125,239.6875 L 628.59375,238.75 L 627.5,237.65625 L 622.96875,238.125 L 618.75,240.46875 L 616.5625,242.65625 L 615.3125,244.375 L 613.59375,245.15625 L 611.71875,247.96875 L 611.5625,249.21875 L 607.34375,251.25 L 605,253.125 L 599.21875,254.0625 L 598.59375,254.6875 L 598.59375,255.625 L 595.15625,257.8125 L 592.5,258.59375 L 590.9375,259.53125 M 688.75238,262.0292 L 689.37738,264.45108 L 692.50239,264.60733 L 693.7524,263.43545 C 693.7524,263.43545 693.67427,262.0292 693.36177,261.87295 C 693.04927,261.7167 691.79927,260.07607 691.79927,260.07607 L 689.68989,260.31044 L 688.12738,260.46669 L 687.81488,261.56045 L 688.75238,262.0292 z M 707.34375,352.8125 L 706.09375,351.5625 L 706.25,350.15625 L 708.28125,346.5625 L 710.42969,344.60937" id="MI_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" />
+ <ns0:path d="M 762.03126,331.40624 L 756.5625,336.875 L 755.3125,337.34375 L 751.25001,340.46875" id="PA_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" />
+ <ns0:path d="M 773.4375,318.28125 L 773.59375,319.21875 L 772.5,320.15625 L 770.46875,322.8125 L 770,324.375 L 768.125,326.09375 L 766.40625,327.1875 L 765.46875,328.75 L 764.21875,329.84375 L 761.56251,331.71874 M 823.75,260.46875 L 821.25,262.34375 L 819.21875,264.6875 L 816.5625,268.28125 L 813.75,272.65625 L 812.34375,275.46875 L 811.71875,276.25 L 806.09375,281.5625 L 806.25,284.0625 L 807.03125,285.15625 L 808.75,285.9375 L 810.46875,285.9375 L 810.46875,287.34375 L 809.375,289.375 L 809.6875,290.78125 L 811.09375,292.8125 L 810.9375,295 L 809.0625,296.09375 L 807.03125,296.09375 L 805.46875,297.96875 L 803.75,301.09375 L 801.71875,302.8125 L 796.71875,303.28125 L 794.21875,304.375 L 792.1875,305.625 L 790.625,305.46875 L 788.75,304.21875 L 782.65625,304.375 L 779.53125,304.84375 L 775.625,306.09375 L 771.40625,307.5 L 768.59375,309.21875" id="NY_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" />
+ <ns0:path d="M 751.40626,340.46875 L 744.375,344.0625 L 740.625,346.25 L 737.34375,349.84375 L 733.4375,353.59375 L 730.3125,354.375 L 727.5,354.84375 L 722.1875,357.34375 L 720.15625,357.5 L 716.875,354.53125 L 711.875,355.15625 L 709.375,353.75 L 706.71874,352.34374" id="OH_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" />
+ <ns0:path d="M 642.5,359.6875 L 641.25,358.90625 L 640.46875,356.40625 L 639.21875,352.8125 L 637.65625,351.09375 L 636.25,348.59375 L 636.09375,343.12504" id="IL_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" />
+ <ns0:path d="M 590.9375,259.53125 L 590.3125,260.78125 L 589.21875,260.625 L 588.59375,259.53125 L 585.9375,258.75 L 584.84375,258.90625 L 583.125,259.84375 L 582.1875,259.21875 L 582.8125,257.34375 L 584.6875,254.375 L 585.78125,253.28125 L 583.90625,251.875 L 581.875,252.65625 L 579.0625,254.53125 L 571.875,257.65625 L 569.0625,258.28125 L 566.25,257.8125 L 565.46875,256.875 M 636.25,343.43744 L 636.09375,339.375 L 634.53125,335 L 633.90625,329.0625 L 632.8125,326.71875 L 633.75,323.75 L 634.53125,320.9375 L 635.9375,318.4375 L 635.3125,315.15625 L 634.6875,311.71875 L 635.15625,310 L 637.03125,307.65625 L 637.1875,305 L 636.40625,303.75 L 637.03125,301.25 L 637.5,298.125 L 640.15625,292.65625 L 642.96875,286.09375 L 643.125,283.90625 L 642.8125,282.96875 L 642.03125,283.4375 L 637.96875,289.53125 L 635.3125,293.4375 L 633.4375,295.15625 L 632.65625,297.34375 L 631.25,298.125 L 630.15625,300 L 628.75,299.6875 L 628.59375,297.96875 L 629.84375,295.625 L 631.875,291.09375 L 633.59375,289.53125 L 634.68749,287.03127" id="WI_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" />
+ <ns0:path d="M 565.9375,257.34374 L 565.3125,256.5625 L 568.59375,253.59375 L 569.84375,253.4375 L 574.21875,248.59375 L 575.9375,247.8125 L 578.125,244.0625 L 580.46875,240.625 L 583.4375,238.125 L 587.5,236.40625 L 595,232.8125 L 597.8125,232.03125 C 597.8125,232.03125 600.78125,230.3125 601.09375,229.6875 C 601.40625,229.0625 601.71875,228.28125 601.71875,228.28125" id="MN_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" />
+ </ns0:g>
+ <ns0:path d="M 152.15345,458.16063 L 151.84095,540.66102 L 153.40345,541.59852 L 156.37222,541.75477 L 157.77847,540.66102 L 160.27848,540.66102 L 160.43474,543.47353 L 167.15352,550.03606 L 167.62227,552.53607 L 170.90353,550.66106 L 171.52854,550.50481 L 171.84104,547.53605 L 173.24729,545.97354 L 174.34105,545.81729 L 176.21606,544.41103 L 179.18482,546.44229 L 179.80983,549.25481 L 181.68483,550.34856 L 182.77859,552.69232 L 186.52861,554.41108 L 189.80987,560.19236 L 192.46613,563.94237 L 194.65364,566.59864 L 196.0599,570.1924 L 200.90367,571.91116 L 205.9037,573.94242 L 206.8412,578.16119 L 207.30995,581.12995 L 206.37245,584.41122 L 204.65369,586.59873 L 203.09118,585.81748 L 201.68493,582.84871 L 199.02866,581.44246 L 197.30991,580.3487 L 196.52865,581.12995 L 197.93491,583.78622 L 198.09116,587.37998 L 196.9974,587.84873 L 195.1224,585.97373 L 193.09114,584.72372 L 193.55989,586.28623 L 194.80989,588.00499 L 194.02864,588.78624 C 194.02864,588.78624 193.24739,588.47374 192.77864,587.84873 C 192.30988,587.22373 190.74738,584.56747 190.74738,584.56747 L 189.80987,582.37996 C 189.80987,582.37996 189.49737,583.62997 188.87237,583.31746 C 188.24736,583.00496 187.62236,581.91121 187.62236,581.91121 L 189.34112,580.0362 L 187.93486,578.62994 L 187.93486,573.78617 L 187.15361,573.78617 L 186.37236,577.06743 L 185.2786,577.53619 L 184.3411,573.94242 L 183.71609,570.34865 L 182.93484,569.8799 L 183.24734,575.34868 L 183.24734,576.44243 L 181.84108,575.19243 L 178.40357,569.41115 L 176.37231,568.9424 L 175.74731,565.34863 L 174.1848,562.53612 L 172.62229,561.44236 L 172.62229,559.25485 L 174.65355,558.00485 L 174.1848,557.69235 L 171.68479,558.31735 L 168.40352,555.97359 L 165.90351,553.16107 L 161.21599,550.66106 L 157.30972,548.16105 L 158.55973,545.03604 L 158.55973,543.47353 L 156.84097,545.03604 L 154.02846,546.12979 L 150.43469,545.03604 L 144.96591,542.69228 L 139.65339,542.69228 L 139.02839,543.16103 L 132.77836,539.41101 L 130.7471,539.09851 L 128.09084,533.47348 L 124.65332,533.78598 L 121.2158,535.19224 L 121.68456,539.56726 L 122.77831,536.75475 L 123.71582,537.06725 L 122.30956,541.28602 L 125.43457,538.62976 L 126.05958,540.19226 L 122.30956,544.41103 L 121.05955,544.09853 L 120.5908,542.22352 L 119.3408,541.44227 L 118.09079,542.53603 L 115.43453,540.81727 L 112.46576,542.84853 L 110.74701,544.87979 L 107.46574,546.91105 L 102.93447,546.75479 L 102.46572,544.72354 L 106.05948,544.09853 L 106.05948,542.84853 L 103.87197,542.22352 L 104.80948,539.87976 L 106.99699,536.12975 L 106.99699,534.41099 L 107.15324,533.62973 L 111.37201,531.44222 L 112.30951,532.69223 L 114.96578,532.69223 L 113.71577,530.19222 L 110.122,529.87972 L 105.27823,532.53598 L 102.93447,535.81724 L 101.21571,538.31726 L 100.12196,540.50477 L 96.059441,541.91102 L 93.090671,544.41103 L 92.778171,545.97354 L 94.965681,546.91105 L 95.746941,548.9423 L 93.090671,552.06732 L 86.840651,556.12984 L 79.340608,560.19236 L 77.309348,561.28611 L 72.153076,562.37987 L 66.996796,564.56738 L 68.715556,565.81738 L 67.309296,567.22364 L 66.840546,568.31739 L 64.184286,567.37989 L 61.059276,567.53614 L 60.278016,569.72365 L 59.340516,569.72365 L 59.653016,567.37989 L 56.215496,568.6299 L 53.402986,569.5674 L 50.121721,568.31739 L 47.309208,570.1924 L 44.184194,570.1924 L 42.152934,571.44241 L 40.590427,572.22366 L 38.559168,571.91116 L 36.059156,570.81741 L 33.871646,571.44241 L 32.934142,572.37991 L 31.371634,571.28616 L 31.371634,569.41115 L 34.340398,568.16114 L 40.434176,568.78615 L 44.652946,567.22364 L 46.684205,565.19238 L 49.496718,564.56738 L 51.215476,563.78612 L 53.871739,563.94237 L 55.434246,565.19238 L 56.371746,564.87988 L 58.559256,562.22362 L 61.528026,561.28611 L 64.809286,560.66111 L 66.059296,560.34861 L 66.684296,560.81736 L 67.465556,560.81736 L 68.715556,557.22359 L 72.621826,555.81734 L 74.496836,552.22357 L 76.684348,547.84855 L 78.246858,546.44229 L 78.559358,543.94228 L 76.996848,545.19229 L 73.715576,545.81729 L 73.090576,543.47353 L 71.840576,543.16103 L 70.903066,544.09853 L 70.746816,546.91105 L 69.340556,546.75479 L 67.934306,541.12977 L 66.684296,542.37977 L 65.590546,541.91102 L 65.278046,540.03601 L 61.371776,540.19226 L 59.340516,541.28602 L 56.840506,540.97352 L 58.246756,539.56726 L 58.715506,537.06725 L 58.090506,535.19224 L 59.496766,534.25474 L 60.746766,534.09849 L 60.121766,532.37973 L 60.121766,528.16096 L 59.184266,527.22345 L 58.403006,528.62971 L 52.465482,528.62971 L 51.059226,527.3797 L 50.434223,523.62969 L 48.402963,520.19217 L 48.402963,519.25467 L 50.434223,518.47341 L 50.590473,516.44215 L 51.684228,515.3484 L 50.902975,514.87965 L 49.652969,515.3484 L 48.559214,512.69214 L 49.496718,507.84836 L 53.871739,504.72335 L 56.371746,503.16084 L 58.246756,499.56708 L 60.903026,498.31707 L 63.403036,499.41083 L 63.715536,501.75459 L 66.059296,501.44208 L 69.184306,499.09832 L 70.746816,499.72333 L 71.684316,500.34833 L 73.246826,500.34833 L 75.434338,499.09832 L 76.215598,494.87955 C 76.215598,494.87955 76.528098,492.06704 77.153098,491.59829 C 77.778098,491.12954 78.090598,490.66079 78.090598,490.66079 L 76.996848,488.78578 L 74.496836,489.56703 L 71.371816,490.34828 L 69.496806,489.87953 L 66.059296,488.16077 L 61.215526,488.00452 L 57.778006,484.41076 L 58.246756,480.66074 L 58.871766,478.31698 L 56.840506,476.59822 L 54.965494,473.00445 L 55.434246,472.2232 L 61.996776,471.75445 L 64.028036,471.75445 L 64.965536,472.69195 L 65.590546,472.69195 L 65.434296,471.12944 L 69.184306,470.50444 L 71.684316,470.81694 L 73.090576,471.9107 L 71.684316,473.94196 L 71.215566,475.34821 L 73.871836,476.91072 L 78.715608,478.62948 L 80.434368,477.69198 L 78.246858,473.47321 L 77.309348,470.34819 L 78.246858,469.56694 L 74.965588,467.69193 L 74.496836,466.59817 L 74.965588,465.03567 L 74.184336,461.28565 L 71.371816,456.75438 L 69.028056,452.69186 L 71.840576,450.81685 L 74.965588,450.81685 L 76.684348,451.44185 L 80.746868,451.2856 L 84.340631,447.84809 L 85.434391,444.87932 L 89.028161,442.53556 L 90.590661,443.47307 L 93.246921,442.84806 L 96.840691,440.8168 L 97.934451,440.66055 L 98.871951,441.44181 L 103.24697,441.28556 L 105.90323,438.31679 L 106.99699,438.31679 L 110.4345,440.66055 L 112.30951,442.69181 L 111.84076,443.78557 L 112.46576,444.87932 L 114.02827,443.31682 L 117.77829,443.62932 L 118.09079,447.22308 L 119.9658,448.62934 L 126.84083,449.25434 L 132.93461,453.31686 L 134.34086,452.37936 L 139.34089,454.87937 L 141.37215,454.25437 L 143.24716,453.47311 L 147.93468,455.34812 L 152.15345,458.16063 z M 40.902929,486.12951 L 42.934188,491.28579 L 42.777937,492.22329 L 39.965424,491.91079 L 38.246666,488.00452 L 36.527908,486.59827 L 34.184147,486.59827 L 34.027897,484.09825 L 35.746655,481.75449 L 36.84041,484.09825 L 38.246666,485.50451 L 40.902929,486.12951 z M 38.402917,518.47341 L 41.996684,519.25467 L 45.59045,520.19217 L 46.371704,521.12968 L 44.809197,524.72344 L 41.840433,524.56719 L 38.559168,521.12968 L 38.402917,518.47341 z M 18.402824,504.8796 L 19.49658,507.37961 L 20.590335,508.94212 L 19.49658,509.72337 L 17.46532,506.75461 L 17.46532,504.8796 L 18.402824,504.8796 z M 5.1215129,575.50493 L 8.4027779,573.31742 L 11.684043,572.37991 L 14.184055,572.69241 L 14.652807,574.25492 L 16.527816,574.72367 L 18.402824,572.84867 L 18.090323,571.28616 L 20.746585,570.66116 L 23.559098,573.16117 L 22.465343,574.87992 L 18.246574,575.97368 L 15.590311,575.50493 L 11.996545,574.41117 L 7.7777749,575.81743 L 6.2152679,576.12993 L 5.1215129,575.50493 z M 52.465482,571.12991 L 54.027989,573.00492 L 56.059246,571.44241 L 54.652992,570.1924 L 52.465482,571.12991 z M 55.277995,574.09867 L 56.371746,571.91116 L 58.403006,572.22366 L 57.621756,574.09867 L 55.277995,574.09867 z M 78.090598,572.22366 L 79.496858,573.94242 L 80.434368,572.84867 L 79.653108,570.97366 L 78.090598,572.22366 z M 86.528141,560.19236 L 87.621901,565.81738 L 90.434411,566.59864 L 95.278181,563.78612 L 99.496951,561.28611 L 97.934451,558.94235 L 98.403201,556.59859 L 96.371941,557.8486 L 93.559431,557.06734 L 95.121931,555.97359 L 96.996941,556.75484 L 100.74696,555.03608 L 101.21571,553.62983 L 98.871951,552.84857 L 99.653201,550.97356 L 96.996941,552.84857 L 92.465671,556.28609 L 87.778151,559.0986 L 86.528141,560.19236 z M 127.46583,540.97352 L 129.80959,539.56726 L 128.87209,537.8485 L 127.15333,538.78601 L 127.46583,540.97352 z " id="AK" style="fill:#f4d7d7" />
+ <ns0:g id="g16325" style="stroke:#000000;stroke-opacity:1">
+ <ns0:g id="g5778" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1">
+ <ns0:path d="M 816.92419,258.09425 C 817.71859,258.70718 818.14466,259.56702 819.77271,260.56631 L 819.77271,260.61385" id="path6654" style="fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:#000000;stroke-width:1.33265233;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" ns1:nodetypes="ccc" />
+ <ns0:g id="g4679" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1">
+ <ns0:g id="g3580" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1">
+ <ns0:g id="State_borders_old" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1" transform="matrix(1.0566302,0,0,1.0756987,-45.325399,-166.80506)">
+ <ns0:path d="M 389.29574,462.33445 L 395.75915,462.99736 L 406.8077,463.54979" id="CO_OK" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 389.79293,462.72114 L 389.2405,473.88019" id="NM_OK" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 389.90342,473.82495 L 388.24613,473.82494 L 386.69931,491.94483 L 382.94283,545.64053 L 380.73312,568.62152 L 352.66979,567.07472 L 324.60654,564.42309 L 316.87248,563.76016 L 317.5354,568.62152" id="NM_TX" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 299.63678,367.31684 L 319.96612,369.74752 L 355.98408,373.72497 L 379.62831,375.71374" id="WY_CO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 379.62831,375.71374 L 411.89008,378.36539 L 410.34328,400.02056" id="NE_CO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 410.34328,400.02056 L 406.36581,463.66023" id="CO_KS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 379.62831,375.71374 L 383.82676,332.84535" id="WY_NE" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 383.82676,332.84535 L 385.37355,308.31756 L 386.63467,290.78272" id="WY_SD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 386.7128,291.15996 L 388.23277,275.63418 L 388.81756,270.31054" id="MT_SD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 388.81755,270.85742 L 389.59881,259.06782 L 391.78171,234.95517 L 393.10754,220.37107 L 394.43337,206.67087" id="MT_ND" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 389.13006,270.75248 L 443.26797,274.28802 L 499.6156,275.83481" id="ND_SD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 490.55578,211.09029 L 492.32355,216.17262 L 491.66064,219.92913 L 491.66064,229.87283 L 493.20744,234.73419 L 494.97521,238.04876 L 495.41715,247.55052 L 497.18492,260.58781 L 498.95269,267.65888 L 499.39463,275.83481" id="ND_MN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 499.39463,275.83481 L 499.17366,278.92841 L 497.84783,280.25424 L 495.85909,281.80103 L 495.85909,283.34783 L 496.96395,285.1156 L 500.94143,288.43017 L 501.38337,291.30279 L 501.60434,320.69194 L 501.1624,327.32107" id="SD_MN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 501.1624,327.32107 L 499.39463,327.32107 L 498.51074,329.75176 L 498.95269,332.40341 L 501.60434,334.39215 L 500.27851,339.91643 L 498.51074,344.11488 L 500.05754,346.76653 L 501.60434,348.97624" id="SD_IA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 501.60434,348.97624 L 499.83657,348.97624 L 498.0688,348.97624 L 497.18492,346.9875 L 496.74297,345.4407 L 493.87035,343.89391 L 489.00899,342.34711 L 487.24122,341.02128 L 484.58957,341.24225 L 480.83306,341.68419 L 476.63461,343.01002 L 475.52975,342.12614 L 470.88936,339.25351 L 468.90062,337.0438 L 458.51498,337.48574 L 435.53399,336.38089 L 418.29824,335.27603 L 398.85279,334.17118 L 383.82676,333.50826" id="SD_NE" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 500.94143,327.32107 L 512.21095,327.32107 L 560.16167,326.65816 L 577.61839,325.99525 L 581.59587,325.99525" id="MN_IA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 581.59587,325.99525 L 580.71198,318.7032 L 578.72324,316.05155 L 571.87314,312.07407 L 568.11663,306.54979 L 564.80207,306.10785 L 563.0343,303.01426 L 559.71973,303.01426 L 556.84711,300.14163 L 556.40516,293.73347 L 556.40516,289.97696 L 558.17293,286.88337 L 557.51002,283.78977 L 555.07934,281.80103 L 554.6374,281.58006 L 556.84711,275.83481 L 561.92944,272.07831 L 563.0343,270.53151 L 563.0343,262.35558 L 563.25527,259.70393 L 566.01741,256.8313" id="MN_WI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 590.71092,259.59344 L 594.85413,264.78626 L 602.80909,265.89112 L 610.76405,268.10083 L 613.19473,269.20568 L 625.34814,271.85734 L 626.67397,274.06705 L 630.2095,275.1719 L 631.7563,285.1156 L 633.08213,286.44143 L 634.62893,287.60152" id="WI_MI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 581.59587,325.77428 L 582.25878,329.08884 L 584.46849,330.63564 L 584.68946,331.96147 L 582.70072,335.27603 L 582.92169,338.36963 L 585.35238,342.12614 L 587.78306,343.23099 L 590.65568,343.67293 L 591.92627,346.3246" id="IA_WI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 591.76054,345.88265 L 601.04136,346.10361 L 626.453,344.55682 L 635.95475,343.45196" id="WI_IL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 591.76054,345.71692 L 591.98151,348.09236 L 594.19122,348.75527 L 595.0751,349.86012 L 595.51704,351.62789 L 599.27355,354.94246 L 599.93647,357.15217 L 599.27355,360.46674 L 597.50578,364.00227 L 596.84287,366.43295 L 594.63316,368.20072 L 592.86539,368.86364 L 587.78306,370.18946 L 587.12014,371.95723 L 586.45723,373.94597 L 587.12014,375.2718 L 588.88791,376.8186 L 588.66694,380.79607 L 586.89917,382.34287 L 586.23626,383.88967 L 586.23626,386.54132 L 584.46849,386.98326 L 582.92169,388.08812 L 582.70072,389.41395 L 582.92169,391.40269 L 581.15393,393.05996" id="IA_IL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 581.3749,393.17045 L 578.06033,389.85589 L 576.95547,387.64618 L 569.44246,388.30909 L 559.9407,388.75103 L 535.41291,389.63492 L 522.37562,389.85589 L 513.31581,390.07686 L 511.98998,390.07686" id="IA_MO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 512.53686,390.31124 L 510.66415,384.99453 L 510.44318,378.58636 L 508.89638,374.60888 L 508.23347,369.52655 L 506.02376,365.99101 L 505.13988,361.35062 L 502.48822,354.05857 L 501.1624,348.75527" id="NE_IA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 410.12231,399.79959 L 431.33554,400.68347 L 470.44713,402.00929 L 513.09483,402.45124 L 519.06105,402.45124" id="NE_KS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 519.06105,402.45124 L 515.96746,398.47376 L 513.53678,394.71725 L 513.75775,392.50754 L 512.43192,390.07686" id="NE_MO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 518.84008,402.45124 L 521.93368,405.10289 L 524.14339,405.32386 L 525.46921,406.20775 L 525.46921,409.08037 L 523.70145,410.62717 L 523.2595,412.83688 L 525.24824,416.15145 L 527.67893,419.02407 L 530.10961,420.79184 L 531.43543,432.06136 L 530.77252,466.53285" id="KS_MO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 530.77252,466.53285 L 517.95614,467.19576 L 473.09935,466.75383 L 429.56781,464.76507 L 406.08959,463.43925" id="KS_OK" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 388.4119,473.88015 L 410.34328,474.92975 L 440.39535,476.03461 L 439.06952,499.0156 L 438.62758,516.25134 L 438.84855,517.79814 L 443.047,521.33368 L 445.03574,522.43853 L 445.69866,522.21756 L 446.36157,520.22882 L 447.6874,521.99659 L 449.67614,521.99659 L 449.67614,520.67076 L 452.32779,521.99659 L 451.88585,525.7531 L 455.86333,525.97407 L 458.29401,527.07893 L 462.27149,527.74184 L 464.70217,529.50961 L 466.91188,527.52087 L 470.22645,528.18378 L 472.65713,531.49835 L 473.54101,531.49835 L 473.54101,533.70806 L 475.75072,534.37097 L 477.96043,532.16126 L 479.7282,532.82417 L 482.15888,532.82417 L 483.04277,535.25486 L 487.68316,537.02262 L 489.00899,536.35971 L 490.77676,532.38223 L 491.88161,532.38223 L 492.98647,534.37097 L 496.96395,535.03388 L 500.49948,536.35971 L 503.37211,537.2436 L 505.13988,536.35971 L 505.80279,533.92903 L 510.00124,533.92903 L 511.98998,534.81291 L 514.64163,532.82417 L 515.74649,532.82417 L 516.4094,534.37097 L 520.38688,534.37097 L 521.93368,532.38223 L 523.70145,532.82417 L 525.69019,535.25486 L 528.78378,537.02262 L 531.87738,537.90651 L 534.52903,539.45331" id="OK_TX" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 534.52903,539.45331 L 534.30806,502.55125 L 532.98222,491.94454 L 532.98223,483.98957 L 531.43543,477.36043" id="OK_AR" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 531.43543,477.36043 L 530.99349,470.7313 L 530.77252,465.86994" id="OK_MO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 531.43543,477.36043 L 553.97448,476.91849 L 576.51353,476.25558 L 596.6219,475.37169 L 607.22851,474.92975 L 608.99628,477.80238 L 608.55434,480.01209 L 605.46074,482.66374 L 604.79783,485.53636 L 610.76405,485.97831 L 615.62541,485.31539" id="MO_AR" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 615.62541,485.31539 L 617.61415,479.1282 L 617.61415,473.38295" id="MO_TN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 617.33793,473.99063 L 620.26581,472.2781 L 621.59163,470.7313 L 623.58037,469.62645 L 623.80134,466.53285 L 624.68523,464.76508 L 623.13842,462.27915" id="MO_KY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 623.58037,462.55537 L 620.48678,462.77634 L 618.71901,460.7876 L 617.17221,456.81012 L 618.05609,453.9375 L 616.06735,450.84391 L 614.07862,445.98254 L 609.65919,445.31963 L 603.472,440.23729 L 601.26229,436.48079 L 601.92521,433.38719 L 604.13492,427.64194 L 604.79783,424.10641 L 602.14618,422.78058 L 595.95899,422.33864 L 595.0751,421.01281 L 595.29607,416.81436 L 589.99277,413.27882 L 582.92169,405.98678 L 580.71198,398.9157 L 580.27004,395.38017 L 581.3749,392.28657" id="MO_IL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 534.52903,538.79039 L 536.73874,541.0001 L 539.61136,539.67428 L 542.26302,540.77913 L 542.92593,551.38574" id="TX_AR" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 542.92593,551.38574 L 542.92593,560.8875 L 543.58884,569.94731 L 544.25176,573.70382 L 546.68244,577.6813 L 547.56632,582.54267 L 551.76477,587.84597 L 551.98574,590.93957 L 552.64866,591.60248 L 551.98574,599.77841 L 549.11312,604.63977 L 550.65992,606.62851 L 549.997,609.05919 L 549.33409,616.13027 L 548.00826,619.22386 L 548.28448,622.70416" id="TX_LA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 542.87069,551.88293 L 564.36012,551.60672 L 582.92169,550.72283 L 593.30733,550.72283" id="AR_LA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 593.52831,550.72283 L 593.74928,556.02614 L 595.73802,560.44556 L 595.51704,562.21333 L 597.72676,563.9811 L 597.06384,567.07469 L 595.95899,567.07469 L 596.84287,569.06343 L 591.76054,577.90227 L 588.44597,583.86849 L 587.78306,591.60248 L 588.225,593.14928 L 611.64793,592.48636 L 619.60289,591.82345 L 621.37066,591.60248 L 622.03357,592.48636 L 620.92872,599.77841 L 624.24328,602.872 L 625.34814,606.62851 L 626.61872,609.00395" id="LA_MS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 593.41781,550.99905 L 594.79888,548.29215 L 594.41219,544.0937 L 593.08636,541.22107 L 594.41219,539.89525 L 593.08636,537.90651 L 593.52831,536.13874 L 594.41219,530.17252 L 597.28481,527.52087 L 596.6219,525.53213 L 600.15744,520.44979 L 602.80909,519.56591 L 602.80909,517.13523 L 602.14618,515.8094 L 604.79783,510.72707 L 607.44948,509.62221 L 607.44948,506.08667" id="AR_MS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 642.50096,359.68676 L 643.28221,370.93678 L 644.53221,385.78055 L 645.78222,401.24932 L 646.25097,407.96808 L 645.46972,413.28059 L 645.78222,416.40559 L 647.50097,419.06185 L 647.96972,424.68686 L 645.46972,428.74936 L 643.75096,432.49937 L 641.56346,435.31187 L 641.09471,439.68688 L 641.09471,443.28063" id="IL_IN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 641.09471,443.28063 L 640.15721,446.56189 L 639.06346,447.34314 L 639.68846,449.21814 L 640.46971,450.9369 L 638.90721,451.5619 L 636.4072,452.1869 L 634.5322,453.59315 L 634.2197,455.78065 L 635.4697,458.12441 L 635.31345,460.31191 L 634.2197,460.62441 L 630.78219,459.37441 L 628.12594,458.12441 L 626.09469,458.74941 L 623.90718,460.46816 L 623.12593,462.34316" id="IL_KY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 641.25096,443.43688 L 644.06346,441.71813 L 649.68847,440.78063 L 653.12598,440.31188 L 654.53223,442.18688 L 656.25098,442.96813 L 657.96973,439.84313 L 660.78224,438.43688 L 662.65724,439.99938 L 663.43849,441.09313 L 665.46975,440.62438 L 665.31349,437.34313 L 668.126,435.78062 L 669.21975,434.99937 L 670.3135,436.56187 L 674.84476,436.56187 L 675.62601,434.53062 L 675.31351,432.34312 L 678.12601,428.90561 L 682.65727,425.15561 L 683.12602,420.7806 L 685.78228,420.4681 L 689.53228,418.74935 L 692.18854,416.87434 L 691.87603,414.99934 L 690.46978,413.59309 L 690.93853,411.40559" id="IN_KY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 690.93853,411.40559 L 690.46978,405.93683 L 687.96978,383.28054 L 686.25103,369.99927 L 684.84477,355.7805" id="IN_OH" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 684.84477,355.7805 L 684.68852,354.68675 L 678.75102,355.31175 L 657.50098,357.49926 L 652.96973,357.49926" id="IN_MI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 615.46967,485.07202 L 614.58577,488.10878 L 613.28217,492.34321 L 612.96967,494.84321 L 609.06341,497.03071 L 610.46966,500.46822 L 609.53216,504.99948 L 606.87591,506.09323" id="AR_TN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 607.65716,506.56198 L 615.93842,506.24948 L 639.21971,504.37448 L 642.96971,504.21823" id="TN_MS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 644.51654,503.77628 L 644.53221,510.31198 L 644.68846,526.40576 L 643.90721,556.4058 L 643.75096,569.99957 L 646.40722,588.1246 L 647.9524,603.33611" id="MS_AL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 642.65721,504.21823 L 652.18848,503.74947 L 679.06352,501.24947 L 689.03673,500.46822" id="TN_AL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 199.86827,240.25868 L 200.31021,231.08839 L 203.84575,215.17847 L 208.15468,195.07011 L 211.8007,182.03283 L 212.5741,178.27632" id="WA_ID" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 105.11516,210.08273 L 107.72336,210.64857 L 108.38627,213.07925 L 112.36375,213.63168 L 114.57346,217.38818 L 114.35249,224.9012 L 113.68958,225.6746 L 119.43482,229.65208 L 125.40104,229.98353 L 127.27929,228.21576 L 131.47774,229.65208 L 132.25114,229.54159 L 137.33348,231.53033 L 138.99076,232.85615 L 143.52066,233.07713 L 144.736,232.41421 L 147.0562,233.96101 L 151.36513,234.51344 L 156.66844,232.5247 L 157.11038,233.85052 L 172.02592,233.74004 L 186.61001,237.16509 L 200.38279,240.66827" id="WA_OR" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 199.97875,240.59014 L 201.19409,244.6781 L 204.50866,247.21927 L 204.28769,250.86529 L 200.53118,255.06374 L 196.77467,260.6985 L 195.89079,261.36141 L 195.33836,264.45501 L 194.23351,265.55986 L 192.46574,266.0018 L 188.1568,271.30511 L 187.82535,274.28822 L 187.38341,275.39307 L 188.1568,276.38744 L 190.58749,276.27696 L 191.80283,278.48667 L 189.37215,284.23191 L 188.04632,288.31988 L 183.84787,305.44513 L 179.53894,322.90184" id="OR_ID" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 134.1294,312.29523 L 126.28493,342.89971 L 119.54531,367.97992 L 117.96384,374.32659 L 129.93095,392.28673 L 151.91756,424.99044 L 170.7001,453.05375 L 184.73175,475.04037 L 187.28633,478.33598" id="CA_NV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 179.25879,323.29247 L 199.53681,327.65271 L 208.92808,329.53097 L 217.8774,331.29873 L 225.72187,333.17699" id="ID_NV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 225.5009,333.28747 L 237.87528,335.49718 L 249.47626,337.48592 L 259.41995,339.25369 L 267.48539,340.57952 L 272.23627,341.24243" id="ID_UT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 187.05195,478.02348 L 186.61001,481.33804 L 189.26166,486.08892 L 190.477,491.72368 L 191.2504,492.71805 L 192.24477,493.27048 L 192.13428,495.48019 L 190.58749,496.80601 L 187.27292,498.46329 L 185.39467,500.34155 L 183.95836,503.87708 L 183.40593,508.62796 L 180.64379,511.27961 L 178.65505,511.94253 L 178.54457,517.57729 L 178.10262,519.23457 L 178.54457,520.00797 L 182.0801,520.56039 L 181.52767,523.21205 L 180.09136,525.31127 L 176.44534,526.19515" id="CA_AZ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 187.27292,478.24445 L 188.59875,476.03474 L 189.15117,463.88133 L 189.37215,462.22405 L 189.48263,455.48444 L 191.36088,454.49007 L 192.24477,453.93764 L 193.12865,453.93764 L 194.01254,455.04249 L 196.66419,455.37395 L 197.87953,458.0256 L 200.31021,458.13609 L 201.96749,455.59492 L 202.40943,455.15298 L 205.40595,437.74758" id="NV_AZ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 205.28206,438.35918 L 207.16031,427.86306 L 210.2539,413.16849 L 213.45798,397.14809 L 215.55721,384.00032 L 217.43546,375.38245 L 221.08148,355.60554 L 224.50653,338.70126 L 225.61139,333.50844" id="NV_UT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 70.710722,294.61753 L 77.560823,296.82724 L 88.609373,299.69986 L 95.901416,301.6886 L 108.05482,305.66608 L 121.09211,308.98065 L 134.1294,312.73715" id="OR_CA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 133.68746,312.68584 L 166.39117,320.47114 L 179.64943,323.34376" id="OR_NV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 226.27435,180.81733 L 221.85493,201.58861 L 225.1695,208.88065 L 223.84367,213.30007 L 225.61144,217.71949 L 228.70504,219.04532 L 232.24057,228.98902 L 235.77611,232.52455 L 236.21805,233.62941 L 239.53262,234.73427 L 239.97456,236.723 L 233.12446,253.73778 L 233.12446,256.16846 L 235.55514,259.26205 L 236.43902,259.26205 L 241.07941,256.38943 L 241.74233,255.28457 L 243.28913,255.94749 L 243.06815,261.02982 L 245.71981,273.18323 L 248.59243,275.61391 L 249.47631,276.27682 L 251.24408,278.48653 L 250.80214,281.8011 L 251.46505,285.11566 L 252.56991,285.99955 L 254.77962,283.78984 L 257.43127,283.78984 L 260.52487,285.33664 L 262.95555,284.45275 L 266.93303,284.45275 L 270.46856,285.99955 L 273.12022,285.55761 L 273.56216,282.68498 L 276.43478,282.02207 L 277.76061,283.3479 L 278.20255,286.44149 L 280.63323,288.6512" id="ID_MT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 280.76267,289.04183 L 282.18003,277.82362 L 301.40451,280.69624 L 328.80492,284.67372 L 344.49387,286.66246 L 375.50794,289.84759 L 386.47836,290.86091" id="MT_WY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 280.8542,288.20926 L 277.0977,312.07413 L 272.01536,341.46328" id="ID_WY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 272.01536,341.46328 L 270.5356,351.6275 L 268.92177,363.11844 L 275.2273,364.01574 L 291.55004,366.22545 L 300.53404,367.40842" id="UT_WY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 300.29966,367.31689 L 297.42703,388.75109 L 294.33344,410.40625 L 290.70637,437.45625 L 289.2511,448.1923 L 288.58819,452.16978" id="UT_CO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 205.06113,437.58569 L 237.54388,443.77288 L 263.83943,448.1923 L 288.80916,451.85728" id="UT_AZ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 288.58819,451.94881 L 284.5839,481.67727 L 277.85214,533.09881 L 274.22507,559.11977 L 272.4573,571.93609" id="AZ_NM" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 288.80916,451.72784 L 321.95482,455.92629 L 357.65689,460.20517 L 389.35099,462.33445" id="CO_NM" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 685.15727,355.93675 L 692.50104,354.68675 L 702.3448,353.12425 L 707.42849,352.54501" id="MI_OH" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 691.09478,411.56184 L 695.00104,411.24934 L 697.34479,410.46809 L 700.1573,412.03059 L 701.7198,416.24934 L 707.34481,416.56184 L 709.06356,418.2806 L 711.09481,418.43685 L 713.43857,417.0306 L 716.40732,417.49935 L 717.65732,418.9056 L 720.31358,416.40559 L 722.03233,415.15559 L 723.59483,415.15559 L 724.21983,417.81185 L 725.93859,418.74935 L 729.37609,420.7806" id="OH_KY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 729.37609,420.7806 L 729.53234,426.09311 L 730.31359,427.65561 L 732.8136,429.06186 L 733.4386,431.24937 L 736.2511,434.84312 L 738.7511,437.49938 L 741.92187,439.62379" id="KY_WV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 742.03236,438.90563 L 738.90736,442.65563 L 734.84485,446.09314 L 730.46984,451.2494 L 728.75109,452.96815 L 728.75109,454.9994 L 725.00108,457.03065 L 719.53233,460.31191 L 716.70413,461.72601" id="KY_VA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 717.31181,461.61552 L 666.876,466.40567 L 651.64388,468.12442 L 647.17734,468.61997 L 643.43846,468.59317 L 643.43846,472.34318 L 635.31345,472.81193 L 628.59469,473.43693 L 618.12592,473.59318" id="KY_TN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 665.15724,603.28087 L 664.84474,595.46836 L 662.34474,593.59336 L 660.62599,591.87461 L 660.93849,588.90585 L 670.78225,587.65585 L 695.46979,584.84335 L 702.0323,584.21835 L 708.12606,584.21835" id="AL_FL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 708.12606,584.21835 L 707.34481,580.78084 L 705.93856,579.99959 L 705.31355,579.21834 L 706.09481,576.87458 L 705.78231,571.71833 L 704.2198,567.49957 L 704.37605,565.62457 L 704.8448,562.49956 L 706.56356,557.81206 L 706.40731,555.7808 L 704.5323,554.99955 L 704.06355,551.7183 L 701.56355,548.43704 L 699.5323,542.34328 L 698.12604,535.62452 L 696.56354,530.93702 L 695.15729,524.99951 L 692.81354,515.46824 L 689.53228,507.81198 L 688.90728,504.53073 L 688.75103,502.49947 L 688.75103,500.31197" id="AL_GA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 707.81356,583.90584 L 710.46981,587.8121 L 711.87606,589.21835 L 719.53233,589.3746 L 729.98996,588.7496 L 750.78237,587.4996 L 756.04583,586.8478 L 760.46989,586.8746 L 760.62614,589.6871 L 763.12614,590.46835 L 763.43864,586.2496 L 761.87614,581.87459 L 762.96989,580.31209 L 768.5949,581.09334 L 773.59491,581.40584" id="GA_FL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 688.75103,500.46822 L 697.03229,499.53072 L 705.1573,498.43697 L 709.84481,497.65572" id="TN_GA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 709.84481,497.65572 L 716.40732,497.18696 L 723.12608,496.40571 L 728.75109,495.46821 L 730.46984,495.31196" id="NC_GA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 730.46984,495.31196 L 730.62609,497.18696 L 728.43859,498.12447 L 727.65734,499.99947 L 727.96984,501.71822 L 729.21984,502.96822 L 733.1261,505.31198 L 736.40735,505.15573 L 739.68861,510.15573 L 740.15736,511.56199 L 742.50111,514.37449 L 742.96986,515.78074 L 747.34487,517.4995 L 750.31362,519.687 L 752.50113,522.4995 L 754.68863,523.7495 L 756.71988,525.62451 L 757.96988,528.12451 L 760.00114,529.99951 L 764.06364,531.87452 L 766.7199,537.65578 L 768.2824,542.34328 L 770.7824,542.96828 L 772.96991,544.99954 L 774.21991,548.43704 L 774.84491,550.46829 L 777.34491,551.7183 L 779.37617,550.7808" id="GA_SC" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 730.15734,494.99946 L 736.2511,492.65571 L 745.00111,488.2807 L 752.03237,487.49945 L 767.9699,487.0307 L 770.1574,488.9057 L 771.7199,492.03071 L 775.93866,491.56196 L 788.12618,490.1557 L 790.93868,490.93696 L 803.1262,498.28072 L 812.97945,506.32368" id="NC_SC" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 709.84481,497.65572 L 710.15731,492.96821 L 711.87606,491.56196 L 714.53232,490.93696 L 715.15732,487.3432 L 719.21983,484.68695 L 722.96983,483.28069 L 727.03234,479.84319 L 731.25109,477.81194 L 731.87609,474.84318 L 735.6261,471.09318 L 736.2511,470.93693 C 736.2511,470.93693 736.2511,472.03068 737.03235,472.03068 C 737.8136,472.03068 738.90736,472.34318 738.90736,472.34318 L 741.09486,468.90567 L 743.12611,468.28067 L 745.31361,468.59317 L 746.87612,465.15567 L 749.68862,462.65566 L 750.15737,460.62441 L 750.31362,456.71815" id="TN_NC" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 717.03232,461.56191 L 726.53223,460.74273 L 738.1261,458.90566 L 745.78237,458.74941 L 748.12612,456.8744 L 750.34206,456.92964" id="VA_TN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 750.00112,456.8744 L 754.53238,457.49941 L 761.42964,456.2494 L 776.71991,454.3744 L 793.28244,451.8744 L 813.19549,448.22704 L 831.71999,444.53064 L 842.81376,441.71813 L 847.56599,440.14615" id="VA_NC" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 741.65514,439.21813 L 743.90736,442.34313 L 745.15736,443.59313 L 747.96987,444.37439 L 750.46987,443.74938 L 752.81363,441.56188 C 752.81363,441.56188 754.53238,442.96813 755.15738,442.81188 C 755.78238,442.65563 759.37614,441.87438 759.37614,441.87438 L 761.09489,437.34313 L 763.59489,438.28063 L 766.7199,435.93687 L 768.12615,436.24937 L 770.1574,434.37437 L 770.31365,431.71812 L 769.5324,430.46812 L 774.37616,419.99935 L 776.40741,413.59309 L 776.71991,409.37433 L 778.43866,409.21808 L 780.00117,411.87434 L 781.25117,412.65559 L 783.75117,412.65559 L 784.84492,407.34308 L 785.15742,404.37433 L 788.43868,404.06183 L 788.90743,401.87432 L 791.87618,399.53057 L 792.50119,397.96807 L 794.06369,395.31181 L 794.53244,393.28056 L 794.68869,388.1243 L 799.06369,389.84305 L 804.53246,392.81181 L 805.46995,387.65555" id="WV_VA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 805.15745,388.1243 L 809.37621,389.84305 L 809.37621,392.65556 L 814.68872,393.90556 L 816.56372,395.15556 L 817.50122,393.28056 L 819.68873,394.84306 L 818.28247,397.96807 L 817.96997,400.62432 L 816.25122,403.12432 L 816.25122,405.15558 L 816.87622,406.87433 L 822.13512,408.2443" id="VA_MD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 805.6262,388.28055 L 804.21995,386.71805 L 802.34495,384.9993 L 799.53245,383.59305 L 797.94314,382.50092 L 796.15966,382.96967 L 794.21994,384.3743 L 791.71993,386.40555 L 788.90743,386.71805 L 787.65743,386.09305 L 785.93868,388.59305 L 784.53242,389.9993 L 782.18867,390.15555 L 780.00117,393.12431 L 777.96991,395.46806 L 777.65741,395.78056 L 776.71991,390.31181 L 775.31366,385.3118" id="WV_MD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 775.64512,385.53277 L 763.62333,387.03055 L 759.14731,387.75656 L 755.60717,368.59302" id="PA_WV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 755.93863,368.59302 L 754.53238,369.84302 L 753.59488,371.24928 L 754.68863,374.21803 L 754.68863,378.74929 L 754.21988,383.59305 L 753.90738,389.0618 L 751.71987,392.81181 L 748.75112,396.09306 L 746.56362,397.65557 L 744.53236,397.18682 L 743.28236,398.59307 L 741.09486,401.87432 L 740.15736,403.12432 L 740.15736,405.46808 L 741.25111,407.18683 L 740.78236,408.74933 L 739.06361,409.68683 L 738.59485,407.96808 L 737.34485,406.87433 L 736.09485,407.49933 L 735.15735,411.24934 L 735.0011,416.09309 L 733.4386,417.49935 L 733.28235,420.1556 L 731.40734,420.93685 L 729.21984,421.24935" id="OH_WV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 751.25169,340.03513 L 754.06419,358.74889 L 755.78295,369.68641" id="OH_PA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 761.7015,331.46032 L 762.34546,338.28009 L 770.93923,336.87384 L 812.50182,328.43632 L 830.47061,324.53006 L 832.72285,326.59924 L 835.62687,327.49882 L 837.97063,332.65508 L 840.15813,334.53008 L 842.65814,334.68633 L 843.90814,335.78009" id="NY_PA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 843.75189,335.62383 L 847.34565,337.34259 L 851.09566,338.43634 L 855.47067,339.37384 L 857.97067,340.4676 L 858.12692,342.49885 L 857.65817,345.15511 L 858.24689,348.66681" id="NY_NJ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 844.06439,335.46758 L 841.72064,337.96759 L 841.72064,340.93635 L 839.84563,343.9051 L 839.68938,345.46761 L 840.93939,346.71761 L 840.78314,349.06137 L 838.59563,350.15512 L 839.37688,352.81137 L 839.53313,353.90513 L 842.18939,354.21763 L 843.12689,356.71763 L 846.5644,359.06139 L 848.90815,360.62389 L 848.90815,361.40514 L 845.78315,364.3739 L 844.22064,366.5614 L 842.81439,369.21766 L 840.62689,370.46766 L 839.53313,371.24891" id="PA_NJ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 839.37688,371.09266 L 839.22063,372.34267 L 838.66983,374.88059" id="DE_NJ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 839.53313,371.09266 L 837.50188,371.09266 L 835.47062,372.65517 L 834.06437,374.06142" id="NY_DE" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 834.06437,374.06142 L 835.47062,378.12393 L 837.65813,383.59269 L 839.68938,392.96771 L 841.25189,399.06148 L 846.09565,398.90523 L 852.03316,397.81147" id="MD_DE" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 834.38635,373.6842 L 826.20281,375.44087 L 811.82448,378.1697 L 774.68924,385.62395" id="PA_MD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 862.06162,339.46537 L 860.15818,337.34259 L 861.25193,335.15508 L 861.25193,327.34257 L 859.84568,320.3113 L 859.06443,316.87379" id="NY_CT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 859.06443,316.87379 L 858.28318,311.56128 L 858.75193,301.09251" id="NY_MA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 858.75193,301.09251 L 858.28318,296.0925 L 855.31442,285.46747 L 854.68942,285.15497 L 851.87691,283.90497 L 852.65816,281.09246 L 851.87691,279.06121 L 849.37691,274.6862 L 850.31441,270.93619 L 849.53316,265.93618 L 847.1894,259.68616 L 846.5644,254.8424" id="NY_VT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 872.65821,247.81114 L 872.97071,253.5924 L 874.84571,256.24866 L 874.84571,260.15492 L 871.25195,264.06117 L 868.75195,265.15493 L 868.75195,266.24868 L 869.8457,267.96743 L 869.8457,276.2487 L 869.06445,285.15497 L 868.9082,289.84248 L 869.8457,291.09249 L 869.68945,295.46749 L 869.2207,297.18625 L 870.7832,299.06125" id="VT_NH" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 859.06443,301.56126 L 863.90819,300.46751 L 870.4707,299.2175" id="VT_MA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 870.4707,299.2175 L 887.34574,295.15499 L 889.53325,294.52999 L 891.5645,291.40499 L 895.50868,289.5947" id="NH_MA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 896.48246,285.35698 L 894.06451,284.21747 L 893.59575,281.24871 L 889.84575,280.15496 L 889.53325,277.4987 L 882.50198,254.8424 L 877.81447,240.46737" id="NH_ME" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 859.37693,317.34254 L 880.31447,312.49878 L 885.31449,311.40503" id="MA_CT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 885.31449,311.40503 L 886.87699,317.18629 L 887.65824,321.40505 L 888.28324,325.46756" id="CT_RI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ <ns0:path d="M 885.15824,311.56128 L 890.78325,309.99878 L 892.34575,311.09253 L 895.62701,315.31129 L 898.43952,319.6863" id="RI_MA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" />
+ </ns0:g>
+ <ns0:g id="g3561" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1">
+ <ns0:path d="M 242.11104,448.52821 L 258.12559,450.54515 L 259.44637,440.12432 L 276.45152,442.81356 L 290.31977,444.66242" id="NM_Mexico" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 289.98957,444.32625 L 292.13584,444.99855 L 294.77744,448.02394 L 296.26332,452.56207 L 301.05119,454.91517 L 302.37198,458.27673 L 309.63631,466.51255 L 310.9571,468.19333 L 316.07515,470.37834 L 317.23084,472.56336 L 318.88182,473.57182 L 319.37712,476.42915 L 322.67909,483.15227 L 322.67909,491.55616 L 324.99047,496.43042 L 332.585,504.49816 L 337.86815,506.68317 L 339.68423,508.70011 L 339.68423,509.37242 L 343.64659,511.72551 L 345.62777,512.39782 L 347.44386,513.57437 L 350.08543,514.58284 L 352.56191,512.06167 L 357.01957,505.67471 L 358.01016,501.80891 L 360.32154,498.44736 L 363.9537,496.93466 L 368.57646,495.0858 L 371.71333,497.43889 L 379.30786,498.1112 L 386.242,499.28775 L 388.88357,501.47276 L 388.88357,502.6493 L 391.52515,505.84279 L 397.63379,511.38936 L 397.79889,512.90206 L 399.61497,514.91899 L 400.44047,519.28902 L 405.88872,532.06294 L 405.72362,534.07988 L 410.01618,536.76912 L 413.64834,543.66032 L 417.11541,548.19842 L 420.41738,549.54304 L 422.06837,551.89614 L 420.74758,556.43424 L 421.40797,557.44271 L 422.72876,558.11502 L 422.39856,561.64466 L 421.73817,562.31697 L 422.39856,564.67006 L 425.70053,566.68699 L 427.02132,573.41011 L 429.1676,577.44398 L 436.92723,580.97362 L 442.21038,582.15016 L 446.50294,585.34364 L 449.80491,586.01595 L 451.1257,585.51172 L 456.73904,586.68827 L 462.51749,590.72214 L 465.65436,588.7052" id="TX_Mexico" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 95.503595,393.0625 L 105.7397,394.5752 L 125.88171,397.43253 L 140.74058,399.1133" id="CA_Mexico" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 140.74058,399.1133 L 138.4292,401.4664 L 138.099,402.9791 L 138.5943,403.98756 L 157.91082,415.08071 L 170.2932,422.98037 L 185.31716,431.8885 L 202.4874,442.30933 L 215.03489,444.8305 L 242.11104,448.52821" id="AZ_Mexico" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:g id="g3547" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1">
+ <ns0:path d="M 876.74955,100.10268 L 848.85548,107.51359" id="VT_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 943.28423,76.73985 L 940.47756,76.907928 L 940.97285,78.084474 L 940.80775,78.588708 L 939.98226,78.588708 L 937.01049,76.235617 L 936.84539,71.193279 L 936.0199,69.176344 L 929.91126,69.176344 L 920.66574,38.081928 L 918.68456,37.073461 L 912.57592,34.552292 L 911.09003,34.384214 L 909.27395,36.233071 L 905.14649,39.258474 L 905.14649,40.266941 L 904.32099,41.107331 L 901.51432,40.435019 L 900.19353,38.081928 L 900.19353,36.905383 L 898.87274,36.737305 L 897.55196,36.737305 L 895.40568,41.107331 L 892.4339,50.351617 L 890.61782,55.393954 L 890.78292,60.436292 L 890.94802,61.948993 L 890.12252,64.806318 L 889.29703,65.814786 L 889.29703,72.033669 L 891.27821,74.554837 L 889.79233,78.756786 L 887.15075,83.631045 L 886.32526,89.345695 L 886.32526,92.034941 L 884.77927,91.608771 L 881.71077,91.733617" id="ME_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 882.59051,91.36264 L 881.5374,92.203019 L 880.87701,93.883799 L 880.21662,93.379565 L 879.22603,92.371097 L 877.74014,94.388032 L 875.84148,100.77501" id="NH_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:g id="g3537" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1">
+ <ns0:path d="M 710.49539,188.67975 L 711.48598,189.52014 L 710.49539,190.69668 L 710.66049,191.36899 L 711.48598,191.53707 L 713.46716,190.5286 L 713.13697,180.61201 M 704.88204,204.47907 L 704.88204,198.9325 L 706.86322,196.91556 L 707.68872,196.57941" id="MI_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 766.79397,165.82115 L 768.77515,172.20811 L 770.59123,172.37619 L 771.91202,175.56967 M 849.4392,107.39474 L 841.58358,109.51505 L 836.96082,111.02775 L 833.65885,110.85967 L 828.0455,112.20429 L 824.7235,113.61855" id="NY_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-opacity:1" />
+ <ns0:g id="g3530" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1">
+ <ns0:path d="M 179.70368,24.971818 L 163.02887,21.106058 L 139.58489,15.223331 L 119.11268,9.3406038 L 110.36246,7.3236687 L 100.45655,4.4663441 L 95.99889,2.9536428" id="WA_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 193.57209,27.997253 L 179.20868,25.139971" id="ID_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 371.21804,55.393954 L 338.69364,52.032396 L 308.81082,48.334682 L 278.92799,44.132734 L 245.9083,38.586162 L 227.08707,35.056526 L 193.57209,27.997253" id="MT_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 472.75352,59.76398 L 442.87077,59.427825 L 423.88445,58.755513 L 396.8083,57.410889 L 371.21804,55.393954" id="ND_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ <ns0:path d="M 590.4688,78.756786 L 585.35075,79.261019 L 584.85546,80.269487 L 584.19506,80.269487 L 582.37898,77.076006 L 573.29856,77.412162 L 572.30797,78.252552 L 571.31738,78.252552 L 570.82209,76.907928 L 569.99659,75.059071 L 567.35502,75.563305 L 564.05305,78.924863 L 562.40206,79.765253 L 559.26519,79.765253 L 556.62362,78.756786 L 556.62362,76.571773 L 555.30283,76.403695 L 554.80753,76.907928 L 552.16596,75.563305 L 551.67066,72.537902 L 550.18478,73.042136 L 549.68948,74.050604 L 547.21301,73.54637 L 541.76476,71.025201 L 537.80239,68.335954 L 534.83062,68.335954 L 533.50983,67.327487 L 531.19845,67.999799 L 530.04276,69.176344 L 529.71257,70.520967 L 524.75961,70.520967 L 524.75961,68.335954 L 518.32077,67.999799 L 517.99058,66.487097 L 513.03762,66.487097 L 511.38664,64.806318 L 509.90075,58.419357 L 509.07526,52.704708 L 507.09408,51.864318 L 504.7827,51.360084 L 504.1223,51.528162 L 503.79211,60.100136 L 472.09313,60.100136" id="MN_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" />
+ </ns0:g>
+ </ns0:g>
+ </ns0:g>
+ </ns0:g>
+ </ns0:g>
+ <ns0:g id="g4675" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1">
+ <ns0:path d="M 846.52085,274.72594 L 847.80814,273.69012 L 850.29359,272.77823 L 852.1404,272.10064 L 852.89317,271.82446 L 853.40684,271.87199" id="path3995" style="fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:#000000;stroke-width:1.33265233;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" ns1:nodetypes="cccccc" />
+ <ns0:path d="M 817.25107,257.90409 C 818.2317,258.85489 818.79206,259.61552 818.79206,259.61552 L 819.39913,260.85156 L 819.53922,260.70894" id="path5767" style="fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:#000000;stroke-width:0.41898587;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" ns1:nodetypes="cccc" />
+ </ns0:g>
+ </ns0:g>
+ </ns0:g>
+ <ns0:path d="M 152.15345,458.16063 L 151.84095,540.66102 L 153.40345,541.59852 L 156.37222,541.75477 L 157.77847,540.66102 L 160.27848,540.66102 L 160.43474,543.47353 L 167.15352,550.03606 L 167.62227,552.53607 L 170.90353,550.66106 L 171.52854,550.50481 L 171.84104,547.53605 L 173.24729,545.97354 L 174.34105,545.81729 L 176.21606,544.41103 L 179.18482,546.44229 L 179.80983,549.25481 L 181.68483,550.34856 L 182.77859,552.69232 L 186.52861,554.41108 L 189.80987,560.19236 L 192.46613,563.94237 L 194.65364,566.59864 L 196.0599,570.1924 L 200.90367,571.91116 L 205.9037,573.94242 L 206.8412,578.16119 L 207.30995,581.12995 L 206.37245,584.41122 L 204.34118,587.14562" id="AK_Canada" style="fill:none;stroke:#000000;stroke-width:2.5;stroke-dasharray:none;stroke-opacity:1" />
+ </ns0:g>
+ <ns0:path d="M 127.46583,540.97352 L 129.80959,539.56726 L 128.87209,537.8485 L 127.15333,538.78601 L 127.46583,540.97352 z M 86.528141,560.19236 L 87.621901,565.81738 L 90.434411,566.59864 L 95.278181,563.78612 L 99.496951,561.28611 L 97.934451,558.94235 L 98.403201,556.59859 L 96.371941,557.8486 L 93.559431,557.06734 L 95.121931,555.97359 L 96.996941,556.75484 L 100.74696,555.03608 L 101.21571,553.62983 L 98.871951,552.84857 L 99.653201,550.97356 L 96.996941,552.84857 L 92.465671,556.28609 L 87.778151,559.0986 L 86.528141,560.19236 z M 78.090601,572.22366 L 79.496861,573.94242 L 80.434371,572.84867 L 79.653111,570.97366 L 78.090601,572.22366 z M 55.278,574.09867 L 56.371751,571.91116 L 58.403011,572.22366 L 57.621761,574.09867 L 55.278,574.09867 z M 52.465487,571.12991 L 54.027994,573.00492 L 56.059251,571.44241 L 54.652997,570.1924 L 52.465487,571.12991 z M 5.1215179,575.50493 L 8.4027829,573.31742 L 11.684048,572.37991 L 14.18406,572.69241 L 14.652812,574.25492 L 16.527821,574.72367 L 18.402829,572.84867 L 18.090328,571.28616 L 20.74659,570.66116 L 23.559103,573.16117 L 22.465348,574.87992 L 18.246579,575.97368 L 15.590316,575.50493 L 11.99655,574.41117 L 7.7777799,575.81743 L 6.2152729,576.12993 L 5.1215179,575.50493 z M 18.402829,504.8796 L 19.496585,507.37961 L 20.59034,508.94212 L 19.496585,509.72337 L 17.465325,506.75461 L 17.465325,504.8796 L 18.402829,504.8796 z M 38.402922,518.47341 L 41.996689,519.25467 L 45.590455,520.19217 L 46.371709,521.12968 L 44.809202,524.72344 L 41.840438,524.56719 L 38.559173,521.12968 L 38.402922,518.47341 z M 40.902934,486.12951 L 42.934193,491.28579 L 42.777942,492.22329 L 39.965429,491.91079 L 38.246671,488.00452 L 36.527913,486.59827 L 34.184152,486.59827 L 34.027902,484.09825 L 35.74666,481.75449 L 36.840415,484.09825 L 38.246671,485.50451 L 40.902934,486.12951 z M 204.65369,586.59873 L 203.09118,585.81748 L 201.68493,582.84871 L 199.02866,581.44246 L 197.30991,580.3487 L 196.52865,581.12995 L 197.93491,583.78622 L 198.09116,587.37998 L 196.9974,587.84873 L 195.1224,585.97373 L 193.09114,584.72372 L 193.55989,586.28623 L 194.80989,588.00499 L 194.02864,588.78624 C 194.02864,588.78624 193.24739,588.47374 192.77864,587.84873 C 192.30988,587.22373 190.74738,584.56747 190.74738,584.56747 L 189.80987,582.37996 C 189.80987,582.37996 189.49737,583.62997 188.87237,583.31746 C 188.24736,583.00496 187.62236,581.91121 187.62236,581.91121 L 189.34112,580.0362 L 187.93486,578.62994 L 187.93486,573.78617 L 187.15361,573.78617 L 186.37236,577.06743 L 185.2786,577.53619 L 184.3411,573.94242 L 183.71609,570.34865 L 182.93484,569.8799 L 183.24734,575.34868 L 183.24734,576.44243 L 181.84108,575.19243 L 178.40357,569.41115 L 176.37231,568.9424 L 175.74731,565.34863 L 174.1848,562.53612 L 172.62229,561.44236 L 172.62229,559.25485 L 174.65355,558.00485 L 174.1848,557.69235 L 171.68479,558.31735 L 168.40352,555.97359 L 165.90351,553.16107 L 161.21599,550.66106 L 157.30972,548.16105 L 158.55973,545.03604 L 158.55973,543.47353 L 156.84097,545.03604 L 154.02846,546.12979 L 150.43469,545.03604 L 144.96591,542.69228 L 139.65339,542.69228 L 139.02839,543.16103 L 132.77836,539.41101 L 130.7471,539.09851 L 128.09084,533.47348 L 124.65332,533.78598 L 121.2158,535.19224 L 121.68456,539.56726 L 122.77831,536.75475 L 123.71582,537.06725 L 122.30956,541.28602 L 125.43457,538.62976 L 126.05958,540.19226 L 122.30956,544.41103 L 121.05955,544.09853 L 120.5908,542.22352 L 119.3408,541.44227 L 118.09079,542.53603 L 115.43453,540.81727 L 112.46576,542.84853 L 110.74701,544.87979 L 107.46574,546.91105 L 102.93447,546.75479 L 102.46572,544.72354 L 106.05948,544.09853 L 106.05948,542.84853 L 103.87197,542.22352 L 104.80948,539.87976 L 106.99699,536.12975 L 106.99699,534.41099 L 107.15324,533.62973 L 111.37201,531.44222 L 112.30951,532.69223 L 114.96578,532.69223 L 113.71577,530.19222 L 110.122,529.87972 L 105.27823,532.53598 L 102.93447,535.81724 L 101.21571,538.31726 L 100.12196,540.50477 L 96.059441,541.91102 L 93.090671,544.41103 L 92.778171,545.97354 L 94.965681,546.91105 L 95.746941,548.9423 L 93.090671,552.06732 L 86.840651,556.12984 L 79.340611,560.19236 L 77.309351,561.28611 L 72.153081,562.37987 L 66.996801,564.56738 L 68.715561,565.81738 L 67.309301,567.22364 L 66.840551,568.31739 L 64.184291,567.37989 L 61.059281,567.53614 L 60.278021,569.72365 L 59.340521,569.72365 L 59.653021,567.37989 L 56.215501,568.6299 L 53.402991,569.5674 L 50.121726,568.31739 L 47.309213,570.1924 L 44.184199,570.1924 L 42.152939,571.44241 L 40.590432,572.22366 L 38.559173,571.91116 L 36.059161,570.81741 L 33.871651,571.44241 L 32.934147,572.37991 L 31.371639,571.28616 L 31.371639,569.41115 L 34.340403,568.16114 L 40.434181,568.78615 L 44.652951,567.22364 L 46.68421,565.19238 L 49.496723,564.56738 L 51.215481,563.78612 L 53.871744,563.94237 L 55.434251,565.19238 L 56.371751,564.87988 L 58.559261,562.22362 L 61.528031,561.28611 L 64.809291,560.66111 L 66.059301,560.34861 L 66.684301,560.81736 L 67.465561,560.81736 L 68.715561,557.22359 L 72.621831,555.81734 L 74.496841,552.22357 L 76.684351,547.84855 L 78.246861,546.44229 L 78.559361,543.94228 L 76.996851,545.19229 L 73.715581,545.81729 L 73.090581,543.47353 L 71.840581,543.16103 L 70.903071,544.09853 L 70.746821,546.91105 L 69.340561,546.75479 L 67.934311,541.12977 L 66.684301,542.37977 L 65.590551,541.91102 L 65.278051,540.03601 L 61.371781,540.19226 L 59.340521,541.28602 L 56.840511,540.97352 L 58.246761,539.56726 L 58.715511,537.06725 L 58.090511,535.19224 L 59.496771,534.25474 L 60.746771,534.09849 L 60.121771,532.37973 L 60.121771,528.16096 L 59.184271,527.22345 L 58.403011,528.62971 L 52.465487,528.62971 L 51.059231,527.3797 L 50.434228,523.62969 L 48.402968,520.19217 L 48.402968,519.25467 L 50.434228,518.47341 L 50.590478,516.44215 L 51.684233,515.3484 L 50.90298,514.87965 L 49.652974,515.3484 L 48.559219,512.69214 L 49.496723,507.84836 L 53.871744,504.72335 L 56.371751,503.16084 L 58.246761,499.56708 L 60.903031,498.31707 L 63.403041,499.41083 L 63.715541,501.75459 L 66.059301,501.44208 L 69.184311,499.09832 L 70.746821,499.72333 L 71.684321,500.34833 L 73.246831,500.34833 L 75.434341,499.09832 L 76.215601,494.87955 C 76.215601,494.87955 76.528101,492.06704 77.153101,491.59829 C 77.778101,491.12954 78.090601,490.66079 78.090601,490.66079 L 76.996851,488.78578 L 74.496841,489.56703 L 71.371821,490.34828 L 69.496811,489.87953 L 66.059301,488.16077 L 61.215531,488.00452 L 57.778011,484.41076 L 58.246761,480.66074 L 58.871771,478.31698 L 56.840511,476.59822 L 54.965499,473.00445 L 55.434251,472.2232 L 61.996781,471.75445 L 64.028041,471.75445 L 64.965541,472.69195 L 65.590551,472.69195 L 65.434301,471.12944 L 69.184311,470.50444 L 71.684321,470.81694 L 73.090581,471.9107 L 71.684321,473.94196 L 71.215571,475.34821 L 73.871841,476.91072 L 78.715611,478.62948 L 80.434371,477.69198 L 78.246861,473.47321 L 77.309351,470.34819 L 78.246861,469.56694 L 74.965591,467.69193 L 74.496841,466.59817 L 74.965591,465.03567 L 74.184341,461.28565 L 71.371821,456.75438 L 69.028061,452.69186 L 71.840581,450.81685 L 74.965591,450.81685 L 76.684351,451.44185 L 80.746871,451.2856 L 84.340631,447.84809 L 85.434391,444.87932 L 89.028161,442.53556 L 90.590661,443.47307 L 93.246921,442.84806 L 96.840691,440.8168 L 97.934451,440.66055 L 98.871951,441.44181 L 103.24697,441.28556 L 105.90323,438.31679 L 106.99699,438.31679 L 110.4345,440.66055 L 112.30951,442.69181 L 111.84076,443.78557 L 112.46576,444.87932 L 114.02827,443.31682 L 117.77829,443.62932 L 118.09079,447.22308 L 119.9658,448.62934 L 126.84083,449.25434 L 132.93461,453.31686 L 134.34086,452.37936 L 139.34089,454.87937 L 141.37215,454.25437 L 143.24716,453.47311 L 147.93468,455.34812 L 152.15345,458.16063" id="AK_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1pt" />
+ <ns0:g id="Frames" transform="translate(-18.307669,-131.99439)">
+ <ns0:path d="M 229.21212,631.12334 L 229.68087,688.43625 L 264.68114,723.74902 M 18.429285,562.99783 L 161.86786,563.31033 L 229.52462,631.59209 L 314.68151,631.74834 L 370.43191,685.18624 L 370.11941,723.43652" id="Inset_border" style="fill:none;fill-opacity:0.75;stroke:#000000;stroke-width:1.875;stroke-dasharray:none" ns1:nodetypes="ccccccccc" />
+ <ns0:rect height="590.28674" id="Outer_border" style="fill:none;stroke:#000000;stroke-width:2.5;stroke-dasharray:none" width="955.48639" x="19.444839" y="133.89751" />
+ </ns0:g>
+ <ns0:path d="M 822.91849,258.28198 A 4.1274123,3.62712 0 1 1 814.66366,258.28198 A 4.1274123,3.62712 0 1 1 822.91849,258.28198 z" id="DC" style="opacity:1;fill:#a02c2c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" transform="matrix(0.9707988,0,0,1.0018987,24.345102,-0.4278704)" ns1:cx="818.79108" ns1:cy="258.28198" ns1:rx="4.1274123" ns1:ry="3.62712" ns1:type="arc" ns2:label="#DC" />
+</ns0:svg> \ No newline at end of file
diff --git a/tests/phpunit/data/media/Wikimedia-logo.svg b/tests/phpunit/data/media/Wikimedia-logo.svg
new file mode 100644
index 00000000..1e17acbe
--- /dev/null
+++ b/tests/phpunit/data/media/Wikimedia-logo.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Wikimedia logo" viewBox="-599 -599 1198 1198" width="1024" height="1024">
+<defs>
+ <clipPath id="mask">
+ <path d="M 47.5,-87.5 v 425 h -95 v -425 l -552,-552 v 1250 h 1199 v -1250 z"/>
+ </clipPath>
+</defs>
+<g clip-path="url(#mask)">
+ <circle id="green parts" fill="#396" r="336.5"/>
+ <circle id="blue arc" fill="none" stroke="#069" r="480.25" stroke-width="135.5"/>
+</g>
+<circle fill="#900" cy="-379.5" r="184.5" id="red circle"/>
+</svg> \ No newline at end of file
diff --git a/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg b/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg
new file mode 100644
index 00000000..f7b23025
--- /dev/null
+++ b/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/animated-xmp.gif b/tests/phpunit/data/media/animated-xmp.gif
new file mode 100644
index 00000000..fcba079d
--- /dev/null
+++ b/tests/phpunit/data/media/animated-xmp.gif
Binary files differ
diff --git a/tests/phpunit/data/media/animated.gif b/tests/phpunit/data/media/animated.gif
new file mode 100644
index 00000000..a8f248b3
--- /dev/null
+++ b/tests/phpunit/data/media/animated.gif
Binary files differ
diff --git a/tests/phpunit/data/media/broken_exif_date.jpg b/tests/phpunit/data/media/broken_exif_date.jpg
new file mode 100644
index 00000000..82f62f57
--- /dev/null
+++ b/tests/phpunit/data/media/broken_exif_date.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/exif-gps.jpg b/tests/phpunit/data/media/exif-gps.jpg
new file mode 100644
index 00000000..40137340
--- /dev/null
+++ b/tests/phpunit/data/media/exif-gps.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/exif-user-comment.jpg b/tests/phpunit/data/media/exif-user-comment.jpg
new file mode 100644
index 00000000..9f23966a
--- /dev/null
+++ b/tests/phpunit/data/media/exif-user-comment.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/greyscale-na-png.png b/tests/phpunit/data/media/greyscale-na-png.png
new file mode 100644
index 00000000..4a4b7452
--- /dev/null
+++ b/tests/phpunit/data/media/greyscale-na-png.png
Binary files differ
diff --git a/tests/phpunit/data/media/greyscale-png.png b/tests/phpunit/data/media/greyscale-png.png
new file mode 100644
index 00000000..340a67b4
--- /dev/null
+++ b/tests/phpunit/data/media/greyscale-png.png
Binary files differ
diff --git a/tests/phpunit/data/media/iptc-invalid-psir.jpg b/tests/phpunit/data/media/iptc-invalid-psir.jpg
new file mode 100644
index 00000000..01b9acf3
--- /dev/null
+++ b/tests/phpunit/data/media/iptc-invalid-psir.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/iptc-timetest-invalid.jpg b/tests/phpunit/data/media/iptc-timetest-invalid.jpg
new file mode 100644
index 00000000..b03e192a
--- /dev/null
+++ b/tests/phpunit/data/media/iptc-timetest-invalid.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/iptc-timetest.jpg b/tests/phpunit/data/media/iptc-timetest.jpg
new file mode 100644
index 00000000..db9932ba
--- /dev/null
+++ b/tests/phpunit/data/media/iptc-timetest.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-comment-binary.jpg b/tests/phpunit/data/media/jpeg-comment-binary.jpg
new file mode 100644
index 00000000..b467fe43
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-comment-binary.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg b/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg
new file mode 100644
index 00000000..d9ffbac1
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-comment-multiple.jpg b/tests/phpunit/data/media/jpeg-comment-multiple.jpg
new file mode 100644
index 00000000..363c7385
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-comment-multiple.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-comment-utf.jpg b/tests/phpunit/data/media/jpeg-comment-utf.jpg
new file mode 100644
index 00000000..d6d35b4b
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-comment-utf.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg b/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg
new file mode 100644
index 00000000..6464c5b8
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-iptc-good-hash.jpg b/tests/phpunit/data/media/jpeg-iptc-good-hash.jpg
new file mode 100644
index 00000000..ef970854
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-iptc-good-hash.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-padding-even.jpg b/tests/phpunit/data/media/jpeg-padding-even.jpg
new file mode 100644
index 00000000..c83c66bd
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-padding-even.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-padding-odd.jpg b/tests/phpunit/data/media/jpeg-padding-odd.jpg
new file mode 100644
index 00000000..25b93308
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-padding-odd.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-xmp-alt.jpg b/tests/phpunit/data/media/jpeg-xmp-alt.jpg
new file mode 100644
index 00000000..0e2c3f63
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-xmp-alt.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-xmp-psir.jpg b/tests/phpunit/data/media/jpeg-xmp-psir.jpg
new file mode 100644
index 00000000..4d19fcbe
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-xmp-psir.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/jpeg-xmp-psir.xmp b/tests/phpunit/data/media/jpeg-xmp-psir.xmp
new file mode 100644
index 00000000..fee6ee18
--- /dev/null
+++ b/tests/phpunit/data/media/jpeg-xmp-psir.xmp
@@ -0,0 +1,35 @@
+<?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:dc='http://purl.org/dc/elements/1.1/'>
+ <dc:identifier>jpeg-xmp-psir.jpg</dc:identifier>
+ </rdf:Description>
+</rdf:RDF>
+</x:xmpmeta>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<?xpacket end='w'?> \ No newline at end of file
diff --git a/tests/phpunit/data/media/landscape-plain.jpg b/tests/phpunit/data/media/landscape-plain.jpg
new file mode 100644
index 00000000..cf296555
--- /dev/null
+++ b/tests/phpunit/data/media/landscape-plain.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/nonanimated.gif b/tests/phpunit/data/media/nonanimated.gif
new file mode 100644
index 00000000..9e52a7f0
--- /dev/null
+++ b/tests/phpunit/data/media/nonanimated.gif
Binary files differ
diff --git a/tests/phpunit/data/media/portrait-rotated.jpg b/tests/phpunit/data/media/portrait-rotated.jpg
new file mode 100644
index 00000000..445feaed
--- /dev/null
+++ b/tests/phpunit/data/media/portrait-rotated.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/rgb-na-png.png b/tests/phpunit/data/media/rgb-na-png.png
new file mode 100644
index 00000000..2f2a5ca0
--- /dev/null
+++ b/tests/phpunit/data/media/rgb-na-png.png
Binary files differ
diff --git a/tests/phpunit/data/media/rgb-png.png b/tests/phpunit/data/media/rgb-png.png
new file mode 100644
index 00000000..6f40cc92
--- /dev/null
+++ b/tests/phpunit/data/media/rgb-png.png
Binary files differ
diff --git a/tests/phpunit/data/media/say-test.ogg b/tests/phpunit/data/media/say-test.ogg
new file mode 100644
index 00000000..5d814fb2
--- /dev/null
+++ b/tests/phpunit/data/media/say-test.ogg
Binary files differ
diff --git a/tests/phpunit/data/media/test.jpg b/tests/phpunit/data/media/test.jpg
new file mode 100644
index 00000000..cb084253
--- /dev/null
+++ b/tests/phpunit/data/media/test.jpg
Binary files differ
diff --git a/tests/phpunit/data/media/test.tiff b/tests/phpunit/data/media/test.tiff
new file mode 100644
index 00000000..6a36f760
--- /dev/null
+++ b/tests/phpunit/data/media/test.tiff
Binary files differ
diff --git a/tests/phpunit/data/media/xmp.png b/tests/phpunit/data/media/xmp.png
new file mode 100644
index 00000000..6b9f7a87
--- /dev/null
+++ b/tests/phpunit/data/media/xmp.png
Binary files differ
diff --git a/tests/phpunit/data/parser/LoremIpsum.djvu b/tests/phpunit/data/parser/LoremIpsum.djvu
new file mode 100644
index 00000000..42f47cd0
--- /dev/null
+++ b/tests/phpunit/data/parser/LoremIpsum.djvu
Binary files differ
diff --git a/tests/phpunit/data/parser/headbg.jpg b/tests/phpunit/data/parser/headbg.jpg
new file mode 100644
index 00000000..5491c6e4
--- /dev/null
+++ b/tests/phpunit/data/parser/headbg.jpg
Binary files differ
diff --git a/tests/phpunit/data/parser/wiki.png b/tests/phpunit/data/parser/wiki.png
new file mode 100644
index 00000000..8c421183
--- /dev/null
+++ b/tests/phpunit/data/parser/wiki.png
Binary files differ
diff --git a/tests/phpunit/data/upload/headbg.jpg b/tests/phpunit/data/upload/headbg.jpg
new file mode 100644
index 00000000..5491c6e4
--- /dev/null
+++ b/tests/phpunit/data/upload/headbg.jpg
Binary files differ
diff --git a/tests/phpunit/data/xmp/1.result.php b/tests/phpunit/data/xmp/1.result.php
new file mode 100644
index 00000000..beead1bd
--- /dev/null
+++ b/tests/phpunit/data/xmp/1.result.php
@@ -0,0 +1,8 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => '9'
+ )
+);
diff --git a/tests/phpunit/data/xmp/1.xmp b/tests/phpunit/data/xmp/1.xmp
new file mode 100644
index 00000000..66e15427
--- /dev/null
+++ b/tests/phpunit/data/xmp/1.xmp
@@ -0,0 +1,11 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+ exif:DigitalZoomRatio="0/10">
+<exif:Flash rdf:parseType='Resource'>
+<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/2.result.php b/tests/phpunit/data/xmp/2.result.php
new file mode 100644
index 00000000..beead1bd
--- /dev/null
+++ b/tests/phpunit/data/xmp/2.result.php
@@ -0,0 +1,8 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => '9'
+ )
+);
diff --git a/tests/phpunit/data/xmp/2.xmp b/tests/phpunit/data/xmp/2.xmp
new file mode 100644
index 00000000..0fa6a894
--- /dev/null
+++ b/tests/phpunit/data/xmp/2.xmp
@@ -0,0 +1,12 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+ exif:DigitalZoomRatio="0/10">
+<exif:Flash>
+<rdf:Description exif:Return="0">
+<exif:Fired>True</exif:Fired> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></rdf:Description></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/3-invalid.result.php b/tests/phpunit/data/xmp/3-invalid.result.php
new file mode 100644
index 00000000..5741b2c9
--- /dev/null
+++ b/tests/phpunit/data/xmp/3-invalid.result.php
@@ -0,0 +1,7 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ )
+);
diff --git a/tests/phpunit/data/xmp/3-invalid.xmp b/tests/phpunit/data/xmp/3-invalid.xmp
new file mode 100644
index 00000000..2425e254
--- /dev/null
+++ b/tests/phpunit/data/xmp/3-invalid.xmp
@@ -0,0 +1,31 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<!--
+This file has an invalid flash compoenent (one of the values are a qualifier)
+-->
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+>
+<exif:DigitalZoomRatio>
+
+<rdf:Description>
+<rdf:value>
+0/10
+</rdf:value>
+<exif:foobarbaz>fred</exif:foobarbaz>
+
+</rdf:Description>
+
+</exif:DigitalZoomRatio>
+
+<exif:Flash>
+<rdf:Description exif:Return="0">
+<exif:Mode><rdf:Description>
+<rdf:value>1</rdf:value>
+<exif:Fired>False</exif:Fired> <!-- qualifier. should be ignored-->
+</rdf:Description>
+</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></rdf:Description></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/3.result.php b/tests/phpunit/data/xmp/3.result.php
new file mode 100644
index 00000000..beead1bd
--- /dev/null
+++ b/tests/phpunit/data/xmp/3.result.php
@@ -0,0 +1,8 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => '9'
+ )
+);
diff --git a/tests/phpunit/data/xmp/3.xmp b/tests/phpunit/data/xmp/3.xmp
new file mode 100644
index 00000000..2cf19883
--- /dev/null
+++ b/tests/phpunit/data/xmp/3.xmp
@@ -0,0 +1,29 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+>
+<exif:DigitalZoomRatio>
+
+<rdf:Description>
+<rdf:value>
+0/10
+</rdf:value>
+<exif:foobarbaz>fred</exif:foobarbaz>
+
+</rdf:Description>
+
+</exif:DigitalZoomRatio>
+
+<exif:Flash>
+<rdf:Description exif:Return="0">
+<exif:Fired>True</exif:Fired>
+<exif:Mode><rdf:Description>
+<rdf:value>1</rdf:value>
+<exif:Fired>False</exif:Fired> <!-- qualifier. should be ignored-->
+</rdf:Description>
+</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></rdf:Description></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/4.result.php b/tests/phpunit/data/xmp/4.result.php
new file mode 100644
index 00000000..5741b2c9
--- /dev/null
+++ b/tests/phpunit/data/xmp/4.result.php
@@ -0,0 +1,7 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ )
+);
diff --git a/tests/phpunit/data/xmp/4.xmp b/tests/phpunit/data/xmp/4.xmp
new file mode 100644
index 00000000..29eb614b
--- /dev/null
+++ b/tests/phpunit/data/xmp/4.xmp
@@ -0,0 +1,22 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<!-- Valid output is just the DigitalZoomRatio
+as the flash is a qualifier
+-->
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/">
+ <exif:DigitalZoomRatio>
+<rdf:Description>
+<rdf:value>
+0/10
+</rdf:value>
+<exif:Flash rdf:parseType='Resource'>
+<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash>
+</rdf:Description>
+</exif:DigitalZoomRatio>
+</rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/5.result.php b/tests/phpunit/data/xmp/5.result.php
new file mode 100644
index 00000000..5741b2c9
--- /dev/null
+++ b/tests/phpunit/data/xmp/5.result.php
@@ -0,0 +1,7 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ )
+);
diff --git a/tests/phpunit/data/xmp/5.xmp b/tests/phpunit/data/xmp/5.xmp
new file mode 100644
index 00000000..3cc61d68
--- /dev/null
+++ b/tests/phpunit/data/xmp/5.xmp
@@ -0,0 +1,16 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/">
+ <exif:DigitalZoomRatio>
+<rdf:Description rdf:value="0/10">
+<exif:Flash rdf:parseType='Resource'>
+<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash>
+</rdf:Description>
+</exif:DigitalZoomRatio>
+</rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/6.result.php b/tests/phpunit/data/xmp/6.result.php
new file mode 100644
index 00000000..beead1bd
--- /dev/null
+++ b/tests/phpunit/data/xmp/6.result.php
@@ -0,0 +1,8 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => '9'
+ )
+);
diff --git a/tests/phpunit/data/xmp/6.xmp b/tests/phpunit/data/xmp/6.xmp
new file mode 100644
index 00000000..f435ab23
--- /dev/null
+++ b/tests/phpunit/data/xmp/6.xmp
@@ -0,0 +1,18 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/">
+<exif:DigitalZoomRatio>
+0/10
+</exif:DigitalZoomRatio>
+</rdf:Description>
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/">
+
+<exif:Flash rdf:parseType='Resource'>
+<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/7.result.php b/tests/phpunit/data/xmp/7.result.php
new file mode 100644
index 00000000..115cdc92
--- /dev/null
+++ b/tests/phpunit/data/xmp/7.result.php
@@ -0,0 +1,52 @@
+<?php
+$result = array(
+ 'xmp-exif' =>
+ array(
+ 'CameraOwnerName' => 'Me!',
+ ),
+ 'xmp-general' =>
+ array(
+ 'LicenseUrl' => 'http://creativecommons.com/cc-by-2.9',
+ 'ImageDescription' =>
+ array(
+ 'x-default' => 'Test image for the cc: xmp: xmpRights: namespaces in xmp',
+ '_type' => 'lang',
+ ),
+ 'ObjectName' =>
+ array(
+ 'x-default' => 'xmp core/xmp rights/cc ns test',
+ '_type' => 'lang',
+ ),
+ 'DateTimeDigitized' => '2005:04:03',
+ 'Software' => 'The one true editor: Vi (ok i used gimp)',
+ 'Identifier' =>
+ array(
+ 0 => 'http://example.com/identifierurl',
+ 1 => 'urn:sha1:342524abcdef',
+ '_type' => 'ul',
+ ),
+ 'Label' => 'Test image',
+ 'DateTimeMetadata' => '2011:05:12',
+ 'DateTime' => '2007:03:04 06:34:10',
+ 'Nickname' => 'My little xmp test image',
+ 'Rating' => '5',
+ 'RightsCertificate' => 'http://example.com/rights-certificate/',
+ 'Copyrighted' => 'True',
+ 'CopyrightOwner' =>
+ array(
+ 0 => 'Bawolff is copyright owner',
+ '_type' => 'ul',
+ ),
+ 'UsageTerms' =>
+ array(
+ 'x-default' => 'do whatever you want',
+ 'en-gb' => 'Do whatever you want in british english',
+ '_type' => 'lang',
+ ),
+ 'WebStatement' => 'http://example.com/web_statement',
+ ),
+ 'xmp-deprecated' =>
+ array(
+ 'Identifier' => 'http://example.com/identifierurl/wrong',
+ ),
+);
diff --git a/tests/phpunit/data/xmp/7.xmp b/tests/phpunit/data/xmp/7.xmp
new file mode 100644
index 00000000..e18e13d9
--- /dev/null
+++ b/tests/phpunit/data/xmp/7.xmp
@@ -0,0 +1,67 @@
+<?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:aux='http://ns.adobe.com/exif/1.0/aux/'>
+ <aux:OwnerName>Me!</aux:OwnerName>
+ </rdf:Description>
+
+ <rdf:Description rdf:about=''
+ xmlns:cc='http://creativecommons.org/ns#'>
+ <cc:license>http://creativecommons.com/cc-by-2.9</cc:license>
+ </rdf:Description>
+
+ <rdf:Description rdf:about=''
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <dc:description>
+ <rdf:Alt>
+ <rdf:li xml:lang='x-default'>Test image for the cc: xmp: xmpRights: namespaces in xmp</rdf:li>
+ </rdf:Alt>
+ </dc:description>
+ <dc:identifier>http://example.com/identifierurl/wrong</dc:identifier>
+ <dc:title>
+ <rdf:Alt>
+ <rdf:li xml:lang='x-default'>xmp core/xmp rights/cc ns test</rdf:li>
+ </rdf:Alt>
+ </dc:title>
+ </rdf:Description>
+
+ <rdf:Description rdf:about=''
+ xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
+ <xmp:CreateDate>2005-04-03</xmp:CreateDate>
+ <xmp:CreatorTool>The one true editor: Vi (ok i used gimp)</xmp:CreatorTool>
+ <xmp:Identifier>
+ <rdf:Bag>
+ <rdf:li>http://example.com/identifierurl
+</rdf:li>
+ <rdf:li>urn:sha1:342524abcdef</rdf:li>
+ </rdf:Bag>
+ </xmp:Identifier>
+ <xmp:Label>Test image</xmp:Label>
+ <xmp:MetadataDate>2011-05-12</xmp:MetadataDate>
+ <xmp:ModifyDate>2007-03-04T12:34:10-06:00</xmp:ModifyDate>
+ <xmp:Nickname>My little xmp test image</xmp:Nickname>
+ <xmp:Rating>7</xmp:Rating>
+ </rdf:Description>
+
+ <rdf:Description rdf:about=''
+ xmlns:xmpRights='http://ns.adobe.com/xap/1.0/rights/'>
+ <xmpRights:Certificate>http://example.com/rights-certificate/</xmpRights:Certificate>
+ <xmpRights:Marked>True</xmpRights:Marked>
+ <xmpRights:Owner>
+ <rdf:Bag>
+ <rdf:li>Bawolff is copyright owner</rdf:li>
+ </rdf:Bag>
+ </xmpRights:Owner>
+ <xmpRights:UsageTerms>
+ <rdf:Alt>
+ <rdf:li xml:lang='x-default'>do whatever you want</rdf:li>
+ <rdf:li xml:lang='en-GB'>Do whatever you want in british english</rdf:li>
+ </rdf:Alt>
+ </xmpRights:UsageTerms>
+ <xmpRights:WebStatement>http://example.com/web_statement</xmpRights:WebStatement>
+ </rdf:Description>
+</rdf:RDF>
+</x:xmpmeta>
+<?xpacket end='r'?>
diff --git a/tests/phpunit/data/xmp/README b/tests/phpunit/data/xmp/README
new file mode 100644
index 00000000..bd949176
--- /dev/null
+++ b/tests/phpunit/data/xmp/README
@@ -0,0 +1,3 @@
+This directory contains a bunch of XMP files
+as well as a bunch of php files containing what the
+parsed version of the XMP looks like.
diff --git a/tests/phpunit/data/xmp/bag-for-seq.result.php b/tests/phpunit/data/xmp/bag-for-seq.result.php
new file mode 100644
index 00000000..b5244f88
--- /dev/null
+++ b/tests/phpunit/data/xmp/bag-for-seq.result.php
@@ -0,0 +1,10 @@
+<?php
+
+$result = array(
+ 'xmp-general' => array(
+ 'Artist' => array(
+ '_type' => 'ul',
+ 0 => 'The author',
+ )
+ )
+);
diff --git a/tests/phpunit/data/xmp/bag-for-seq.xmp b/tests/phpunit/data/xmp/bag-for-seq.xmp
new file mode 100644
index 00000000..c6ed5b7c
--- /dev/null
+++ b/tests/phpunit/data/xmp/bag-for-seq.xmp
@@ -0,0 +1 @@
+<?xpacket begin=""?> <x:xmpmeta xmlns:x="adobe:ns:meta/"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"> <dc:creator> <rdf:Bag> <rdf:li>The author</rdf:li> </rdf:Bag> </dc:creator> </rdf:Description> </rdf:RDF> </x:xmpmeta>
diff --git a/tests/phpunit/data/xmp/doctype-included.result.php b/tests/phpunit/data/xmp/doctype-included.result.php
new file mode 100644
index 00000000..9a9cc36a
--- /dev/null
+++ b/tests/phpunit/data/xmp/doctype-included.result.php
@@ -0,0 +1,3 @@
+<?php
+
+$result = array();
diff --git a/tests/phpunit/data/xmp/doctype-included.xmp b/tests/phpunit/data/xmp/doctype-included.xmp
new file mode 100644
index 00000000..8c946755
--- /dev/null
+++ b/tests/phpunit/data/xmp/doctype-included.xmp
@@ -0,0 +1,12 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <!DOCTYPE x:xmpmeta [ <!ENTITY lol "lollollollollollollollollollollol"> ]>
+<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+ exif:DigitalZoomRatio="0/10">
+<exif:Flash rdf:parseType='Resource'>
+<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/doctype-not-included.xmp b/tests/phpunit/data/xmp/doctype-not-included.xmp
new file mode 100644
index 00000000..9a40b4b0
--- /dev/null
+++ b/tests/phpunit/data/xmp/doctype-not-included.xmp
@@ -0,0 +1,11 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+ exif:DigitalZoomRatio="0/10">
+<exif:Flash rdf:parseType='Resource'>
+<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/flash.result.php b/tests/phpunit/data/xmp/flash.result.php
new file mode 100644
index 00000000..018c0ac1
--- /dev/null
+++ b/tests/phpunit/data/xmp/flash.result.php
@@ -0,0 +1,8 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => '127'
+ )
+);
diff --git a/tests/phpunit/data/xmp/flash.xmp b/tests/phpunit/data/xmp/flash.xmp
new file mode 100644
index 00000000..b1373cc2
--- /dev/null
+++ b/tests/phpunit/data/xmp/flash.xmp
@@ -0,0 +1,11 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+ exif:DigitalZoomRatio="0/10">
+<exif:Flash rdf:parseType='Resource'>
+<exif:Fired>True</exif:Fired> <exif:Return>3</exif:Return> <exif:Mode>3</exif:Mode> <exif:Function>True</exif:Function> <exif:RedEyeMode>True</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/gps.result.php b/tests/phpunit/data/xmp/gps.result.php
new file mode 100644
index 00000000..bf7fb219
--- /dev/null
+++ b/tests/phpunit/data/xmp/gps.result.php
@@ -0,0 +1,11 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'GPSAltitude' => -3.14159265301,
+ 'GPSDOP' => '5/1',
+ 'GPSLatitude' => 88.51805555,
+ 'GPSLongitude' => -21.12356945,
+ 'GPSVersionID' => '2.2.0.0'
+ )
+);
diff --git a/tests/phpunit/data/xmp/gps.xmp b/tests/phpunit/data/xmp/gps.xmp
new file mode 100644
index 00000000..e52d2c8a
--- /dev/null
+++ b/tests/phpunit/data/xmp/gps.xmp
@@ -0,0 +1,17 @@
+<?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:exif='http://ns.adobe.com/exif/1.0/'>
+ <exif:GPSAltitude>103993/33102</exif:GPSAltitude>
+ <exif:GPSAltitudeRef>1</exif:GPSAltitudeRef>
+ <exif:GPSDOP>5/1</exif:GPSDOP>
+ <exif:GPSLatitude>88,31.083333N</exif:GPSLatitude>
+ <exif:GPSLongitude>21,7.414167W</exif:GPSLongitude>
+ <exif:GPSVersionID>2.2.0.0</exif:GPSVersionID>
+ </rdf:Description>
+
+</rdf:RDF>
+</x:xmpmeta>
+<?xpacket end='w'?>
diff --git a/tests/phpunit/data/xmp/invalid-child-not-struct.result.php b/tests/phpunit/data/xmp/invalid-child-not-struct.result.php
new file mode 100644
index 00000000..5741b2c9
--- /dev/null
+++ b/tests/phpunit/data/xmp/invalid-child-not-struct.result.php
@@ -0,0 +1,7 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ )
+);
diff --git a/tests/phpunit/data/xmp/invalid-child-not-struct.xmp b/tests/phpunit/data/xmp/invalid-child-not-struct.xmp
new file mode 100644
index 00000000..6aa0c10b
--- /dev/null
+++ b/tests/phpunit/data/xmp/invalid-child-not-struct.xmp
@@ -0,0 +1,12 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+ exif:DigitalZoomRatio="0/10">
+<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode>
+
+ </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/no-namespace.result.php b/tests/phpunit/data/xmp/no-namespace.result.php
new file mode 100644
index 00000000..3ff69201
--- /dev/null
+++ b/tests/phpunit/data/xmp/no-namespace.result.php
@@ -0,0 +1,7 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'FNumber' => '28/10',
+ )
+);
diff --git a/tests/phpunit/data/xmp/no-namespace.xmp b/tests/phpunit/data/xmp/no-namespace.xmp
new file mode 100644
index 00000000..7d6cdb2f
--- /dev/null
+++ b/tests/phpunit/data/xmp/no-namespace.xmp
@@ -0,0 +1,11 @@
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<!-- Testing it handles random non-namespaced properties in files ok.
+ Some older photoshop's did not include the rdf: prefix on about. -->
+<rdf:Description
+ about=""
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+ exif:FNumber="28/10">
+</rdf:Description>
+</rdf:RDF>
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/no-recognized-props.result.php b/tests/phpunit/data/xmp/no-recognized-props.result.php
new file mode 100644
index 00000000..b3ca9f5a
--- /dev/null
+++ b/tests/phpunit/data/xmp/no-recognized-props.result.php
@@ -0,0 +1,2 @@
+<?php
+$result = array();
diff --git a/tests/phpunit/data/xmp/no-recognized-props.xmp b/tests/phpunit/data/xmp/no-recognized-props.xmp
new file mode 100644
index 00000000..54e80901
--- /dev/null
+++ b/tests/phpunit/data/xmp/no-recognized-props.xmp
@@ -0,0 +1,8 @@
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/not-exif-namespace"
+ exif:FNumber="2/10">
+</rdf:Description>
+</rdf:RDF>
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/utf16BE.result.php b/tests/phpunit/data/xmp/utf16BE.result.php
new file mode 100644
index 00000000..ac7ea506
--- /dev/null
+++ b/tests/phpunit/data/xmp/utf16BE.result.php
@@ -0,0 +1,12 @@
+<?php
+
+$result = array(
+ 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ ),
+ 'xmp-general' =>
+ array(
+ 'Label' => '􊯍'
+ ),
+);
diff --git a/tests/phpunit/data/xmp/utf16BE.xmp b/tests/phpunit/data/xmp/utf16BE.xmp
new file mode 100644
index 00000000..0cf60d60
--- /dev/null
+++ b/tests/phpunit/data/xmp/utf16BE.xmp
Binary files differ
diff --git a/tests/phpunit/data/xmp/utf16LE.result.php b/tests/phpunit/data/xmp/utf16LE.result.php
new file mode 100644
index 00000000..ac7ea506
--- /dev/null
+++ b/tests/phpunit/data/xmp/utf16LE.result.php
@@ -0,0 +1,12 @@
+<?php
+
+$result = array(
+ 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ ),
+ 'xmp-general' =>
+ array(
+ 'Label' => '􊯍'
+ ),
+);
diff --git a/tests/phpunit/data/xmp/utf16LE.xmp b/tests/phpunit/data/xmp/utf16LE.xmp
new file mode 100644
index 00000000..66d71f4c
--- /dev/null
+++ b/tests/phpunit/data/xmp/utf16LE.xmp
Binary files differ
diff --git a/tests/phpunit/data/xmp/utf32BE.result.php b/tests/phpunit/data/xmp/utf32BE.result.php
new file mode 100644
index 00000000..ac7ea506
--- /dev/null
+++ b/tests/phpunit/data/xmp/utf32BE.result.php
@@ -0,0 +1,12 @@
+<?php
+
+$result = array(
+ 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ ),
+ 'xmp-general' =>
+ array(
+ 'Label' => '􊯍'
+ ),
+);
diff --git a/tests/phpunit/data/xmp/utf32BE.xmp b/tests/phpunit/data/xmp/utf32BE.xmp
new file mode 100644
index 00000000..06afdf92
--- /dev/null
+++ b/tests/phpunit/data/xmp/utf32BE.xmp
Binary files differ
diff --git a/tests/phpunit/data/xmp/utf32LE.result.php b/tests/phpunit/data/xmp/utf32LE.result.php
new file mode 100644
index 00000000..ac7ea506
--- /dev/null
+++ b/tests/phpunit/data/xmp/utf32LE.result.php
@@ -0,0 +1,12 @@
+<?php
+
+$result = array(
+ 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ ),
+ 'xmp-general' =>
+ array(
+ 'Label' => '􊯍'
+ ),
+);
diff --git a/tests/phpunit/data/xmp/utf32LE.xmp b/tests/phpunit/data/xmp/utf32LE.xmp
new file mode 100644
index 00000000..bf2097fe
--- /dev/null
+++ b/tests/phpunit/data/xmp/utf32LE.xmp
Binary files differ
diff --git a/tests/phpunit/data/xmp/xmpExt.result.php b/tests/phpunit/data/xmp/xmpExt.result.php
new file mode 100644
index 00000000..beead1bd
--- /dev/null
+++ b/tests/phpunit/data/xmp/xmpExt.result.php
@@ -0,0 +1,8 @@
+<?php
+
+$result = array( 'xmp-exif' =>
+ array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => '9'
+ )
+);
diff --git a/tests/phpunit/data/xmp/xmpExt.xmp b/tests/phpunit/data/xmp/xmpExt.xmp
new file mode 100644
index 00000000..da0383f8
--- /dev/null
+++ b/tests/phpunit/data/xmp/xmpExt.xmp
@@ -0,0 +1,13 @@
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core
+ 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 ">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+ xmlns:xmpNote="http://ns.adobe.com/xmp/note/"
+ exif:DigitalZoomRatio="0/10"
+ xmpNote:HasExtendedXMP="28C74E0AC2D796886759006FBE2E57B7">
+<exif:Flash rdf:parseType='Resource'>
+<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta>
+
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/xmp/xmpExt2.xmp b/tests/phpunit/data/xmp/xmpExt2.xmp
new file mode 100644
index 00000000..060abb2c
--- /dev/null
+++ b/tests/phpunit/data/xmp/xmpExt2.xmp
@@ -0,0 +1,8 @@
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description
+ rdf:about=""
+ xmlns:exif="http://ns.adobe.com/exif/1.0/"
+ exif:FNumber="2/10">
+</rdf:Description>
+</rdf:RDF>
+<?xpacket end="w"?>
diff --git a/tests/phpunit/data/zip/cd-gap.zip b/tests/phpunit/data/zip/cd-gap.zip
new file mode 100644
index 00000000..b5ae6ccd
--- /dev/null
+++ b/tests/phpunit/data/zip/cd-gap.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/cd-truncated.zip b/tests/phpunit/data/zip/cd-truncated.zip
new file mode 100644
index 00000000..4d40d7d4
--- /dev/null
+++ b/tests/phpunit/data/zip/cd-truncated.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/class-trailing-null.zip b/tests/phpunit/data/zip/class-trailing-null.zip
new file mode 100644
index 00000000..31dcf3d8
--- /dev/null
+++ b/tests/phpunit/data/zip/class-trailing-null.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/class-trailing-slash.zip b/tests/phpunit/data/zip/class-trailing-slash.zip
new file mode 100644
index 00000000..9eb1f037
--- /dev/null
+++ b/tests/phpunit/data/zip/class-trailing-slash.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/class.zip b/tests/phpunit/data/zip/class.zip
new file mode 100644
index 00000000..98a625b7
--- /dev/null
+++ b/tests/phpunit/data/zip/class.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/empty.zip b/tests/phpunit/data/zip/empty.zip
new file mode 100644
index 00000000..15cb0ecb
--- /dev/null
+++ b/tests/phpunit/data/zip/empty.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/looks-like-zip64.zip b/tests/phpunit/data/zip/looks-like-zip64.zip
new file mode 100644
index 00000000..7428cddd
--- /dev/null
+++ b/tests/phpunit/data/zip/looks-like-zip64.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/nosig.zip b/tests/phpunit/data/zip/nosig.zip
new file mode 100644
index 00000000..a22c73a4
--- /dev/null
+++ b/tests/phpunit/data/zip/nosig.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/split.zip b/tests/phpunit/data/zip/split.zip
new file mode 100644
index 00000000..6984ae6d
--- /dev/null
+++ b/tests/phpunit/data/zip/split.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/trail.zip b/tests/phpunit/data/zip/trail.zip
new file mode 100644
index 00000000..50bcea12
--- /dev/null
+++ b/tests/phpunit/data/zip/trail.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/wrong-cd-start-disk.zip b/tests/phpunit/data/zip/wrong-cd-start-disk.zip
new file mode 100644
index 00000000..59b45938
--- /dev/null
+++ b/tests/phpunit/data/zip/wrong-cd-start-disk.zip
Binary files differ
diff --git a/tests/phpunit/data/zip/wrong-central-entry-sig.zip b/tests/phpunit/data/zip/wrong-central-entry-sig.zip
new file mode 100644
index 00000000..05329b43
--- /dev/null
+++ b/tests/phpunit/data/zip/wrong-central-entry-sig.zip
Binary files differ
diff --git a/tests/phpunit/docs/ExportDemoTest.php b/tests/phpunit/docs/ExportDemoTest.php
new file mode 100644
index 00000000..8288cae0
--- /dev/null
+++ b/tests/phpunit/docs/ExportDemoTest.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * Test making sure the demo export xml is valid.
+ * This is NOT a unit test
+ *
+ * @group Dump
+ * @group large
+ */
+class ExportDemoTest extends DumpTestCase {
+
+ public function testExportDemo() {
+ $fname = "../../docs/export-demo.xml";
+ $version = WikiExporter::schemaVersion();
+ $dom = new DomDocument();
+ $dom->load( $fname );
+
+ // Ensure, the demo is for the current version
+ $this->assertEquals(
+ $dom->documentElement->getAttribute( 'version' ),
+ $version,
+ 'export-demo.xml should have the current version'
+ );
+
+ $this->assertTrue(
+ $dom->schemaValidate( "../../docs/export-" . $version . ".xsd" ),
+ "schemaValidate has found an error"
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/ArrayUtilsTest.php b/tests/phpunit/includes/ArrayUtilsTest.php
new file mode 100644
index 00000000..7bdb1ca4
--- /dev/null
+++ b/tests/phpunit/includes/ArrayUtilsTest.php
@@ -0,0 +1,311 @@
+<?php
+/**
+ * Test class for ArrayUtils class
+ *
+ * @group Database
+ */
+
+class ArrayUtilsTest extends MediaWikiTestCase {
+ private $search;
+
+ /**
+ * @covers ArrayUtils::findLowerBound
+ * @dataProvider provideFindLowerBound
+ */
+ function testFindLowerBound(
+ $valueCallback, $valueCount, $comparisonCallback, $target, $expected
+ ) {
+ $this->assertSame(
+ ArrayUtils::findLowerBound(
+ $valueCallback, $valueCount, $comparisonCallback, $target
+ ), $expected
+ );
+ }
+
+ function provideFindLowerBound() {
+ $self = $this;
+ $indexValueCallback = function ( $size ) use ( $self ) {
+ return function ( $val ) use ( $self, $size ) {
+ $self->assertTrue( $val >= 0 );
+ $self->assertTrue( $val < $size );
+ return $val;
+ };
+ };
+ $comparisonCallback = function ( $a, $b ) {
+ return $a - $b;
+ };
+
+ return array(
+ array(
+ $indexValueCallback( 0 ),
+ 0,
+ $comparisonCallback,
+ 1,
+ false,
+ ),
+ array(
+ $indexValueCallback( 1 ),
+ 1,
+ $comparisonCallback,
+ -1,
+ false,
+ ),
+ array(
+ $indexValueCallback( 1 ),
+ 1,
+ $comparisonCallback,
+ 0,
+ 0,
+ ),
+ array(
+ $indexValueCallback( 1 ),
+ 1,
+ $comparisonCallback,
+ 1,
+ 0,
+ ),
+ array(
+ $indexValueCallback( 2 ),
+ 2,
+ $comparisonCallback,
+ -1,
+ false,
+ ),
+ array(
+ $indexValueCallback( 2 ),
+ 2,
+ $comparisonCallback,
+ 0,
+ 0,
+ ),
+ array(
+ $indexValueCallback( 2 ),
+ 2,
+ $comparisonCallback,
+ 0.5,
+ 0,
+ ),
+ array(
+ $indexValueCallback( 2 ),
+ 2,
+ $comparisonCallback,
+ 1,
+ 1,
+ ),
+ array(
+ $indexValueCallback( 2 ),
+ 2,
+ $comparisonCallback,
+ 1.5,
+ 1,
+ ),
+ array(
+ $indexValueCallback( 3 ),
+ 3,
+ $comparisonCallback,
+ 1,
+ 1,
+ ),
+ array(
+ $indexValueCallback( 3 ),
+ 3,
+ $comparisonCallback,
+ 1.5,
+ 1,
+ ),
+ array(
+ $indexValueCallback( 3 ),
+ 3,
+ $comparisonCallback,
+ 2,
+ 2,
+ ),
+ array(
+ $indexValueCallback( 3 ),
+ 3,
+ $comparisonCallback,
+ 3,
+ 2,
+ ),
+ );
+ }
+
+ /**
+ * @covers ArrayUtils::arrayDiffAssocRecursive
+ * @dataProvider provideArrayDiffAssocRecursive
+ */
+ function testArrayDiffAssocRecursive( $expected ) {
+ $args = func_get_args();
+ array_shift( $args );
+ $this->assertEquals( call_user_func_array(
+ 'ArrayUtils::arrayDiffAssocRecursive', $args
+ ), $expected );
+ }
+
+ function provideArrayDiffAssocRecursive() {
+ return array(
+ array(
+ array(),
+ array(),
+ array(),
+ ),
+ array(
+ array(),
+ array(),
+ array(),
+ array(),
+ ),
+ array(
+ array( 1 ),
+ array( 1 ),
+ array(),
+ ),
+ array(
+ array( 1 ),
+ array( 1 ),
+ array(),
+ array(),
+ ),
+ array(
+ array(),
+ array(),
+ array( 1 ),
+ ),
+ array(
+ array(),
+ array(),
+ array( 1 ),
+ array( 2 ),
+ ),
+ array(
+ array( '' => 1 ),
+ array( '' => 1 ),
+ array(),
+ ),
+ array(
+ array(),
+ array(),
+ array( '' => 1 ),
+ ),
+ array(
+ array( 1 ),
+ array( 1 ),
+ array( 2 ),
+ ),
+ array(
+ array(),
+ array( 1 ),
+ array( 2 ),
+ array( 1 ),
+ ),
+ array(
+ array(),
+ array( 1 ),
+ array( 1, 2 ),
+ ),
+ array(
+ array( 1 => 1 ),
+ array( 1 => 1 ),
+ array( 1 ),
+ ),
+ array(
+ array(),
+ array( 1 => 1 ),
+ array( 1 ),
+ array( 1 => 1),
+ ),
+ array(
+ array(),
+ array( 1 => 1 ),
+ array( 1, 1, 1 ),
+ ),
+ array(
+ array(),
+ array( array() ),
+ array(),
+ ),
+ array(
+ array(),
+ array( array( array() ) ),
+ array(),
+ ),
+ array(
+ array( 1, array( 1 ) ),
+ array( 1, array( 1 ) ),
+ array(),
+ ),
+ array(
+ array( 1 ),
+ array( 1, array( 1 ) ),
+ array( 2, array( 1 ) ),
+ ),
+ array(
+ array(),
+ array( 1, array( 1 ) ),
+ array( 2, array( 1 ) ),
+ array( 1, array( 2 ) ),
+ ),
+ array(
+ array( 1 ),
+ array( 1, array() ),
+ array( 2 ),
+ ),
+ array(
+ array(),
+ array( 1, array() ),
+ array( 2 ),
+ array( 1 ),
+ ),
+ array(
+ array( 1, array( 1 => 2 ) ),
+ array( 1, array( 1, 2 ) ),
+ array( 2, array( 1 ) ),
+ ),
+ array(
+ array( 1 ),
+ array( 1, array( 1, 2 ) ),
+ array( 2, array( 1 ) ),
+ array( 2, array( 1 => 2 ) ),
+ ),
+ array(
+ array( 1 => array( 1, 2 ) ),
+ array( 1, array( 1, 2 ) ),
+ array( 1, array( 2 ) ),
+ ),
+ array(
+ array( 1 => array( array( 2, 3 ), 2 ) ),
+ array( 1, array( array( 2, 3 ), 2 ) ),
+ array( 1, array( 2 ) ),
+ ),
+ array(
+ array( 1 => array( array( 2 ), 2 ) ),
+ array( 1, array( array( 2, 3 ), 2 ) ),
+ array( 1, array( array( 1 => 3 ) ) ),
+ ),
+ array(
+ array( 1 => array( 1 => 2 ) ),
+ array( 1, array( array( 2, 3 ), 2 ) ),
+ array( 1, array( array( 1 => 3, 0 => 2 ) ) ),
+ ),
+ array(
+ array( 1 => array( 1 => 2 ) ),
+ array( 1, array( array( 2, 3 ), 2 ) ),
+ array( 1, array( array( 1 => 3 ) ) ),
+ array( 1 => array( array( 2 ) ) ),
+ ),
+ array(
+ array(),
+ array( 1, array( array( 2, 3 ), 2 ) ),
+ array( 1 => array( 1 => 2, 0 => array( 1 => 3, 0 => 2 ) ), 0 => 1 ),
+ ),
+ array(
+ array(),
+ array( 1, array( array( 2, 3 ), 2 ) ),
+ array( 1 => array( 1 => 2 ) ),
+ array( 1 => array( array( 1 => 3 ) ) ),
+ array( 1 => array( array( 2 ) ) ),
+ array( 1 ),
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/ArticleTablesTest.php b/tests/phpunit/includes/ArticleTablesTest.php
new file mode 100644
index 00000000..9f2b7a05
--- /dev/null
+++ b/tests/phpunit/includes/ArticleTablesTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @group Database
+ */
+class ArticleTablesTest extends MediaWikiLangTestCase {
+ /**
+ * Make sure that bug 14404 doesn't strike again. We don't want
+ * templatelinks based on the user language when {{int:}} is used, only the
+ * content language.
+ *
+ * @covers Title::getTemplateLinksFrom
+ * @covers Title::getLinksFrom
+ */
+ public function testTemplatelinksUsesContentLanguage() {
+ $title = Title::newFromText( 'Bug 14404' );
+ $page = WikiPage::factory( $title );
+ $user = new User();
+ $user->mRights = array( 'createpage', 'edit', 'purge' );
+ $this->setMwGlobals( 'wgLanguageCode', 'es' );
+ $this->setMwGlobals( 'wgContLang', Language::factory( 'es' ) );
+ $this->setMwGlobals( 'wgLang', Language::factory( 'fr' ) );
+
+ $page->doEditContent(
+ new WikitextContent( '{{:{{int:history}}}}' ),
+ 'Test code for bug 14404',
+ 0,
+ false,
+ $user
+ );
+ $templates1 = $title->getTemplateLinksFrom();
+
+ $this->setMwGlobals( 'wgLang', Language::factory( 'de' ) );
+ $page = WikiPage::factory( $title ); // In order to force the re-rendering of the same wikitext
+
+ // We need an edit, a purge is not enough to regenerate the tables
+ $page->doEditContent(
+ new WikitextContent( '{{:{{int:history}}}}' ),
+ 'Test code for bug 14404',
+ EDIT_UPDATE,
+ false,
+ $user
+ );
+ $templates2 = $title->getTemplateLinksFrom();
+
+ /**
+ * @var Title[] $templates1
+ * @var Title[] $templates2
+ */
+ $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..ae069eaf
--- /dev/null
+++ b/tests/phpunit/includes/ArticleTest.php
@@ -0,0 +1,95 @@
+<?php
+
+class ArticleTest extends MediaWikiTestCase {
+
+ /**
+ * @var Title
+ */
+ private $title;
+ /**
+ * @var Article
+ */
+ private $article;
+
+ /** creates a title object and its article object */
+ protected function setUp() {
+ parent::setUp();
+ $this->title = Title::makeTitle( NS_MAIN, 'SomePage' );
+ $this->article = new Article( $this->title );
+ }
+
+ /** cleanup title object and its article object */
+ protected function tearDown() {
+ parent::tearDown();
+ $this->title = null;
+ $this->article = null;
+ }
+
+ /**
+ * @covers Article::__get
+ */
+ public function testImplementsGetMagic() {
+ $this->assertEquals( false, $this->article->mLatest, "Article __get magic" );
+ }
+
+ /**
+ * @depends testImplementsGetMagic
+ * @covers Article::__set
+ */
+ public function testImplementsSetMagic() {
+ $this->article->mLatest = 2;
+ $this->assertEquals( 2, $this->article->mLatest, "Article __set magic" );
+ }
+
+ /**
+ * @depends testImplementsSetMagic
+ * @covers Article::__call
+ */
+ public function testImplementsCallMagic() {
+ $this->article->mLatest = 33;
+ $this->article->mDataLoaded = true;
+ $this->assertEquals( 33, $this->article->getLatest(), "Article __call magic" );
+ }
+
+ /**
+ * @covers Article::__get
+ * @covers Article::__set
+ */
+ public 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)
+ *
+ * @covers Article::selectFields
+ * @covers Article::onArticleCreate
+ * @covers Article::onArticleDelete
+ * @covers Article::onArticleEdit
+ * @covers Article::getAutosummary
+ */
+ public function testStaticFunctions() {
+ $this->hideDeprecated( 'Article::selectFields' );
+ $this->hideDeprecated( 'Article::getAutosummary' );
+ $this->hideDeprecated( 'WikiPage::getAutosummary' );
+ $this->hideDeprecated( 'CategoryPage::getAutosummary' ); // Inherited from Article
+
+ $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" );
+ }
+}
diff --git a/tests/phpunit/includes/BlockTest.php b/tests/phpunit/includes/BlockTest.php
new file mode 100644
index 00000000..b248d24e
--- /dev/null
+++ b/tests/phpunit/includes/BlockTest.php
@@ -0,0 +1,368 @@
+<?php
+
+/**
+ * @group Database
+ * @group Blocking
+ */
+class BlockTest extends MediaWikiLangTestCase {
+
+ /** @var Block */
+ private $block;
+ private $madeAt;
+
+ /* variable used to save up the blockID we insert in this test suite */
+ private $blockId;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => Language::factory( 'en' )
+ ) );
+ }
+
+ function addDBData() {
+
+ $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', $user->getID(), 0,
+ 'Parce que', 0, false, time() + 100500
+ );
+ $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?" );
+ }
+
+ $this->addXffBlocks();
+ }
+
+ /**
+ * debug function : dump the ipblocks table
+ */
+ function dumpBlocks() {
+ $v = $this->db->select( 'ipblocks', '*' );
+ print "Got " . $v->numRows() . " rows. Full dump follow:\n";
+ foreach ( $v as $row ) {
+ print_r( $row );
+ }
+ }
+
+ /**
+ * @covers Block::newFromTarget
+ */
+ public function testINewFromTargetReturnsCorrectBlock() {
+ $this->assertTrue(
+ $this->block->equals( Block::newFromTarget( 'UTBlockee' ) ),
+ "newFromTarget() returns the same block as the one that was made"
+ );
+ }
+
+ /**
+ * @covers Block::newFromID
+ */
+ public function testINewFromIDReturnsCorrectBlock() {
+ $this->assertTrue(
+ $this->block->equals( Block::newFromID( $this->blockId ) ),
+ "newFromID() returns the same block as the one that was made"
+ );
+ }
+
+ /**
+ * per bug 26425
+ */
+ public function testBug26425BlockTimestampDefaultsToTime() {
+ // delta to stop one-off errors when things happen to go over a second mark.
+ $delta = abs( $this->madeAt - $this->block->mTimestamp );
+ $this->assertLessThan(
+ 2,
+ $delta,
+ "If no timestamp is specified, the block is recorded as time()"
+ );
+ }
+
+ /**
+ * 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 provideBug29116Data
+ * @covers Block::newFromTarget
+ */
+ public 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 )
+ );
+ }
+
+ public static function provideBug29116Data() {
+ return array(
+ array( null ),
+ array( '' ),
+ array( false )
+ );
+ }
+
+ /**
+ * @covers Block::prevents
+ */
+ public function testBlockedUserCanNotCreateAccount() {
+ $username = 'BlockedUserToCreateAccountWith';
+ $u = User::newFromName( $username );
+ $u->setPassword( 'NotRandomPass' );
+ $u->addToDatabase();
+ unset( $u );
+
+ // Sanity check
+ $this->assertNull(
+ Block::newFromTarget( $username ),
+ "$username should not be blocked"
+ );
+
+ // Reload user
+ $u = User::newFromName( $username );
+ $this->assertFalse(
+ $u->isBlockedFromCreateAccount(),
+ "Our sandbox user should be able to create account before being blocked"
+ );
+
+ // Foreign perspective (blockee not on current wiki)...
+ $block = new Block(
+ /* $address */ $username,
+ /* $user */ 14146,
+ /* $by */ 0,
+ /* $reason */ 'crosswiki block...',
+ /* $timestamp */ wfTimestampNow(),
+ /* $auto */ false,
+ /* $expiry */ $this->db->getInfinity(),
+ /* anonOnly */ false,
+ /* $createAccount */ true,
+ /* $enableAutoblock */ true,
+ /* $hideName (ipb_deleted) */ true,
+ /* $blockEmail */ true,
+ /* $allowUsertalk */ false,
+ /* $byName */ 'MetaWikiUser'
+ );
+ $block->insert();
+
+ // Reload block from DB
+ $userBlock = Block::newFromTarget( $username );
+ $this->assertTrue(
+ (bool)$block->prevents( 'createaccount' ),
+ "Block object in DB should prevents 'createaccount'"
+ );
+
+ $this->assertInstanceOf(
+ 'Block',
+ $userBlock,
+ "'$username' block block object should be existent"
+ );
+
+ // Reload user
+ $u = User::newFromName( $username );
+ $this->assertTrue(
+ (bool)$u->isBlockedFromCreateAccount(),
+ "Our sandbox user '$username' should NOT be able to create account"
+ );
+ }
+
+ /**
+ * @covers Block::insert
+ */
+ public function testCrappyCrossWikiBlocks() {
+ // Delete the last round's block if it's still there
+ $oldBlock = Block::newFromTarget( 'UserOnForeignWiki' );
+ if ( $oldBlock ) {
+ // An old block will prevent our new one from saving.
+ $oldBlock->delete();
+ }
+
+ // Foreign perspective (blockee not on current wiki)...
+ $block = new Block(
+ /* $address */ 'UserOnForeignWiki',
+ /* $user */ 14146,
+ /* $by */ 0,
+ /* $reason */ 'crosswiki block...',
+ /* $timestamp */ wfTimestampNow(),
+ /* $auto */ false,
+ /* $expiry */ $this->db->getInfinity(),
+ /* anonOnly */ false,
+ /* $createAccount */ true,
+ /* $enableAutoblock */ true,
+ /* $hideName (ipb_deleted) */ true,
+ /* $blockEmail */ true,
+ /* $allowUsertalk */ false,
+ /* $byName */ 'MetaWikiUser'
+ );
+
+ $res = $block->insert( $this->db );
+ $this->assertTrue( (bool)$res['id'], 'Block succeeded' );
+
+ // Local perspective (blockee on current wiki)...
+ $user = User::newFromName( 'UserOnForeignWiki' );
+ $user->addToDatabase();
+ // Set user ID to match the test value
+ $this->db->update( 'user', array( 'user_id' => 14146 ), array( 'user_id' => $user->getId() ) );
+ $user = null; // clear
+
+ $block = Block::newFromID( $res['id'] );
+ $this->assertEquals(
+ 'UserOnForeignWiki',
+ $block->getTarget()->getName(),
+ 'Correct blockee name'
+ );
+ $this->assertEquals( '14146', $block->getTarget()->getId(), 'Correct blockee id' );
+ $this->assertEquals( 'MetaWikiUser', $block->getBlocker(), 'Correct blocker name' );
+ $this->assertEquals( 'MetaWikiUser', $block->getByName(), 'Correct blocker name' );
+ $this->assertEquals( 0, $block->getBy(), 'Correct blocker id' );
+ }
+
+ protected function addXffBlocks() {
+ static $inited = false;
+
+ if ( $inited ) {
+ return;
+ }
+
+ $inited = true;
+
+ $blockList = array(
+ array( 'target' => '70.2.0.0/16',
+ 'type' => Block::TYPE_RANGE,
+ 'desc' => 'Range Hardblock',
+ 'ACDisable' => false,
+ 'isHardblock' => true,
+ 'isAutoBlocking' => false,
+ ),
+ array( 'target' => '2001:4860:4001::/48',
+ 'type' => Block::TYPE_RANGE,
+ 'desc' => 'Range6 Hardblock',
+ 'ACDisable' => false,
+ 'isHardblock' => true,
+ 'isAutoBlocking' => false,
+ ),
+ array( 'target' => '60.2.0.0/16',
+ 'type' => Block::TYPE_RANGE,
+ 'desc' => 'Range Softblock with AC Disabled',
+ 'ACDisable' => true,
+ 'isHardblock' => false,
+ 'isAutoBlocking' => false,
+ ),
+ array( 'target' => '50.2.0.0/16',
+ 'type' => Block::TYPE_RANGE,
+ 'desc' => 'Range Softblock',
+ 'ACDisable' => false,
+ 'isHardblock' => false,
+ 'isAutoBlocking' => false,
+ ),
+ array( 'target' => '50.1.1.1',
+ 'type' => Block::TYPE_IP,
+ 'desc' => 'Exact Softblock',
+ 'ACDisable' => false,
+ 'isHardblock' => false,
+ 'isAutoBlocking' => false,
+ ),
+ );
+
+ foreach ( $blockList as $insBlock ) {
+ $target = $insBlock['target'];
+
+ if ( $insBlock['type'] === Block::TYPE_IP ) {
+ $target = User::newFromName( IP::sanitizeIP( $target ), false )->getName();
+ } elseif ( $insBlock['type'] === Block::TYPE_RANGE ) {
+ $target = IP::sanitizeRange( $target );
+ }
+
+ $block = new Block();
+ $block->setTarget( $target );
+ $block->setBlocker( 'testblocker@global' );
+ $block->mReason = $insBlock['desc'];
+ $block->mExpiry = 'infinity';
+ $block->prevents( 'createaccount', $insBlock['ACDisable'] );
+ $block->isHardblock( $insBlock['isHardblock'] );
+ $block->isAutoblocking( $insBlock['isAutoBlocking'] );
+ $block->insert();
+ }
+ }
+
+ public static function providerXff() {
+ return array(
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Hardblock'
+ ),
+ array( 'xff' => '1.2.3.4, 50.2.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Softblock with AC Disabled'
+ ),
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 50.1.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Exact Softblock'
+ ),
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 50.2.1.1, 50.1.1.1, 2.3.4.5',
+ 'count' => 3,
+ 'result' => 'Exact Softblock'
+ ),
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 50.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Hardblock'
+ ),
+ array( 'xff' => '1.2.3.4, 70.2.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Hardblock'
+ ),
+ array( 'xff' => '50.2.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range Softblock with AC Disabled'
+ ),
+ array( 'xff' => '1.2.3.4, 50.1.1.1, 60.2.1.1, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Exact Softblock'
+ ),
+ array( 'xff' => '1.2.3.4, <$A_BUNCH-OF{INVALID}TEXT\>, 60.2.1.1, 2.3.4.5',
+ 'count' => 1,
+ 'result' => 'Range Softblock with AC Disabled'
+ ),
+ array( 'xff' => '1.2.3.4, 50.2.1.1, 2001:4860:4001:802::1003, 2.3.4.5',
+ 'count' => 2,
+ 'result' => 'Range6 Hardblock'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider providerXff
+ * @covers Block::getBlocksForIPList
+ * @covers Block::chooseBlock
+ */
+ public function testBlocksOnXff( $xff, $exCount, $exResult ) {
+ $list = array_map( 'trim', explode( ',', $xff ) );
+ $xffblocks = Block::getBlocksForIPList( $list, true );
+ $this->assertEquals( $exCount, count( $xffblocks ), 'Number of blocks for ' . $xff );
+ $block = Block::chooseBlock( $xffblocks, $list );
+ $this->assertEquals( $exResult, $block->mReason, 'Correct block type for XFF header ' . $xff );
+ }
+}
diff --git a/tests/phpunit/includes/CollationTest.php b/tests/phpunit/includes/CollationTest.php
new file mode 100644
index 00000000..74b12967
--- /dev/null
+++ b/tests/phpunit/includes/CollationTest.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * Class CollationTest
+ * @covers Collation
+ * @covers IcuCollation
+ * @covers IdentityCollation
+ * @covers UppercaseCollation
+ */
+class CollationTest extends MediaWikiLangTestCase {
+ protected function setUp() {
+ parent::setUp();
+ $this->checkPHPExtension( 'intl' );
+ }
+
+ /**
+ * Test to make sure, that if you
+ * have "X" and "XY", the binary
+ * sortkey also has "X" being a
+ * prefix of "XY". Our collation
+ * code makes this assumption.
+ *
+ * @param string $lang Language code for collator
+ * @param string $base Base string
+ * @param string $extended String containing base as a prefix.
+ *
+ * @dataProvider prefixDataProvider
+ */
+ public function testIsPrefix( $lang, $base, $extended ) {
+ $cp = Collator::create( $lang );
+ $cp->setStrength( Collator::PRIMARY );
+ $baseBin = $cp->getSortKey( $base );
+ // Remove sortkey terminator
+ $baseBin = rtrim( $baseBin, "\0" );
+ $extendedBin = $cp->getSortKey( $extended );
+ $this->assertStringStartsWith( $baseBin, $extendedBin, "$base is not a prefix of $extended" );
+ }
+
+ public static function prefixDataProvider() {
+ return array(
+ array( 'en', 'A', 'AA' ),
+ array( 'en', 'A', 'AAA' ),
+ array( 'en', 'Д', 'ДЂ' ),
+ array( 'en', 'Д', 'ДA' ),
+ // 'Ʒ' should expand to 'Z ' (note space).
+ array( 'fi', 'Z', 'Ʒ' ),
+ // 'Þ' should expand to 'th'
+ array( 'sv', 't', 'Þ' ),
+ // Javanese is a limited use alphabet, so should have 3 bytes
+ // per character, so do some tests with it.
+ array( 'en', 'ꦲ', 'ꦲꦤ' ),
+ array( 'en', 'ꦲ', 'ꦲД' ),
+ array( 'en', 'A', 'Aꦲ' ),
+ );
+ }
+
+ /**
+ * Opposite of testIsPrefix
+ *
+ * @dataProvider notPrefixDataProvider
+ */
+ public function testNotIsPrefix( $lang, $base, $extended ) {
+ $cp = Collator::create( $lang );
+ $cp->setStrength( Collator::PRIMARY );
+ $baseBin = $cp->getSortKey( $base );
+ // Remove sortkey terminator
+ $baseBin = rtrim( $baseBin, "\0" );
+ $extendedBin = $cp->getSortKey( $extended );
+ $this->assertStringStartsNotWith( $baseBin, $extendedBin, "$base is a prefix of $extended" );
+ }
+
+ public static function notPrefixDataProvider() {
+ return array(
+ array( 'en', 'A', 'B' ),
+ array( 'en', 'AC', 'ABC' ),
+ array( 'en', 'Z', 'Ʒ' ),
+ array( 'en', 'A', 'ꦲ' ),
+ );
+ }
+
+ /**
+ * Test correct first letter is fetched.
+ *
+ * @param string $collation Collation name (aka uca-en)
+ * @param string $string String to get first letter of
+ * @param string $firstLetter Expected first letter.
+ *
+ * @dataProvider firstLetterProvider
+ */
+ public function testGetFirstLetter( $collation, $string, $firstLetter ) {
+ $col = Collation::factory( $collation );
+ $this->assertEquals( $firstLetter, $col->getFirstLetter( $string ) );
+ }
+
+ function firstLetterProvider() {
+ return array(
+ array( 'uppercase', 'Abc', 'A' ),
+ array( 'uppercase', 'abc', 'A' ),
+ array( 'identity', 'abc', 'a' ),
+ array( 'uca-en', 'abc', 'A' ),
+ array( 'uca-en', ' ', ' ' ),
+ array( 'uca-en', 'Êveryone', 'E' ),
+ array( 'uca-vi', 'Êveryone', 'Ê' ),
+ // Make sure thorn is not a first letter.
+ array( 'uca-sv', 'The', 'T' ),
+ array( 'uca-sv', 'Å', 'Å' ),
+ array( 'uca-hu', 'dzsdo', 'Dzs' ),
+ array( 'uca-hu', 'dzdso', 'Dz' ),
+ array( 'uca-hu', 'CSD', 'Cs' ),
+ array( 'uca-root', 'CSD', 'C' ),
+ array( 'uca-fi', 'Ǥ', 'G' ),
+ array( 'uca-fi', 'Ŧ', 'T' ),
+ array( 'uca-fi', 'Ʒ', 'Z' ),
+ array( 'uca-fi', 'Ŋ', 'N' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/DiffHistoryBlobTest.php b/tests/phpunit/includes/DiffHistoryBlobTest.php
new file mode 100644
index 00000000..e28a92cf
--- /dev/null
+++ b/tests/phpunit/includes/DiffHistoryBlobTest.php
@@ -0,0 +1,40 @@
+<?php
+
+class DiffHistoryBlobTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->checkPHPExtension( 'hash' );
+ $this->checkPHPExtension( 'xdiff' );
+
+ if ( !function_exists( 'xdiff_string_rabdiff' ) ) {
+ $this->markTestSkipped( 'The version of xdiff extension is lower than 1.5.0' );
+
+ return;
+ }
+ }
+
+ /**
+ * Test for DiffHistoryBlob::xdiffAdler32()
+ * @dataProvider provideXdiffAdler32
+ * @covers DiffHistoryBlob::xdiffAdler32
+ */
+ public function testXdiffAdler32( $input ) {
+ $xdiffHash = substr( xdiff_string_rabdiff( $input, '' ), 0, 4 );
+ $dhb = new DiffHistoryBlob;
+ $myHash = $dhb->xdiffAdler32( $input );
+ $this->assertSame( bin2hex( $xdiffHash ), bin2hex( $myHash ),
+ "Hash of " . addcslashes( $input, "\0..\37!@\@\177..\377" ) );
+ }
+
+ public static function provideXdiffAdler32() {
+ return array(
+ array( '', 'Empty string' ),
+ array( "\0", 'Null' ),
+ array( "\0\0\0", "Several nulls" ),
+ array( "Hello", "An ASCII string" ),
+ array( str_repeat( "x", 6000 ), "A string larger than xdiff's NMAX (5552)" )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/EditPageTest.php b/tests/phpunit/includes/EditPageTest.php
new file mode 100644
index 00000000..702fce4c
--- /dev/null
+++ b/tests/phpunit/includes/EditPageTest.php
@@ -0,0 +1,499 @@
+<?php
+
+/**
+ * @group Editing
+ *
+ * @group Database
+ * ^--- tell jenkins this test needs the database
+ *
+ * @group medium
+ * ^--- tell phpunit that these test cases may take longer than 2 seconds.
+ */
+class EditPageTest extends MediaWikiLangTestCase {
+
+ /**
+ * @dataProvider provideExtractSectionTitle
+ * @covers EditPage::extractSectionTitle
+ */
+ public function testExtractSectionTitle( $section, $title ) {
+ $extracted = EditPage::extractSectionTitle( $section );
+ $this->assertEquals( $title, $extracted );
+ }
+
+ public static function provideExtractSectionTitle() {
+ return array(
+ array(
+ "== Test ==\n\nJust a test section.",
+ "Test"
+ ),
+ array(
+ "An initial section, no header.",
+ false
+ ),
+ array(
+ "An initial section with a fake heder (bug 32617)\n\n== Test == ??\nwtf",
+ false
+ ),
+ array(
+ "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo",
+ "Section"
+ ),
+ array(
+ "== Section== \t\r\n followed by whitespace (bug 35051)",
+ 'Section',
+ ),
+ );
+ }
+
+ protected function forceRevisionDate( WikiPage $page, $timestamp ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->update( 'revision',
+ array( 'rev_timestamp' => $dbw->timestamp( $timestamp ) ),
+ array( 'rev_id' => $page->getLatest() ) );
+
+ $page->clear();
+ }
+
+ /**
+ * User input text is passed to rtrim() by edit page. This is a simple
+ * wrapper around assertEquals() which calls rrtrim() to normalize the
+ * expected and actual texts.
+ * @param string $expected
+ * @param string $actual
+ * @param string $msg
+ */
+ protected function assertEditedTextEquals( $expected, $actual, $msg = '' ) {
+ return $this->assertEquals( rtrim( $expected ), rtrim( $actual ), $msg );
+ }
+
+ /**
+ * Performs an edit and checks the result.
+ *
+ * @param string|Title $title The title of the page to edit
+ * @param string|null $baseText Some text to create the page with before attempting the edit.
+ * @param User|string|null $user The user to perform the edit as.
+ * @param array $edit An array of request parameters used to define the edit to perform.
+ * Some well known fields are:
+ * * wpTextbox1: the text to submit
+ * * wpSummary: the edit summary
+ * * wpEditToken: the edit token (will be inserted if not provided)
+ * * wpEdittime: timestamp of the edit's base revision (will be inserted
+ * if not provided)
+ * * wpStarttime: timestamp when the edit started (will be inserted if not provided)
+ * * wpSectionTitle: the section to edit
+ * * wpMinorEdit: mark as minor edit
+ * * wpWatchthis: whether to watch the page
+ * @param int|null $expectedCode The expected result code (EditPage::AS_XXX constants).
+ * Set to null to skip the check.
+ * @param string|null $expectedText The text expected to be on the page after the edit.
+ * Set to null to skip the check.
+ * @param string|null $message An optional message to show along with any error message.
+ *
+ * @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc.
+ */
+ protected function assertEdit( $title, $baseText, $user = null, array $edit,
+ $expectedCode = null, $expectedText = null, $message = null
+ ) {
+ if ( is_string( $title ) ) {
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( $title, $ns );
+ }
+ $this->assertNotNull( $title );
+
+ if ( is_string( $user ) ) {
+ $user = User::newFromName( $user );
+
+ if ( $user->getId() === 0 ) {
+ $user->addToDatabase();
+ }
+ }
+
+ $page = WikiPage::factory( $title );
+
+ if ( $baseText !== null ) {
+ $content = ContentHandler::makeContent( $baseText, $title );
+ $page->doEditContent( $content, "base text for test" );
+ $this->forceRevisionDate( $page, '20120101000000' );
+
+ //sanity check
+ $page->clear();
+ $currentText = ContentHandler::getContentText( $page->getContent() );
+
+ # EditPage rtrim() the user input, so we alter our expected text
+ # to reflect that.
+ $this->assertEditedTextEquals( $baseText, $currentText );
+ }
+
+ if ( $user == null ) {
+ $user = $GLOBALS['wgUser'];
+ } else {
+ $this->setMwGlobals( 'wgUser', $user );
+ }
+
+ if ( !isset( $edit['wpEditToken'] ) ) {
+ $edit['wpEditToken'] = $user->getEditToken();
+ }
+
+ if ( !isset( $edit['wpEdittime'] ) ) {
+ $edit['wpEdittime'] = $page->exists() ? $page->getTimestamp() : '';
+ }
+
+ if ( !isset( $edit['wpStarttime'] ) ) {
+ $edit['wpStarttime'] = wfTimestampNow();
+ }
+
+ $req = new FauxRequest( $edit, true ); // session ??
+
+ $article = new Article( $title );
+ $article->getContext()->setTitle( $title );
+ $ep = new EditPage( $article );
+ $ep->setContextTitle( $title );
+ $ep->importFormData( $req );
+
+ $bot = isset( $edit['bot'] ) ? (bool)$edit['bot'] : false;
+
+ // this is where the edit happens!
+ // Note: don't want to use EditPage::AttemptSave, because it messes with $wgOut
+ // and throws exceptions like PermissionsError
+ $status = $ep->internalAttemptSave( $result, $bot );
+
+ if ( $expectedCode !== null ) {
+ // check edit code
+ $this->assertEquals( $expectedCode, $status->value,
+ "Expected result code mismatch. $message" );
+ }
+
+ $page = WikiPage::factory( $title );
+
+ if ( $expectedText !== null ) {
+ // check resulting page text
+ $content = $page->getContent();
+ $text = ContentHandler::getContentText( $content );
+
+ # EditPage rtrim() the user input, so we alter our expected text
+ # to reflect that.
+ $this->assertEditedTextEquals( $expectedText, $text,
+ "Expected article text mismatch. $message" );
+ }
+
+ return $page;
+ }
+
+ public static function provideCreatePages() {
+ return array(
+ array( 'expected article being created',
+ 'EditPageTest_testCreatePage',
+ null,
+ 'Hello World!',
+ EditPage::AS_SUCCESS_NEW_ARTICLE,
+ 'Hello World!'
+ ),
+ array( 'expected article not being created if empty',
+ 'EditPageTest_testCreatePage',
+ null,
+ '',
+ EditPage::AS_BLANK_ARTICLE,
+ null
+ ),
+ array( 'expected MediaWiki: page being created',
+ 'MediaWiki:January',
+ 'UTSysop',
+ 'Not January',
+ EditPage::AS_SUCCESS_NEW_ARTICLE,
+ 'Not January'
+ ),
+ array( 'expected not-registered MediaWiki: page not being created if empty',
+ 'MediaWiki:EditPageTest_testCreatePage',
+ 'UTSysop',
+ '',
+ EditPage::AS_BLANK_ARTICLE,
+ null
+ ),
+ array( 'expected registered MediaWiki: page being created even if empty',
+ 'MediaWiki:January',
+ 'UTSysop',
+ '',
+ EditPage::AS_SUCCESS_NEW_ARTICLE,
+ ''
+ ),
+ array( 'expected registered MediaWiki: page whose default content is empty not being created if empty',
+ 'MediaWiki:Ipb-default-expiry',
+ 'UTSysop',
+ '',
+ EditPage::AS_BLANK_ARTICLE,
+ ''
+ ),
+ array( 'expected MediaWiki: page not being created if text equals default message',
+ 'MediaWiki:January',
+ 'UTSysop',
+ 'January',
+ EditPage::AS_BLANK_ARTICLE,
+ null
+ ),
+ array( 'expected empty article being created',
+ 'EditPageTest_testCreatePage',
+ null,
+ '',
+ EditPage::AS_SUCCESS_NEW_ARTICLE,
+ '',
+ true
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCreatePages
+ * @covers EditPage
+ */
+ public function testCreatePage( $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false ) {
+ $edit = array( 'wpTextbox1' => $editText );
+ if ( $ignoreBlank ) {
+ $edit['wpIgnoreBlankArticle'] = 1;
+ }
+
+ $page = $this->assertEdit( $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
+
+ if ( $expectedCode != EditPage::AS_BLANK_ARTICLE ) {
+ $page->doDeleteArticleReal( $pageTitle );
+ }
+ }
+
+ public function testUpdatePage() {
+ $text = "one";
+ $edit = array(
+ 'wpTextbox1' => $text,
+ 'wpSummary' => 'first update',
+ );
+
+ $page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit,
+ EditPage::AS_SUCCESS_UPDATE, $text,
+ "expected successfull update with given text" );
+
+ $this->forceRevisionDate( $page, '20120101000000' );
+
+ $text = "two";
+ $edit = array(
+ 'wpTextbox1' => $text,
+ 'wpSummary' => 'second update',
+ );
+
+ $this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit,
+ EditPage::AS_SUCCESS_UPDATE, $text,
+ "expected successfull update with given text" );
+ }
+
+ public static function provideSectionEdit() {
+ $text = 'Intro
+
+== one ==
+first section.
+
+== two ==
+second section.
+';
+
+ $sectionOne = '== one ==
+hello
+';
+
+ $newSection = '== new section ==
+
+hello
+';
+
+ $textWithNewSectionOne = preg_replace(
+ '/== one ==.*== two ==/ms',
+ "$sectionOne\n== two ==", $text
+ );
+
+ $textWithNewSectionAdded = "$text\n$newSection";
+
+ return array(
+ array( #0
+ $text,
+ '',
+ 'hello',
+ 'replace all',
+ 'hello'
+ ),
+
+ array( #1
+ $text,
+ '1',
+ $sectionOne,
+ 'replace first section',
+ $textWithNewSectionOne,
+ ),
+
+ array( #2
+ $text,
+ 'new',
+ 'hello',
+ 'new section',
+ $textWithNewSectionAdded,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideSectionEdit
+ * @covers EditPage
+ */
+ public function testSectionEdit( $base, $section, $text, $summary, $expected ) {
+ $edit = array(
+ 'wpTextbox1' => $text,
+ 'wpSummary' => $summary,
+ 'wpSection' => $section,
+ );
+
+ $this->assertEdit( 'EditPageTest_testSectionEdit', $base, null, $edit,
+ EditPage::AS_SUCCESS_UPDATE, $expected,
+ "expected successfull update of section" );
+ }
+
+ public static function provideAutoMerge() {
+ $tests = array();
+
+ $tests[] = array( #0: plain conflict
+ "Elmo", # base edit user
+ "one\n\ntwo\n\nthree\n",
+ array( #adam's edit
+ 'wpStarttime' => 1,
+ 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
+ ),
+ array( #berta's edit
+ 'wpStarttime' => 2,
+ 'wpTextbox1' => "(one)\n\ntwo\n\nthree\n",
+ ),
+ EditPage::AS_CONFLICT_DETECTED, # expected code
+ "ONE\n\ntwo\n\nthree\n", # expected text
+ 'expected edit conflict', # message
+ );
+
+ $tests[] = array( #1: successful merge
+ "Elmo", # base edit user
+ "one\n\ntwo\n\nthree\n",
+ array( #adam's edit
+ 'wpStarttime' => 1,
+ 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
+ ),
+ array( #berta's edit
+ 'wpStarttime' => 2,
+ 'wpTextbox1' => "one\n\ntwo\n\nTHREE\n",
+ ),
+ EditPage::AS_SUCCESS_UPDATE, # expected code
+ "ONE\n\ntwo\n\nTHREE\n", # expected text
+ 'expected automatic merge', # message
+ );
+
+ $text = "Intro\n\n";
+ $text .= "== first section ==\n\n";
+ $text .= "one\n\ntwo\n\nthree\n\n";
+ $text .= "== second section ==\n\n";
+ $text .= "four\n\nfive\n\nsix\n\n";
+
+ // extract the first section.
+ $section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text );
+
+ // generate expected text after merge
+ $expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) );
+
+ $tests[] = array( #2: merge in section
+ "Elmo", # base edit user
+ $text,
+ array( #adam's edit
+ 'wpStarttime' => 1,
+ 'wpTextbox1' => str_replace( 'one', 'ONE', $section ),
+ 'wpSection' => '1'
+ ),
+ array( #berta's edit
+ 'wpStarttime' => 2,
+ 'wpTextbox1' => str_replace( 'three', 'THREE', $section ),
+ 'wpSection' => '1'
+ ),
+ EditPage::AS_SUCCESS_UPDATE, # expected code
+ $expected, # expected text
+ 'expected automatic section merge', # message
+ );
+
+ // see whether it makes a difference who did the base edit
+ $testsWithAdam = array_map( function ( $test ) {
+ $test[0] = 'Adam'; // change base edit user
+ return $test;
+ }, $tests );
+
+ $testsWithBerta = array_map( function ( $test ) {
+ $test[0] = 'Berta'; // change base edit user
+ return $test;
+ }, $tests );
+
+ return array_merge( $tests, $testsWithAdam, $testsWithBerta );
+ }
+
+ /**
+ * @dataProvider provideAutoMerge
+ * @covers EditPage
+ */
+ public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit,
+ $expectedCode, $expectedText, $message = null
+ ) {
+ $this->checkHasDiff3();
+
+ //create page
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( 'EditPageTest_testAutoMerge', $ns );
+ $page = WikiPage::factory( $title );
+
+ if ( $page->exists() ) {
+ $page->doDeleteArticle( "clean slate for testing" );
+ }
+
+ $baseEdit = array(
+ 'wpTextbox1' => $text,
+ );
+
+ $page = $this->assertEdit( 'EditPageTest_testAutoMerge', null,
+ $baseUser, $baseEdit, null, null, __METHOD__ );
+
+ $this->forceRevisionDate( $page, '20120101000000' );
+
+ $edittime = $page->getTimestamp();
+
+ // start timestamps for conflict detection
+ if ( !isset( $adamsEdit['wpStarttime'] ) ) {
+ $adamsEdit['wpStarttime'] = 1;
+ }
+
+ if ( !isset( $bertasEdit['wpStarttime'] ) ) {
+ $bertasEdit['wpStarttime'] = 2;
+ }
+
+ $starttime = wfTimestampNow();
+ $adamsTime = wfTimestamp(
+ TS_MW,
+ (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$adamsEdit['wpStarttime']
+ );
+ $bertasTime = wfTimestamp(
+ TS_MW,
+ (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$bertasEdit['wpStarttime']
+ );
+
+ $adamsEdit['wpStarttime'] = $adamsTime;
+ $bertasEdit['wpStarttime'] = $bertasTime;
+
+ $adamsEdit['wpSummary'] = 'Adam\'s edit';
+ $bertasEdit['wpSummary'] = 'Bertas\'s edit';
+
+ $adamsEdit['wpEdittime'] = $edittime;
+ $bertasEdit['wpEdittime'] = $edittime;
+
+ // first edit
+ $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit,
+ EditPage::AS_SUCCESS_UPDATE, null, "expected successfull update" );
+
+ // second edit
+ $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit,
+ $expectedCode, $expectedText, $message );
+ }
+}
diff --git a/tests/phpunit/includes/ExternalStoreTest.php b/tests/phpunit/includes/ExternalStoreTest.php
new file mode 100644
index 00000000..07c2957c
--- /dev/null
+++ b/tests/phpunit/includes/ExternalStoreTest.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * External Store tests
+ */
+
+class ExternalStoreTest extends MediaWikiTestCase {
+
+ /**
+ * @covers ExternalStore::fetchFromURL
+ */
+ public function testExternalFetchFromURL() {
+ $this->setMwGlobals( 'wgExternalStores', false );
+
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'FOO://cluster1/200' ),
+ 'Deny if wgExternalStores is not set to a non-empty array'
+ );
+
+ $this->setMwGlobals( 'wgExternalStores', array( 'FOO' ) );
+
+ $this->assertEquals(
+ ExternalStore::fetchFromURL( 'FOO://cluster1/200' ),
+ 'Hello',
+ 'Allow FOO://cluster1/200'
+ );
+ $this->assertEquals(
+ ExternalStore::fetchFromURL( 'FOO://cluster1/300/0' ),
+ 'Hello',
+ 'Allow FOO://cluster1/300/0'
+ );
+ # Assertions for r68900
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'ftp.example.org' ),
+ 'Deny domain ftp.example.org'
+ );
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( '/example.txt' ),
+ 'Deny path /example.txt'
+ );
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'http://' ),
+ 'Deny protocol http://'
+ );
+ }
+}
+
+class ExternalStoreFOO {
+
+ protected $data = array(
+ 'cluster1' => array(
+ '200' => 'Hello',
+ '300' => array(
+ 'Hello', 'World',
+ ),
+ ),
+ );
+
+ /**
+ * Fetch data from given URL
+ * @param string $url An url of the form FOO://cluster/id or FOO://cluster/id/itemid.
+ * @return mixed
+ */
+ function fetchFromURL( $url ) {
+ // Based on ExternalStoreDB
+ $path = explode( '/', $url );
+ $cluster = $path[2];
+ $id = $path[3];
+ if ( isset( $path[4] ) ) {
+ $itemID = $path[4];
+ } else {
+ $itemID = false;
+ }
+
+ if ( !isset( $this->data[$cluster][$id] ) ) {
+ return null;
+ }
+
+ if ( $itemID !== false
+ && is_array( $this->data[$cluster][$id] )
+ && isset( $this->data[$cluster][$id][$itemID] )
+ ) {
+ return $this->data[$cluster][$id][$itemID];
+ }
+
+ return $this->data[$cluster][$id];
+ }
+}
diff --git a/tests/phpunit/includes/ExtraParserTest.php b/tests/phpunit/includes/ExtraParserTest.php
new file mode 100644
index 00000000..4a4130e0
--- /dev/null
+++ b/tests/phpunit/includes/ExtraParserTest.php
@@ -0,0 +1,218 @@
+<?php
+
+/**
+ * Parser-related tests that don't suit for parserTests.txt
+ */
+class ExtraParserTest extends MediaWikiTestCase {
+
+ /** @var ParserOptions */
+ protected $options;
+ /** @var Parser */
+ protected $parser;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $contLang = Language::factory( 'en' );
+ $this->setMwGlobals( array(
+ 'wgShowDBErrorBacktrace' => true,
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => $contLang,
+ 'wgLang' => Language::factory( 'en' ),
+ 'wgMemc' => new EmptyBagOStuff,
+ 'wgAlwaysUseTidy' => false,
+ 'wgCleanSignatures' => true,
+ ) );
+
+ $this->options = ParserOptions::newFromUserAndLang( new User, $contLang );
+ $this->options->setTemplateCallback( array( __CLASS__, 'statelessFetchTemplate' ) );
+ $this->parser = new Parser;
+
+ MagicWord::clearCache();
+ }
+
+ /**
+ * @see Bug 8689
+ * @covers Parser::parse
+ */
+ public function testLongNumericLinesDontKillTheParser() {
+ $longLine = '1.' . str_repeat( '1234567890', 100000 ) . "\n";
+
+ $title = Title::newFromText( 'Unit test' );
+ $options = ParserOptions::newFromUser( new User() );
+ $this->assertEquals( "<p>$longLine</p>",
+ $this->parser->parse( $longLine, $title, $options )->getText() );
+ }
+
+ /**
+ * Test the parser entry points
+ * @covers Parser::parse
+ */
+ public 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()
+ );
+ }
+
+ /**
+ * @covers Parser::preSaveTransform
+ */
+ public function testPreSaveTransform() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->preSaveTransform(
+ "Test\r\n{{subst:Foo}}\n{{Bar}}",
+ $title,
+ new User(),
+ $this->options
+ );
+
+ $this->assertEquals( "Test\nContent of ''Template:Foo''\n{{Bar}}", $outputText );
+ }
+
+ /**
+ * @covers Parser::preprocess
+ */
+ public 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
+ * @covers Parser::cleanSig
+ */
+ public function testCleanSig() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->cleanSig( "{{Foo}} ~~~~" );
+
+ $this->assertEquals( "{{SUBST:Foo}} ", $outputText );
+ }
+
+ /**
+ * cleanSig() should do nothing if disabled
+ * @covers Parser::cleanSig
+ */
+ public function testCleanSigDisabled() {
+ $this->setMwGlobals( 'wgCleanSignatures', false );
+
+ $title = Title::newFromText( __FUNCTION__ );
+ $outputText = $this->parser->cleanSig( "{{Foo}} ~~~~" );
+
+ $this->assertEquals( "{{Foo}} ~~~~", $outputText );
+ }
+
+ /**
+ * cleanSigInSig() just removes tildes
+ * @dataProvider provideStringsForCleanSigInSig
+ * @covers Parser::cleanSigInSig
+ */
+ public function testCleanSigInSig( $in, $out ) {
+ $this->assertEquals( Parser::cleanSigInSig( $in ), $out );
+ }
+
+ public static function provideStringsForCleanSigInSig() {
+ return array(
+ array( "{{Foo}} ~~~~", "{{Foo}} " ),
+ array( "~~~", "" ),
+ array( "~~~~~", "" ),
+ );
+ }
+
+ /**
+ * @covers Parser::getSection
+ */
+ public function testGetSection() {
+ $outputText2 = $this->parser->getSection(
+ "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\n"
+ . "Section 2\n== Heading 3 ==\nSection 3\n",
+ 2
+ );
+ $outputText1 = $this->parser->getSection(
+ "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\n"
+ . "Section 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 );
+ }
+
+ /**
+ * @covers Parser::replaceSection
+ */
+ public function testReplaceSection() {
+ $outputText = $this->parser->replaceSection(
+ "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\n"
+ . "Section 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.
+ * @covers Parser::getPreloadText
+ */
+ public 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 );
+ }
+
+ /**
+ * @param Title $title
+ * @param bool $parser
+ *
+ * @return array
+ */
+ static function statelessFetchTemplate( $title, $parser = false ) {
+ $text = "Content of ''" . $title->getFullText() . "''";
+ $deps = array();
+
+ return array(
+ 'text' => $text,
+ 'finalTitle' => $title,
+ 'deps' => $deps );
+ }
+
+ /**
+ * @group Database
+ * @covers Parser::parse
+ */
+ public function testTrackingCategory() {
+ $title = Title::newFromText( __FUNCTION__ );
+ $catName = wfMessage( 'broken-file-category' )->inContentLanguage()->text();
+ $cat = Title::makeTitleSafe( NS_CATEGORY, $catName );
+ $expected = array( $cat->getDBkey() );
+ $parserOutput = $this->parser->parse( "[[file:nonexistent]]", $title, $this->options );
+ $result = $parserOutput->getCategoryLinks();
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * @group Database
+ * @covers Parser::parse
+ */
+ public function testTrackingCategorySpecial() {
+ // Special pages shouldn't have tracking cats.
+ $title = SpecialPage::getTitleFor( 'Contributions' );
+ $parserOutput = $this->parser->parse( "[[file:nonexistent]]", $title, $this->options );
+ $result = $parserOutput->getCategoryLinks();
+ $this->assertEmpty( $result );
+ }
+}
diff --git a/tests/phpunit/includes/FallbackTest.php b/tests/phpunit/includes/FallbackTest.php
new file mode 100644
index 00000000..c60170f3
--- /dev/null
+++ b/tests/phpunit/includes/FallbackTest.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @covers Fallback
+ */
+class FallbackTest extends MediaWikiTestCase {
+ public 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(
+ call_user_func_array( 'mb_substr', $param_set ),
+ call_user_func_array( '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(
+ call_user_func_array( 'mb_strpos', $param_set ),
+ call_user_func_array( 'Fallback::mb_strpos', $param_set ),
+ 'Fallback mb_strpos with params ' . implode( ', ', $old_param_set )
+ );
+
+ $this->assertEquals(
+ call_user_func_array( 'mb_strrpos', $param_set ),
+ call_user_func_array( 'Fallback::mb_strrpos', $param_set ),
+ 'Fallback mb_strrpos with params ' . implode( ', ', $old_param_set )
+ );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/FauxRequestTest.php b/tests/phpunit/includes/FauxRequestTest.php
new file mode 100644
index 00000000..745a5b42
--- /dev/null
+++ b/tests/phpunit/includes/FauxRequestTest.php
@@ -0,0 +1,18 @@
+<?php
+
+class FauxRequestTest extends MediaWikiTestCase {
+ /**
+ * @covers FauxRequest::setHeader
+ * @covers FauxRequest::getHeader
+ */
+ public function testGetSetHeader() {
+ $value = 'test/test';
+
+ $request = new FauxRequest();
+ $request->setHeader( 'Content-Type', $value );
+
+ $this->assertEquals( $request->getHeader( 'Content-Type' ), $value );
+ $this->assertEquals( $request->getHeader( 'CONTENT-TYPE' ), $value );
+ $this->assertEquals( $request->getHeader( 'content-type' ), $value );
+ }
+}
diff --git a/tests/phpunit/includes/FauxResponseTest.php b/tests/phpunit/includes/FauxResponseTest.php
new file mode 100644
index 00000000..4a974ba2
--- /dev/null
+++ b/tests/phpunit/includes/FauxResponseTest.php
@@ -0,0 +1,118 @@
+<?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 FauxResponse */
+ protected $response;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->response = new FauxResponse;
+ }
+
+ /**
+ * @covers FauxResponse::getcookie
+ * @covers FauxResponse::setcookie
+ */
+ public 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' );
+ }
+
+ /**
+ * @covers FauxResponse::getheader
+ * @covers FauxResponse::header
+ */
+ public 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'
+ );
+
+ $this->response->header( 'Location: http://localhost/' );
+ $this->assertEquals(
+ 'http://localhost/',
+ $this->response->getheader( 'LOCATION' ),
+ 'Get header case insensitive'
+ );
+ }
+
+ /**
+ * @covers FauxResponse::getStatusCode
+ */
+ public 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..1531b569
--- /dev/null
+++ b/tests/phpunit/includes/FormOptionsInitializationTest.php
@@ -0,0 +1,89 @@
+<?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, Antoine Musso
+ *
+ * @author Antoine Musso
+ */
+class FormOptionsInitializationTest extends MediaWikiTestCase {
+ /**
+ * @var FormOptions
+ */
+ protected $object;
+
+ /**
+ * A new fresh and empty FormOptions object to test initialization
+ * with.
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->object = new FormOptionsExposed();
+ }
+
+ /**
+ * @covers FormOptionsExposed::add
+ */
+ 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()
+ );
+ }
+
+ /**
+ * @covers FormOptionsExposed::add
+ */
+ 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..665fa390
--- /dev/null
+++ b/tests/phpunit/includes/FormOptionsTest.php
@@ -0,0 +1,103 @@
+<?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, Antoine Musso
+ *
+ * @author Antoine Musso
+ */
+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() {
+ parent::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( 'float', 0.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 assertGuessFloat( $data ) {
+ $this->guess( FormOptions::FLOAT, $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
+ * @covers FormOptions::guessType
+ */
+ public function testGuessTypeDetection() {
+ $this->assertGuessBoolean( true );
+ $this->assertGuessBoolean( false );
+
+ $this->assertGuessInt( 0 );
+ $this->assertGuessInt( -5 );
+ $this->assertGuessInt( 5 );
+ $this->assertGuessInt( 0x0F );
+
+ $this->assertGuessFloat( 0.0 );
+ $this->assertGuessFloat( 1.5 );
+ $this->assertGuessFloat( 1e3 );
+
+ $this->assertGuessString( 'true' );
+ $this->assertGuessString( 'false' );
+ $this->assertGuessString( '5' );
+ $this->assertGuessString( '0' );
+ $this->assertGuessString( '1.5' );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers FormOptions::guessType
+ */
+ public function testGuessTypeOnArrayThrowException() {
+ $this->object->guessType( array( 'foo' ) );
+ }
+ /**
+ * @expectedException MWException
+ * @covers FormOptions::guessType
+ */
+ public function testGuessTypeOnNullThrowException() {
+ $this->object->guessType( null );
+ }
+}
diff --git a/tests/phpunit/includes/GitInfoTest.php b/tests/phpunit/includes/GitInfoTest.php
new file mode 100644
index 00000000..e22f5050
--- /dev/null
+++ b/tests/phpunit/includes/GitInfoTest.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @covers GitInfo
+ */
+class GitInfoTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( 'wgGitInfoCacheDirectory', __DIR__ . '/../data/gitinfo' );
+ }
+
+ public function testValidJsonData() {
+ $dir = $GLOBALS['IP'] . '/testValidJsonData';
+ $fixture = new GitInfo( $dir );
+
+ $this->assertTrue( $fixture->cacheIsComplete() );
+ $this->assertEquals( 'refs/heads/master', $fixture->getHead() );
+ $this->assertEquals( '0123456789abcdef0123456789abcdef01234567',
+ $fixture->getHeadSHA1() );
+ $this->assertEquals( '1070884800', $fixture->getHeadCommitDate() );
+ $this->assertEquals( 'master', $fixture->getCurrentBranch() );
+ $this->assertContains( '0123456789abcdef0123456789abcdef01234567',
+ $fixture->getHeadViewUrl() );
+ }
+
+ public function testMissingJsonData() {
+ $dir = $GLOBALS['IP'] . '/testMissingJsonData';
+ $fixture = new GitInfo( $dir );
+
+ $this->assertFalse( $fixture->cacheIsComplete() );
+
+ $this->assertEquals( false, $fixture->getHead() );
+ $this->assertEquals( false, $fixture->getHeadSHA1() );
+ $this->assertEquals( false, $fixture->getHeadCommitDate() );
+ $this->assertEquals( false, $fixture->getCurrentBranch() );
+ $this->assertEquals( false, $fixture->getHeadViewUrl() );
+
+ // After calling all the outputs, the cache should be complete
+ $this->assertTrue( $fixture->cacheIsComplete() );
+ }
+
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php
new file mode 100644
index 00000000..3acc48e2
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php
@@ -0,0 +1,745 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ */
+class GlobalTest extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $readOnlyFile = tempnam( wfTempDir(), "mwtest_readonly" );
+ unlink( $readOnlyFile );
+
+ $this->setMwGlobals( array(
+ 'wgReadOnlyFile' => $readOnlyFile,
+ 'wgUrlProtocols' => array(
+ 'http://',
+ 'https://',
+ 'mailto:',
+ '//',
+ 'file://', # Non-default
+ ),
+ ) );
+ }
+
+ protected function tearDown() {
+ global $wgReadOnlyFile;
+
+ if ( file_exists( $wgReadOnlyFile ) ) {
+ unlink( $wgReadOnlyFile );
+ }
+
+ parent::tearDown();
+ }
+
+ /**
+ * @dataProvider provideForWfArrayDiff2
+ * @covers ::wfArrayDiff2
+ */
+ public function testWfArrayDiff2( $a, $b, $expected ) {
+ $this->assertEquals(
+ wfArrayDiff2( $a, $b ), $expected
+ );
+ }
+
+ // @todo Provide more tests
+ public static 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' ) ),
+ ),
+ );
+ }
+
+ /*
+ * Test cases for random functions could hypothetically fail,
+ * even though they shouldn't.
+ */
+
+ /**
+ * @covers ::wfRandom
+ */
+ public function testRandom() {
+ $this->assertFalse(
+ wfRandom() == wfRandom()
+ );
+ }
+
+ /**
+ * @covers ::wfRandomString
+ */
+ public function testRandomString() {
+ $this->assertFalse(
+ wfRandomString() == wfRandomString()
+ );
+ $this->assertEquals(
+ strlen( wfRandomString( 10 ) ), 10
+ );
+ $this->assertTrue(
+ preg_match( '/^[0-9a-f]+$/i', wfRandomString() ) === 1
+ );
+ }
+
+ /**
+ * @covers ::wfUrlencode
+ */
+ public function testUrlencode() {
+ $this->assertEquals(
+ "%E7%89%B9%E5%88%A5:Contributions/Foobar",
+ wfUrlencode( "\xE7\x89\xB9\xE5\x88\xA5:Contributions/Foobar" ) );
+ }
+
+ /**
+ * @covers ::wfExpandIRI
+ */
+ public function testExpandIRI() {
+ $this->assertEquals(
+ "https://te.wikibooks.org/wiki/ఉబుంటు_వాడుకరి_మార్గదర్శని",
+ wfExpandIRI( "https://te.wikibooks.org/wiki/"
+ . "%E0%B0%89%E0%B0%AC%E0%B1%81%E0%B0%82%E0%B0%9F%E0%B1%81_"
+ . "%E0%B0%B5%E0%B0%BE%E0%B0%A1%E0%B1%81%E0%B0%95%E0%B0%B0%E0%B0%BF_"
+ . "%E0%B0%AE%E0%B0%BE%E0%B0%B0%E0%B1%8D%E0%B0%97%E0%B0%A6%E0%B0%B0"
+ . "%E0%B1%8D%E0%B0%B6%E0%B0%A8%E0%B0%BF" ) );
+ }
+
+ /**
+ * @covers ::wfReadOnly
+ */
+ public function testReadOnlyEmpty() {
+ global $wgReadOnly;
+ $wgReadOnly = null;
+
+ $this->assertFalse( wfReadOnly() );
+ $this->assertFalse( wfReadOnly() );
+ }
+
+ /**
+ * @covers ::wfReadOnly
+ */
+ public 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() );
+ }
+
+ public static function provideArrayToCGI() {
+ return array(
+ array( array(), '' ), // empty
+ array( array( 'foo' => 'bar' ), 'foo=bar' ), // string test
+ array( array( 'foo' => '' ), 'foo=' ), // empty string test
+ array( array( 'foo' => 1 ), 'foo=1' ), // number test
+ array( array( 'foo' => true ), 'foo=1' ), // true test
+ array( array( 'foo' => false ), '' ), // false test
+ array( array( 'foo' => null ), '' ), // null test
+ array( array( 'foo' => 'A&B=5+6@!"\'' ), 'foo=A%26B%3D5%2B6%40%21%22%27' ), // urlencoding test
+ array(
+ array( 'foo' => 'bar', 'baz' => 'is', 'asdf' => 'qwerty' ),
+ 'foo=bar&baz=is&asdf=qwerty'
+ ), // multi-item test
+ array( array( 'foo' => array( 'bar' => 'baz' ) ), 'foo%5Bbar%5D=baz' ),
+ array(
+ array( 'foo' => array( 'bar' => 'baz', 'qwerty' => 'asdf' ) ),
+ 'foo%5Bbar%5D=baz&foo%5Bqwerty%5D=asdf'
+ ),
+ array( array( 'foo' => array( 'bar', 'baz' ) ), 'foo%5B0%5D=bar&foo%5B1%5D=baz' ),
+ array(
+ array( 'foo' => array( 'bar' => array( 'bar' => 'baz' ) ) ),
+ 'foo%5Bbar%5D%5Bbar%5D=baz'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideArrayToCGI
+ * @covers ::wfArrayToCgi
+ */
+ public function testArrayToCGI( $array, $result ) {
+ $this->assertEquals( $result, wfArrayToCgi( $array ) );
+ }
+
+ /**
+ * @covers ::wfArrayToCgi
+ */
+ public function testArrayToCGI2() {
+ $this->assertEquals(
+ "baz=bar&foo=bar",
+ wfArrayToCgi(
+ array( 'baz' => 'bar' ),
+ array( 'foo' => 'bar', 'baz' => 'overridden value' ) ) );
+ }
+
+ public static function provideCgiToArray() {
+ return array(
+ array( '', array() ), // empty
+ array( 'foo=bar', array( 'foo' => 'bar' ) ), // string
+ array( 'foo=', array( 'foo' => '' ) ), // empty string
+ array( 'foo', array( 'foo' => '' ) ), // missing =
+ array( 'foo=bar&qwerty=asdf', array( 'foo' => 'bar', 'qwerty' => 'asdf' ) ), // multiple value
+ array( 'foo=A%26B%3D5%2B6%40%21%22%27', array( 'foo' => 'A&B=5+6@!"\'' ) ), // urldecoding test
+ array( 'foo%5Bbar%5D=baz', array( 'foo' => array( 'bar' => 'baz' ) ) ),
+ array(
+ 'foo%5Bbar%5D=baz&foo%5Bqwerty%5D=asdf',
+ array( 'foo' => array( 'bar' => 'baz', 'qwerty' => 'asdf' ) )
+ ),
+ array( 'foo%5B0%5D=bar&foo%5B1%5D=baz', array( 'foo' => array( 0 => 'bar', 1 => 'baz' ) ) ),
+ array(
+ 'foo%5Bbar%5D%5Bbar%5D=baz',
+ array( 'foo' => array( 'bar' => array( 'bar' => 'baz' ) ) )
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCgiToArray
+ * @covers ::wfCgiToArray
+ */
+ public function testCgiToArray( $cgi, $result ) {
+ $this->assertEquals( $result, wfCgiToArray( $cgi ) );
+ }
+
+ public static function provideCgiRoundTrip() {
+ return array(
+ array( '' ),
+ array( 'foo=bar' ),
+ array( 'foo=' ),
+ array( 'foo=bar&baz=biz' ),
+ array( 'foo=A%26B%3D5%2B6%40%21%22%27' ),
+ array( 'foo%5Bbar%5D=baz' ),
+ array( 'foo%5B0%5D=bar&foo%5B1%5D=baz' ),
+ array( 'foo%5Bbar%5D%5Bbar%5D=baz' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCgiRoundTrip
+ * @covers ::wfArrayToCgi
+ */
+ public function testCgiRoundTrip( $cgi ) {
+ $this->assertEquals( $cgi, wfArrayToCgi( wfCgiToArray( $cgi ) ) );
+ }
+
+ /**
+ * @covers ::mimeTypeMatch
+ */
+ public 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 ) ) );
+ }
+
+ /**
+ * @covers ::wfNegotiateType
+ */
+ public 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 ) ) );
+ }
+
+ /**
+ * @covers ::wfDebug
+ * @covers ::wfDebugMem
+ */
+ public function testDebugFunctionTest() {
+
+ global $wgDebugLogFile, $wgDebugTimestamps;
+
+ $old_log_file = $wgDebugLogFile;
+ $wgDebugLogFile = tempnam( wfTempDir(), 'mw-' );
+ # @todo FIXME: $wgDebugTimestamps should be tested
+ $old_wgDebugTimestamps = $wgDebugTimestamps;
+ $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 );
+
+ wfDebugMem();
+ $this->assertGreaterThan(
+ 1000,
+ preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) )
+ );
+ unlink( $wgDebugLogFile );
+
+ wfDebugMem( true );
+ $this->assertGreaterThan(
+ 1000000,
+ preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) )
+ );
+ unlink( $wgDebugLogFile );
+
+ $wgDebugLogFile = $old_log_file;
+ $wgDebugTimestamps = $old_wgDebugTimestamps;
+ }
+
+ /**
+ * @covers ::wfClientAcceptsGzip
+ */
+ public 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;
+ }
+ }
+
+ /**
+ * @covers ::swap
+ */
+ public function testSwapVarsTest() {
+ $this->hideDeprecated( 'swap' );
+
+ $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' );
+ }
+
+ /**
+ * @covers ::wfPercent
+ */
+ public 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] );
+ }
+ }
+
+ /**
+ * test @see wfShorthandToInteger()
+ * @dataProvider provideShorthand
+ * @covers ::wfShorthandToInteger
+ */
+ public function testWfShorthandToInteger( $shorthand, $expected ) {
+ $this->assertEquals( $expected,
+ wfShorthandToInteger( $shorthand )
+ );
+ }
+
+ /** array( shorthand, expected integer ) */
+ public static 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 ),
+ );
+ }
+
+ /**
+ * @param string $old Text as it was in the database
+ * @param string $mine Text submitted while user was editing
+ * @param string $yours Text submitted by the user
+ * @param bool $expectedMergeResult Whether the merge should be a success
+ * @param string $expectedText Text after merge has been completed
+ *
+ * @dataProvider provideMerge()
+ * @group medium
+ * @covers ::wfMerge
+ */
+ public function testMerge( $old, $mine, $yours, $expectedMergeResult, $expectedText ) {
+ $this->checkHasDiff3();
+
+ $mergedText = null;
+ $isMerged = wfMerge( $old, $mine, $yours, $mergedText );
+
+ $msg = 'Merge should be a ';
+ $msg .= $expectedMergeResult ? 'success' : 'failure';
+ $this->assertEquals( $expectedMergeResult, $isMerged, $msg );
+
+ if ( $isMerged ) {
+ // Verify the merged text
+ $this->assertEquals( $expectedText, $mergedText,
+ 'is merged text as expected?' );
+ }
+ }
+
+ public static function provideMerge() {
+ $EXPECT_MERGE_SUCCESS = true;
+ $EXPECT_MERGE_FAILURE = false;
+
+ return array(
+ // #0: clean merge
+ array(
+ // old:
+ "one one one\n" . // trimmed
+ "\n" .
+ "two two two",
+
+ // mine:
+ "one one one ONE ONE\n" .
+ "\n" .
+ "two two two\n", // with tailing whitespace
+
+ // yours:
+ "one one one\n" .
+ "\n" .
+ "two two TWO TWO", // trimmed
+
+ // ok:
+ $EXPECT_MERGE_SUCCESS,
+
+ // result:
+ "one one one ONE ONE\n" .
+ "\n" .
+ "two two TWO TWO\n", // note: will always end in a newline
+ ),
+
+ // #1: conflict, fail
+ array(
+ // old:
+ "one one one", // trimmed
+
+ // mine:
+ "one one one ONE ONE\n" .
+ "\n" .
+ "bla bla\n" .
+ "\n", // with tailing whitespace
+
+ // yours:
+ "one one one\n" .
+ "\n" .
+ "two two", // trimmed
+
+ $EXPECT_MERGE_FAILURE,
+
+ // result:
+ null,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideMakeUrlIndexes()
+ * @covers ::wfMakeUrlIndexes
+ */
+ public function testMakeUrlIndexes( $url, $expected ) {
+ $index = wfMakeUrlIndexes( $url );
+ $this->assertEquals( $expected, $index, "wfMakeUrlIndexes(\"$url\")" );
+ }
+
+ public static 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'
+ )
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideWfMatchesDomainList
+ * @covers ::wfMatchesDomainList
+ */
+ public function testWfMatchesDomainList( $url, $domains, $expected, $description ) {
+ $actual = wfMatchesDomainList( $url, $domains );
+ $this->assertEquals( $expected, $actual, $description );
+ }
+
+ public static function provideWfMatchesDomainList() {
+ $a = array();
+ $protocols = array( 'HTTP' => 'http:', 'HTTPS' => 'https:', 'protocol-relative' => '' );
+ foreach ( $protocols as $pDesc => $p ) {
+ $a = array_merge( $a, array(
+ array(
+ "$p//www.example.com",
+ array(),
+ false,
+ "No matches for empty domains array, $pDesc URL"
+ ),
+ array(
+ "$p//www.example.com",
+ array( 'www.example.com' ),
+ true,
+ "Exact match in domains array, $pDesc URL"
+ ),
+ array(
+ "$p//www.example.com",
+ array( 'example.com' ),
+ true,
+ "Match without subdomain in domains array, $pDesc URL"
+ ),
+ array(
+ "$p//www.example2.com",
+ array( 'www.example.com', 'www.example2.com', 'www.example3.com' ),
+ true,
+ "Exact match with other domains in array, $pDesc URL"
+ ),
+ array(
+ "$p//www.example2.com",
+ array( 'example.com', 'example2.com', 'example3,com' ),
+ true,
+ "Match without subdomain with other domains in array, $pDesc URL"
+ ),
+ array(
+ "$p//www.example4.com",
+ array( 'example.com', 'example2.com', 'example3,com' ),
+ false,
+ "Domain not in array, $pDesc URL"
+ ),
+ array(
+ "$p//nds-nl.wikipedia.org",
+ array( 'nl.wikipedia.org' ),
+ false,
+ "Non-matching substring of domain, $pDesc URL"
+ ),
+ ) );
+ }
+
+ return $a;
+ }
+
+ /**
+ * @covers ::wfMkdirParents
+ */
+ public function testWfMkdirParents() {
+ // Should not return true if file exists instead of directory
+ $fname = $this->getNewTempFile();
+ wfSuppressWarnings();
+ $ok = wfMkdirParents( $fname );
+ wfRestoreWarnings();
+ $this->assertFalse( $ok );
+ }
+
+ /**
+ * @dataProvider provideWfShellMaintenanceCmdList
+ * @covers ::wfShellMaintenanceCmd
+ */
+ public function testWfShellMaintenanceCmd( $script, $parameters, $options,
+ $expected, $description
+ ) {
+ if ( wfIsWindows() ) {
+ // Approximation that's good enough for our purposes just now
+ $expected = str_replace( "'", '"', $expected );
+ }
+ $actual = wfShellMaintenanceCmd( $script, $parameters, $options );
+ $this->assertEquals( $expected, $actual, $description );
+ }
+
+ public static function provideWfShellMaintenanceCmdList() {
+ global $wgPhpCli;
+
+ return array(
+ array( 'eval.php', array( '--help', '--test' ), array(),
+ "'$wgPhpCli' 'eval.php' '--help' '--test'",
+ "Called eval.php --help --test" ),
+ array( 'eval.php', array( '--help', '--test space' ), array( 'php' => 'php5' ),
+ "'php5' 'eval.php' '--help' '--test space'",
+ "Called eval.php --help --test with php option" ),
+ array( 'eval.php', array( '--help', '--test', 'X' ), array( 'wrapper' => 'MWScript.php' ),
+ "'$wgPhpCli' 'MWScript.php' 'eval.php' '--help' '--test' 'X'",
+ "Called eval.php --help --test with wrapper option" ),
+ array(
+ 'eval.php',
+ array( '--help', '--test', 'y' ),
+ array( 'php' => 'php5', 'wrapper' => 'MWScript.php' ),
+ "'php5' 'MWScript.php' 'eval.php' '--help' '--test' 'y'",
+ "Called eval.php --help --test with wrapper and php option"
+ ),
+ );
+ }
+ /* @todo many more! */
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php
new file mode 100644
index 00000000..9588ffdc
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @group Database
+ */
+class GlobalWithDBTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideWfIsBadImageList
+ * @covers ::wfIsBadImage
+ */
+ public function testWfIsBadImage( $name, $title, $blacklist, $expected, $desc ) {
+ $this->assertEquals( $expected, wfIsBadImage( $name, $title, $blacklist ), $desc );
+ }
+
+ public static function provideWfIsBadImageList() {
+ $blacklist = '* [[File:Bad.jpg]] except [[Nasty page]]';
+
+ return array(
+ array( 'Bad.jpg', false, $blacklist, true,
+ 'Called on a bad image' ),
+ array( 'Bad.jpg', Title::makeTitle( NS_MAIN, 'A page' ), $blacklist, true,
+ 'Called on a bad image' ),
+ array( 'NotBad.jpg', false, $blacklist, false,
+ 'Called on a non-bad image' ),
+ array( 'Bad.jpg', Title::makeTitle( NS_MAIN, 'Nasty page' ), $blacklist, false,
+ 'Called on a bad image but is on a whitelisted page' ),
+ array( 'File:Bad.jpg', false, $blacklist, false,
+ 'Called on a bad image with File:' ),
+ );
+ }
+}
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/wfAssembleUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php
new file mode 100644
index 00000000..13f49f79
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php
@@ -0,0 +1,112 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfAssembleUrl
+ */
+class WfAssembleUrlTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideURLParts
+ */
+ public function testWfAssembleUrl( $parts, $output ) {
+ $partsDump = print_r( $parts, true );
+ $this->assertEquals(
+ $output,
+ wfAssembleUrl( $parts ),
+ "Testing $partsDump assembles to $output"
+ );
+ }
+
+ /**
+ * Provider of URL parts for testing wfAssembleUrl()
+ *
+ * @return array
+ */
+ public static function provideURLParts() {
+ $schemes = array(
+ '' => array(),
+ '//' => array(
+ 'delimiter' => '//',
+ ),
+ 'http://' => array(
+ 'scheme' => 'http',
+ 'delimiter' => '://',
+ ),
+ );
+
+ $hosts = array(
+ '' => array(),
+ 'example.com' => array(
+ 'host' => 'example.com',
+ ),
+ 'example.com:123' => array(
+ 'host' => 'example.com',
+ 'port' => 123,
+ ),
+ 'id@example.com' => array(
+ 'user' => 'id',
+ 'host' => 'example.com',
+ ),
+ 'id@example.com:123' => array(
+ 'user' => 'id',
+ 'host' => 'example.com',
+ 'port' => 123,
+ ),
+ 'id:key@example.com' => array(
+ 'user' => 'id',
+ 'pass' => 'key',
+ 'host' => 'example.com',
+ ),
+ 'id:key@example.com:123' => array(
+ 'user' => 'id',
+ 'pass' => 'key',
+ 'host' => 'example.com',
+ 'port' => 123,
+ ),
+ );
+
+ $cases = array();
+ foreach ( $schemes as $scheme => $schemeParts ) {
+ foreach ( $hosts as $host => $hostParts ) {
+ foreach ( array( '', '/path' ) as $path ) {
+ foreach ( array( '', 'query' ) as $query ) {
+ foreach ( array( '', 'fragment' ) as $fragment ) {
+ $parts = array_merge(
+ $schemeParts,
+ $hostParts
+ );
+ $url = $scheme .
+ $host .
+ $path;
+
+ if ( $path ) {
+ $parts['path'] = $path;
+ }
+ if ( $query ) {
+ $parts['query'] = $query;
+ $url .= '?' . $query;
+ }
+ if ( $fragment ) {
+ $parts['fragment'] = $fragment;
+ $url .= '#' . $fragment;
+ }
+
+ $cases[] = array(
+ $parts,
+ $url,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ $complexURL = 'http://id:key@example.org:321' .
+ '/over/there?name=ferret&foo=bar#nose';
+ $cases[] = array(
+ wfParseUrl( $complexURL ),
+ $complexURL,
+ );
+
+ return $cases;
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php b/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php
new file mode 100644
index 00000000..166d641f
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfBCP47
+ */
+class WfBCP47Test extends MediaWikiTestCase {
+ /**
+ * 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()
+ */
+ public 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)
+ */
+ public static 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
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php b/tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php
new file mode 100644
index 00000000..9d55e85c
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php
@@ -0,0 +1,195 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfBaseConvert
+ */
+class WfBaseConvertTest extends MediaWikiTestCase {
+ public static function provideSingleDigitConversions() {
+ return array(
+ // 2 3 5 8 10 16 36
+ array( '0', '0', '0', '0', '0', '0', '0' ),
+ array( '1', '1', '1', '1', '1', '1', '1' ),
+ array( '10', '2', '2', '2', '2', '2', '2' ),
+ array( '11', '10', '3', '3', '3', '3', '3' ),
+ array( '100', '11', '4', '4', '4', '4', '4' ),
+ array( '101', '12', '10', '5', '5', '5', '5' ),
+ array( '110', '20', '11', '6', '6', '6', '6' ),
+ array( '111', '21', '12', '7', '7', '7', '7' ),
+ array( '1000', '22', '13', '10', '8', '8', '8' ),
+ array( '1001', '100', '14', '11', '9', '9', '9' ),
+ array( '1010', '101', '20', '12', '10', 'a', 'a' ),
+ array( '1011', '102', '21', '13', '11', 'b', 'b' ),
+ array( '1100', '110', '22', '14', '12', 'c', 'c' ),
+ array( '1101', '111', '23', '15', '13', 'd', 'd' ),
+ array( '1110', '112', '24', '16', '14', 'e', 'e' ),
+ array( '1111', '120', '30', '17', '15', 'f', 'f' ),
+ array( '10000', '121', '31', '20', '16', '10', 'g' ),
+ array( '10001', '122', '32', '21', '17', '11', 'h' ),
+ array( '10010', '200', '33', '22', '18', '12', 'i' ),
+ array( '10011', '201', '34', '23', '19', '13', 'j' ),
+ array( '10100', '202', '40', '24', '20', '14', 'k' ),
+ array( '10101', '210', '41', '25', '21', '15', 'l' ),
+ array( '10110', '211', '42', '26', '22', '16', 'm' ),
+ array( '10111', '212', '43', '27', '23', '17', 'n' ),
+ array( '11000', '220', '44', '30', '24', '18', 'o' ),
+ array( '11001', '221', '100', '31', '25', '19', 'p' ),
+ array( '11010', '222', '101', '32', '26', '1a', 'q' ),
+ array( '11011', '1000', '102', '33', '27', '1b', 'r' ),
+ array( '11100', '1001', '103', '34', '28', '1c', 's' ),
+ array( '11101', '1002', '104', '35', '29', '1d', 't' ),
+ array( '11110', '1010', '110', '36', '30', '1e', 'u' ),
+ array( '11111', '1011', '111', '37', '31', '1f', 'v' ),
+ array( '100000', '1012', '112', '40', '32', '20', 'w' ),
+ array( '100001', '1020', '113', '41', '33', '21', 'x' ),
+ array( '100010', '1021', '114', '42', '34', '22', 'y' ),
+ array( '100011', '1022', '120', '43', '35', '23', 'z' )
+ );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase2( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base2, wfBaseConvert( $base3, '3', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base5, '5', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base8, '8', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base10, '10', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base16, '16', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base36, '36', '2' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase3( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base3, wfBaseConvert( $base2, '2', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base5, '5', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base8, '8', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base10, '10', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base16, '16', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base36, '36', '3' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase5( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base5, wfBaseConvert( $base2, '2', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base3, '3', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base8, '8', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base10, '10', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base16, '16', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base36, '36', '5' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase8( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base8, wfBaseConvert( $base2, '2', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base3, '3', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base5, '5', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base10, '10', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base16, '16', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base36, '36', '8' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase10( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base10, wfBaseConvert( $base2, '2', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base3, '3', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base5, '5', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base8, '8', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base16, '16', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base36, '36', '10' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase16( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base16, wfBaseConvert( $base2, '2', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base3, '3', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base5, '5', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base8, '8', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base10, '10', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base36, '36', '16' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase36( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base36, wfBaseConvert( $base2, '2', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base3, '3', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base5, '5', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base8, '8', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base10, '10', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base16, '16', '36' ) );
+ }
+
+ public function testLargeNumber() {
+ $this->assertSame( '1100110001111010000000101110100', wfBaseConvert( 'sd89ys', 36, 2 ) );
+ $this->assertSame( '11102112120221201101', wfBaseConvert( 'sd89ys', 36, 3 ) );
+ $this->assertSame( '12003102232400', wfBaseConvert( 'sd89ys', 36, 5 ) );
+ $this->assertSame( '14617200564', wfBaseConvert( 'sd89ys', 36, 8 ) );
+ $this->assertSame( '1715274100', wfBaseConvert( 'sd89ys', 36, 10 ) );
+ $this->assertSame( '663d0174', wfBaseConvert( 'sd89ys', 36, 16 ) );
+ }
+
+ public static function provideNumbers() {
+ $x = array();
+ $chars = '0123456789abcdefghijklmnopqrstuvwxyz';
+ for ( $i = 0; $i < 50; $i++ ) {
+ $base = mt_rand( 2, 36 );
+ $len = mt_rand( 10, 100 );
+
+ $str = '';
+ for ( $j = 0; $j < $len; $j++ ) {
+ $str .= $chars[mt_rand( 0, $base - 1 )];
+ }
+
+ $x[] = array( $base, $str );
+ }
+
+ return $x;
+ }
+
+ /**
+ * @dataProvider provideNumbers
+ */
+ public function testIdentity( $base, $number ) {
+ $this->assertSame( $number, wfBaseConvert( $number, $base, $base, strlen( $number ) ) );
+ }
+
+ public function testInvalid() {
+ $this->assertFalse( wfBaseConvert( '101', 1, 15 ) );
+ $this->assertFalse( wfBaseConvert( '101', 15, 1 ) );
+ $this->assertFalse( wfBaseConvert( '101', 37, 15 ) );
+ $this->assertFalse( wfBaseConvert( '101', 15, 37 ) );
+ $this->assertFalse( wfBaseConvert( 'abcde', 10, 11 ) );
+ $this->assertFalse( wfBaseConvert( '12930', 2, 10 ) );
+ $this->assertFalse( wfBaseConvert( '101', 'abc', 15 ) );
+ $this->assertFalse( wfBaseConvert( '101', 15, 'abc' ) );
+ }
+
+ public function testPadding() {
+ $number = "10101010101";
+ $this->assertSame(
+ strlen( $number ) + 5,
+ strlen( wfBaseConvert( $number, 2, 2, strlen( $number ) + 5 ) )
+ );
+ $this->assertSame(
+ strlen( $number ),
+ strlen( wfBaseConvert( $number, 2, 2, strlen( $number ) - 5 ) )
+ );
+ }
+
+ public function testLeadingZero() {
+ $this->assertSame( '24', wfBaseConvert( '010', 36, 16 ) );
+ $this->assertSame( '37d4', wfBaseConvert( '0b10', 36, 16 ) );
+ $this->assertSame( 'a734', wfBaseConvert( '0x10', 36, 16 ) );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php b/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php
new file mode 100644
index 00000000..705730a7
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfBaseName
+ */
+class WfBaseNameTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider providePaths
+ */
+ public function testBaseName( $fullpath, $basename ) {
+ $this->assertEquals( $basename, wfBaseName( $fullpath ),
+ "wfBaseName('$fullpath') => '$basename'" );
+ }
+
+ public static function providePaths() {
+ return array(
+ array( '', '' ),
+ array( '/', '' ),
+ array( '\\', '' ),
+ array( '//', '' ),
+ array( '\\\\', '' ),
+ array( 'a', 'a' ),
+ array( 'aaaa', 'aaaa' ),
+ array( '/a', 'a' ),
+ array( '\\a', 'a' ),
+ array( '/aaaa', 'aaaa' ),
+ array( '\\aaaa', 'aaaa' ),
+ array( '/aaaa/', 'aaaa' ),
+ array( '\\aaaa\\', 'aaaa' ),
+ array( '\\aaaa\\', 'aaaa' ),
+ array(
+ '/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'
+ ),
+ array( 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE', 'VIEWER.EXE' ),
+ array( 'Östergötland_coat_of_arms.png', 'Östergötland_coat_of_arms.png' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php
new file mode 100644
index 00000000..a69defb3
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php
@@ -0,0 +1,117 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfExpandUrl
+ */
+class WfExpandUrlTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideExpandableUrls
+ */
+ public function testWfExpandUrl( $fullUrl, $shortUrl, $defaultProto,
+ $server, $canServer, $httpsMode, $message
+ ) {
+ // Fake $wgServer, $wgCanonicalServer and $wgRequest->getProtocol()
+ $this->setMwGlobals( array(
+ 'wgServer' => $server,
+ 'wgCanonicalServer' => $canServer,
+ 'wgRequest' => new FauxRequest( array(), false, null, $httpsMode ? 'https' : 'http' )
+ ) );
+
+ $this->assertEquals( $fullUrl, wfExpandUrl( $shortUrl, $defaultProto ), $message );
+ }
+
+ /**
+ * Provider of URL examples for testing wfExpandUrl()
+ *
+ * @return array
+ */
+ public static 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
+ if ( $protoDesc == 'protocol-relative' ) {
+ $p = '';
+ } elseif ( $protoDesc == 'current' ) {
+ $p = "$mode:";
+ } elseif ( $protoDesc == 'canonical' ) {
+ $p = "$canServerMode:";
+ } else {
+ $p = $protoDesc . ':';
+ }
+ // Determine expected server name
+ if ( $protoDesc == 'canonical' ) {
+ $srv = $canServer;
+ } elseif ( $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/wfGetCallerTest.php b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php
new file mode 100644
index 00000000..bb2b33fe
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfGetCaller
+ */
+class WfGetCallerTest extends MediaWikiTestCase {
+ public function testZero() {
+ $this->assertEquals( __METHOD__, wfGetCaller( 1 ) );
+ }
+
+ function callerOne() {
+ return wfGetCaller();
+ }
+
+ public function testOne() {
+ $this->assertEquals( 'WfGetCallerTest::testOne', self::callerOne() );
+ }
+
+ function intermediateFunction( $level = 2, $n = 0 ) {
+ if ( $n > 0 ) {
+ return self::intermediateFunction( $level, $n - 1 );
+ }
+
+ return wfGetCaller( $level );
+ }
+
+ public function testTwo() {
+ $this->assertEquals( 'WfGetCallerTest::testTwo', self::intermediateFunction() );
+ }
+
+ public function testN() {
+ $this->assertEquals( 'WfGetCallerTest::testN', self::intermediateFunction( 2, 0 ) );
+ $this->assertEquals(
+ 'WfGetCallerTest::intermediateFunction',
+ self::intermediateFunction( 1, 0 )
+ );
+
+ for ( $i = 0; $i < 10; $i++ ) {
+ $this->assertEquals(
+ 'WfGetCallerTest::intermediateFunction',
+ self::intermediateFunction( $i + 1, $i )
+ );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php
new file mode 100644
index 00000000..232fa922
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Copyright © 2013 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
+ */
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfParseUrl
+ */
+class WfParseUrlTest extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgUrlProtocols', array(
+ '//',
+ 'http://',
+ 'https://',
+ 'file://',
+ 'mailto:',
+ ) );
+ }
+
+ /**
+ * @dataProvider provideURLs
+ */
+ public function testWfParseUrl( $url, $parts ) {
+ $this->assertEquals(
+ $parts,
+ wfParseUrl( $url )
+ );
+ }
+
+ /**
+ * Provider of URLs for testing wfParseUrl()
+ *
+ * @return array
+ */
+ public static function provideURLs() {
+ return array(
+ array(
+ '//example.org',
+ array(
+ 'scheme' => '',
+ 'delimiter' => '//',
+ 'host' => 'example.org',
+ )
+ ),
+ array(
+ 'http://example.org',
+ array(
+ 'scheme' => 'http',
+ 'delimiter' => '://',
+ 'host' => 'example.org',
+ )
+ ),
+ array(
+ 'https://example.org',
+ array(
+ 'scheme' => 'https',
+ 'delimiter' => '://',
+ 'host' => 'example.org',
+ )
+ ),
+ array(
+ 'http://id:key@example.org:123/path?foo=bar#baz',
+ array(
+ 'scheme' => 'http',
+ 'delimiter' => '://',
+ 'user' => 'id',
+ 'pass' => 'key',
+ 'host' => 'example.org',
+ 'port' => 123,
+ 'path' => '/path',
+ 'query' => 'foo=bar',
+ 'fragment' => 'baz',
+ )
+ ),
+ array(
+ 'file://example.org/etc/php.ini',
+ array(
+ 'scheme' => 'file',
+ 'delimiter' => '://',
+ 'host' => 'example.org',
+ 'path' => '/etc/php.ini',
+ )
+ ),
+ array(
+ 'file:///etc/php.ini',
+ array(
+ 'scheme' => 'file',
+ 'delimiter' => '://',
+ 'host' => '',
+ 'path' => '/etc/php.ini',
+ )
+ ),
+ array(
+ 'file:///c:/',
+ array(
+ 'scheme' => 'file',
+ 'delimiter' => '://',
+ 'host' => '',
+ 'path' => '/c:/',
+ )
+ ),
+ array(
+ 'mailto:id@example.org',
+ array(
+ 'scheme' => 'mailto',
+ 'delimiter' => ':',
+ 'host' => 'id@example.org',
+ 'path' => '',
+ )
+ ),
+ array(
+ 'mailto:id@example.org?subject=Foo',
+ array(
+ 'scheme' => 'mailto',
+ 'delimiter' => ':',
+ 'host' => 'id@example.org',
+ 'path' => '',
+ 'query' => 'subject=Foo',
+ )
+ ),
+ array(
+ 'mailto:?subject=Foo',
+ array(
+ 'scheme' => 'mailto',
+ 'delimiter' => ':',
+ 'host' => '',
+ 'path' => '',
+ 'query' => 'subject=Foo',
+ )
+ ),
+ array(
+ 'invalid://test/',
+ false
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php b/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php
new file mode 100644
index 00000000..1faad52a
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfRemoveDotSegments
+ */
+class WfRemoveDotSegmentsTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider providePaths
+ */
+ public function testWfRemoveDotSegments( $inputPath, $outputPath ) {
+ $this->assertEquals(
+ $outputPath,
+ wfRemoveDotSegments( $inputPath ),
+ "Testing $inputPath expands to $outputPath"
+ );
+ }
+
+ /**
+ * Provider of URL paths for testing wfRemoveDotSegments()
+ *
+ * @return array
+ */
+ public static function providePaths() {
+ return array(
+ array( '/a/b/c/./../../g', '/a/g' ),
+ array( 'mid/content=5/../6', 'mid/6' ),
+ array( '/a//../b', '/a/b' ),
+ array( '/.../a', '/.../a' ),
+ array( '.../a', '.../a' ),
+ array( '', '' ),
+ array( '/', '/' ),
+ array( '//', '//' ),
+ array( '.', '' ),
+ array( '..', '' ),
+ array( '...', '...' ),
+ array( '/.', '/' ),
+ array( '/..', '/' ),
+ array( './', '' ),
+ array( '../', '' ),
+ array( './a', 'a' ),
+ array( '../a', 'a' ),
+ array( '../../a', 'a' ),
+ array( '.././a', 'a' ),
+ array( './../a', 'a' ),
+ array( '././a', 'a' ),
+ array( '../../', '' ),
+ array( '.././', '' ),
+ array( './../', '' ),
+ array( '././', '' ),
+ array( '../..', '' ),
+ array( '../.', '' ),
+ array( './..', '' ),
+ array( './.', '' ),
+ array( '/../../a', '/a' ),
+ array( '/.././a', '/a' ),
+ array( '/./../a', '/a' ),
+ array( '/././a', '/a' ),
+ array( '/../../', '/' ),
+ array( '/.././', '/' ),
+ array( '/./../', '/' ),
+ array( '/././', '/' ),
+ array( '/../..', '/' ),
+ array( '/../.', '/' ),
+ array( '/./..', '/' ),
+ array( '/./.', '/' ),
+ array( 'b/../../a', '/a' ),
+ array( 'b/.././a', '/a' ),
+ array( 'b/./../a', '/a' ),
+ array( 'b/././a', 'b/a' ),
+ array( 'b/../../', '/' ),
+ array( 'b/.././', '/' ),
+ array( 'b/./../', '/' ),
+ array( 'b/././', 'b/' ),
+ array( 'b/../..', '/' ),
+ array( 'b/../.', '/' ),
+ array( 'b/./..', '/' ),
+ array( 'b/./.', 'b/' ),
+ array( '/b/../../a', '/a' ),
+ array( '/b/.././a', '/a' ),
+ array( '/b/./../a', '/a' ),
+ array( '/b/././a', '/b/a' ),
+ array( '/b/../../', '/' ),
+ array( '/b/.././', '/' ),
+ array( '/b/./../', '/' ),
+ array( '/b/././', '/b/' ),
+ array( '/b/../..', '/' ),
+ array( '/b/../.', '/' ),
+ array( '/b/./..', '/' ),
+ array( '/b/./.', '/b/' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfShellExecTest.php b/tests/phpunit/includes/GlobalFunctions/wfShellExecTest.php
new file mode 100644
index 00000000..fcd26f54
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfShellExecTest.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfShellExec
+ */
+class WfShellExecTest extends MediaWikiTestCase {
+ public function testBug67870() {
+ $command = wfIsWindows()
+ // 333 = 331 + CRLF
+ ? ( 'for /l %i in (1, 1, 1001) do @echo ' . str_repeat( '*', 331 ) )
+ : 'printf "%-333333s" "*"';
+
+ // Test several times because it involves a race condition that may randomly succeed or fail
+ for ( $i = 0; $i < 10; $i++ ) {
+ $output = wfShellExec( $command );
+ $this->assertEquals( 333333, strlen( $output ) );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php b/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php
new file mode 100644
index 00000000..67284d27
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfShorthandToInteger
+ */
+class WfShorthandToIntegerTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideABunchOfShorthands
+ */
+ public function testWfShorthandToInteger( $input, $output, $description ) {
+ $this->assertEquals(
+ wfShorthandToInteger( $input ),
+ $output,
+ $description
+ );
+ }
+
+ public static function provideABunchOfShorthands() {
+ return array(
+ array( '', -1, 'Empty string' ),
+ array( ' ', -1, 'String of spaces' ),
+ array( '1G', 1024 * 1024 * 1024, 'One gig uppercased' ),
+ array( '1g', 1024 * 1024 * 1024, 'One gig lowercased' ),
+ array( '1M', 1024 * 1024, 'One meg uppercased' ),
+ array( '1m', 1024 * 1024, 'One meg lowercased' ),
+ array( '1K', 1024, 'One kb uppercased' ),
+ array( '1k', 1024, 'One kb lowercased' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php b/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php
new file mode 100644
index 00000000..bea496c4
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php
@@ -0,0 +1,196 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfTimestamp
+ */
+class WfTimestampTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideNormalTimestamps
+ */
+ public function testNormalTimestamps( $input, $format, $output, $desc ) {
+ $this->assertEquals( $output, wfTimestamp( $format, $input ), $desc );
+ }
+
+ public static function provideNormalTimestamps() {
+ $t = gmmktime( 12, 34, 56, 1, 15, 2001 );
+
+ return array(
+ // TS_UNIX
+ array( $t, TS_MW, '20010115123456', 'TS_UNIX to TS_MW' ),
+ array( -30281104, TS_MW, '19690115123456', 'Negative TS_UNIX to TS_MW' ),
+ array( $t, TS_UNIX, 979562096, 'TS_UNIX to TS_UNIX' ),
+ array( $t, TS_DB, '2001-01-15 12:34:56', 'TS_UNIX to TS_DB' ),
+
+ array( $t, TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_ISO_8601_BASIC to TS_DB' ),
+
+ // TS_MW
+ array( '20010115123456', TS_MW, '20010115123456', 'TS_MW to TS_MW' ),
+ array( '20010115123456', TS_UNIX, 979562096, 'TS_MW to TS_UNIX' ),
+ array( '20010115123456', TS_DB, '2001-01-15 12:34:56', 'TS_MW to TS_DB' ),
+ array( '20010115123456', TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_MW to TS_ISO_8601_BASIC' ),
+
+ // TS_DB
+ array( '2001-01-15 12:34:56', TS_MW, '20010115123456', 'TS_DB to TS_MW' ),
+ array( '2001-01-15 12:34:56', TS_UNIX, 979562096, 'TS_DB to TS_UNIX' ),
+ array( '2001-01-15 12:34:56', TS_DB, '2001-01-15 12:34:56', 'TS_DB to TS_DB' ),
+ array(
+ '2001-01-15 12:34:56',
+ TS_ISO_8601_BASIC,
+ '20010115T123456Z',
+ 'TS_DB to TS_ISO_8601_BASIC'
+ ),
+
+ # rfc2822 section 3.3
+ array( '20010115123456', TS_RFC2822, 'Mon, 15 Jan 2001 12:34:56 GMT', 'TS_MW to TS_RFC2822' ),
+ array( 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ),
+ array(
+ ' Mon, 15 Jan 2001 12:34:56 GMT',
+ TS_MW,
+ '20010115123456',
+ 'TS_RFC2822 with leading space to TS_MW'
+ ),
+ array(
+ '15 Jan 2001 12:34:56 GMT',
+ TS_MW,
+ '20010115123456',
+ '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
+ array( 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ),
+
+ # WSP = SP / HTAB ; rfc2234
+ array(
+ "Mon, 15 Jan\x092001 12:34:56 GMT",
+ TS_MW,
+ '20010115123456',
+ 'TS_RFC2822 with HTAB to TS_MW'
+ ),
+ array(
+ "Mon, 15 Jan\x09 \x09 2001 12:34:56 GMT",
+ TS_MW,
+ '20010115123456',
+ 'TS_RFC2822 with HTAB and SP to TS_MW'
+ ),
+ array(
+ 'Sun, 6 Nov 94 08:49:37 GMT',
+ TS_MW,
+ '19941106084937',
+ '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
+ * @dataProvider provideOldTimestamps
+ */
+ public function testOldTimestamps( $input, $outputType, $output, $message ) {
+ $timestamp = wfTimestamp( $outputType, $input );
+ if ( substr( $output, 0, 1 ) === '/' ) {
+ // Bug 64946: Day of the week calculations for very old
+ // timestamps varies from system to system.
+ $this->assertRegExp( $output, $timestamp, $message );
+ } else {
+ $this->assertEquals( $output, $timestamp, $message );
+ }
+ }
+
+ public static function provideOldTimestamps() {
+ return array(
+ array(
+ '19011213204554',
+ TS_RFC2822,
+ 'Fri, 13 Dec 1901 20:45:54 GMT',
+ 'Earliest time according to PHP documentation'
+ ),
+ array( '20380119031407', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:07 GMT', 'Latest 32 bit time' ),
+ array( '19011213204552', TS_UNIX, '-2147483648', 'Earliest 32 bit unix time' ),
+ array( '20380119031407', TS_UNIX, '2147483647', 'Latest 32 bit unix time' ),
+ array( '19011213204552', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:52 GMT', 'Earliest 32 bit time' ),
+ array(
+ '19011213204551',
+ TS_RFC2822,
+ 'Fri, 13 Dec 1901 20:45:51 GMT', 'Earliest 32 bit time - 1'
+ ),
+ array( '20380119031408', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:08 GMT', 'Latest 32 bit time + 1' ),
+ array( '19011212000000', TS_MW, '19011212000000', 'Convert to itself r74778#c10645' ),
+ array( '19011213204551', TS_UNIX, '-2147483649', 'Earliest 32 bit unix time - 1' ),
+ array( '20380119031408', TS_UNIX, '2147483648', 'Latest 32 bit unix time + 1' ),
+ array( '-2147483649', TS_MW, '19011213204551', '1901 negative unix time to MediaWiki' ),
+ array( '-5331871504', TS_MW, '18010115123456', '1801 negative unix time to MediaWiki' ),
+ array(
+ '0117-08-09 12:34:56',
+ TS_RFC2822,
+ '/, 09 Aug 0117 12:34:56 GMT$/',
+ 'Death of Roman Emperor [[Trajan]]'
+ ),
+
+ /* @todo FIXME: 00 to 101 years are taken as being in [1970-2069] */
+ array( '-58979923200', TS_RFC2822, '/, 01 Jan 0101 00:00:00 GMT$/', '1/1/101' ),
+ array( '-62135596800', TS_RFC2822, 'Mon, 01 Jan 0001 00:00:00 GMT', '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.
+ */
+ array(
+ '-62142076800',
+ TS_RFC2822,
+ 'Wed, 18 Oct 0000 00:00:00 GMT',
+ 'ISO 8601:2004 [[year 0]], also called [[1 BC]]'
+ ),
+ );
+ }
+
+ /**
+ * The Resource Loader uses wfTimestamp() to convert timestamps
+ * from If-Modified-Since header. Thus it must be able to parse all
+ * rfc2616 date formats
+ * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
+ * @dataProvider provideHttpDates
+ */
+ public function testHttpDate( $input, $output, $desc ) {
+ $this->assertEquals( $output, wfTimestamp( TS_MW, $input ), $desc );
+ }
+
+ public static function provideHttpDates() {
+ return array(
+ array( 'Sun, 06 Nov 1994 08:49:37 GMT', '19941106084937', 'RFC 822 date' ),
+ array( 'Sunday, 06-Nov-94 08:49:37 GMT', '19941106084937', 'RFC 850 date' ),
+ array( 'Sun Nov 6 08:49:37 1994', '19941106084937', "ANSI C's asctime() format" ),
+ // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171
+ array(
+ 'Mon, 22 Nov 2010 14:12:42 GMT; length=52626',
+ '20101122141242',
+ 'Netscape extension to HTTP/1.0'
+ ),
+ );
+ }
+
+ /**
+ * 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
+ */
+ public function testTimestampParameter() {
+ $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 );
+ }
+}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php
new file mode 100644
index 00000000..d11668b7
--- /dev/null
+++ b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * The function only need a string parameter and might react to IIS7.0
+ *
+ * @group GlobalFunctions
+ * @covers ::wfUrlencode
+ */
+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 static 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..74d4b091
--- /dev/null
+++ b/tests/phpunit/includes/HooksTest.php
@@ -0,0 +1,202 @@
+<?php
+
+class HooksTest extends MediaWikiTestCase {
+
+ function setUp() {
+ global $wgHooks;
+ parent::setUp();
+ Hooks::clear( 'MediaWikiHooksTest001' );
+ unset( $wgHooks['MediaWikiHooksTest001'] );
+ }
+
+ public static function provideHooks() {
+ $i = new NothingClass();
+
+ return array(
+ array(
+ 'Object and method',
+ array( $i, 'someNonStatic' ),
+ 'changed-nonstatic',
+ 'changed-nonstatic'
+ ),
+ array( 'Object and no method', array( $i ), 'changed-onevent', 'original' ),
+ array(
+ 'Object and method with data',
+ array( $i, 'someNonStaticWithData', 'data' ),
+ 'data',
+ 'original'
+ ),
+ array( 'Object and static method', array( $i, 'someStatic' ), 'changed-static', 'original' ),
+ array(
+ 'Class::method static call',
+ array( 'NothingClass::someStatic' ),
+ 'changed-static',
+ 'original'
+ ),
+ array( 'Global function', array( 'NothingFunction' ), 'changed-func', 'original' ),
+ array( 'Global function with data', array( 'NothingFunctionData', 'data' ), 'data', 'original' ),
+ array( 'Closure', array( function ( &$foo, $bar ) {
+ $foo = 'changed-closure';
+
+ return true;
+ } ), 'changed-closure', 'original' ),
+ array( 'Closure with data', array( function ( $data, &$foo, $bar ) {
+ $foo = $data;
+
+ return true;
+ }, 'data' ), 'data', 'original' )
+ );
+ }
+
+ /**
+ * @dataProvider provideHooks
+ * @covers ::wfRunHooks
+ */
+ public function testOldStyleHooks( $msg, array $hook, $expectedFoo, $expectedBar ) {
+ global $wgHooks;
+ $foo = $bar = 'original';
+
+ $wgHooks['MediaWikiHooksTest001'][] = $hook;
+ wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertSame( $expectedFoo, $foo, $msg );
+ $this->assertSame( $expectedBar, $bar, $msg );
+ }
+
+ /**
+ * @dataProvider provideHooks
+ * @covers Hooks::register
+ * @covers Hooks::run
+ */
+ public function testNewStyleHooks( $msg, $hook, $expectedFoo, $expectedBar ) {
+ $foo = $bar = 'original';
+
+ Hooks::register( 'MediaWikiHooksTest001', $hook );
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+
+ $this->assertSame( $expectedFoo, $foo, $msg );
+ $this->assertSame( $expectedBar, $bar, $msg );
+ }
+
+ /**
+ * @covers Hooks::isRegistered
+ * @covers Hooks::register
+ * @covers Hooks::getHandlers
+ * @covers Hooks::run
+ */
+ public function testNewStyleHookInteraction() {
+ global $wgHooks;
+
+ $a = new NothingClass();
+ $b = new NothingClass();
+
+ $wgHooks['MediaWikiHooksTest001'][] = $a;
+ $this->assertTrue(
+ Hooks::isRegistered( 'MediaWikiHooksTest001' ),
+ 'Hook registered via $wgHooks should be noticed by Hooks::isRegistered'
+ );
+
+ Hooks::register( 'MediaWikiHooksTest001', $b );
+ $this->assertEquals(
+ 2,
+ count( Hooks::getHandlers( 'MediaWikiHooksTest001' ) ),
+ 'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register'
+ );
+
+ $foo = 'quux';
+ $bar = 'qaax';
+
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) );
+ $this->assertEquals(
+ 1,
+ $a->calls,
+ 'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register'
+ );
+ $this->assertEquals(
+ 1,
+ $b->calls,
+ 'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register'
+ );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers Hooks::run
+ */
+ public function testUncallableFunction() {
+ Hooks::register( 'MediaWikiHooksTest001', 'ThisFunctionDoesntExist' );
+ Hooks::run( 'MediaWikiHooksTest001', array() );
+ }
+
+ /**
+ * @covers Hooks::run
+ */
+ public function testFalseReturn() {
+ Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+ return false;
+ } );
+ Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+ $foo = 'test';
+
+ return true;
+ } );
+ $foo = 'original';
+ Hooks::run( 'MediaWikiHooksTest001', array( &$foo ) );
+ $this->assertSame( 'original', $foo, 'Hooks continued processing after a false return.' );
+ }
+
+ /**
+ * @expectedException FatalError
+ * @covers Hooks::run
+ */
+ public function testFatalError() {
+ Hooks::register( 'MediaWikiHooksTest001', function () {
+ return 'test';
+ } );
+ Hooks::run( 'MediaWikiHooksTest001', array() );
+ }
+}
+
+function NothingFunction( &$foo, &$bar ) {
+ $foo = 'changed-func';
+
+ return true;
+}
+
+function NothingFunctionData( $data, &$foo, &$bar ) {
+ $foo = $data;
+
+ return true;
+}
+
+class NothingClass {
+ public $calls = 0;
+
+ public static function someStatic( &$foo, &$bar ) {
+ $foo = 'changed-static';
+
+ return true;
+ }
+
+ public function someNonStatic( &$foo, &$bar ) {
+ $this->calls++;
+ $foo = 'changed-nonstatic';
+ $bar = 'changed-nonstatic';
+
+ return true;
+ }
+
+ public function onMediaWikiHooksTest001( &$foo, &$bar ) {
+ $this->calls++;
+ $foo = 'changed-onevent';
+
+ return true;
+ }
+
+ public function someNonStaticWithData( $data, &$foo, &$bar ) {
+ $this->calls++;
+ $foo = $data;
+
+ return true;
+ }
+}
diff --git a/tests/phpunit/includes/HtmlFormatterTest.php b/tests/phpunit/includes/HtmlFormatterTest.php
new file mode 100644
index 00000000..9dbfa452
--- /dev/null
+++ b/tests/phpunit/includes/HtmlFormatterTest.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * @group HtmlFormatter
+ */
+class HtmlFormatterTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider getHtmlData
+ *
+ * @param string $input
+ * @param string $expectedText
+ * @param array $expectedRemoved
+ * @param callable|bool $callback
+ */
+ public function testTransform( $input, $expectedText,
+ $expectedRemoved = array(), $callback = false
+ ) {
+ $input = self::normalize( $input );
+ $formatter = new HtmlFormatter( HtmlFormatter::wrapHTML( $input ) );
+ if ( $callback ) {
+ $callback( $formatter );
+ }
+ $removedElements = $formatter->filterContent();
+ $html = $formatter->getText();
+ $removed = array();
+ foreach ( $removedElements as $removedElement ) {
+ $removed[] = self::normalize( $formatter->getText( $removedElement ) );
+ }
+ $expectedRemoved = array_map( 'self::normalize', $expectedRemoved );
+
+ $this->assertValidHtmlSnippet( $html );
+ $this->assertEquals( self::normalize( $expectedText ), self::normalize( $html ) );
+ $this->assertEquals( asort( $expectedRemoved ), asort( $removed ) );
+ }
+
+ private static function normalize( $s ) {
+ return str_replace( "\n", '',
+ str_replace( "\r", '', $s ) // "yay" to Windows!
+ );
+ }
+
+ public function getHtmlData() {
+ $removeImages = function ( HtmlFormatter $f ) {
+ $f->setRemoveMedia();
+ };
+ $removeTags = function ( HtmlFormatter $f ) {
+ $f->remove( array( 'table', '.foo', '#bar', 'div.baz' ) );
+ };
+ $flattenSomeStuff = function ( HtmlFormatter $f ) {
+ $f->flatten( array( 's', 'div' ) );
+ };
+ $flattenEverything = function ( HtmlFormatter $f ) {
+ $f->flattenAllTags();
+ };
+ return array(
+ // remove images if asked
+ array(
+ '<img src="/foo/bar.jpg" alt="Blah"/>',
+ '',
+ array( '<img src="/foo/bar.jpg" alt="Blah">' ),
+ $removeImages,
+ ),
+ // basic tag removal
+ array(
+ // @codingStandardsIgnoreStart Ignore long line warnings.
+ '<table><tr><td>foo</td></tr></table><div class="foo">foo</div><div class="foo quux">foo</div><span id="bar">bar</span>
+<strong class="foo" id="bar">foobar</strong><div class="notfoo">test</div><div class="baz"/>
+<span class="baz">baz</span>',
+ // @codingStandardsIgnoreEnd
+ '<div class="notfoo">test</div>
+<span class="baz">baz</span>',
+ array(
+ '<table><tr><td>foo</td></tr></table>',
+ '<div class="foo">foo</div>',
+ '<div class="foo quux">foo</div>',
+ '<span id="bar">bar</span>',
+ '<strong class="foo" id="bar">foobar</strong>',
+ '<div class="baz"/>',
+ ),
+ $removeTags,
+ ),
+ // don't flatten tags that start like chosen ones
+ array(
+ '<div><s>foo</s> <span>bar</span></div>',
+ 'foo <span>bar</span>',
+ array(),
+ $flattenSomeStuff,
+ ),
+ // total flattening
+ array(
+ '<div style="foo">bar<sup>2</sup></div>',
+ 'bar2',
+ array(),
+ $flattenEverything,
+ ),
+ // UTF-8 preservation and security
+ array(
+ '<span title="&quot; \' &amp;">&lt;Тест!&gt;</span> &amp;&lt;&#38;&#0038;&#x26;&#x026;',
+ '<span title="&quot; \' &amp;">&lt;Тест!&gt;</span> &amp;&lt;&amp;&amp;&amp;&amp;',
+ array(),
+ $removeTags, // Have some rules to trigger a DOM parse
+ ),
+ // https://bugzilla.wikimedia.org/show_bug.cgi?id=53086
+ array(
+ 'Foo<sup id="cite_ref-1" class="reference"><a href="#cite_note-1">[1]</a></sup>'
+ . ' <a href="/wiki/Bar" title="Bar" class="mw-redirect">Bar</a>',
+ 'Foo<sup id="cite_ref-1" class="reference"><a href="#cite_note-1">[1]</a></sup>'
+ . ' <a href="/wiki/Bar" title="Bar" class="mw-redirect">Bar</a>',
+ ),
+ );
+ }
+
+ public function testQuickProcessing() {
+ $f = new MockHtmlFormatter( 'foo' );
+ $f->filterContent();
+ $this->assertFalse( $f->hasDoc, 'HtmlFormatter should not needlessly parse HTML' );
+ }
+}
+
+class MockHtmlFormatter extends HtmlFormatter {
+ public $hasDoc = false;
+
+ public function getDoc() {
+ $this->hasDoc = true;
+ return parent::getDoc();
+ }
+}
diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php
new file mode 100644
index 00000000..a8829cd8
--- /dev/null
+++ b/tests/phpunit/includes/HtmlTest.php
@@ -0,0 +1,773 @@
+<?php
+/** tests for includes/Html.php */
+
+class HtmlTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $langCode = 'en';
+ $langObj = Language::factory( $langCode );
+
+ // Hardcode namespaces during test runs,
+ // so that html output based on existing namespaces
+ // can be properly evaluated.
+ $langObj->setNamespaces( array(
+ -2 => 'Media',
+ -1 => 'Special',
+ 0 => '',
+ 1 => 'Talk',
+ 2 => 'User',
+ 3 => 'User_talk',
+ 4 => 'MyWiki',
+ 5 => 'MyWiki_Talk',
+ 6 => 'File',
+ 7 => 'File_talk',
+ 8 => 'MediaWiki',
+ 9 => 'MediaWiki_talk',
+ 10 => 'Template',
+ 11 => 'Template_talk',
+ 14 => 'Category',
+ 15 => 'Category_talk',
+ 100 => 'Custom',
+ 101 => 'Custom_talk',
+ ) );
+
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => $langCode,
+ 'wgContLang' => $langObj,
+ 'wgLang' => $langObj,
+ 'wgWellFormedXml' => false,
+ ) );
+ }
+
+ /**
+ * @covers Html::element
+ */
+ public function testElementBasics() {
+ $this->assertEquals(
+ '<img>',
+ Html::element( 'img', null, '' ),
+ 'No close tag for short-tag elements'
+ );
+
+ $this->assertEquals(
+ '<element></element>',
+ Html::element( 'element', null, null ),
+ 'Close tag for empty element (null, null)'
+ );
+
+ $this->assertEquals(
+ '<element></element>',
+ Html::element( 'element', array(), '' ),
+ 'Close tag for empty element (array, string)'
+ );
+
+ $this->setMwGlobals( 'wgWellFormedXml', true );
+
+ $this->assertEquals(
+ '<img />',
+ Html::element( 'img', null, '' ),
+ 'Self-closing tag for short-tag elements (wgWellFormedXml = true)'
+ );
+ }
+
+ public function dataXmlMimeType() {
+ return array(
+ // ( $mimetype, $isXmlMimeType )
+ # HTML is not an XML MimeType
+ array( 'text/html', false ),
+ # XML is an XML MimeType
+ array( 'text/xml', true ),
+ array( 'application/xml', true ),
+ # XHTML is an XML MimeType
+ array( 'application/xhtml+xml', true ),
+ # Make sure other +xml MimeTypes are supported
+ # SVG is another random MimeType even though we don't use it
+ array( 'image/svg+xml', true ),
+ # Complete random other MimeTypes are not XML
+ array( 'text/plain', false ),
+ );
+ }
+
+ /**
+ * @dataProvider dataXmlMimeType
+ * @covers Html::isXmlMimeType
+ */
+ public function testXmlMimeType( $mimetype, $isXmlMimeType ) {
+ $this->assertEquals( $isXmlMimeType, Html::isXmlMimeType( $mimetype ) );
+ }
+
+ /**
+ * @covers HTML::expandAttributes
+ */
+ 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->assertEquals(
+ ' foo=""',
+ Html::expandAttributes( array( 'foo' => '' ) ),
+ 'keep keys with an empty string'
+ );
+ }
+
+ /**
+ * @covers HTML::expandAttributes
+ */
+ 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'
+ );
+
+ $this->assertEquals(
+ ' selected',
+ Html::expandAttributes( array( 'selected' => true ) ),
+ 'Boolean attributes have no value when value is true'
+ );
+ $this->assertEquals(
+ ' selected',
+ Html::expandAttributes( array( 'selected' ) ),
+ 'Boolean attributes have no value when value is true (passed as numerical array)'
+ );
+
+ $this->setMwGlobals( 'wgWellFormedXml', true );
+
+ $this->assertEquals(
+ ' selected=""',
+ Html::expandAttributes( array( 'selected' => true ) ),
+ 'Boolean attributes have empty string value when value is true (wgWellFormedXml)'
+ );
+ }
+
+ /**
+ * @covers HTML::expandAttributes
+ */
+ public function testExpandAttributesForNumbers() {
+ $this->assertEquals(
+ ' value=1',
+ Html::expandAttributes( array( 'value' => 1 ) ),
+ 'Integer value is cast to a string'
+ );
+ $this->assertEquals(
+ ' value=1.1',
+ Html::expandAttributes( array( 'value' => 1.1 ) ),
+ 'Float value is cast to a string'
+ );
+ }
+
+ /**
+ * @covers HTML::expandAttributes
+ */
+ public function testExpandAttributesForObjects() {
+ $this->assertEquals(
+ ' value=stringValue',
+ Html::expandAttributes( array( 'value' => new HtmlTestValue() ) ),
+ 'Object value is converted to a string'
+ );
+ }
+
+ /**
+ * Test for Html::expandAttributes()
+ * Please note it output a string prefixed with a space!
+ * @covers Html::expandAttributes
+ */
+ public function testExpandAttributesVariousExpansions() {
+ ### NOT EMPTY ####
+ $this->assertEquals(
+ ' empty_string=""',
+ Html::expandAttributes( array( 'empty_string' => '' ) ),
+ 'Empty string is always quoted'
+ );
+ $this->assertEquals(
+ ' key=value',
+ Html::expandAttributes( array( 'key' => 'value' ) ),
+ 'Simple string value needs no quotes'
+ );
+ $this->assertEquals(
+ ' one=1',
+ Html::expandAttributes( array( 'one' => 1 ) ),
+ 'Number 1 value needs no quotes'
+ );
+ $this->assertEquals(
+ ' zero=0',
+ Html::expandAttributes( array( 'zero' => 0 ) ),
+ 'Number 0 value needs no quotes'
+ );
+
+ $this->setMwGlobals( 'wgWellFormedXml', true );
+
+ $this->assertEquals(
+ ' empty_string=""',
+ Html::expandAttributes( array( 'empty_string' => '' ) ),
+ 'Attribute values are always quoted (wgWellFormedXml): Empty string'
+ );
+ $this->assertEquals(
+ ' key="value"',
+ Html::expandAttributes( array( 'key' => 'value' ) ),
+ 'Attribute values are always quoted (wgWellFormedXml): Simple string'
+ );
+ $this->assertEquals(
+ ' one="1"',
+ Html::expandAttributes( array( 'one' => 1 ) ),
+ 'Attribute values are always quoted (wgWellFormedXml): Number 1'
+ );
+ $this->assertEquals(
+ ' zero="0"',
+ Html::expandAttributes( array( 'zero' => 0 ) ),
+ 'Attribute values are always quoted (wgWellFormedXml): Number 0'
+ );
+ }
+
+ /**
+ * Html::expandAttributes has special features for HTML
+ * attributes that use space separated lists and also
+ * allows arrays to be used as values.
+ * @covers Html::expandAttributes
+ */
+ public function testExpandAttributesListValueAttributes() {
+ ### STRING VALUES
+ $this->assertEquals(
+ ' class="redundant spaces here"',
+ Html::expandAttributes( array( 'class' => ' redundant spaces here ' ) ),
+ 'Normalization should strip redundant spaces'
+ );
+ $this->assertEquals(
+ ' class="foo bar"',
+ Html::expandAttributes( array( 'class' => 'foo bar foo bar bar' ) ),
+ 'Normalization should remove duplicates in string-lists'
+ );
+ ### "EMPTY" ARRAY VALUES
+ $this->assertEquals(
+ ' class=""',
+ Html::expandAttributes( array( 'class' => array() ) ),
+ 'Value with an empty array'
+ );
+ $this->assertEquals(
+ ' class=""',
+ Html::expandAttributes( array( 'class' => array( null, '', ' ', ' ' ) ) ),
+ 'Array with null, empty string and spaces'
+ );
+ ### NON-EMPTY ARRAY VALUES
+ $this->assertEquals(
+ ' class="foo bar"',
+ Html::expandAttributes( array( 'class' => array(
+ 'foo',
+ 'bar',
+ 'foo',
+ 'bar',
+ 'bar',
+ ) ) ),
+ 'Normalization should remove duplicates in the array'
+ );
+ $this->assertEquals(
+ ' class="foo bar"',
+ Html::expandAttributes( array( 'class' => array(
+ 'foo bar',
+ 'bar foo',
+ 'foo',
+ 'bar bar',
+ ) ) ),
+ 'Normalization should remove duplicates in string-lists in the array'
+ );
+ }
+
+ /**
+ * Test feature added by r96188, let pass attributes values as
+ * a PHP array. Restricted to class,rel, accesskey.
+ * @covers Html::expandAttributes
+ */
+ public function testExpandAttributesSpaceSeparatedAttributesWithBoolean() {
+ $this->assertEquals(
+ ' class="booltrue one"',
+ Html::expandAttributes( array( 'class' => array(
+ 'booltrue' => true,
+ 'one' => 1,
+
+ # Method use isset() internally, make sure we do discard
+ # attributes values which have been assigned well known values
+ 'emptystring' => '',
+ 'boolfalse' => false,
+ 'zero' => 0,
+ 'null' => null,
+ ) ) )
+ );
+ }
+
+ /**
+ * How do we handle duplicate keys in HTML attributes expansion?
+ * We could pass a "class" the values: 'GREEN' and array( 'GREEN' => false )
+ * The later will take precedence.
+ *
+ * Feature added by r96188
+ * @covers Html::expandAttributes
+ */
+ public function testValueIsAuthoritativeInSpaceSeparatedAttributesArrays() {
+ $this->assertEquals(
+ ' class=""',
+ Html::expandAttributes( array( 'class' => array(
+ 'GREEN',
+ 'GREEN' => false,
+ 'GREEN',
+ ) ) )
+ );
+ }
+
+ /**
+ * @covers Html::expandAttributes
+ * @expectedException MWException
+ */
+ public function testExpandAttributes_ArrayOnNonListValueAttribute_ThrowsException() {
+ // Real-life test case found in the Popups extension (see Gerrit cf0fd64),
+ // when used with an outdated BetaFeatures extension (see Gerrit deda1e7)
+ Html::expandAttributes( array(
+ 'src' => array(
+ 'ltr' => 'ltr.svg',
+ 'rtl' => 'rtl.svg'
+ )
+ ) );
+ }
+
+ /**
+ * @covers Html::namespaceSelector
+ */
+ public function testNamespaceSelector() {
+ $this->assertEquals(
+ '<select id=namespace name=namespace>' . "\n" .
+ '<option value=0>(Main)</option>' . "\n" .
+ '<option value=1>Talk</option>' . "\n" .
+ '<option value=2>User</option>' . "\n" .
+ '<option value=3>User talk</option>' . "\n" .
+ '<option value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '<option value=100>Custom</option>' . "\n" .
+ '<option value=101>Custom talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector(),
+ 'Basic namespace selector without custom options'
+ );
+
+ $this->assertEquals(
+ '<label for=mw-test-namespace>Select a namespace:</label>&#160;' .
+ '<select id=mw-test-namespace name=wpNamespace>' . "\n" .
+ '<option value=all>all</option>' . "\n" .
+ '<option value=0>(Main)</option>' . "\n" .
+ '<option value=1>Talk</option>' . "\n" .
+ '<option value=2 selected>User</option>' . "\n" .
+ '<option value=3>User talk</option>' . "\n" .
+ '<option value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '<option value=100>Custom</option>' . "\n" .
+ '<option value=101>Custom talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector(
+ array( 'selected' => '2', 'all' => 'all', 'label' => 'Select a namespace:' ),
+ array( 'name' => 'wpNamespace', 'id' => 'mw-test-namespace' )
+ ),
+ 'Basic namespace selector with custom values'
+ );
+
+ $this->assertEquals(
+ '<label for=namespace>Select a namespace:</label>&#160;' .
+ '<select id=namespace name=namespace>' . "\n" .
+ '<option value=0>(Main)</option>' . "\n" .
+ '<option value=1>Talk</option>' . "\n" .
+ '<option value=2>User</option>' . "\n" .
+ '<option value=3>User talk</option>' . "\n" .
+ '<option value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '<option value=100>Custom</option>' . "\n" .
+ '<option value=101>Custom talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector(
+ array( 'label' => 'Select a namespace:' )
+ ),
+ 'Basic namespace selector with a custom label but no id attribtue for the <select>'
+ );
+ }
+
+ public function testCanFilterOutNamespaces() {
+ $this->assertEquals(
+ '<select id=namespace name=namespace>' . "\n" .
+ '<option value=2>User</option>' . "\n" .
+ '<option value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector(
+ array( 'exclude' => array( 0, 1, 3, 100, 101 ) )
+ ),
+ 'Namespace selector namespace filtering.'
+ );
+ }
+
+ public function testCanDisableANamespaces() {
+ $this->assertEquals(
+ '<select id=namespace name=namespace>' . "\n" .
+ '<option disabled value=0>(Main)</option>' . "\n" .
+ '<option disabled value=1>Talk</option>' . "\n" .
+ '<option disabled value=2>User</option>' . "\n" .
+ '<option disabled value=3>User talk</option>' . "\n" .
+ '<option disabled value=4>MyWiki</option>' . "\n" .
+ '<option value=5>MyWiki Talk</option>' . "\n" .
+ '<option value=6>File</option>' . "\n" .
+ '<option value=7>File talk</option>' . "\n" .
+ '<option value=8>MediaWiki</option>' . "\n" .
+ '<option value=9>MediaWiki talk</option>' . "\n" .
+ '<option value=10>Template</option>' . "\n" .
+ '<option value=11>Template talk</option>' . "\n" .
+ '<option value=14>Category</option>' . "\n" .
+ '<option value=15>Category talk</option>' . "\n" .
+ '<option value=100>Custom</option>' . "\n" .
+ '<option value=101>Custom talk</option>' . "\n" .
+ '</select>',
+ Html::namespaceSelector( array(
+ 'disable' => array( 0, 1, 2, 3, 4 )
+ ) ),
+ 'Namespace selector namespace disabling'
+ );
+ }
+
+ /**
+ * @dataProvider provideHtml5InputTypes
+ * @covers Html::element
+ */
+ public function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) {
+ $this->assertEquals(
+ '<input type=' . $HTML5InputType . '>',
+ Html::element( 'input', array( 'type' => $HTML5InputType ) ),
+ 'In HTML5, HTML::element() should accept type="' . $HTML5InputType . '"'
+ );
+ }
+
+ /**
+ * List of input element types values introduced by HTML5
+ * Full list at http://www.w3.org/TR/html-markup/input.html
+ */
+ public static function provideHtml5InputTypes() {
+ $types = array(
+ 'datetime',
+ 'datetime-local',
+ 'date',
+ 'month',
+ 'time',
+ 'week',
+ 'number',
+ 'range',
+ 'email',
+ 'url',
+ 'search',
+ 'tel',
+ 'color',
+ );
+ $cases = array();
+ foreach ( $types as $type ) {
+ $cases[] = array( $type );
+ }
+
+ return $cases;
+ }
+
+ /**
+ * Test out Html::element drops or enforces default value
+ * @covers Html::dropDefaults
+ * @dataProvider provideElementsWithAttributesHavingDefaultValues
+ */
+ public function testDropDefaults( $expected, $element, $attribs, $message = '' ) {
+ $this->assertEquals( $expected, Html::element( $element, $attribs ), $message );
+ }
+
+ public static function provideElementsWithAttributesHavingDefaultValues() {
+ # Use cases in a concise format:
+ # <expected>, <element name>, <array of attributes> [, <message>]
+ # Will be mapped to Html::element()
+ $cases = array();
+
+ ### Generic cases, match $attribDefault static array
+ $cases[] = array( '<area>',
+ 'area', array( 'shape' => 'rect' )
+ );
+
+ $cases[] = array( '<button type=submit></button>',
+ 'button', array( 'formaction' => 'GET' )
+ );
+ $cases[] = array( '<button type=submit></button>',
+ 'button', array( 'formenctype' => 'application/x-www-form-urlencoded' )
+ );
+
+ $cases[] = array( '<canvas></canvas>',
+ 'canvas', array( 'height' => '150' )
+ );
+ $cases[] = array( '<canvas></canvas>',
+ 'canvas', array( 'width' => '300' )
+ );
+ # Also check with numeric values
+ $cases[] = array( '<canvas></canvas>',
+ 'canvas', array( 'height' => 150 )
+ );
+ $cases[] = array( '<canvas></canvas>',
+ 'canvas', array( 'width' => 300 )
+ );
+
+ $cases[] = array( '<command>',
+ 'command', array( 'type' => 'command' )
+ );
+
+ $cases[] = array( '<form></form>',
+ 'form', array( 'action' => 'GET' )
+ );
+ $cases[] = array( '<form></form>',
+ 'form', array( 'autocomplete' => 'on' )
+ );
+ $cases[] = array( '<form></form>',
+ 'form', array( 'enctype' => 'application/x-www-form-urlencoded' )
+ );
+
+ $cases[] = array( '<input>',
+ 'input', array( 'formaction' => 'GET' )
+ );
+ $cases[] = array( '<input>',
+ 'input', array( 'type' => 'text' )
+ );
+
+ $cases[] = array( '<keygen>',
+ 'keygen', array( 'keytype' => 'rsa' )
+ );
+
+ $cases[] = array( '<link>',
+ 'link', array( 'media' => 'all' )
+ );
+
+ $cases[] = array( '<menu></menu>',
+ 'menu', array( 'type' => 'list' )
+ );
+
+ $cases[] = array( '<script></script>',
+ 'script', array( 'type' => 'text/javascript' )
+ );
+
+ $cases[] = array( '<style></style>',
+ 'style', array( 'media' => 'all' )
+ );
+ $cases[] = array( '<style></style>',
+ 'style', array( 'type' => 'text/css' )
+ );
+
+ $cases[] = array( '<textarea></textarea>',
+ 'textarea', array( 'wrap' => 'soft' )
+ );
+
+ ### SPECIFIC CASES
+
+ # <link type="text/css">
+ $cases[] = array( '<link>',
+ 'link', array( 'type' => 'text/css' )
+ );
+
+ # <input> specific handling
+ $cases[] = array( '<input type=checkbox>',
+ 'input', array( 'type' => 'checkbox', 'value' => 'on' ),
+ 'Default value "on" is stripped of checkboxes',
+ );
+ $cases[] = array( '<input type=radio>',
+ 'input', array( 'type' => 'radio', 'value' => 'on' ),
+ 'Default value "on" is stripped of radio buttons',
+ );
+ $cases[] = array( '<input type=submit value=Submit>',
+ 'input', array( 'type' => 'submit', 'value' => 'Submit' ),
+ 'Default value "Submit" is kept on submit buttons (for possible l10n issues)',
+ );
+ $cases[] = array( '<input type=color>',
+ 'input', array( 'type' => 'color', 'value' => '' ),
+ );
+ $cases[] = array( '<input type=range>',
+ 'input', array( 'type' => 'range', 'value' => '' ),
+ );
+
+ # <button> specific handling
+ # see remarks on http://msdn.microsoft.com/en-us/library/ie/ms535211%28v=vs.85%29.aspx
+ $cases[] = array( '<button type=submit></button>',
+ 'button', array( 'type' => 'submit' ),
+ 'According to standard the default type is "submit". '
+ . 'Depending on compatibility mode IE might use "button", instead.',
+ );
+
+ # <select> specifc handling
+ $cases[] = array( '<select multiple></select>',
+ 'select', array( 'size' => '4', 'multiple' => true ),
+ );
+ # .. with numeric value
+ $cases[] = array( '<select multiple></select>',
+ 'select', array( 'size' => 4, 'multiple' => true ),
+ );
+ $cases[] = array( '<select></select>',
+ 'select', array( 'size' => '1', 'multiple' => false ),
+ );
+ # .. with numeric value
+ $cases[] = array( '<select></select>',
+ 'select', array( 'size' => 1, 'multiple' => false ),
+ );
+
+ # Passing an array as value
+ $cases[] = array( '<a class="css-class-one css-class-two"></a>',
+ 'a', array( 'class' => array( 'css-class-one', 'css-class-two' ) ),
+ "dropDefaults accepts values given as an array"
+ );
+
+ # FIXME: doDropDefault should remove defaults given in an array
+ # Expected should be '<a></a>'
+ $cases[] = array( '<a class=""></a>',
+ 'a', array( 'class' => array( '', '' ) ),
+ "dropDefaults accepts values given as an array"
+ );
+
+ # Craft the Html elements
+ $ret = array();
+ foreach ( $cases as $case ) {
+ $ret[] = array(
+ $case[0],
+ $case[1], $case[2],
+ isset( $case[3] ) ? $case[3] : ''
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @covers Html::expandAttributes
+ */
+ public function testFormValidationBlacklist() {
+ $this->assertEmpty(
+ Html::expandAttributes( array(
+ 'min' => 1,
+ 'max' => 100,
+ 'pattern' => 'abc',
+ 'required' => true,
+ 'step' => 2
+ ) ),
+ 'Blacklist form validation attributes.'
+ );
+ $this->assertEquals(
+ ' step=any',
+ Html::expandAttributes(
+ array(
+ 'min' => 1,
+ 'max' => 100,
+ 'pattern' => 'abc',
+ 'required' => true,
+ 'step' => 'any'
+ ),
+ 'Allow special case "step=any".'
+ )
+ );
+ }
+
+ public function testWrapperInput() {
+ $this->assertEquals(
+ '<input type=radio value=testval name=testname>',
+ Html::input( 'testname', 'testval', 'radio' ),
+ 'Input wrapper with type and value.'
+ );
+ $this->assertEquals(
+ '<input name=testname class=mw-ui-input>',
+ Html::input( 'testname' ),
+ 'Input wrapper with all default values.'
+ );
+ }
+
+ public function testWrapperCheck() {
+ $this->assertEquals(
+ '<input type=checkbox value=1 name=testname>',
+ Html::check( 'testname' ),
+ 'Checkbox wrapper unchecked.'
+ );
+ $this->assertEquals(
+ '<input checked type=checkbox value=1 name=testname>',
+ Html::check( 'testname', true ),
+ 'Checkbox wrapper checked.'
+ );
+ $this->assertEquals(
+ '<input type=checkbox value=testval name=testname>',
+ Html::check( 'testname', false, array( 'value' => 'testval' ) ),
+ 'Checkbox wrapper with a value override.'
+ );
+ }
+
+ public function testWrapperRadio() {
+ $this->assertEquals(
+ '<input type=radio value=1 name=testname>',
+ Html::radio( 'testname' ),
+ 'Radio wrapper unchecked.'
+ );
+ $this->assertEquals(
+ '<input checked type=radio value=1 name=testname>',
+ Html::radio( 'testname', true ),
+ 'Radio wrapper checked.'
+ );
+ $this->assertEquals(
+ '<input type=radio value=testval name=testname>',
+ Html::radio( 'testname', false, array( 'value' => 'testval' ) ),
+ 'Radio wrapper with a value override.'
+ );
+ }
+
+ public function testWrapperLabel() {
+ $this->assertEquals(
+ '<label for=testid>testlabel</label>',
+ Html::label( 'testlabel', 'testid' ),
+ 'Label wrapper'
+ );
+ }
+}
+
+class HtmlTestValue {
+ function __toString() {
+ return 'stringValue';
+ }
+}
diff --git a/tests/phpunit/includes/HttpTest.php b/tests/phpunit/includes/HttpTest.php
new file mode 100644
index 00000000..9b53381e
--- /dev/null
+++ b/tests/phpunit/includes/HttpTest.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * @group Broken
+ */
+class HttpTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider cookieDomains
+ * @covers Cookie::validateCookieDomain
+ */
+ public 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 );
+ }
+
+ public static 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" ),
+ );
+ }
+
+ /**
+ * Test Http::isValidURI()
+ * @bug 27854 : Http::isValidURI is too lax
+ * @dataProvider provideURI
+ * @covers Http::isValidURI
+ */
+ public function testIsValidUri( $expect, $URI, $message = '' ) {
+ $this->assertEquals(
+ $expect,
+ (bool)Http::isValidURI( $URI ),
+ $message
+ );
+ }
+
+ /**
+ * Feeds URI to test a long regular expression in Http::isValidURI
+ */
+ public static 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' ),
+ );
+ }
+
+ /**
+ * Warning:
+ *
+ * These tests are for code that makes use of an artifact of how CURL
+ * handles header reporting on redirect pages, and will need to be
+ * rewritten when bug 29232 is taken care of (high-level handling of
+ * HTTP redirects).
+ */
+ public function testRelativeRedirections() {
+ $h = MWHttpRequestTester::factory( 'http://oldsite/file.ext' );
+
+ # Forge a Location header
+ $h->setRespHeaders( 'location', array(
+ 'http://newsite/file.ext',
+ '/newfile.ext',
+ )
+ );
+ # Verify we correctly fix the Location
+ $this->assertEquals(
+ 'http://newsite/newfile.ext',
+ $h->getFinalUrl(),
+ "Relative file path Location: interpreted as full URL"
+ );
+
+ $h->setRespHeaders( 'location', array(
+ 'https://oldsite/file.ext'
+ )
+ );
+ $this->assertEquals(
+ 'https://oldsite/file.ext',
+ $h->getFinalUrl(),
+ "Location to the HTTPS version of the site"
+ );
+
+ $h->setRespHeaders( 'location', array(
+ '/anotherfile.ext',
+ 'http://anotherfile/hoster.ext',
+ 'https://anotherfile/hoster.ext'
+ )
+ );
+ $this->assertEquals(
+ 'https://anotherfile/hoster.ext',
+ $h->getFinalUrl( "Relative file path Location: should keep the latest host and scheme!" )
+ );
+ }
+}
+
+/**
+ * Class to let us overwrite MWHttpRequest respHeaders variable
+ */
+class MWHttpRequestTester extends MWHttpRequest {
+ // function derived from the MWHttpRequest factory function but
+ // returns appropriate tester class here
+ public static function factory( $url, $options = null ) {
+ if ( !Http::$httpEngine ) {
+ Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
+ } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
+ throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
+ 'Http::$httpEngine is set to "curl"' );
+ }
+
+ switch ( Http::$httpEngine ) {
+ case 'curl':
+ return new CurlHttpRequestTester( $url, $options );
+ case 'php':
+ if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
+ throw new MWException( __METHOD__ .
+ ': allow_url_fopen needs to be enabled for pure PHP HTTP requests to work. '
+ . 'If possible, curl should be used instead. See http://php.net/curl.' );
+ }
+
+ return new PhpHttpRequestTester( $url, $options );
+ default:
+ }
+ }
+}
+
+class CurlHttpRequestTester extends CurlHttpRequest {
+ function setRespHeaders( $name, $value ) {
+ $this->respHeaders[$name] = $value;
+ }
+}
+
+class PhpHttpRequestTester extends PhpHttpRequest {
+ function setRespHeaders( $name, $value ) {
+ $this->respHeaders[$name] = $value;
+ }
+}
diff --git a/tests/phpunit/includes/ImagePage404Test.php b/tests/phpunit/includes/ImagePage404Test.php
new file mode 100644
index 00000000..197a2b32
--- /dev/null
+++ b/tests/phpunit/includes/ImagePage404Test.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * For doing Image Page tests that rely on 404 thumb handling
+ */
+class ImagePage404Test extends MediaWikiMediaTestCase {
+
+ protected function getRepoOptions() {
+ return parent::getRepoOptions() + array( 'transformVia404' => true );
+ }
+
+ function setUp() {
+ $this->setMwGlobals( 'wgImageLimits', array(
+ array( 320, 240 ),
+ array( 640, 480 ),
+ array( 800, 600 ),
+ array( 1024, 768 ),
+ array( 1280, 1024 )
+ ) );
+ parent::setUp();
+ }
+
+ function getImagePage( $filename ) {
+ $title = Title::makeTitleSafe( NS_FILE, $filename );
+ $file = $this->dataFile( $filename );
+ $iPage = new ImagePage( $title );
+ $iPage->setFile( $file );
+ return $iPage;
+ }
+
+ /**
+ * @dataProvider providerGetThumbSizes
+ * @param string $filename
+ * @param int $expectedNumberThumbs How many thumbnails to show
+ */
+ function testGetThumbSizes( $filename, $expectedNumberThumbs ) {
+ $iPage = $this->getImagePage( $filename );
+ $reflection = new ReflectionClass( $iPage );
+ $reflMethod = $reflection->getMethod( 'getThumbSizes' );
+ $reflMethod->setAccessible( true );
+
+ $actual = $reflMethod->invoke( $iPage, 545, 700 );
+ $this->assertEquals( count( $actual ), $expectedNumberThumbs );
+ }
+
+ function providerGetThumbSizes() {
+ return array(
+ array( 'animated.gif', 6 ),
+ array( 'Toll_Texas_1.svg', 6 ),
+ array( '80x60-Greyscale.xcf', 6 ),
+ array( 'jpeg-comment-binary.jpg', 6 ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/ImagePageTest.php b/tests/phpunit/includes/ImagePageTest.php
new file mode 100644
index 00000000..3c255b5f
--- /dev/null
+++ b/tests/phpunit/includes/ImagePageTest.php
@@ -0,0 +1,90 @@
+<?php
+class ImagePageTest extends MediaWikiMediaTestCase {
+
+ function setUp() {
+ $this->setMwGlobals( 'wgImageLimits', array(
+ array( 320, 240 ),
+ array( 640, 480 ),
+ array( 800, 600 ),
+ array( 1024, 768 ),
+ array( 1280, 1024 )
+ ) );
+ parent::setUp();
+ }
+
+ function getImagePage( $filename ) {
+ $title = Title::makeTitleSafe( NS_FILE, $filename );
+ $file = $this->dataFile( $filename );
+ $iPage = new ImagePage( $title );
+ $iPage->setFile( $file );
+ return $iPage;
+ }
+
+ /**
+ * @dataProvider providerGetDisplayWidthHeight
+ * @param array $dim Array [maxWidth, maxHeight, width, height]
+ * @param array $expected Array [width, height] The width and height we expect to display at
+ */
+ function testGetDisplayWidthHeight( $dim, $expected ) {
+ $iPage = $this->getImagePage( 'animated.gif' );
+ $reflection = new ReflectionClass( $iPage );
+ $reflMethod = $reflection->getMethod( 'getDisplayWidthHeight' );
+ $reflMethod->setAccessible( true );
+
+ $actual = $reflMethod->invoke( $iPage, $dim[0], $dim[1], $dim[2], $dim[3] );
+ $this->assertEquals( $actual, $expected );
+ }
+
+ function providerGetDisplayWidthHeight() {
+ return array(
+ array(
+ array( 1024.0, 768.0, 600.0, 600.0 ),
+ array( 600.0, 600.0 )
+ ),
+ array(
+ array( 1024.0, 768.0, 1600.0, 600.0 ),
+ array( 1024.0, 384.0 )
+ ),
+ array(
+ array( 1024.0, 768.0, 1024.0, 768.0 ),
+ array( 1024.0, 768.0 )
+ ),
+ array(
+ array( 1024.0, 768.0, 800.0, 1000.0 ),
+ array( 614.0, 768.0 )
+ ),
+ array(
+ array( 1024.0, 768.0, 0, 1000 ),
+ array( 0, 0 )
+ ),
+ array(
+ array( 1024.0, 768.0, 2000, 0 ),
+ array( 0, 0 )
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider providerGetThumbSizes
+ * @param string $filename
+ * @param int $expectedNumberThumbs How many thumbnails to show
+ */
+ function testGetThumbSizes( $filename, $expectedNumberThumbs ) {
+ $iPage = $this->getImagePage( $filename );
+ $reflection = new ReflectionClass( $iPage );
+ $reflMethod = $reflection->getMethod( 'getThumbSizes' );
+ $reflMethod->setAccessible( true );
+
+ $actual = $reflMethod->invoke( $iPage, 545, 700 );
+ $this->assertEquals( count( $actual ), $expectedNumberThumbs );
+ }
+
+ function providerGetThumbSizes() {
+ return array(
+ array( 'animated.gif', 2 ),
+ array( 'Toll_Texas_1.svg', 1 ),
+ array( '80x60-Greyscale.xcf', 1 ),
+ array( 'jpeg-comment-binary.jpg', 2 ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/ImportTest.php b/tests/phpunit/includes/ImportTest.php
new file mode 100644
index 00000000..2fce6bfb
--- /dev/null
+++ b/tests/phpunit/includes/ImportTest.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * Test class for Import methods.
+ *
+ * @group Database
+ *
+ * @author Sebastian Brückner < sebastian.brueckner@student.hpi.uni-potsdam.de >
+ */
+class ImportTest extends MediaWikiLangTestCase {
+
+ private function getInputStreamSource( $xml ) {
+ $file = 'data:application/xml,' . $xml;
+ $status = ImportStreamSource::newFromFile( $file );
+ if ( !$status->isGood() ) {
+ throw new MWException( "Cannot create InputStreamSource." );
+ }
+ return $status->value;
+ }
+
+ /**
+ * @covers WikiImporter::handlePage
+ * @dataProvider getRedirectXML
+ * @param string $xml
+ * @param string|null $redirectTitle
+ */
+ public function testHandlePageContainsRedirect( $xml, $redirectTitle ) {
+ $source = $this->getInputStreamSource( $xml );
+
+ $redirect = null;
+ $callback = function ( $title, $origTitle, $revCount, $sRevCount, $pageInfo ) use ( &$redirect ) {
+ if ( array_key_exists( 'redirect', $pageInfo ) ) {
+ $redirect = $pageInfo['redirect'];
+ }
+ };
+
+ $importer = new WikiImporter( $source );
+ $importer->setPageOutCallback( $callback );
+ $importer->doImport();
+
+ $this->assertEquals( $redirectTitle, $redirect );
+ }
+
+ public function getRedirectXML() {
+ return array(
+ array(
+ <<< EOF
+<mediawiki>
+ <page>
+ <title>Test</title>
+ <ns>0</ns>
+ <id>21</id>
+ <redirect title="Test22"/>
+ <revision>
+ <id>20</id>
+ <timestamp>2014-05-27T10:00:00Z</timestamp>
+ <contributor>
+ <username>Admin</username>
+ <id>10</id>
+ </contributor>
+ <comment>Admin moved page [[Test]] to [[Test22]]</comment>
+ <text xml:space="preserve" bytes="20">#REDIRECT [[Test22]]</text>
+ <sha1>tq456o9x3abm7r9ozi6km8yrbbc56o6</sha1>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ </revision>
+ </page>
+</mediawiki>
+EOF
+ ,
+ 'Test22'
+ ),
+ array(
+ <<< EOF
+<mediawiki>
+ <page>
+ <title>Test</title>
+ <ns>0</ns>
+ <id>42</id>
+ <revision>
+ <id>421</id>
+ <timestamp>2014-05-27T11:00:00Z</timestamp>
+ <contributor>
+ <username>Admin</username>
+ <id>10</id>
+ </contributor>
+ <text xml:space="preserve" bytes="4">Abcd</text>
+ <sha1>n7uomjq96szt60fy5w3x7ahf7q8m8rh</sha1>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ </revision>
+ </page>
+</mediawiki>
+EOF
+ ,
+ null
+ ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/LanguageConverterTest.php b/tests/phpunit/includes/LanguageConverterTest.php
new file mode 100644
index 00000000..d4ccca99
--- /dev/null
+++ b/tests/phpunit/includes/LanguageConverterTest.php
@@ -0,0 +1,187 @@
+<?php
+
+class LanguageConverterTest extends MediaWikiLangTestCase {
+ /** @var LanguageToTest */
+ protected $lang = null;
+ /** @var TestConverter */
+ protected $lc = null;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgContLang' => Language::factory( 'tg' ),
+ 'wgLanguageCode' => 'tg',
+ 'wgDefaultLanguageVariant' => false,
+ 'wgMemc' => new EmptyBagOStuff,
+ 'wgRequest' => new FauxRequest( array() ),
+ 'wgUser' => new User,
+ ) );
+
+ $this->lang = new LanguageToTest();
+ $this->lc = new TestConverter(
+ $this->lang, 'tg',
+ array( 'tg', 'tg-latn' )
+ );
+ }
+
+ protected function tearDown() {
+ unset( $this->lc );
+ unset( $this->lang );
+
+ parent::tearDown();
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ */
+ public function testGetPreferredVariantDefaults() {
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getHeaderVariant
+ */
+ public function testGetPreferredVariantHeaders() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getHeaderVariant
+ */
+ public function testGetPreferredVariantHeaderWeight() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg;q=1' );
+
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getHeaderVariant
+ */
+ public function testGetPreferredVariantHeaderWeight2() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg-latn;q=1' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getHeaderVariant
+ */
+ public function testGetPreferredVariantHeaderMulti() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'en, tg-latn;q=1' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ */
+ public 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() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getUserVariant
+ */
+ public function testGetPreferredVariantUserOptionForForeignLanguage() {
+ global $wgContLang, $wgUser;
+
+ $wgContLang = Language::factory( 'en' );
+ $wgUser = new User;
+ $wgUser->load(); // from 'defaults'
+ $wgUser->mId = 1;
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant-tg', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getUserVariant
+ * @covers LanguageConverter::getURLVariant
+ */
+ public function testGetPreferredVariantHeaderUserVsUrl() {
+ global $wgContLang, $wgRequest, $wgUser;
+
+ $wgContLang = Language::factory( 'tg-latn' );
+ $wgRequest->setVal( 'variant', 'tg' );
+ $wgUser = User::newFromId( "admin" );
+ $wgUser->setId( 1 );
+ $wgUser->mFrom = 'defaults';
+ $wgUser->mOptionsLoaded = true;
+ // The user's data is ignored because the variant is set in the URL.
+ $wgUser->setOption( 'variant', 'tg-latn' );
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ */
+ public function testGetPreferredVariantDefaultLanguageVariant() {
+ global $wgDefaultLanguageVariant;
+
+ $wgDefaultLanguageVariant = 'tg-latn';
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ /**
+ * @covers LanguageConverter::getPreferredVariant
+ * @covers LanguageConverter::getURLVariant
+ */
+ public 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..63b2c395
--- /dev/null
+++ b/tests/phpunit/includes/LicensesTest.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @covers Licenses
+ */
+class LicensesTest extends MediaWikiTestCase {
+
+ public 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/LinkFilterTest.php b/tests/phpunit/includes/LinkFilterTest.php
new file mode 100644
index 00000000..f2c9cb43
--- /dev/null
+++ b/tests/phpunit/includes/LinkFilterTest.php
@@ -0,0 +1,274 @@
+<?php
+
+/**
+ * @group Database
+ */
+class LinkFilterTest extends MediaWikiLangTestCase {
+
+ protected function setUp() {
+
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgUrlProtocols', array(
+ 'http://',
+ 'https://',
+ 'ftp://',
+ 'irc://',
+ 'ircs://',
+ 'gopher://',
+ 'telnet://',
+ 'nntp://',
+ 'worldwind://',
+ 'mailto:',
+ 'news:',
+ 'svn://',
+ 'git://',
+ 'mms://',
+ '//',
+ ) );
+
+ }
+
+ /**
+ * createRegexFromLike($like)
+ *
+ * Takes an array as created by LinkFilter::makeLikeArray() and creates a regex from it
+ *
+ * @param array $like Array as created by LinkFilter::makeLikeArray()
+ * @return string Regex
+ */
+ function createRegexFromLIKE( $like ) {
+
+ $regex = '!^';
+
+ foreach ( $like as $item ) {
+
+ if ( $item instanceof LikeMatch ) {
+ if ( $item->toString() == '%' ) {
+ $regex .= '.*';
+ } elseif ( $item->toString() == '_' ) {
+ $regex .= '.';
+ }
+ } else {
+ $regex .= preg_quote( $item, '!' );
+ }
+
+ }
+
+ $regex .= '$!';
+
+ return $regex;
+
+ }
+
+ /**
+ * provideValidPatterns()
+ *
+ * @return array
+ */
+ public static function provideValidPatterns() {
+
+ return array(
+ // Protocol, Search pattern, URL which matches the pattern
+ array( 'http://', '*.test.com', 'http://www.test.com' ),
+ array( 'http://', 'test.com:8080/dir/file', 'http://name:pass@test.com:8080/dir/file' ),
+ array( 'https://', '*.com', 'https://s.s.test..com:88/dir/file?a=1&b=2' ),
+ array( 'https://', '*.com', 'https://name:pass@secure.com/index.html' ),
+ array( 'http://', 'name:pass@test.com', 'http://test.com' ),
+ array( 'http://', 'test.com', 'http://name:pass@test.com' ),
+ array( 'http://', '*.test.com', 'http://a.b.c.test.com/dir/dir/file?a=6'),
+ array( null, 'http://*.test.com', 'http://www.test.com' ),
+ array( 'mailto:', 'name@mail.test123.com', 'mailto:name@mail.test123.com' ),
+ array( '',
+ 'http://name:pass@www.test.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]=4rtg',
+ 'http://name:pass@www.test.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]=4rtg'
+ ),
+ array( '', 'http://name:pass@*.test.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]=4rtg',
+ 'http://name:pass@www.test.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]=4rtg' ),
+ array( '', 'http://name:wrongpass@*.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]',
+ 'http://name:pass@www.test.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]=4rtg' ),
+ array( 'http://', 'name:pass@*.test.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]=4rtg',
+ 'http://name:pass@www.test.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]=4rtg' ),
+ array( '', 'http://name:pass@www.test.com:12345',
+ 'http://name:pass@www.test.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]=4rtg' ),
+ array( 'ftp://', 'user:pass@ftp.test.com:1233/home/user/file;type=efw',
+ 'ftp://user:pass@ftp.test.com:1233/home/user/file;type=efw' ),
+ array( null, 'ftp://otheruser:otherpass@ftp.test.com:1233/home/user/file;type=',
+ 'ftp://user:pass@ftp.test.com:1233/home/user/file;type=efw' ),
+ array( null, 'ftp://@ftp.test.com:1233/home/user/file;type=',
+ 'ftp://user:pass@ftp.test.com:1233/home/user/file;type=efw' ),
+ array( null, 'ftp://ftp.test.com/',
+ 'ftp://user:pass@ftp.test.com/home/user/file;type=efw' ),
+ array( null, 'ftp://ftp.test.com/',
+ 'ftp://user:pass@ftp.test.com/home/user/file;type=efw' ),
+ array( null, 'ftp://*.test.com:222/',
+ 'ftp://user:pass@ftp.test.com:222/home' ),
+ array( 'irc://', '*.myserver:6667/', 'irc://test.myserver:6667/' ),
+ array( 'irc://', 'name:pass@*.myserver/', 'irc://test.myserver:6667/' ),
+ array( 'irc://', 'name:pass@*.myserver/', 'irc://other:@test.myserver:6667/' ),
+ array( '', 'irc://test/name,string,abc?msg=t', 'irc://test/name,string,abc?msg=test' ),
+ array( '', 'https://gerrit.wikimedia.org/r/#/q/status:open,n,z',
+ 'https://gerrit.wikimedia.org/r/#/q/status:open,n,z' ),
+ array( '', 'https://gerrit.wikimedia.org',
+ 'https://gerrit.wikimedia.org/r/#/q/status:open,n,z' ),
+ array( 'mailto:', '*.test.com', 'mailto:name@pop3.test.com' ),
+ array( 'mailto:', 'test.com', 'mailto:name@test.com' ),
+ array( 'news:', 'test.1234afc@news.test.com', 'news:test.1234afc@news.test.com' ),
+ array( 'news:', '*.test.com', 'news:test.1234afc@news.test.com' ),
+ array( '', 'news:4df8kh$iagfewewf(at)newsbf02aaa.news.aol.com',
+ 'news:4df8kh$iagfewewf(at)newsbf02aaa.news.aol.com' ),
+ array( '', 'news:*.aol.com',
+ 'news:4df8kh$iagfewewf(at)newsbf02aaa.news.aol.com' ),
+ array( '', 'git://github.com/prwef/abc-def.git', 'git://github.com/prwef/abc-def.git' ),
+ array( 'git://', 'github.com/', 'git://github.com/prwef/abc-def.git' ),
+ array( 'git://', '*.github.com/', 'git://a.b.c.d.e.f.github.com/prwef/abc-def.git' ),
+ array( '', 'gopher://*.test.com/', 'gopher://gopher.test.com/0/v2/vstat'),
+ array( 'telnet://', '*.test.com', 'telnet://shell.test.com/~home/'),
+
+ //
+ // The following only work in PHP >= 5.3.7, due to a bug in parse_url which eats
+ // the path from the url (https://bugs.php.net/bug.php?id=54180)
+ //
+ // array( '', 'http://test.com', 'http://test.com/index?arg=1' ),
+ // array( 'http://', '*.test.com', 'http://www.test.com/index?arg=1' ),
+ // array( '' ,
+ // 'http://xx23124:__ffdfdef__@www.test.com:12345/dir' ,
+ // 'http://name:pass@www.test.com:12345/dir/dir/file.xyz.php#__se__?arg1=_&arg2[]=4rtg'
+ // ),
+ //
+
+ //
+ // Tests for false positives
+ //
+ array( 'http://', 'test.com', 'http://www.test.com', false ),
+ array( 'http://', 'www1.test.com', 'http://www.test.com', false ),
+ array( 'http://', '*.test.com', 'http://www.test.t.com', false ),
+ array( '', 'http://test.com:8080', 'http://www.test.com:8080', false ),
+ array( '', 'https://test.com', 'http://test.com', false ),
+ array( '', 'http://test.com', 'https://test.com', false ),
+ array( 'http://', 'http://test.com', 'http://test.com', false ),
+ array( null, 'http://www.test.com', 'http://www.test.com:80', false ),
+ array( null, 'http://www.test.com:80', 'http://www.test.com', false ),
+ array( null, 'http://*.test.com:80', 'http://www.test.com', false ),
+ array( '', 'https://gerrit.wikimedia.org/r/#/XXX/status:open,n,z',
+ 'https://gerrit.wikimedia.org/r/#/q/status:open,n,z', false ),
+ array( '', 'https://*.wikimedia.org/r/#/q/status:open,n,z',
+ 'https://gerrit.wikimedia.org/r/#/XXX/status:open,n,z', false ),
+ array( 'mailto:', '@test.com', '@abc.test.com', false ),
+ array( 'mailto:', 'mail@test.com', 'mail2@test.com', false ),
+ array( '', 'mailto:mail@test.com', 'mail2@test.com', false ),
+ array( '', 'mailto:@test.com', '@abc.test.com', false ),
+ array( 'ftp://', '*.co', 'ftp://www.co.uk', false ),
+ array( 'ftp://', '*.co', 'ftp://www.co.m', false ),
+ array( 'ftp://', '*.co/dir/', 'ftp://www.co/dir2/', false ),
+ array( 'ftp://', 'www.co/dir/', 'ftp://www.co/dir2/', false ),
+ array( 'ftp://', 'test.com/dir/', 'ftp://test.com/', false ),
+ array( '', 'http://test.com:8080/dir/', 'http://test.com:808/dir/', false ),
+ array( '', 'http://test.com/dir/index.html', 'http://test.com/dir/index.php', false ),
+
+ //
+ // These are false positives too and ideally shouldn't match, but that
+ // would require using regexes and RLIKE instead of LIKE
+ //
+ // array( null, 'http://*.test.com', 'http://www.test.com:80', false ),
+ // array( '', 'https://*.wikimedia.org/r/#/q/status:open,n,z',
+ // 'https://gerrit.wikimedia.org/XXX/r/#/q/status:open,n,z', false ),
+ );
+
+ }
+
+ /**
+ * testMakeLikeArrayWithValidPatterns()
+ *
+ * Tests whether the LIKE clause produced by LinkFilter::makeLikeArray($pattern, $protocol)
+ * will find one of the URL indexes produced by wfMakeUrlIndexes($url)
+ *
+ * @dataProvider provideValidPatterns
+ *
+ * @param string $protocol Protocol, e.g. 'http://' or 'mailto:'
+ * @param string $pattern Search pattern to feed to LinkFilter::makeLikeArray
+ * @param string $url URL to feed to wfMakeUrlIndexes
+ * @param bool $shouldBeFound Should the URL be found? (defaults true)
+ */
+ function testMakeLikeArrayWithValidPatterns( $protocol, $pattern, $url, $shouldBeFound = true ) {
+
+ $indexes = wfMakeUrlIndexes( $url );
+ $likeArray = LinkFilter::makeLikeArray( $pattern, $protocol );
+
+ $this->assertTrue( $likeArray !== false,
+ "LinkFilter::makeLikeArray('$pattern', '$protocol') returned false on a valid pattern"
+ );
+
+ $regex = $this->createRegexFromLIKE( $likeArray );
+ $debugmsg = "Regex: '" . $regex . "'\n";
+ $debugmsg .= count( $indexes ) . " index(es) created by wfMakeUrlIndexes():\n";
+
+ $matches = 0;
+
+ foreach ( $indexes as $index ) {
+ $matches += preg_match( $regex, $index );
+ $debugmsg .= "\t'$index'\n";
+ }
+
+ if ( $shouldBeFound ) {
+ $this->assertTrue(
+ $matches > 0,
+ "Search pattern '$protocol$pattern' does not find url '$url' \n$debugmsg"
+ );
+ } else {
+ $this->assertFalse(
+ $matches > 0,
+ "Search pattern '$protocol$pattern' should not find url '$url' \n$debugmsg"
+ );
+ }
+
+ }
+
+ /**
+ * provideInvalidPatterns()
+ *
+ * @return array
+ */
+ public static function provideInvalidPatterns() {
+
+ return array(
+ array( '' ),
+ array( '*' ),
+ array( 'http://*' ),
+ array( 'http://*/' ),
+ array( 'http://*/dir/file' ),
+ array( 'test.*.com' ),
+ array( 'http://test.*.com' ),
+ array( 'test.*.com' ),
+ array( 'http://*.test.*' ),
+ array( 'http://*test.com' ),
+ array( 'https://*' ),
+ array( '*://test.com'),
+ array( 'mailto:name:pass@t*est.com' ),
+ array( 'http://*:888/'),
+ array( '*http://'),
+ array( 'test.com/*/index' ),
+ array( 'test.com/dir/index?arg=*' ),
+ );
+
+ }
+
+ /**
+ * testMakeLikeArrayWithInvalidPatterns()
+ *
+ * Tests whether LinkFilter::makeLikeArray($pattern) will reject invalid search patterns
+ *
+ * @dataProvider provideInvalidPatterns
+ *
+ * @param string $pattern Invalid search pattern
+ */
+ function testMakeLikeArrayWithInvalidPatterns( $pattern ) {
+
+ $this->assertFalse(
+ LinkFilter::makeLikeArray( $pattern ),
+ "'$pattern' is not a valid pattern and should be rejected"
+ );
+
+ }
+
+}
diff --git a/tests/phpunit/includes/LinkerTest.php b/tests/phpunit/includes/LinkerTest.php
new file mode 100644
index 00000000..7b84107e
--- /dev/null
+++ b/tests/phpunit/includes/LinkerTest.php
@@ -0,0 +1,192 @@
+<?php
+
+/**
+ * @group Database
+ */
+
+class LinkerTest extends MediaWikiLangTestCase {
+
+ /**
+ * @dataProvider provideCasesForUserLink
+ * @covers Linker::userLink
+ */
+ public function testUserLink( $expected, $userId, $userName, $altUserName = false, $msg = '' ) {
+ $this->setMwGlobals( array(
+ 'wgArticlePath' => '/wiki/$1',
+ 'wgWellFormedXml' => true,
+ ) );
+
+ $this->assertEquals( $expected,
+ Linker::userLink( $userId, $userName, $altUserName, $msg )
+ );
+ }
+
+ public static function provideCasesForUserLink() {
+ # Format:
+ # - expected
+ # - userid
+ # - username
+ # - optional altUserName
+ # - optional message
+ return array(
+
+ ### ANONYMOUS USER ########################################
+ array(
+ '<a href="/wiki/Special:Contributions/JohnDoe" '
+ . 'title="Special:Contributions/JohnDoe" '
+ . 'class="mw-userlink mw-anonuserlink">JohnDoe</a>',
+ 0, 'JohnDoe', false,
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/::1" '
+ . 'title="Special:Contributions/::1" '
+ . 'class="mw-userlink mw-anonuserlink">::1</a>',
+ 0, '::1', false,
+ 'Anonymous with pretty IPv6'
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/0:0:0:0:0:0:0:1" '
+ . 'title="Special:Contributions/0:0:0:0:0:0:0:1" '
+ . 'class="mw-userlink mw-anonuserlink">::1</a>',
+ 0, '0:0:0:0:0:0:0:1', false,
+ 'Anonymous with almost pretty IPv6'
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/0000:0000:0000:0000:0000:0000:0000:0001" '
+ . 'title="Special:Contributions/0000:0000:0000:0000:0000:0000:0000:0001" '
+ . 'class="mw-userlink mw-anonuserlink">::1</a>',
+ 0, '0000:0000:0000:0000:0000:0000:0000:0001', false,
+ 'Anonymous with full IPv6'
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/::1" '
+ . 'title="Special:Contributions/::1" '
+ . 'class="mw-userlink mw-anonuserlink">AlternativeUsername</a>',
+ 0, '::1', 'AlternativeUsername',
+ 'Anonymous with pretty IPv6 and an alternative username'
+ ),
+
+ # IPV4
+ array(
+ '<a href="/wiki/Special:Contributions/127.0.0.1" '
+ . 'title="Special:Contributions/127.0.0.1" '
+ . 'class="mw-userlink mw-anonuserlink">127.0.0.1</a>',
+ 0, '127.0.0.1', false,
+ 'Anonymous with IPv4'
+ ),
+ array(
+ '<a href="/wiki/Special:Contributions/127.0.0.1" '
+ . 'title="Special:Contributions/127.0.0.1" '
+ . 'class="mw-userlink mw-anonuserlink">AlternativeUsername</a>',
+ 0, '127.0.0.1', 'AlternativeUsername',
+ 'Anonymous with IPv4 and an alternative username'
+ ),
+
+ ### Regular user ##########################################
+ # TODO!
+ );
+ }
+
+ /**
+ * @dataProvider provideCasesForFormatComment
+ * @covers Linker::formatComment
+ * @covers Linker::formatAutocomments
+ * @covers Linker::formatLinksInComment
+ */
+ public function testFormatComment( $expected, $comment, $title = false, $local = false ) {
+ $this->setMwGlobals( array(
+ 'wgScript' => '/wiki/index.php',
+ 'wgArticlePath' => '/wiki/$1',
+ 'wgWellFormedXml' => true,
+ 'wgCapitalLinks' => true,
+ ) );
+
+ if ( $title === false ) {
+ // We need a page title that exists
+ $title = Title::newFromText( 'Special:BlankPage' );
+ }
+
+ $this->assertEquals(
+ $expected,
+ Linker::formatComment( $comment, $title, $local )
+ );
+ }
+
+ public static function provideCasesForFormatComment() {
+ return array(
+ // Linker::formatComment
+ array(
+ 'a&lt;script&gt;b',
+ 'a<script>b',
+ ),
+ array(
+ 'a—b',
+ 'a&mdash;b',
+ ),
+ array(
+ "&#039;&#039;&#039;not bolded&#039;&#039;&#039;",
+ "'''not bolded'''",
+ ),
+ // Linker::formatAutocomments
+ array(
+ '<a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→</a>‎<span dir="auto"><span class="autocomment">autocomment</span></span>',
+ "/* autocomment */",
+ ),
+ array(
+ '<a href="/wiki/Special:BlankPage#linkie.3F" title="Special:BlankPage">→</a>‎<span dir="auto"><span class="autocomment"><a href="/wiki/index.php?title=Linkie%3F&amp;action=edit&amp;redlink=1" class="new" title="Linkie? (page does not exist)">linkie?</a></span></span>',
+ "/* [[linkie?]] */",
+ ),
+ array(
+ '<a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→</a>‎<span dir="auto"><span class="autocomment">autocomment: </span> post</span>',
+ "/* autocomment */ post",
+ ),
+ array(
+ 'pre <a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→</a>‎<span dir="auto"><span class="autocomment">autocomment</span></span>',
+ "pre /* autocomment */",
+ ),
+ array(
+ 'pre <a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→</a>‎<span dir="auto"><span class="autocomment">autocomment: </span> post</span>',
+ "pre /* autocomment */ post",
+ ),
+ array(
+ '/* autocomment */ multiple? <a href="/wiki/Special:BlankPage#autocomment2" title="Special:BlankPage">→</a>‎<span dir="auto"><span class="autocomment">autocomment2: </span> </span>',
+ "/* autocomment */ multiple? /* autocomment2 */ ",
+ ),
+ array(
+ '<a href="#autocomment">→</a>‎<span dir="auto"><span class="autocomment">autocomment</span></span>',
+ "/* autocomment */",
+ false, true
+ ),
+ array(
+ '‎<span dir="auto"><span class="autocomment">autocomment</span></span>',
+ "/* autocomment */",
+ null
+ ),
+ // Linker::formatLinksInComment
+ array(
+ 'abc <a href="/wiki/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">link</a> def',
+ "abc [[link]] def",
+ ),
+ array(
+ 'abc <a href="/wiki/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">text</a> def',
+ "abc [[link|text]] def",
+ ),
+ array(
+ 'abc <a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a> def',
+ "abc [[Special:BlankPage|]] def",
+ ),
+ array(
+ 'abc <a href="/wiki/index.php?title=%C4%84%C5%9B%C5%BC&amp;action=edit&amp;redlink=1" class="new" title="Ąśż (page does not exist)">ąśż</a> def',
+ "abc [[%C4%85%C5%9B%C5%BC]] def",
+ ),
+ array(
+ 'abc <a href="/wiki/Special:BlankPage#section" title="Special:BlankPage">#section</a> def',
+ "abc [[#section]] def",
+ ),
+ array(
+ 'abc <a href="/wiki/index.php?title=/subpage&amp;action=edit&amp;redlink=1" class="new" title="/subpage (page does not exist)">/subpage</a> def',
+ "abc [[/subpage]] def",
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/LinksUpdateTest.php b/tests/phpunit/includes/LinksUpdateTest.php
new file mode 100644
index 00000000..02f6b2ab
--- /dev/null
+++ b/tests/phpunit/includes/LinksUpdateTest.php
@@ -0,0 +1,266 @@
+<?php
+
+/**
+ * @group Database
+ * ^--- make sure temporary tables are used.
+ */
+class LinksUpdateTest extends MediaWikiTestCase {
+
+ function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed = array_merge( $this->tablesUsed,
+ array(
+ 'interwiki',
+ 'page_props',
+ 'pagelinks',
+ 'categorylinks',
+ 'langlinks',
+ 'externallinks',
+ 'imagelinks',
+ 'templatelinks',
+ 'iwlinks'
+ )
+ );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace(
+ 'interwiki',
+ array( 'iw_prefix' ),
+ array(
+ 'iw_prefix' => 'linksupdatetest',
+ 'iw_url' => 'http://testing.com/wiki/$1',
+ 'iw_api' => 'http://testing.com/w/api.php',
+ 'iw_local' => 0,
+ 'iw_trans' => 0,
+ 'iw_wikiid' => 'linksupdatetest',
+ )
+ );
+ }
+
+ protected function makeTitleAndParserOutput( $name, $id ) {
+ $t = Title::newFromText( $name );
+ $t->mArticleID = $id; # XXX: this is fugly
+
+ $po = new ParserOutput();
+ $po->setTitleText( $t->getPrefixedText() );
+
+ return array( $t, $po );
+ }
+
+ /**
+ * @covers ParserOutput::addLink
+ */
+ public function testUpdate_pagelinks() {
+ /** @var ParserOutput $po */
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addLink( Title::newFromText( "Foo" ) );
+ $po->addLink( Title::newFromText( "Special:Foo" ) ); // special namespace should be ignored
+ $po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored
+ $po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored
+
+ $update = $this->assertLinksUpdate(
+ $t,
+ $po,
+ 'pagelinks',
+ 'pl_namespace,
+ pl_title',
+ 'pl_from = 111',
+ array( array( NS_MAIN, 'Foo' ) )
+ );
+ $this->assertArrayEquals( array(
+ Title::makeTitle( NS_MAIN, 'Foo' ), // newFromText doesn't yield the same internal state....
+ ), $update->getAddedLinks() );
+
+ $po = new ParserOutput();
+ $po->setTitleText( $t->getPrefixedText() );
+
+ $po->addLink( Title::newFromText( "Bar" ) );
+ $po->addLink( Title::newFromText( "Talk:Bar" ) );
+
+ $update = $this->assertLinksUpdate(
+ $t,
+ $po,
+ 'pagelinks',
+ 'pl_namespace,
+ pl_title',
+ 'pl_from = 111',
+ array(
+ array( NS_MAIN, 'Bar' ),
+ array( NS_TALK, 'Bar' ),
+ )
+ );
+ $this->assertArrayEquals( array(
+ Title::makeTitle( NS_MAIN, 'Bar' ),
+ Title::makeTitle( NS_TALK, 'Bar' ),
+ ), $update->getAddedLinks() );
+ $this->assertArrayEquals( array(
+ Title::makeTitle( NS_MAIN, 'Foo' ),
+ ), $update->getRemovedLinks() );
+ }
+
+ /**
+ * @covers ParserOutput::addExternalLink
+ */
+ public function testUpdate_externallinks() {
+ /** @var ParserOutput $po */
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addExternalLink( "http://testing.com/wiki/Foo" );
+
+ $this->assertLinksUpdate( $t, $po, 'externallinks', 'el_to, el_index', 'el_from = 111', array(
+ array( 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ),
+ ) );
+ }
+
+ /**
+ * @covers ParserOutput::addCategory
+ */
+ public function testUpdate_categorylinks() {
+ /** @var ParserOutput $po */
+ $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' );
+
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addCategory( "Foo", "FOO" );
+
+ $this->assertLinksUpdate( $t, $po, 'categorylinks', 'cl_to, cl_sortkey', 'cl_from = 111', array(
+ array( 'Foo', "FOO\nTESTING" ),
+ ) );
+ }
+
+ /**
+ * @covers ParserOutput::addInterwikiLink
+ */
+ public function testUpdate_iwlinks() {
+ /** @var ParserOutput $po */
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $target = Title::makeTitleSafe( NS_MAIN, "Foo", '', 'linksupdatetest' );
+ $po->addInterwikiLink( $target );
+
+ $this->assertLinksUpdate( $t, $po, 'iwlinks', 'iwl_prefix, iwl_title', 'iwl_from = 111', array(
+ array( 'linksupdatetest', 'Foo' ),
+ ) );
+ }
+
+ /**
+ * @covers ParserOutput::addTemplate
+ */
+ public function testUpdate_templatelinks() {
+ /** @var ParserOutput $po */
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addTemplate( Title::newFromText( "Template:Foo" ), 23, 42 );
+
+ $this->assertLinksUpdate(
+ $t,
+ $po,
+ 'templatelinks',
+ 'tl_namespace,
+ tl_title',
+ 'tl_from = 111',
+ array( array( NS_TEMPLATE, 'Foo' ) )
+ );
+ }
+
+ /**
+ * @covers ParserOutput::addImage
+ */
+ public function testUpdate_imagelinks() {
+ /** @var ParserOutput $po */
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addImage( "Foo.png" );
+
+ $this->assertLinksUpdate( $t, $po, 'imagelinks', 'il_to', 'il_from = 111', array(
+ array( 'Foo.png' ),
+ ) );
+ }
+
+ /**
+ * @covers ParserOutput::addLanguageLink
+ */
+ public function testUpdate_langlinks() {
+ $this->setMwGlobals( array(
+ 'wgCapitalLinks' => true,
+ ) );
+
+ /** @var ParserOutput $po */
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $po->addLanguageLink( Title::newFromText( "en:Foo" )->getFullText() );
+
+ $this->assertLinksUpdate( $t, $po, 'langlinks', 'll_lang, ll_title', 'll_from = 111', array(
+ array( 'En', 'Foo' ),
+ ) );
+ }
+
+ /**
+ * @covers ParserOutput::setProperty
+ */
+ public function testUpdate_page_props() {
+ global $wgPagePropsHaveSortkey;
+
+ /** @var ParserOutput $po */
+ list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
+
+ $fields = array( 'pp_propname', 'pp_value' );
+ $expected = array();
+
+ $po->setProperty( "bool", true );
+ $expected[] = array( "bool", true );
+
+ $po->setProperty( "float", 4.0 + 1.0 / 4.0 );
+ $expected[] = array( "float", 4.0 + 1.0 / 4.0 );
+
+ $po->setProperty( "int", -7 );
+ $expected[] = array( "int", -7 );
+
+ $po->setProperty( "string", "33 bar" );
+ $expected[] = array( "string", "33 bar" );
+
+ // compute expected sortkey values
+ if ( $wgPagePropsHaveSortkey ) {
+ $fields[] = 'pp_sortkey';
+
+ foreach ( $expected as &$row ) {
+ $value = $row[1];
+
+ if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
+ $row[] = floatval( $value );
+ } else {
+ $row[] = null;
+ }
+ }
+ }
+
+ $this->assertLinksUpdate( $t, $po, 'page_props', $fields, 'pp_page = 111', $expected );
+ }
+
+ public function testUpdate_page_props_without_sortkey() {
+ $this->setMwGlobals( 'wgPagePropsHaveSortkey', false );
+
+ $this->testUpdate_page_props();
+ }
+
+ // @todo test recursive, too!
+
+ protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput,
+ $table, $fields, $condition, array $expectedRows
+ ) {
+ $update = new LinksUpdate( $title, $parserOutput );
+
+ //NOTE: make sure LinksUpdate does not generate warnings when called inside a transaction.
+ $update->beginTransaction();
+ $update->doUpdate();
+ $update->commitTransaction();
+
+ $this->assertSelect( $table, $fields, $condition, $expectedRows );
+ return $update;
+ }
+}
diff --git a/tests/phpunit/includes/LocalFileTest.php b/tests/phpunit/includes/LocalFileTest.php
new file mode 100644
index 00000000..5c5052e4
--- /dev/null
+++ b/tests/phpunit/includes/LocalFileTest.php
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * These tests should work regardless of $wgCapitalLinks
+ * @group Database
+ * @todo Split tests into providers and test methods
+ */
+
+class LocalFileTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgCapitalLinks', true );
+
+ $info = array(
+ 'name' => 'test',
+ 'directory' => '/testdir',
+ 'url' => '/testurl',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => new FSFileBackend( array(
+ 'name' => 'local-backend',
+ 'wikiId' => wfWikiId(),
+ 'containerPaths' => array(
+ 'cont1' => "/testdir/local-backend/tempimages/cont1",
+ 'cont2' => "/testdir/local-backend/tempimages/cont2"
+ )
+ ) )
+ );
+ $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!' );
+ }
+
+ /**
+ * @covers File::getHashPath
+ */
+ public function testGetHashPath() {
+ $this->assertEquals( '', $this->file_hl0->getHashPath() );
+ $this->assertEquals( 'a/a2/', $this->file_hl2->getHashPath() );
+ $this->assertEquals( 'c/c4/', $this->file_lc->getHashPath() );
+ }
+
+ /**
+ * @covers File::getRel
+ */
+ public 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() );
+ }
+
+ /**
+ * @covers File::getUrlRel
+ */
+ public 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() );
+ }
+
+ /**
+ * @covers File::getArchivePath
+ */
+ public function testGetArchivePath() {
+ $this->assertEquals(
+ 'mwstore://local-backend/test-public/archive',
+ $this->file_hl0->getArchivePath()
+ );
+ $this->assertEquals(
+ 'mwstore://local-backend/test-public/archive/a/a2',
+ $this->file_hl2->getArchivePath()
+ );
+ $this->assertEquals(
+ 'mwstore://local-backend/test-public/archive/!',
+ $this->file_hl0->getArchivePath( '!' )
+ );
+ $this->assertEquals(
+ 'mwstore://local-backend/test-public/archive/a/a2/!',
+ $this->file_hl2->getArchivePath( '!' )
+ );
+ }
+
+ /**
+ * @covers File::getThumbPath
+ */
+ public function testGetThumbPath() {
+ $this->assertEquals(
+ 'mwstore://local-backend/test-thumb/Test!',
+ $this->file_hl0->getThumbPath()
+ );
+ $this->assertEquals(
+ 'mwstore://local-backend/test-thumb/a/a2/Test!',
+ $this->file_hl2->getThumbPath()
+ );
+ $this->assertEquals(
+ 'mwstore://local-backend/test-thumb/Test!/x',
+ $this->file_hl0->getThumbPath( 'x' )
+ );
+ $this->assertEquals(
+ 'mwstore://local-backend/test-thumb/a/a2/Test!/x',
+ $this->file_hl2->getThumbPath( 'x' )
+ );
+ }
+
+ /**
+ * @covers File::getArchiveUrl
+ */
+ public 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( '!' ) );
+ }
+
+ /**
+ * @covers File::getThumbUrl
+ */
+ public 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' ) );
+ }
+
+ /**
+ * @covers File::getArchiveVirtualUrl
+ */
+ public 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( '!' )
+ );
+ }
+
+ /**
+ * @covers File::getThumbVirtualUrl
+ */
+ public 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( '!' )
+ );
+ }
+
+ /**
+ * @covers File::getUrl
+ */
+ public function testGetUrl() {
+ $this->assertEquals( '/testurl/Test%21', $this->file_hl0->getUrl() );
+ $this->assertEquals( '/testurl/a/a2/Test%21', $this->file_hl2->getUrl() );
+ }
+
+ /**
+ * @covers ::wfLocalFile
+ */
+ public 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..f2a720e8
--- /dev/null
+++ b/tests/phpunit/includes/MWFunctionTest.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @covers MWFunction
+ */
+class MWFunctionTest extends MediaWikiTestCase {
+ public 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
+ );
+ }
+}
+
+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..311350b5
--- /dev/null
+++ b/tests/phpunit/includes/MWNamespaceTest.php
@@ -0,0 +1,612 @@
+<?php
+/**
+ * @author Antoine Musso
+ * @copyright Copyright © 2011, Antoine Musso
+ * @file
+ */
+
+/**
+ * Test class for MWNamespace.
+ * Generated by PHPUnit on 2011-02-20 at 21:01:55.
+ * @todo covers tags
+ * @todo FIXME: this test file is a mess
+ *
+ */
+class MWNamespaceTest extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgContentNamespaces' => array( NS_MAIN ),
+ 'wgNamespacesWithSubpages' => array(
+ NS_TALK => true,
+ NS_USER => true,
+ NS_USER_TALK => true,
+ ),
+ 'wgCapitalLinks' => true,
+ 'wgCapitalLinkOverrides' => array(),
+ 'wgNonincludableNamespaces' => array(),
+ ) );
+ }
+
+#### START OF TESTS #########################################################
+
+ /**
+ * @todo Write more texts, handle $wgAllowImageMoving setting
+ * @covers MWNamespace::isMovable
+ */
+ public function testIsMovable() {
+ $this->assertFalse( MWNamespace::isMovable( NS_SPECIAL ) );
+ # @todo FIXME: Write more tests!!
+ }
+
+ /**
+ * Please make sure to change testIsTalk() if you change the assertions below
+ * @covers MWNamespace::isSubject
+ */
+ public function testIsSubject() {
+ // Special namespaces
+ $this->assertIsSubject( NS_MEDIA );
+ $this->assertIsSubject( NS_SPECIAL );
+
+ // Subject pages
+ $this->assertIsSubject( NS_MAIN );
+ $this->assertIsSubject( NS_USER );
+ $this->assertIsSubject( 100 ); # user defined
+
+ // Talk pages
+ $this->assertIsNotSubject( NS_TALK );
+ $this->assertIsNotSubject( NS_USER_TALK );
+ $this->assertIsNotSubject( 101 ); # user defined
+ }
+
+ /**
+ * Reverse of testIsSubject().
+ * Please update testIsSubject() if you change assertions below
+ * @covers MWNamespace::isTalk
+ */
+ public function testIsTalk() {
+ // Special namespaces
+ $this->assertIsNotTalk( NS_MEDIA );
+ $this->assertIsNotTalk( NS_SPECIAL );
+
+ // Subject pages
+ $this->assertIsNotTalk( NS_MAIN );
+ $this->assertIsNotTalk( NS_USER );
+ $this->assertIsNotTalk( 100 ); # user defined
+
+ // Talk pages
+ $this->assertIsTalk( NS_TALK );
+ $this->assertIsTalk( NS_USER_TALK );
+ $this->assertIsTalk( 101 ); # user defined
+ }
+
+ /**
+ * @covers MWNamespace::getSubject
+ */
+ 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 ) );
+ }
+
+ /**
+ * Regular getTalk() calls
+ * Namespaces without a talk page (NS_MEDIA, NS_SPECIAL) are tested in
+ * the function testGetTalkExceptions()
+ * @covers MWNamespace::getTalk
+ */
+ public function testGetTalk() {
+ $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_MAIN ) );
+ $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_TALK ) );
+ $this->assertEquals( NS_USER_TALK, MWNamespace::getTalk( NS_USER ) );
+ $this->assertEquals( NS_USER_TALK, MWNamespace::getTalk( NS_USER_TALK ) );
+ }
+
+ /**
+ * Exceptions with getTalk()
+ * NS_MEDIA does not have talk pages. MediaWiki raise an exception for them.
+ * @expectedException MWException
+ * @covers MWNamespace::getTalk
+ */
+ 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
+ * @covers MWNamespace::getTalk
+ */
+ 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()
+ * @covers MWNamespace::getAssociated
+ */
+ 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
+ * @covers MWNamespace::getAssociated
+ */
+ public function testGetAssociatedExceptionsForNsMedia() {
+ $this->assertNull( MWNamespace::getAssociated( NS_MEDIA ) );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers MWNamespace::getAssociated
+ */
+ public function testGetAssociatedExceptionsForNsSpecial() {
+ $this->assertNull( MWNamespace::getAssociated( NS_SPECIAL ) );
+ }
+
+ /**
+ * @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.'
+ );
+ }
+ */
+
+ /**
+ * Test MWNamespace::equals
+ * Note if we add a namespace registration system with keys like 'MAIN'
+ * we should add tests here for equivilance on things like 'MAIN' == 0
+ * and 'MAIN' == NS_MAIN.
+ * @covers MWNamespace::equals
+ */
+ public function testEquals() {
+ $this->assertTrue( MWNamespace::equals( NS_MAIN, NS_MAIN ) );
+ $this->assertTrue( MWNamespace::equals( NS_MAIN, 0 ) ); // In case we make NS_MAIN 'MAIN'
+ $this->assertTrue( MWNamespace::equals( NS_USER, NS_USER ) );
+ $this->assertTrue( MWNamespace::equals( NS_USER, 2 ) );
+ $this->assertTrue( MWNamespace::equals( NS_USER_TALK, NS_USER_TALK ) );
+ $this->assertTrue( MWNamespace::equals( NS_SPECIAL, NS_SPECIAL ) );
+ $this->assertFalse( MWNamespace::equals( NS_MAIN, NS_TALK ) );
+ $this->assertFalse( MWNamespace::equals( NS_USER, NS_USER_TALK ) );
+ $this->assertFalse( MWNamespace::equals( NS_PROJECT, NS_TEMPLATE ) );
+ }
+
+ /**
+ * @covers MWNamespace::subjectEquals
+ */
+ public function testSubjectEquals() {
+ $this->assertSameSubject( NS_MAIN, NS_MAIN );
+ $this->assertSameSubject( NS_MAIN, 0 ); // In case we make NS_MAIN 'MAIN'
+ $this->assertSameSubject( NS_USER, NS_USER );
+ $this->assertSameSubject( NS_USER, 2 );
+ $this->assertSameSubject( NS_USER_TALK, NS_USER_TALK );
+ $this->assertSameSubject( NS_SPECIAL, NS_SPECIAL );
+ $this->assertSameSubject( NS_MAIN, NS_TALK );
+ $this->assertSameSubject( NS_USER, NS_USER_TALK );
+
+ $this->assertDifferentSubject( NS_PROJECT, NS_TEMPLATE );
+ $this->assertDifferentSubject( NS_SPECIAL, NS_MAIN );
+ }
+
+ /**
+ * @covers MWNamespace::subjectEquals
+ */
+ public function testSpecialAndMediaAreDifferentSubjects() {
+ $this->assertDifferentSubject(
+ NS_MEDIA, NS_SPECIAL,
+ "NS_MEDIA and NS_SPECIAL are different subject namespaces"
+ );
+ $this->assertDifferentSubject(
+ NS_SPECIAL, NS_MEDIA,
+ "NS_SPECIAL and NS_MEDIA are different subject namespaces"
+ );
+ }
+
+ /**
+ * @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.'
+ );
+ }
+ */
+
+ /**
+ * @covers MWNamespace::canTalk
+ */
+ public function testCanTalk() {
+ $this->assertCanNotTalk( NS_MEDIA );
+ $this->assertCanNotTalk( NS_SPECIAL );
+
+ $this->assertCanTalk( NS_MAIN );
+ $this->assertCanTalk( NS_TALK );
+ $this->assertCanTalk( NS_USER );
+ $this->assertCanTalk( NS_USER_TALK );
+
+ // User defined namespaces
+ $this->assertCanTalk( 100 );
+ $this->assertCanTalk( 101 );
+ }
+
+ /**
+ * @covers MWNamespace::isContent
+ */
+ public function testIsContent() {
+ // NS_MAIN is a content namespace per DefaultSettings.php
+ // and per function definition.
+
+ $this->assertIsContent( NS_MAIN );
+
+ // Other namespaces which are not expected to be content
+
+ $this->assertIsNotContent( NS_MEDIA );
+ $this->assertIsNotContent( NS_SPECIAL );
+ $this->assertIsNotContent( NS_TALK );
+ $this->assertIsNotContent( NS_USER );
+ $this->assertIsNotContent( NS_CATEGORY );
+ $this->assertIsNotContent( 100 );
+ }
+
+ /**
+ * Similar to testIsContent() but alters the $wgContentNamespaces
+ * global variable.
+ * @covers MWNamespace::isContent
+ */
+ public function testIsContentAdvanced() {
+ global $wgContentNamespaces;
+
+ // Test that user defined namespace #252 is not content
+ $this->assertIsNotContent( 252 );
+
+ // Bless namespace # 252 as a content namespace
+ $wgContentNamespaces[] = 252;
+
+ $this->assertIsContent( 252 );
+
+ // Makes sure NS_MAIN was not impacted
+ $this->assertIsContent( NS_MAIN );
+ }
+
+ /**
+ * @covers MWNamespace::isWatchable
+ */
+ public function testIsWatchable() {
+ // Specials namespaces are not watchable
+ $this->assertIsNotWatchable( NS_MEDIA );
+ $this->assertIsNotWatchable( NS_SPECIAL );
+
+ // Core defined namespaces are watchables
+ $this->assertIsWatchable( NS_MAIN );
+ $this->assertIsWatchable( NS_TALK );
+
+ // Additional, user defined namespaces are watchables
+ $this->assertIsWatchable( 100 );
+ $this->assertIsWatchable( 101 );
+ }
+
+ /**
+ * @covers MWNamespace::hasSubpages
+ */
+ public function testHasSubpages() {
+ global $wgNamespacesWithSubpages;
+
+ // Special namespaces:
+ $this->assertHasNotSubpages( NS_MEDIA );
+ $this->assertHasNotSubpages( NS_SPECIAL );
+
+ // Namespaces without subpages
+ $this->assertHasNotSubpages( NS_MAIN );
+
+ $wgNamespacesWithSubpages[NS_MAIN] = true;
+ $this->assertHasSubpages( NS_MAIN );
+
+ $wgNamespacesWithSubpages[NS_MAIN] = false;
+ $this->assertHasNotSubpages( NS_MAIN );
+
+ // Some namespaces with subpages
+ $this->assertHasSubpages( NS_TALK );
+ $this->assertHasSubpages( NS_USER );
+ $this->assertHasSubpages( NS_USER_TALK );
+ }
+
+ /**
+ * @covers MWNamespace::getContentNamespaces
+ */
+ public function testGetContentNamespaces() {
+ global $wgContentNamespaces;
+
+ $this->assertEquals(
+ array( NS_MAIN ),
+ MWNamespace::getContentNamespaces(),
+ '$wgContentNamespaces is an array with only NS_MAIN by default'
+ );
+
+ # test !is_array( $wgcontentNamespaces )
+ $wgContentNamespaces = '';
+ $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() );
+
+ $wgContentNamespaces = false;
+ $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() );
+
+ $wgContentNamespaces = null;
+ $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() );
+
+ $wgContentNamespaces = 5;
+ $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() );
+
+ # test $wgContentNamespaces === array()
+ $wgContentNamespaces = array();
+ $this->assertEquals( array( 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()
+ );
+ }
+
+ /**
+ * @covers MWNamespace::getSubjectNamespaces
+ */
+ public function testGetSubjectNamespaces() {
+ $subjectsNS = MWNamespace::getSubjectNamespaces();
+ $this->assertContains( NS_MAIN, $subjectsNS,
+ "Talk namespaces should have NS_MAIN" );
+ $this->assertNotContains( NS_TALK, $subjectsNS,
+ "Talk namespaces should have NS_TALK" );
+
+ $this->assertNotContains( NS_MEDIA, $subjectsNS,
+ "Talk namespaces should not have NS_MEDIA" );
+ $this->assertNotContains( NS_SPECIAL, $subjectsNS,
+ "Talk namespaces should not have NS_SPECIAL" );
+ }
+
+ /**
+ * @covers MWNamespace::getTalkNamespaces
+ */
+ public function testGetTalkNamespaces() {
+ $talkNS = MWNamespace::getTalkNamespaces();
+ $this->assertContains( NS_TALK, $talkNS,
+ "Subject namespaces should have NS_TALK" );
+ $this->assertNotContains( NS_MAIN, $talkNS,
+ "Subject namespaces should not have NS_MAIN" );
+
+ $this->assertNotContains( NS_MEDIA, $talkNS,
+ "Subject namespaces should not have NS_MEDIA" );
+ $this->assertNotContains( NS_SPECIAL, $talkNS,
+ "Subject namespaces should not have NS_SPECIAL" );
+ }
+
+ /**
+ * Some namespaces are always capitalized per code definition
+ * in MWNamespace::$alwaysCapitalizedNamespaces
+ * @covers MWNamespace::isCapitalized
+ */
+ 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->assertIsCapitalized( NS_MEDIA );
+ $this->assertIsCapitalized( NS_FILE );
+
+ // Always capitalized namespaces
+ // @see MWNamespace::$alwaysCapitalizedNamespaces
+ $this->assertIsCapitalized( NS_SPECIAL );
+ $this->assertIsCapitalized( NS_USER );
+ $this->assertIsCapitalized( 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
+ * @covers MWNamespace::isCapitalized
+ */
+ public function testIsCapitalizedWithWgCapitalLinks() {
+ global $wgCapitalLinks;
+
+ $this->assertIsCapitalized( NS_PROJECT );
+ $this->assertIsCapitalized( NS_PROJECT_TALK );
+
+ $wgCapitalLinks = false;
+
+ // hardcoded namespaces (see above function) are still capitalized:
+ $this->assertIsCapitalized( NS_SPECIAL );
+ $this->assertIsCapitalized( NS_USER );
+ $this->assertIsCapitalized( NS_MEDIAWIKI );
+
+ // setting is correctly applied
+ $this->assertIsNotCapitalized( NS_PROJECT );
+ $this->assertIsNotCapitalized( NS_PROJECT_TALK );
+ }
+
+ /**
+ * Counter part for MWNamespace::testIsCapitalizedWithWgCapitalLinks() now
+ * testing the $wgCapitalLinkOverrides global.
+ *
+ * @todo split groups of assertions in autonomous testing functions
+ * @covers MWNamespace::isCapitalized
+ */
+ public function testIsCapitalizedWithWgCapitalLinkOverrides() {
+ global $wgCapitalLinkOverrides;
+
+ // Test default settings
+ $this->assertIsCapitalized( NS_PROJECT );
+ $this->assertIsCapitalized( NS_PROJECT_TALK );
+
+ // hardcoded namespaces (see above function) are capitalized:
+ $this->assertIsCapitalized( NS_SPECIAL );
+ $this->assertIsCapitalized( NS_USER );
+ $this->assertIsCapitalized( NS_MEDIAWIKI );
+
+ // Hardcoded namespaces remains capitalized
+ $wgCapitalLinkOverrides[NS_SPECIAL] = false;
+ $wgCapitalLinkOverrides[NS_USER] = false;
+ $wgCapitalLinkOverrides[NS_MEDIAWIKI] = false;
+
+ $this->assertIsCapitalized( NS_SPECIAL );
+ $this->assertIsCapitalized( NS_USER );
+ $this->assertIsCapitalized( NS_MEDIAWIKI );
+
+ $wgCapitalLinkOverrides[NS_PROJECT] = false;
+ $this->assertIsNotCapitalized( NS_PROJECT );
+
+ $wgCapitalLinkOverrides[NS_PROJECT] = true;
+ $this->assertIsCapitalized( NS_PROJECT );
+
+ unset( $wgCapitalLinkOverrides[NS_PROJECT] );
+ $this->assertIsCapitalized( NS_PROJECT );
+ }
+
+ /**
+ * @covers MWNamespace::hasGenderDistinction
+ */
+ 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 ) );
+ }
+
+ /**
+ * @covers MWNamespace::isNonincludable
+ */
+ public function testIsNonincludable() {
+ global $wgNonincludableNamespaces;
+
+ $wgNonincludableNamespaces = array( NS_USER );
+
+ $this->assertTrue( MWNamespace::isNonincludable( NS_USER ) );
+ $this->assertFalse( MWNamespace::isNonincludable( NS_TEMPLATE ) );
+ }
+
+ ####### HELPERS ###########################################################
+ function __call( $method, $args ) {
+ // Call the real method if it exists
+ if ( method_exists( $this, $method ) ) {
+ return $this->$method( $args );
+ }
+
+ if ( preg_match(
+ '/^assert(Has|Is|Can)(Not|)(Subject|Talk|Watchable|Content|Subpages|Capitalized)$/',
+ $method,
+ $m
+ ) ) {
+ # Interprets arguments:
+ $ns = $args[0];
+ $msg = isset( $args[1] ) ? $args[1] : " dummy message";
+
+ # Forge the namespace constant name:
+ if ( $ns === 0 ) {
+ $ns_name = "NS_MAIN";
+ } else {
+ $ns_name = "NS_" . strtoupper( MWNamespace::getCanonicalName( $ns ) );
+ }
+ # ... and the MWNamespace method name
+ $nsMethod = strtolower( $m[1] ) . $m[3];
+
+ $expect = ( $m[2] === '' );
+ $expect_name = $expect ? 'TRUE' : 'FALSE';
+
+ return $this->assertEquals( $expect,
+ MWNamespace::$nsMethod( $ns, $msg ),
+ "MWNamespace::$nsMethod( $ns_name ) should returns $expect_name"
+ );
+ }
+
+ throw new Exception( __METHOD__ . " could not find a method named $method\n" );
+ }
+
+ function assertSameSubject( $ns1, $ns2, $msg = '' ) {
+ $this->assertTrue( MWNamespace::subjectEquals( $ns1, $ns2, $msg ) );
+ }
+
+ function assertDifferentSubject( $ns1, $ns2, $msg = '' ) {
+ $this->assertFalse( MWNamespace::subjectEquals( $ns1, $ns2, $msg ) );
+ }
+}
diff --git a/tests/phpunit/includes/MWTimestampTest.php b/tests/phpunit/includes/MWTimestampTest.php
new file mode 100644
index 00000000..dcb98563
--- /dev/null
+++ b/tests/phpunit/includes/MWTimestampTest.php
@@ -0,0 +1,342 @@
+<?php
+
+/**
+ * Tests timestamp parsing and output.
+ */
+class MWTimestampTest extends MediaWikiLangTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ RequestContext::getMain()->setLanguage( Language::factory( 'en' ) );
+ }
+
+ /**
+ * @covers MWTimestamp::__construct
+ */
+ public function testConstructWithNoTimestamp() {
+ $timestamp = new MWTimestamp();
+ $this->assertInternalType( 'string', $timestamp->getTimestamp() );
+ $this->assertNotEmpty( $timestamp->getTimestamp() );
+ $this->assertNotEquals( false, strtotime( $timestamp->getTimestamp( TS_MW ) ) );
+ }
+
+ /**
+ * @covers MWTimestamp::__toString
+ */
+ public function testToString() {
+ $timestamp = new MWTimestamp( '1406833268' ); // Equivalent to 20140731190108
+ $this->assertEquals( '1406833268', $timestamp->__toString() );
+ }
+
+ public static function provideValidTimestampDifferences() {
+ return array(
+ array( '1406833268', '1406833269', '00 00 00 01' ),
+ array( '1406833268', '1406833329', '00 00 01 01' ),
+ array( '1406833268', '1406836929', '00 01 01 01' ),
+ array( '1406833268', '1406923329', '01 01 01 01' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideValidTimestampDifferences
+ * @covers MWTimestamp::diff
+ */
+ public function testDiff( $timestamp1, $timestamp2, $expected ) {
+ $timestamp1 = new MWTimestamp( $timestamp1 );
+ $timestamp2 = new MWTimestamp( $timestamp2 );
+ $diff = $timestamp1->diff( $timestamp2 );
+ $this->assertEquals( $expected, $diff->format( '%D %H %I %S' ) );
+ }
+
+ /**
+ * Test parsing of valid timestamps and outputing to MW format.
+ * @dataProvider provideValidTimestamps
+ * @covers MWTimestamp::getTimestamp
+ */
+ public function testValidParse( $format, $original, $expected ) {
+ $timestamp = new MWTimestamp( $original );
+ $this->assertEquals( $expected, $timestamp->getTimestamp( TS_MW ) );
+ }
+
+ /**
+ * Test outputting valid timestamps to different formats.
+ * @dataProvider provideValidTimestamps
+ * @covers MWTimestamp::getTimestamp
+ */
+ public function testValidOutput( $format, $expected, $original ) {
+ $timestamp = new MWTimestamp( $original );
+ $this->assertEquals( $expected, (string)$timestamp->getTimestamp( $format ) );
+ }
+
+ /**
+ * Test an invalid timestamp.
+ * @expectedException TimestampException
+ * @covers MWTimestamp
+ */
+ public function testInvalidParse() {
+ new MWTimestamp( "This is not a timestamp." );
+ }
+
+ /**
+ * Test requesting an invalid output format.
+ * @expectedException TimestampException
+ * @covers MWTimestamp::getTimestamp
+ */
+ public function testInvalidOutput() {
+ $timestamp = new MWTimestamp( '1343761268' );
+ $timestamp->getTimestamp( 98 );
+ }
+
+ /**
+ * Returns a list of valid timestamps in the format:
+ * array( type, timestamp_of_type, timestamp_in_MW )
+ */
+ public static function provideValidTimestamps() {
+ return array(
+ // Various formats
+ array( TS_UNIX, '1343761268', '20120731190108' ),
+ array( TS_MW, '20120731190108', '20120731190108' ),
+ array( TS_DB, '2012-07-31 19:01:08', '20120731190108' ),
+ array( TS_ISO_8601, '2012-07-31T19:01:08Z', '20120731190108' ),
+ array( TS_ISO_8601_BASIC, '20120731T190108Z', '20120731190108' ),
+ array( TS_EXIF, '2012:07:31 19:01:08', '20120731190108' ),
+ array( TS_RFC2822, 'Tue, 31 Jul 2012 19:01:08 GMT', '20120731190108' ),
+ array( TS_ORACLE, '31-07-2012 19:01:08.000000', '20120731190108' ),
+ array( TS_POSTGRES, '2012-07-31 19:01:08 GMT', '20120731190108' ),
+ // Some extremes and weird values
+ array( TS_ISO_8601, '9999-12-31T23:59:59Z', '99991231235959' ),
+ array( TS_UNIX, '-62135596801', '00001231235959' )
+ );
+ }
+
+ /**
+ * @dataProvider provideHumanTimestampTests
+ * @covers MWTimestamp::getHumanTimestamp
+ */
+ public function testHumanTimestamp(
+ $tsTime, // The timestamp to format
+ $currentTime, // The time to consider "now"
+ $timeCorrection, // The time offset to use
+ $dateFormat, // The date preference to use
+ $expectedOutput, // The expected output
+ $desc // Description
+ ) {
+ $user = $this->getMock( 'User' );
+ $user->expects( $this->any() )
+ ->method( 'getOption' )
+ ->with( 'timecorrection' )
+ ->will( $this->returnValue( $timeCorrection ) );
+
+ $user->expects( $this->any() )
+ ->method( 'getDatePreference' )
+ ->will( $this->returnValue( $dateFormat ) );
+
+ $tsTime = new MWTimestamp( $tsTime );
+ $currentTime = new MWTimestamp( $currentTime );
+
+ $this->assertEquals(
+ $expectedOutput,
+ $tsTime->getHumanTimestamp( $currentTime, $user ),
+ $desc
+ );
+ }
+
+ public static function provideHumanTimestampTests() {
+ return array(
+ array(
+ '20111231170000',
+ '20120101000000',
+ 'Offset|0',
+ 'mdy',
+ 'Yesterday at 17:00',
+ '"Yesterday" across years',
+ ),
+ array(
+ '20120717190900',
+ '20120717190929',
+ 'Offset|0',
+ 'mdy',
+ 'just now',
+ '"Just now"',
+ ),
+ array(
+ '20120717190900',
+ '20120717191530',
+ 'Offset|0',
+ 'mdy',
+ '6 minutes ago',
+ 'X minutes ago',
+ ),
+ array(
+ '20121006173100',
+ '20121006173200',
+ 'Offset|0',
+ 'mdy',
+ '1 minute ago',
+ '"1 minute ago"',
+ ),
+ array(
+ '20120617190900',
+ '20120717190900',
+ 'Offset|0',
+ 'mdy',
+ 'June 17',
+ 'Another month'
+ ),
+ array(
+ '19910130151500',
+ '20120716193700',
+ 'Offset|0',
+ 'mdy',
+ '15:15, January 30, 1991',
+ 'Different year',
+ ),
+ array(
+ '20120101050000',
+ '20120101080000',
+ 'Offset|-360',
+ 'mdy',
+ 'Yesterday at 23:00',
+ '"Yesterday" across years with time correction',
+ ),
+ array(
+ '20120714184300',
+ '20120716184300',
+ 'Offset|-420',
+ 'mdy',
+ 'Saturday at 11:43',
+ 'Recent weekday with time correction',
+ ),
+ array(
+ '20120714184300',
+ '20120715040000',
+ 'Offset|-420',
+ 'mdy',
+ '11:43',
+ 'Today at another time with time correction',
+ ),
+ array(
+ '20120617190900',
+ '20120717190900',
+ 'Offset|0',
+ 'dmy',
+ '17 June',
+ 'Another month with dmy'
+ ),
+ array(
+ '20120617190900',
+ '20120717190900',
+ 'Offset|0',
+ 'ISO 8601',
+ '06-17',
+ 'Another month with ISO-8601'
+ ),
+ array(
+ '19910130151500',
+ '20120716193700',
+ 'Offset|0',
+ 'ISO 8601',
+ '1991-01-30T15:15:00',
+ 'Different year with ISO-8601',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideRelativeTimestampTests
+ * @covers MWTimestamp::getRelativeTimestamp
+ */
+ public function testRelativeTimestamp(
+ $tsTime, // The timestamp to format
+ $currentTime, // The time to consider "now"
+ $timeCorrection, // The time offset to use
+ $dateFormat, // The date preference to use
+ $expectedOutput, // The expected output
+ $desc // Description
+ ) {
+ $user = $this->getMock( 'User' );
+ $user->expects( $this->any() )
+ ->method( 'getOption' )
+ ->with( 'timecorrection' )
+ ->will( $this->returnValue( $timeCorrection ) );
+
+ $tsTime = new MWTimestamp( $tsTime );
+ $currentTime = new MWTimestamp( $currentTime );
+
+ $this->assertEquals(
+ $expectedOutput,
+ $tsTime->getRelativeTimestamp( $currentTime, $user ),
+ $desc
+ );
+ }
+
+ public static function provideRelativeTimestampTests() {
+ return array(
+ array(
+ '20111231170000',
+ '20120101000000',
+ 'Offset|0',
+ 'mdy',
+ '7 hours ago',
+ '"Yesterday" across years',
+ ),
+ array(
+ '20120717190900',
+ '20120717190929',
+ 'Offset|0',
+ 'mdy',
+ '29 seconds ago',
+ '"Just now"',
+ ),
+ array(
+ '20120717190900',
+ '20120717191530',
+ 'Offset|0',
+ 'mdy',
+ '6 minutes and 30 seconds ago',
+ 'Combination of multiple units',
+ ),
+ array(
+ '20121006173100',
+ '20121006173200',
+ 'Offset|0',
+ 'mdy',
+ '1 minute ago',
+ '"1 minute ago"',
+ ),
+ array(
+ '19910130151500',
+ '20120716193700',
+ 'Offset|0',
+ 'mdy',
+ '2 decades, 1 year, 168 days, 2 hours, 8 minutes and 48 seconds ago',
+ 'A long time ago',
+ ),
+ array(
+ '20120101050000',
+ '20120101080000',
+ 'Offset|-360',
+ 'mdy',
+ '3 hours ago',
+ '"Yesterday" across years with time correction',
+ ),
+ array(
+ '20120714184300',
+ '20120716184300',
+ 'Offset|-420',
+ 'mdy',
+ '2 days ago',
+ 'Recent weekday with time correction',
+ ),
+ array(
+ '20120714184300',
+ '20120715040000',
+ 'Offset|-420',
+ 'mdy',
+ '9 hours and 17 minutes ago',
+ 'Today at another time with time correction',
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/MediaWikiVersionFetcherTest.php b/tests/phpunit/includes/MediaWikiVersionFetcherTest.php
new file mode 100644
index 00000000..e548f817
--- /dev/null
+++ b/tests/phpunit/includes/MediaWikiVersionFetcherTest.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * Note: this is not a unit test, as it touches the file system and reads an actual file.
+ * If unit tests are added for MediaWikiVersionFetcher, this should be done in a distinct test case.
+ *
+ * @covers MediaWikiVersionFetcher
+ *
+ * @group ComposerHooks
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class MediaWikiVersionFetcherTest extends PHPUnit_Framework_TestCase {
+
+ public function testReturnsResult() {
+ $versionFetcher = new MediaWikiVersionFetcher();
+ $this->assertInternalType( 'string', $versionFetcher->fetchVersion() );
+ }
+
+}
diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php
new file mode 100644
index 00000000..f3d2a84a
--- /dev/null
+++ b/tests/phpunit/includes/MessageTest.php
@@ -0,0 +1,368 @@
+<?php
+
+class MessageTest extends MediaWikiLangTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgLang' => Language::factory( 'en' ),
+ 'wgForceUIMsgAsContentMsg' => array(),
+ ) );
+ }
+
+ /**
+ * @covers Message::__construct
+ * @dataProvider provideConstructor
+ */
+ public function testConstructor( $expectedLang, $key, $params, $language ) {
+ $reflection = new ReflectionClass( 'Message' );
+
+ $keyProperty = $reflection->getProperty( 'key' );
+ $keyProperty->setAccessible( true );
+
+ $paramsProperty = $reflection->getProperty( 'parameters' );
+ $paramsProperty->setAccessible( true );
+
+ $langProperty = $reflection->getProperty( 'language' );
+ $langProperty->setAccessible( true );
+
+ $message = new Message( $key, $params, $language );
+
+ $this->assertEquals( $key, $keyProperty->getValue( $message ) );
+ $this->assertEquals( $params, $paramsProperty->getValue( $message ) );
+ $this->assertEquals( $expectedLang, $langProperty->getValue( $message ) );
+ }
+
+ public static function provideConstructor() {
+ $langDe = Language::factory( 'de' );
+ $langEn = Language::factory( 'en' );
+
+ return array(
+ array( $langDe, 'foo', array(), $langDe ),
+ array( $langDe, 'foo', array( 'bar' ), $langDe ),
+ array( $langEn, 'foo', array( 'bar' ), null )
+ );
+ }
+
+ public static function provideTestParams() {
+ return array(
+ array( array() ),
+ array( array( 'foo' ), 'foo' ),
+ array( array( 'foo', 'bar' ), 'foo', 'bar' ),
+ array( array( 'baz' ), array( 'baz' ) ),
+ array( array( 'baz', 'foo' ), array( 'baz', 'foo' ) ),
+ array( array( 'baz', 'foo' ), array( 'baz', 'foo' ), 'hhh' ),
+ array( array( 'baz', 'foo' ), array( 'baz', 'foo' ), 'hhh', array( 'ahahahahha' ) ),
+ array( array( 'baz', 'foo' ), array( 'baz', 'foo' ), array( 'ahahahahha' ) ),
+ array( array( 'baz' ), array( 'baz' ), array( 'ahahahahha' ) ),
+ );
+ }
+
+ public function getLanguageProvider() {
+ return array(
+ array( 'foo', array( 'bar' ), 'en' ),
+ array( 'foo', array( 'bar' ), 'de' )
+ );
+ }
+
+ /**
+ * @covers Message::getLanguage
+ * @dataProvider getLanguageProvider
+ */
+ public function testGetLanguageCode( $key, $params, $languageCode ) {
+ $language = Language::factory( $languageCode );
+ $message = new Message( $key, $params, $language );
+
+ $this->assertEquals( $language, $message->getLanguage() );
+ }
+
+ /**
+ * @covers Message::params
+ * @dataProvider provideTestParams
+ */
+ public function testParams( $expected ) {
+ $msg = new Message( 'imasomething' );
+
+ $returned = call_user_func_array( array( $msg, 'params' ), array_slice( func_get_args(), 1 ) );
+
+ $this->assertSame( $msg, $returned );
+ $this->assertEquals( $expected, $msg->getParams() );
+ }
+
+ /**
+ * @covers Message::exists
+ */
+ public 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() );
+ }
+
+ /**
+ * @covers Message::__construct
+ */
+ public 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() );
+ $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->plain() );
+ $this->assertEquals( '&lt;i-dont-exist-evar&gt;', wfMessage( 'i-dont-exist-evar' )->escaped() );
+ }
+
+ /**
+ * @covers Message::inLanguage
+ */
+ public function testInLanguage() {
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inLanguage( 'en' )->text() );
+ $this->assertEquals( 'Заглавная страница',
+ wfMessage( 'mainpage' )->inLanguage( 'ru' )->text() );
+
+ // NOTE: make sure internal caching of the message text is reset appropriately
+ $msg = wfMessage( 'mainpage' );
+ $this->assertEquals( 'Main Page', $msg->inLanguage( Language::factory( 'en' ) )->text() );
+ $this->assertEquals(
+ 'Заглавная страница',
+ $msg->inLanguage( Language::factory( 'ru' ) )->text()
+ );
+ }
+
+ /**
+ * @covers Message::__construct
+ */
+ public 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()
+ );
+ }
+
+ /**
+ * @covers Message::__construct
+ * @covers Message::rawParams
+ */
+ public 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()
+ );
+ }
+
+ /**
+ * @covers Message::__construct
+ * @covers Message::params
+ */
+ public function testDeliciouslyManyParams() {
+ $msg = new RawMessage( '$1$2$3$4$5$6$7$8$9$10$11$12' );
+ // One less than above has placeholders
+ $params = array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k' );
+ $this->assertEquals(
+ 'abcdefghijka2',
+ $msg->params( $params )->plain(),
+ 'Params > 9 are replaced correctly'
+ );
+ }
+
+ /**
+ * @covers Message::numParams
+ */
+ public function testMessageNumParams() {
+ $lang = Language::factory( 'en' );
+ $msg = new RawMessage( '$1' );
+
+ $this->assertEquals(
+ $lang->formatNum( 123456.789 ),
+ $msg->inLanguage( $lang )->numParams( 123456.789 )->plain(),
+ 'numParams is handled correctly'
+ );
+ }
+
+ /**
+ * @covers Message::durationParams
+ */
+ public function testMessageDurationParams() {
+ $lang = Language::factory( 'en' );
+ $msg = new RawMessage( '$1' );
+
+ $this->assertEquals(
+ $lang->formatDuration( 1234 ),
+ $msg->inLanguage( $lang )->durationParams( 1234 )->plain(),
+ 'durationParams is handled correctly'
+ );
+ }
+
+ /**
+ * FIXME: This should not need database, but Language#formatExpiry does (bug 55912)
+ * @group Database
+ * @covers Message::expiryParams
+ */
+ public function testMessageExpiryParams() {
+ $lang = Language::factory( 'en' );
+ $msg = new RawMessage( '$1' );
+
+ $this->assertEquals(
+ $lang->formatExpiry( wfTimestampNow() ),
+ $msg->inLanguage( $lang )->expiryParams( wfTimestampNow() )->plain(),
+ 'expiryParams is handled correctly'
+ );
+ }
+
+ /**
+ * @covers Message::timeperiodParams
+ */
+ public function testMessageTimeperiodParams() {
+ $lang = Language::factory( 'en' );
+ $msg = new RawMessage( '$1' );
+
+ $this->assertEquals(
+ $lang->formatTimePeriod( 1234 ),
+ $msg->inLanguage( $lang )->timeperiodParams( 1234 )->plain(),
+ 'timeperiodParams is handled correctly'
+ );
+ }
+
+ /**
+ * @covers Message::sizeParams
+ */
+ public function testMessageSizeParams() {
+ $lang = Language::factory( 'en' );
+ $msg = new RawMessage( '$1' );
+
+ $this->assertEquals(
+ $lang->formatSize( 123456 ),
+ $msg->inLanguage( $lang )->sizeParams( 123456 )->plain(),
+ 'sizeParams is handled correctly'
+ );
+ }
+
+ /**
+ * @covers Message::bitrateParams
+ */
+ public function testMessageBitrateParams() {
+ $lang = Language::factory( 'en' );
+ $msg = new RawMessage( '$1' );
+
+ $this->assertEquals(
+ $lang->formatBitrate( 123456 ),
+ $msg->inLanguage( $lang )->bitrateParams( 123456 )->plain(),
+ 'bitrateParams is handled correctly'
+ );
+ }
+
+ /**
+ * @covers Message::inContentLanguage
+ */
+ public function testInContentLanguage() {
+ $this->setMwGlobals( 'wgLang', Language::factory( 'fr' ) );
+
+ // NOTE: make sure internal caching of the message text is reset appropriately
+ $msg = wfMessage( 'mainpage' );
+ $this->assertEquals( 'Hauptseite', $msg->inLanguage( 'de' )->plain(), "inLanguage( 'de' )" );
+ $this->assertEquals( 'Main Page', $msg->inContentLanguage()->plain(), "inContentLanguage()" );
+ $this->assertEquals( 'Accueil', $msg->inLanguage( 'fr' )->plain(), "inLanguage( 'fr' )" );
+ }
+
+ /**
+ * @covers Message::inContentLanguage
+ */
+ public function testInContentLanguageOverride() {
+ $this->setMwGlobals( array(
+ 'wgLang' => Language::factory( 'fr' ),
+ 'wgForceUIMsgAsContentMsg' => array( 'mainpage' ),
+ ) );
+
+ // NOTE: make sure internal caching of the message text is reset appropriately.
+ // NOTE: wgForceUIMsgAsContentMsg forces the messages *current* language to be used.
+ $msg = wfMessage( 'mainpage' );
+ $this->assertEquals(
+ 'Accueil',
+ $msg->inContentLanguage()->plain(),
+ 'inContentLanguage() with ForceUIMsg override enabled'
+ );
+ $this->assertEquals( 'Main Page', $msg->inLanguage( 'en' )->plain(), "inLanguage( 'en' )" );
+ $this->assertEquals(
+ 'Main Page',
+ $msg->inContentLanguage()->plain(),
+ 'inContentLanguage() with ForceUIMsg override enabled'
+ );
+ $this->assertEquals( 'Hauptseite', $msg->inLanguage( 'de' )->plain(), "inLanguage( 'de' )" );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers Message::inLanguage
+ */
+ public function testInLanguageThrows() {
+ wfMessage( 'foo' )->inLanguage( 123 );
+ }
+
+ public function keyProvider() {
+ return array(
+ 'string' => array(
+ 'key' => 'mainpage',
+ 'expected' => array( 'mainpage' ),
+ ),
+ 'single' => array(
+ 'key' => array( 'mainpage' ),
+ 'expected' => array( 'mainpage' ),
+ ),
+ 'multi' => array(
+ 'key' => array( 'mainpage-foo', 'mainpage-bar', 'mainpage' ),
+ 'expected' => array( 'mainpage-foo', 'mainpage-bar', 'mainpage' ),
+ ),
+ 'empty' => array(
+ 'key' => array(),
+ 'expected' => null,
+ 'exception' => 'InvalidArgumentException',
+ ),
+ 'null' => array(
+ 'key' => null,
+ 'expected' => null,
+ 'exception' => 'InvalidArgumentException',
+ ),
+ 'bad type' => array(
+ 'key' => 17,
+ 'expected' => null,
+ 'exception' => 'InvalidArgumentException',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider keyProvider()
+ *
+ * @covers Message::getKey
+ */
+ public function testGetKey( $key, $expected, $exception = null ) {
+ if ( $exception ) {
+ $this->setExpectedException( $exception );
+ }
+
+ $msg = new Message( $key );
+ $this->assertEquals( $expected, $msg->getKeysToTry() );
+ $this->assertEquals( count( $expected ) > 1, $msg->isMultiKey() );
+ $this->assertContains( $msg->getKey(), $expected );
+ }
+}
diff --git a/tests/phpunit/includes/MimeMagicTest.php b/tests/phpunit/includes/MimeMagicTest.php
new file mode 100644
index 00000000..742d3827
--- /dev/null
+++ b/tests/phpunit/includes/MimeMagicTest.php
@@ -0,0 +1,49 @@
+<?php
+class MimeMagicTest extends MediaWikiTestCase {
+
+ /** @var MimeMagic */
+ private $mimeMagic;
+
+ function setUp() {
+ $this->mimeMagic = MimeMagic::singleton();
+ parent::setUp();
+ }
+
+ /**
+ * @dataProvider providerImproveTypeFromExtension
+ * @param string $ext File extension (no leading dot)
+ * @param string $oldMime Initially detected MIME
+ * @param string $expectedMime MIME type after taking extension into account
+ */
+ function testImproveTypeFromExtension( $ext, $oldMime, $expectedMime ) {
+ $actualMime = $this->mimeMagic->improveTypeFromExtension( $oldMime, $ext );
+ $this->assertEquals( $expectedMime, $actualMime );
+ }
+
+ function providerImproveTypeFromExtension() {
+ return array(
+ array( 'gif', 'image/gif', 'image/gif' ),
+ array( 'gif', 'unknown/unknown', 'unknown/unknown' ),
+ array( 'wrl', 'unknown/unknown', 'model/vrml' ),
+ array( 'txt', 'text/plain', 'text/plain' ),
+ array( 'csv', 'text/plain', 'text/csv' ),
+ array( 'tsv', 'text/plain', 'text/tab-separated-values' ),
+ array( 'json', 'text/plain', 'application/json' ),
+ array( 'foo', 'application/x-opc+zip', 'application/zip' ),
+ array( 'docx', 'application/x-opc+zip',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ),
+ array( 'djvu', 'image/x-djvu', 'image/vnd.djvu' ),
+ array( 'wav', 'audio/wav', 'audio/wav' ),
+ );
+ }
+
+ /**
+ * Test to make sure that encoder=ffmpeg2theora doesn't trigger
+ * MEDIATYPE_VIDEO (bug 63584)
+ */
+ function testOggRecognize() {
+ $oggFile = __DIR__ . '/../data/media/say-test.ogg';
+ $actualType = $this->mimeMagic->getMediaType( $oggFile, 'application/ogg' );
+ $this->assertEquals( $actualType, MEDIATYPE_AUDIO );
+ }
+}
diff --git a/tests/phpunit/includes/OutputPageTest.php b/tests/phpunit/includes/OutputPageTest.php
new file mode 100644
index 00000000..d7e8cd31
--- /dev/null
+++ b/tests/phpunit/includes/OutputPageTest.php
@@ -0,0 +1,273 @@
+<?php
+
+/**
+ *
+ * @author Matthew Flaschen
+ *
+ * @group Output
+ *
+ * @todo factor tests in this class into providers and test methods
+ *
+ */
+class OutputPageTest extends MediaWikiTestCase {
+ const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
+ const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
+
+ /**
+ * Tests a particular case of transformCssMedia, using the given input, globals,
+ * expected return, and message
+ *
+ * Asserts that $expectedReturn is returned.
+ *
+ * options['printableQuery'] - value of query string for printable, or omitted for none
+ * options['handheldQuery'] - value of query string for handheld, or omitted for none
+ * options['media'] - passed into the method under the same name
+ * options['expectedReturn'] - expected return value
+ * options['message'] - PHPUnit message for assertion
+ *
+ * @param array $args Key-value array of arguments as shown above
+ */
+ protected function assertTransformCssMediaCase( $args ) {
+ $queryData = array();
+ if ( isset( $args['printableQuery'] ) ) {
+ $queryData['printable'] = $args['printableQuery'];
+ }
+
+ if ( isset( $args['handheldQuery'] ) ) {
+ $queryData['handheld'] = $args['handheldQuery'];
+ }
+
+ $fauxRequest = new FauxRequest( $queryData, false );
+ $this->setMwGlobals( array(
+ 'wgRequest' => $fauxRequest,
+ ) );
+
+ $actualReturn = OutputPage::transformCssMedia( $args['media'] );
+ $this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] );
+ }
+
+ /**
+ * Tests print requests
+ * @covers OutputPage::transformCssMedia
+ */
+ public function testPrintRequests() {
+ $this->assertTransformCssMediaCase( array(
+ 'printableQuery' => '1',
+ 'media' => 'screen',
+ 'expectedReturn' => null,
+ 'message' => 'On printable request, screen returns null'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'printableQuery' => '1',
+ 'media' => self::SCREEN_MEDIA_QUERY,
+ 'expectedReturn' => null,
+ 'message' => 'On printable request, screen media query returns null'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'printableQuery' => '1',
+ 'media' => self::SCREEN_ONLY_MEDIA_QUERY,
+ 'expectedReturn' => null,
+ 'message' => 'On printable request, screen media query with only returns null'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'printableQuery' => '1',
+ 'media' => 'print',
+ 'expectedReturn' => '',
+ 'message' => 'On printable request, media print returns empty string'
+ ) );
+ }
+
+ /**
+ * Tests screen requests, without either query parameter set
+ * @covers OutputPage::transformCssMedia
+ */
+ public function testScreenRequests() {
+ $this->assertTransformCssMediaCase( array(
+ 'media' => 'screen',
+ 'expectedReturn' => 'screen',
+ 'message' => 'On screen request, screen media type is preserved'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'media' => 'handheld',
+ 'expectedReturn' => 'handheld',
+ 'message' => 'On screen request, handheld media type is preserved'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'media' => self::SCREEN_MEDIA_QUERY,
+ 'expectedReturn' => self::SCREEN_MEDIA_QUERY,
+ 'message' => 'On screen request, screen media query is preserved.'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'media' => self::SCREEN_ONLY_MEDIA_QUERY,
+ 'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY,
+ 'message' => 'On screen request, screen media query with only is preserved.'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'media' => 'print',
+ 'expectedReturn' => 'print',
+ 'message' => 'On screen request, print media type is preserved'
+ ) );
+ }
+
+ /**
+ * Tests handheld behavior
+ * @covers OutputPage::transformCssMedia
+ */
+ public function testHandheld() {
+ $this->assertTransformCssMediaCase( array(
+ 'handheldQuery' => '1',
+ 'media' => 'handheld',
+ 'expectedReturn' => '',
+ 'message' => 'On request with handheld querystring and media is handheld, returns empty string'
+ ) );
+
+ $this->assertTransformCssMediaCase( array(
+ 'handheldQuery' => '1',
+ 'media' => 'screen',
+ 'expectedReturn' => null,
+ 'message' => 'On request with handheld querystring and media is screen, returns null'
+ ) );
+ }
+
+ public static function provideMakeResourceLoaderLink() {
+ return array(
+ // Load module script only
+ array(
+ array( 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ),
+ '<script>if(window.mw){
+document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false\u0026amp;lang=en\u0026amp;modules=test.foo\u0026amp;only=scripts\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");
+}</script>
+'
+ ),
+ array(
+ // Don't condition wrap raw modules (like the startup module)
+ array( 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ),
+ '<script src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback&amp;*"></script>
+'
+ ),
+ // Load module styles only
+ // This also tests the order the modules are put into the url
+ array(
+ array( array( 'test.baz', 'test.foo', 'test.bar' ), ResourceLoaderModule::TYPE_STYLES ),
+ '<link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback&amp;*">
+'
+ ),
+ // Load private module (only=scripts)
+ array(
+ array( 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ),
+ '<script>if(window.mw){
+mw.test.baz({token:123});mw.loader.state({"test.quux":"ready"});
+
+}</script>
+'
+ ),
+ // Load private module (combined)
+ array(
+ array( 'test.quux', ResourceLoaderModule::TYPE_COMBINED ),
+ '<script>if(window.mw){
+mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"css":[".mw-icon{transition:none}\n"]},{});
+
+}</script>
+'
+ ),
+ // Load module script with with ESI
+ array(
+ array( 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS, true ),
+ '<script><esi:include src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.foo&amp;only=scripts&amp;skin=fallback&amp;*" /></script>
+'
+ ),
+ // Load module styles with with ESI
+ array(
+ array( 'test.foo', ResourceLoaderModule::TYPE_STYLES, true ),
+ '<style><esi:include src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.foo&amp;only=styles&amp;skin=fallback&amp;*" /></style>
+',
+ ),
+ // Load no modules
+ array(
+ array( array(), ResourceLoaderModule::TYPE_COMBINED ),
+ '',
+ ),
+ // noscript group
+ array(
+ array( 'test.noscript', ResourceLoaderModule::TYPE_STYLES ),
+ '<noscript><link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback&amp;*"></noscript>
+'
+ ),
+ // Load two modules in separate groups
+ array(
+ array( array( 'test.group.foo', 'test.group.bar' ), ResourceLoaderModule::TYPE_COMBINED ),
+ '<script src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.group.bar&amp;skin=fallback&amp;*"></script>
+<script src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.group.foo&amp;skin=fallback&amp;*"></script>
+',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideMakeResourceLoaderLink
+ * @covers OutputPage::makeResourceLoaderLink
+ */
+ public function testMakeResourceLoaderLink( $args, $expectedHtml ) {
+ $this->setMwGlobals( array(
+ 'wgResourceLoaderDebug' => false,
+ 'wgResourceLoaderUseESI' => true,
+ 'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
+ // Affects whether CDATA is inserted
+ 'wgWellFormedXml' => false,
+ ) );
+ $class = new ReflectionClass( 'OutputPage' );
+ $method = $class->getMethod( 'makeResourceLoaderLink' );
+ $method->setAccessible( true );
+ $ctx = new RequestContext();
+ $ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
+ $ctx->setLanguage( 'en' );
+ $out = new OutputPage( $ctx );
+ $rl = $out->getResourceLoader();
+ $rl->register( array(
+ 'test.foo' => new ResourceLoaderTestModule( array(
+ 'script' => 'mw.test.foo( { a: true } );',
+ 'styles' => '.mw-test-foo { content: "style"; }',
+ )),
+ 'test.bar' => new ResourceLoaderTestModule( array(
+ 'script' => 'mw.test.bar( { a: true } );',
+ 'styles' => '.mw-test-bar { content: "style"; }',
+ )),
+ 'test.baz' => new ResourceLoaderTestModule( array(
+ 'script' => 'mw.test.baz( { a: true } );',
+ 'styles' => '.mw-test-baz { content: "style"; }',
+ )),
+ 'test.quux' => new ResourceLoaderTestModule( array(
+ 'script' => 'mw.test.baz( { token: 123 } );',
+ 'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
+ 'group' => 'private',
+ )),
+ 'test.raw' => new ResourceLoaderTestModule( array(
+ 'script' => 'mw.test.baz( { token: 123 } );',
+ 'isRaw' => true,
+ )),
+ 'test.noscript' => new ResourceLoaderTestModule( array(
+ 'styles' => '.mw-test-noscript { content: "style"; }',
+ 'group' => 'noscript',
+ )),
+ 'test.group.bar' => new ResourceLoaderTestModule( array(
+ 'styles' => '.mw-group-bar { content: "style"; }',
+ 'group' => 'bar',
+ )),
+ 'test.group.foo' => new ResourceLoaderTestModule( array(
+ 'styles' => '.mw-group-foo { content: "style"; }',
+ 'group' => 'foo',
+ )),
+ ) );
+ $links = $method->invokeArgs( $out, $args );
+ // Strip comments to avoid variation due to wgDBname in WikiID and cache key
+ $actualHtml = preg_replace( '#/\*[^*]+\*/#', '', $links['html'] );
+ $this->assertEquals( $expectedHtml, $actualHtml );
+ }
+}
diff --git a/tests/phpunit/includes/PasswordTest.php b/tests/phpunit/includes/PasswordTest.php
new file mode 100644
index 00000000..ceb794b5
--- /dev/null
+++ b/tests/phpunit/includes/PasswordTest.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Testing framework for the Password infrastructure
+ *
+ * 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 PasswordTest extends MediaWikiTestCase {
+ /**
+ * @covers InvalidPassword::equals
+ */
+ public function testInvalidUnequalInvalid() {
+ $invalid1 = User::getPasswordFactory()->newFromCiphertext( null );
+ $invalid2 = User::getPasswordFactory()->newFromCiphertext( null );
+
+ $this->assertFalse( $invalid1->equals( $invalid2 ) );
+ }
+}
diff --git a/tests/phpunit/includes/PathRouterTest.php b/tests/phpunit/includes/PathRouterTest.php
new file mode 100644
index 00000000..0d782687
--- /dev/null
+++ b/tests/phpunit/includes/PathRouterTest.php
@@ -0,0 +1,264 @@
+<?php
+
+/**
+ * Tests for the PathRouter parsing.
+ *
+ * @covers PathRouter
+ */
+class PathRouterTest extends MediaWikiTestCase {
+
+ /**
+ * @var PathRouter
+ */
+ protected $basicRouter;
+
+ protected function setUp() {
+ parent::setUp();
+ $router = new PathRouter;
+ $router->add( "/wiki/$1" );
+ $this->basicRouter = $router;
+ }
+
+ /**
+ * Test basic path parsing
+ */
+ public function testBasic() {
+ $matches = $this->basicRouter->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+ }
+
+ /**
+ * Test loose path auto-$1
+ */
+ public function testLoose() {
+ $router = new PathRouter;
+ $router->add( "/" ); # Should be the same as "/$1"
+ $matches = $router->parse( "/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+
+ $router = new PathRouter;
+ $router->add( "/wiki" ); # Should be the same as /wiki/$1
+ $matches = $router->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+
+ $router = new PathRouter;
+ $router->add( "/wiki/" ); # Should be the same as /wiki/$1
+ $matches = $router->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+ }
+
+ /**
+ * Test to ensure that path is based on specifity, not order
+ */
+ public function testOrder() {
+ $router = new PathRouter;
+ $router->add( "/$1" );
+ $router->add( "/a/$1" );
+ $router->add( "/b/$1" );
+ $matches = $router->parse( "/a/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+
+ $router = new PathRouter;
+ $router->add( "/b/$1" );
+ $router->add( "/a/$1" );
+ $router->add( "/$1" );
+ $matches = $router->parse( "/a/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo" ) );
+ }
+
+ /**
+ * Test the handling of key based arrays with a url parameter
+ */
+ public function testKeyParameter() {
+ $router = new PathRouter;
+ $router->add( array( 'edit' => "/edit/$1" ), array( 'action' => '$key' ) );
+ $matches = $router->parse( "/edit/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo", 'action' => 'edit' ) );
+ }
+
+ /**
+ * Test the handling of $2 inside paths
+ */
+ public function testAdditionalParameter() {
+ // Basic $2
+ $router = new PathRouter;
+ $router->add( '/$2/$1', array( 'test' => '$2' ) );
+ $matches = $router->parse( "/asdf/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo", 'test' => 'asdf' ) );
+ }
+
+ /**
+ * Test additional restricted value parameter
+ */
+ public function testRestrictedValue() {
+ $router = new PathRouter;
+ $router->add( '/$2/$1',
+ array( 'test' => '$2' ),
+ array( '$2' => array( 'a', 'b' ) )
+ );
+ $router->add( '/$2/$1',
+ array( 'test2' => '$2' ),
+ array( '$2' => 'c' )
+ );
+ $router->add( '/$1' );
+
+ $matches = $router->parse( "/asdf/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "asdf/Foo" ) );
+
+ $matches = $router->parse( "/a/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo", 'test' => 'a' ) );
+
+ $matches = $router->parse( "/c/Foo" );
+ $this->assertEquals( $matches, array( 'title' => "Foo", 'test2' => 'c' ) );
+ }
+
+ public function callbackForTest( &$matches, $data ) {
+ $matches['x'] = $data['$1'];
+ $matches['foo'] = $data['foo'];
+ }
+
+ public function testCallback() {
+ $router = new PathRouter;
+ $router->add( "/$1",
+ array( 'a' => 'b', 'data:foo' => 'bar' ),
+ array( 'callback' => array( $this, 'callbackForTest' ) )
+ );
+ $matches = $router->parse( '/Foo' );
+ $this->assertEquals( $matches, array(
+ 'title' => "Foo",
+ 'x' => 'Foo',
+ 'a' => 'b',
+ 'foo' => 'bar'
+ ) );
+ }
+
+ /**
+ * Test to ensure that matches are not made if a parameter expects nonexistent input
+ */
+ public function testFail() {
+ $router = new PathRouter;
+ $router->add( "/wiki/$1", array( 'title' => "$1$2" ) );
+ $matches = $router->parse( "/wiki/A" );
+ $this->assertEquals( array(), $matches );
+ }
+
+ /**
+ * Test to ensure weight of paths is handled correctly
+ */
+ public function testWeight() {
+ $router = new PathRouter;
+ $router->addStrict( "/Bar", array( 'ping' => 'pong' ) );
+ $router->add( "/asdf-$1", array( 'title' => 'qwerty-$1' ) );
+ $router->add( "/$1" );
+ $router->add( "/qwerty-$1", array( 'title' => 'asdf-$1' ) );
+ $router->addStrict( "/Baz", array( 'marco' => 'polo' ) );
+ $router->add( "/a/$1" );
+ $router->add( "/asdf/$1" );
+ $router->add( "/$2/$1", array( 'unrestricted' => '$2' ) );
+ $router->add( array( 'qwerty' => "/qwerty/$1" ), array( 'qwerty' => '$key' ) );
+ $router->add( "/$2/$1", array( 'restricted-to-y' => '$2' ), array( '$2' => 'y' ) );
+
+ foreach (
+ array(
+ '/Foo' => array( 'title' => 'Foo' ),
+ '/Bar' => array( 'ping' => 'pong' ),
+ '/Baz' => array( 'marco' => 'polo' ),
+ '/asdf-foo' => array( 'title' => 'qwerty-foo' ),
+ '/qwerty-bar' => array( 'title' => 'asdf-bar' ),
+ '/a/Foo' => array( 'title' => 'Foo' ),
+ '/asdf/Foo' => array( 'title' => 'Foo' ),
+ '/qwerty/Foo' => array( 'title' => 'Foo', 'qwerty' => 'qwerty' ),
+ '/baz/Foo' => array( 'title' => 'Foo', 'unrestricted' => 'baz' ),
+ '/y/Foo' => array( 'title' => 'Foo', 'restricted-to-y' => 'y' ),
+ ) as $path => $result
+ ) {
+ $this->assertEquals( $router->parse( $path ), $result );
+ }
+ }
+
+ /**
+ * Make sure the router handles titles like Special:Recentchanges correctly
+ */
+ public function testSpecial() {
+ $matches = $this->basicRouter->parse( "/wiki/Special:Recentchanges" );
+ $this->assertEquals( $matches, array( 'title' => "Special:Recentchanges" ) );
+ }
+
+ /**
+ * Make sure the router decodes urlencoding properly
+ */
+ public function testUrlencoding() {
+ $matches = $this->basicRouter->parse( "/wiki/Title_With%20Space" );
+ $this->assertEquals( $matches, array( 'title' => "Title_With Space" ) );
+ }
+
+ public static function provideRegexpChars() {
+ return array(
+ array( "$" ),
+ array( "$1" ),
+ array( "\\" ),
+ array( "\\$1" ),
+ );
+ }
+
+ /**
+ * Make sure the router doesn't break on special characters like $ used in regexp replacements
+ * @dataProvider provideRegexpChars
+ */
+ public function testRegexpChars( $char ) {
+ $matches = $this->basicRouter->parse( "/wiki/$char" );
+ $this->assertEquals( $matches, array( 'title' => "$char" ) );
+ }
+
+ /**
+ * Make sure the router handles characters like +&() properly
+ */
+ public function testCharacters() {
+ $matches = $this->basicRouter->parse( "/wiki/Plus+And&Dollar\\Stuff();[]{}*" );
+ $this->assertEquals( $matches, array( 'title' => "Plus+And&Dollar\\Stuff();[]{}*" ) );
+ }
+
+ /**
+ * Make sure the router handles unicode characters correctly
+ * @depends testSpecial
+ * @depends testUrlencoding
+ * @depends testCharacters
+ */
+ public function testUnicode() {
+ $matches = $this->basicRouter->parse( "/wiki/Spécial:Modifications_récentes" );
+ $this->assertEquals( $matches, array( 'title' => "Spécial:Modifications_récentes" ) );
+
+ $matches = $this->basicRouter->parse( "/wiki/Sp%C3%A9cial:Modifications_r%C3%A9centes" );
+ $this->assertEquals( $matches, array( 'title' => "Spécial:Modifications_récentes" ) );
+ }
+
+ /**
+ * Ensure the router doesn't choke on long paths.
+ */
+ public function testLength() {
+ // @codingStandardsIgnoreStart Ignore long line warnings
+ $matches = $this->basicRouter->parse( "/wiki/Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum." );
+ $this->assertEquals( $matches, array( 'title' => "Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum." ) );
+ // @codingStandardsIgnoreEnd
+ }
+
+ /**
+ * Ensure that the php passed site of parameter values are not urldecoded
+ */
+ public function testPatternUrlencoding() {
+ $router = new PathRouter;
+ $router->add( "/wiki/$1", array( 'title' => '%20:$1' ) );
+ $matches = $router->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => '%20:Foo' ) );
+ }
+
+ /**
+ * Ensure that raw parameter values do not have any variable replacements or urldecoding
+ */
+ public function testRawParamValue() {
+ $router = new PathRouter;
+ $router->add( "/wiki/$1", array( 'title' => array( 'value' => 'bar%20$1' ) ) );
+ $matches = $router->parse( "/wiki/Foo" );
+ $this->assertEquals( $matches, array( 'title' => 'bar%20$1' ) );
+ }
+}
diff --git a/tests/phpunit/includes/PreferencesTest.php b/tests/phpunit/includes/PreferencesTest.php
new file mode 100644
index 00000000..5841bb6f
--- /dev/null
+++ b/tests/phpunit/includes/PreferencesTest.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @group Database
+ */
+class PreferencesTest extends MediaWikiTestCase {
+ /**
+ * @var User[]
+ */
+ private $prefUsers;
+ /**
+ * @var RequestContext
+ */
+ private $context;
+
+ public function __construct() {
+ parent::__construct();
+
+ $this->prefUsers['noemail'] = new User;
+
+ $this->prefUsers['notauth'] = new User;
+ $this->prefUsers['notauth']
+ ->setEmail( 'noauth@example.org' );
+
+ $this->prefUsers['auth'] = new User;
+ $this->prefUsers['auth']
+ ->setEmail( 'noauth@example.org' );
+ $this->prefUsers['auth']
+ ->setEmailAuthenticationTimestamp( 1330946623 );
+
+ $this->context = new RequestContext;
+ $this->context->setTitle( Title::newFromText( 'PreferencesTest' ) );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgEnableEmail' => true,
+ 'wgEmailAuthentication' => true,
+ ) );
+ }
+
+ /**
+ * Placeholder to verify bug 34302
+ * @covers Preferences::profilePreferences
+ */
+ public function testEmailFieldsWhenUserHasNoEmail() {
+ $prefs = $this->prefsFor( 'noemail' );
+ $this->assertArrayHasKey( 'cssclass',
+ $prefs['emailaddress']
+ );
+ $this->assertEquals( 'mw-email-none', $prefs['emailaddress']['cssclass'] );
+ }
+
+ /**
+ * Placeholder to verify bug 34302
+ * @covers Preferences::profilePreferences
+ */
+ public function testEmailFieldsWhenUserEmailNotAuthenticated() {
+ $prefs = $this->prefsFor( 'notauth' );
+ $this->assertArrayHasKey( 'cssclass',
+ $prefs['emailaddress']
+ );
+ $this->assertEquals( 'mw-email-not-authenticated', $prefs['emailaddress']['cssclass'] );
+ }
+
+ /**
+ * Placeholder to verify bug 34302
+ * @covers Preferences::profilePreferences
+ */
+ public function testEmailFieldsWhenUserEmailIsAuthenticated() {
+ $prefs = $this->prefsFor( 'auth' );
+ $this->assertArrayHasKey( 'cssclass',
+ $prefs['emailaddress']
+ );
+ $this->assertEquals( 'mw-email-authenticated', $prefs['emailaddress']['cssclass'] );
+ }
+
+ /** Helper */
+ protected function prefsFor( $user_key ) {
+ $preferences = array();
+ Preferences::profilePreferences(
+ $this->prefUsers[$user_key],
+ $this->context,
+ $preferences
+ );
+
+ return $preferences;
+ }
+}
diff --git a/tests/phpunit/includes/RequestContextTest.php b/tests/phpunit/includes/RequestContextTest.php
new file mode 100644
index 00000000..cae0e52e
--- /dev/null
+++ b/tests/phpunit/includes/RequestContextTest.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @group Database
+ * @group RequestContext
+ */
+class RequestContextTest extends MediaWikiTestCase {
+
+ /**
+ * Test the relationship between title and wikipage in RequestContext
+ * @covers RequestContext::getWikiPage
+ * @covers RequestContext::getTitle
+ */
+ public function testWikiPageTitle() {
+ $context = new RequestContext();
+
+ $curTitle = Title::newFromText( "A" );
+ $context->setTitle( $curTitle );
+ $this->assertTrue( $curTitle->equals( $context->getWikiPage()->getTitle() ),
+ "When a title is first set WikiPage should be created on-demand for that title." );
+
+ $curTitle = Title::newFromText( "B" );
+ $context->setWikiPage( WikiPage::factory( $curTitle ) );
+ $this->assertTrue( $curTitle->equals( $context->getTitle() ),
+ "Title must be updated when a new WikiPage is provided." );
+
+ $curTitle = Title::newFromText( "C" );
+ $context->setTitle( $curTitle );
+ $this->assertTrue(
+ $curTitle->equals( $context->getWikiPage()->getTitle() ),
+ "When a title is updated the WikiPage should be purged "
+ . "and recreated on-demand with the new title."
+ );
+ }
+
+ /**
+ * @covers RequestContext::importScopedSession
+ */
+ public function testImportScopedSession() {
+ $context = RequestContext::getMain();
+
+ $oInfo = $context->exportSession();
+ $this->assertEquals( '127.0.0.1', $oInfo['ip'], "Correct initial IP address." );
+ $this->assertEquals( 0, $oInfo['userId'], "Correct initial user ID." );
+
+ $user = User::newFromName( 'UnitTestContextUser' );
+ $user->addToDatabase();
+
+ $sinfo = array(
+ 'sessionId' => 'd612ee607c87e749ef14da4983a702cd',
+ 'userId' => $user->getId(),
+ 'ip' => '192.0.2.0',
+ 'headers' => array(
+ 'USER-AGENT' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:18.0) Gecko/20100101 Firefox/18.0'
+ )
+ );
+ // importScopedSession() sets these variables
+ $this->setMwGlobals( array(
+ 'wgUser' => new User,
+ 'wgRequest' => new FauxRequest,
+ ) );
+ $sc = RequestContext::importScopedSession( $sinfo ); // load new context
+
+ $info = $context->exportSession();
+ $this->assertEquals( $sinfo['ip'], $info['ip'], "Correct IP address." );
+ $this->assertEquals( $sinfo['headers'], $info['headers'], "Correct headers." );
+ $this->assertEquals( $sinfo['sessionId'], $info['sessionId'], "Correct session ID." );
+ $this->assertEquals( $sinfo['userId'], $info['userId'], "Correct user ID." );
+ $this->assertEquals(
+ $sinfo['ip'],
+ $context->getRequest()->getIP(),
+ "Correct context IP address."
+ );
+ $this->assertEquals(
+ $sinfo['headers'],
+ $context->getRequest()->getAllHeaders(),
+ "Correct context headers."
+ );
+ $this->assertEquals( $sinfo['sessionId'], session_id(), "Correct context session ID." );
+ $this->assertEquals( true, $context->getUser()->isLoggedIn(), "Correct context user." );
+ $this->assertEquals( $sinfo['userId'], $context->getUser()->getId(), "Correct context user ID." );
+ $this->assertEquals(
+ 'UnitTestContextUser',
+ $context->getUser()->getName(),
+ "Correct context user name."
+ );
+
+ unset( $sc ); // restore previous context
+
+ $info = $context->exportSession();
+ $this->assertEquals( $oInfo['ip'], $info['ip'], "Correct initial IP address." );
+ $this->assertEquals( $oInfo['headers'], $info['headers'], "Correct initial headers." );
+ $this->assertEquals( $oInfo['sessionId'], $info['sessionId'], "Correct initial session ID." );
+ $this->assertEquals( $oInfo['userId'], $info['userId'], "Correct initial user ID." );
+ }
+}
diff --git a/tests/phpunit/includes/RevisionStorageTest.php b/tests/phpunit/includes/RevisionStorageTest.php
new file mode 100644
index 00000000..9a429bcb
--- /dev/null
+++ b/tests/phpunit/includes/RevisionStorageTest.php
@@ -0,0 +1,574 @@
+<?php
+
+/**
+ * Test class for Revision storage.
+ *
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ *
+ * @group medium
+ * ^--- important, causes tests not to fail with timeout
+ */
+class RevisionStorageTest extends MediaWikiTestCase {
+ /**
+ * @var WikiPage $the_page
+ */
+ private $the_page;
+
+ function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed = array_merge( $this->tablesUsed,
+ array( 'page',
+ 'revision',
+ 'text',
+
+ 'recentchanges',
+ 'logging',
+
+ 'page_props',
+ 'pagelinks',
+ 'categorylinks',
+ 'langlinks',
+ 'externallinks',
+ 'imagelinks',
+ 'templatelinks',
+ 'iwlinks' ) );
+ }
+
+ protected function setUp() {
+ global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+ parent::setUp();
+
+ $wgExtraNamespaces[12312] = 'Dummy';
+ $wgExtraNamespaces[12313] = 'Dummy_talk';
+
+ $wgNamespaceContentModels[12312] = 'DUMMY';
+ $wgContentHandlers['DUMMY'] = 'DummyContentHandlerForTesting';
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ if ( !$this->the_page ) {
+ $this->the_page = $this->createPage(
+ 'RevisionStorageTest_the_page',
+ "just a dummy page",
+ CONTENT_MODEL_WIKITEXT
+ );
+ }
+ }
+
+ protected function tearDown() {
+ global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+ parent::tearDown();
+
+ unset( $wgExtraNamespaces[12312] );
+ unset( $wgExtraNamespaces[12313] );
+
+ unset( $wgNamespaceContentModels[12312] );
+ unset( $wgContentHandlers['DUMMY'] );
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ protected function makeRevision( $props = null ) {
+ if ( $props === null ) {
+ $props = array();
+ }
+
+ if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) {
+ $props['text'] = 'Lorem Ipsum';
+ }
+
+ if ( !isset( $props['comment'] ) ) {
+ $props['comment'] = 'just a test';
+ }
+
+ if ( !isset( $props['page'] ) ) {
+ $props['page'] = $this->the_page->getId();
+ }
+
+ $rev = new Revision( $props );
+
+ $dbw = wfgetDB( DB_MASTER );
+ $rev->insertOn( $dbw );
+
+ return $rev;
+ }
+
+ protected function createPage( $page, $text, $model = null ) {
+ if ( is_string( $page ) ) {
+ if ( !preg_match( '/:/', $page ) &&
+ ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
+ ) {
+ $ns = $this->getDefaultWikitextNS();
+ $page = MWNamespace::getCanonicalName( $ns ) . ':' . $page;
+ }
+
+ $page = Title::newFromText( $page );
+ }
+
+ if ( $page instanceof Title ) {
+ $page = new WikiPage( $page );
+ }
+
+ if ( $page->exists() ) {
+ $page->doDeleteArticle( "done" );
+ }
+
+ $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+ $page->doEditContent( $content, "testing", EDIT_NEW );
+
+ return $page;
+ }
+
+ protected function assertRevEquals( Revision $orig, Revision $rev = null ) {
+ $this->assertNotNull( $rev, 'missing revision' );
+
+ $this->assertEquals( $orig->getId(), $rev->getId() );
+ $this->assertEquals( $orig->getPage(), $rev->getPage() );
+ $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
+ $this->assertEquals( $orig->getUser(), $rev->getUser() );
+ $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
+ $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
+ $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
+ }
+
+ /**
+ * @covers Revision::__construct
+ */
+ public function testConstructFromRow() {
+ $orig = $this->makeRevision();
+
+ $dbr = wfgetDB( DB_SLAVE );
+ $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) );
+ $this->assertTrue( is_object( $res ), 'query failed' );
+
+ $row = $res->fetchObject();
+ $res->free();
+
+ $rev = new Revision( $row );
+
+ $this->assertRevEquals( $orig, $rev );
+ }
+
+ /**
+ * @covers Revision::newFromRow
+ */
+ public function testNewFromRow() {
+ $orig = $this->makeRevision();
+
+ $dbr = wfgetDB( DB_SLAVE );
+ $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) );
+ $this->assertTrue( is_object( $res ), 'query failed' );
+
+ $row = $res->fetchObject();
+ $res->free();
+
+ $rev = Revision::newFromRow( $row );
+
+ $this->assertRevEquals( $orig, $rev );
+ }
+
+ /**
+ * @covers Revision::newFromArchiveRow
+ */
+ public function testNewFromArchiveRow() {
+ $page = $this->createPage(
+ 'RevisionStorageTest_testNewFromArchiveRow',
+ 'Lorem Ipsum',
+ CONTENT_MODEL_WIKITEXT
+ );
+ $orig = $page->getRevision();
+ $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
+
+ $dbr = wfgetDB( DB_SLAVE );
+ $res = $dbr->select( 'archive', '*', array( 'ar_rev_id' => $orig->getId() ) );
+ $this->assertTrue( is_object( $res ), 'query failed' );
+
+ $row = $res->fetchObject();
+ $res->free();
+
+ $rev = Revision::newFromArchiveRow( $row );
+
+ $this->assertRevEquals( $orig, $rev );
+ }
+
+ /**
+ * @covers Revision::newFromId
+ */
+ public function testNewFromId() {
+ $orig = $this->makeRevision();
+
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertRevEquals( $orig, $rev );
+ }
+
+ /**
+ * @covers Revision::fetchRevision
+ */
+ public function testFetchRevision() {
+ $page = $this->createPage(
+ 'RevisionStorageTest_testFetchRevision',
+ 'one',
+ CONTENT_MODEL_WIKITEXT
+ );
+
+ // Hidden process cache assertion below
+ $page->getRevision()->getId();
+
+ $page->doEditContent( new WikitextContent( 'two' ), 'second rev' );
+ $id = $page->getRevision()->getId();
+
+ $res = Revision::fetchRevision( $page->getTitle() );
+
+ #note: order is unspecified
+ $rows = array();
+ while ( ( $row = $res->fetchObject() ) ) {
+ $rows[$row->rev_id] = $row;
+ }
+
+ $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' );
+ $this->assertArrayHasKey( $id, $rows, 'missing revision with id ' . $id );
+ }
+
+ /**
+ * @covers Revision::selectFields
+ */
+ public function testSelectFields() {
+ global $wgContentHandlerUseDB;
+
+ $fields = Revision::selectFields();
+
+ $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields' );
+ $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields' );
+ $this->assertTrue(
+ in_array( 'rev_timestamp', $fields ),
+ 'missing rev_timestamp in list of fields'
+ );
+ $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields' );
+
+ if ( $wgContentHandlerUseDB ) {
+ $this->assertTrue( in_array( 'rev_content_model', $fields ),
+ 'missing rev_content_model in list of fields' );
+ $this->assertTrue( in_array( 'rev_content_format', $fields ),
+ 'missing rev_content_format in list of fields' );
+ }
+ }
+
+ /**
+ * @covers Revision::getPage
+ */
+ public function testGetPage() {
+ $page = $this->the_page;
+
+ $orig = $this->makeRevision( array( 'page' => $page->getId() ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( $page->getId(), $rev->getPage() );
+ }
+
+ /**
+ * @covers Revision::getText
+ */
+ public function testGetText() {
+ $this->hideDeprecated( 'Revision::getText' );
+
+ $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( 'hello hello.', $rev->getText() );
+ }
+
+ /**
+ * @covers Revision::getContent
+ */
+ public function testGetContent_failure() {
+ $rev = new Revision( array(
+ 'page' => $this->the_page->getId(),
+ 'content_model' => $this->the_page->getContentModel(),
+ 'text_id' => 123456789, // not in the test DB
+ ) );
+
+ $this->assertNull( $rev->getContent(),
+ "getContent() should return null if the revision's text blob could not be loaded." );
+
+ //NOTE: check this twice, once for lazy initialization, and once with the cached value.
+ $this->assertNull( $rev->getContent(),
+ "getContent() should return null if the revision's text blob could not be loaded." );
+ }
+
+ /**
+ * @covers Revision::getContent
+ */
+ public function testGetContent() {
+ $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() );
+ }
+
+ /**
+ * @covers Revision::getRawText
+ */
+ public function testGetRawText() {
+ $this->hideDeprecated( 'Revision::getRawText' );
+
+ $orig = $this->makeRevision( array( 'text' => 'hello hello raw.' ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( 'hello hello raw.', $rev->getRawText() );
+ }
+
+ /**
+ * @covers Revision::getContentModel
+ */
+ public function testGetContentModel() {
+ global $wgContentHandlerUseDB;
+
+ if ( !$wgContentHandlerUseDB ) {
+ $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+ }
+
+ $orig = $this->makeRevision( array( 'text' => 'hello hello.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+ }
+
+ /**
+ * @covers Revision::getContentFormat
+ */
+ public function testGetContentFormat() {
+ global $wgContentHandlerUseDB;
+
+ if ( !$wgContentHandlerUseDB ) {
+ $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+ }
+
+ $orig = $this->makeRevision( array(
+ 'text' => 'hello hello.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT,
+ 'content_format' => CONTENT_FORMAT_JAVASCRIPT
+ ) );
+ $rev = Revision::newFromId( $orig->getId() );
+
+ $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() );
+ }
+
+ /**
+ * @covers Revision::isCurrent
+ */
+ public function testIsCurrent() {
+ $page = $this->createPage(
+ 'RevisionStorageTest_testIsCurrent',
+ 'Lorem Ipsum',
+ CONTENT_MODEL_WIKITEXT
+ );
+ $rev1 = $page->getRevision();
+
+ # @todo find out if this should be true
+ # $this->assertTrue( $rev1->isCurrent() );
+
+ $rev1x = Revision::newFromId( $rev1->getId() );
+ $this->assertTrue( $rev1x->isCurrent() );
+
+ $page->doEditContent(
+ ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ 'second rev'
+ );
+ $rev2 = $page->getRevision();
+
+ # @todo find out if this should be true
+ # $this->assertTrue( $rev2->isCurrent() );
+
+ $rev1x = Revision::newFromId( $rev1->getId() );
+ $this->assertFalse( $rev1x->isCurrent() );
+
+ $rev2x = Revision::newFromId( $rev2->getId() );
+ $this->assertTrue( $rev2x->isCurrent() );
+ }
+
+ /**
+ * @covers Revision::getPrevious
+ */
+ public function testGetPrevious() {
+ $page = $this->createPage(
+ 'RevisionStorageTest_testGetPrevious',
+ 'Lorem Ipsum testGetPrevious',
+ CONTENT_MODEL_WIKITEXT
+ );
+ $rev1 = $page->getRevision();
+
+ $this->assertNull( $rev1->getPrevious() );
+
+ $page->doEditContent(
+ ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ 'second rev testGetPrevious' );
+ $rev2 = $page->getRevision();
+
+ $this->assertNotNull( $rev2->getPrevious() );
+ $this->assertEquals( $rev1->getId(), $rev2->getPrevious()->getId() );
+ }
+
+ /**
+ * @covers Revision::getNext
+ */
+ public function testGetNext() {
+ $page = $this->createPage(
+ 'RevisionStorageTest_testGetNext',
+ 'Lorem Ipsum testGetNext',
+ CONTENT_MODEL_WIKITEXT
+ );
+ $rev1 = $page->getRevision();
+
+ $this->assertNull( $rev1->getNext() );
+
+ $page->doEditContent(
+ ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ 'second rev testGetNext'
+ );
+ $rev2 = $page->getRevision();
+
+ $this->assertNotNull( $rev1->getNext() );
+ $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() );
+ }
+
+ /**
+ * @covers Revision::newNullRevision
+ */
+ public function testNewNullRevision() {
+ $page = $this->createPage(
+ 'RevisionStorageTest_testNewNullRevision',
+ 'some testing text',
+ CONTENT_MODEL_WIKITEXT
+ );
+ $orig = $page->getRevision();
+
+ $dbw = wfGetDB( DB_MASTER );
+ $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false );
+
+ $this->assertNotEquals( $orig->getId(), $rev->getId(),
+ 'new null revision shold have a different id from the original revision' );
+ $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
+ 'new null revision shold have the same text id as the original revision' );
+ $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() );
+ }
+
+ public static function provideUserWasLastToEdit() {
+ return array(
+ array( #0
+ 3, true, # actually the last edit
+ ),
+ array( #1
+ 2, true, # not the current edit, but still by this user
+ ),
+ array( #2
+ 1, false, # edit by another user
+ ),
+ array( #3
+ 0, false, # first edit, by this user, but another user edited in the mean time
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUserWasLastToEdit
+ */
+ public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) {
+ $userA = User::newFromName( "RevisionStorageTest_userA" );
+ $userB = User::newFromName( "RevisionStorageTest_userB" );
+
+ if ( $userA->getId() === 0 ) {
+ $userA = User::createNew( $userA->getName() );
+ }
+
+ if ( $userB->getId() === 0 ) {
+ $userB = User::createNew( $userB->getName() );
+ }
+
+ $ns = $this->getDefaultWikitextNS();
+
+ $dbw = wfGetDB( DB_MASTER );
+ $revisions = array();
+
+ // create revisions -----------------------------
+ $page = WikiPage::factory( Title::newFromText(
+ 'RevisionStorageTest_testUserWasLastToEdit', $ns ) );
+ $page->insertOn( $dbw );
+
+ # zero
+ $revisions[0] = new Revision( array(
+ 'page' => $page->getId(),
+ // we need the title to determine the page's default content model
+ 'title' => $page->getTitle(),
+ 'timestamp' => '20120101000000',
+ 'user' => $userA->getId(),
+ 'text' => 'zero',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit zero'
+ ) );
+ $revisions[0]->insertOn( $dbw );
+
+ # one
+ $revisions[1] = new Revision( array(
+ 'page' => $page->getId(),
+ // still need the title, because $page->getId() is 0 (there's no entry in the page table)
+ 'title' => $page->getTitle(),
+ 'timestamp' => '20120101000100',
+ 'user' => $userA->getId(),
+ 'text' => 'one',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit one'
+ ) );
+ $revisions[1]->insertOn( $dbw );
+
+ # two
+ $revisions[2] = new Revision( array(
+ 'page' => $page->getId(),
+ 'title' => $page->getTitle(),
+ 'timestamp' => '20120101000200',
+ 'user' => $userB->getId(),
+ 'text' => 'two',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit two'
+ ) );
+ $revisions[2]->insertOn( $dbw );
+
+ # three
+ $revisions[3] = new Revision( array(
+ 'page' => $page->getId(),
+ 'title' => $page->getTitle(),
+ 'timestamp' => '20120101000300',
+ 'user' => $userA->getId(),
+ 'text' => 'three',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit three'
+ ) );
+ $revisions[3]->insertOn( $dbw );
+
+ # four
+ $revisions[4] = new Revision( array(
+ 'page' => $page->getId(),
+ 'title' => $page->getTitle(),
+ 'timestamp' => '20120101000200',
+ 'user' => $userA->getId(),
+ 'text' => 'zero',
+ 'content_model' => CONTENT_MODEL_WIKITEXT,
+ 'summary' => 'edit four'
+ ) );
+ $revisions[4]->insertOn( $dbw );
+
+ // test it ---------------------------------
+ $since = $revisions[$sinceIdx]->getTimestamp();
+
+ $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
+
+ $this->assertEquals( $expectedLast, $wasLast );
+ }
+}
diff --git a/tests/phpunit/includes/RevisionStorageTestContentHandlerUseDB.php b/tests/phpunit/includes/RevisionStorageTestContentHandlerUseDB.php
new file mode 100644
index 00000000..d5e47c82
--- /dev/null
+++ b/tests/phpunit/includes/RevisionStorageTestContentHandlerUseDB.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ */
+class RevisionTestContentHandlerUseDB extends RevisionStorageTest {
+
+ protected function setUp() {
+ $this->setMwGlobals( 'wgContentHandlerUseDB', false );
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $page_table = $dbw->tableName( 'page' );
+ $revision_table = $dbw->tableName( 'revision' );
+ $archive_table = $dbw->tableName( 'archive' );
+
+ if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) {
+ $dbw->query( "alter table $page_table drop column page_content_model" );
+ $dbw->query( "alter table $revision_table drop column rev_content_model" );
+ $dbw->query( "alter table $revision_table drop column rev_content_format" );
+ $dbw->query( "alter table $archive_table drop column ar_content_model" );
+ $dbw->query( "alter table $archive_table drop column ar_content_format" );
+ }
+
+ parent::setUp();
+ }
+
+ /**
+ * @covers Revision::selectFields
+ */
+ public function testSelectFields() {
+ $fields = Revision::selectFields();
+
+ $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields' );
+ $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields' );
+ $this->assertTrue(
+ in_array( 'rev_timestamp', $fields ),
+ 'missing rev_timestamp in list of fields'
+ );
+ $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields' );
+
+ $this->assertFalse(
+ in_array( 'rev_content_model', $fields ),
+ 'missing rev_content_model in list of fields'
+ );
+ $this->assertFalse(
+ in_array( 'rev_content_format', $fields ),
+ 'missing rev_content_format in list of fields'
+ );
+ }
+
+ /**
+ * @covers Revision::getContentModel
+ */
+ public function testGetContentModel() {
+ try {
+ $this->makeRevision( array( 'text' => 'hello hello.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
+
+ $this->fail( "Creating JavaScript content on a wikitext page should fail with "
+ . "\$wgContentHandlerUseDB disabled" );
+ } catch ( MWException $ex ) {
+ $this->assertTrue( true ); // ok
+ }
+ }
+
+ /**
+ * @covers Revision::getContentFormat
+ */
+ public function testGetContentFormat() {
+ try {
+ // @todo change this to test failure on using a non-standard (but supported) format
+ // for a content model supported in the given location. As of 1.21, there are
+ // no alternative formats for any of the standard content models that could be
+ // used for this though.
+
+ $this->makeRevision( array( 'text' => 'hello hello.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT,
+ 'content_format' => 'text/javascript' ) );
+
+ $this->fail( "Creating JavaScript content on a wikitext page should fail with "
+ . "\$wgContentHandlerUseDB disabled" );
+ } catch ( MWException $ex ) {
+ $this->assertTrue( true ); // ok
+ }
+ }
+}
diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php
new file mode 100644
index 00000000..4623b383
--- /dev/null
+++ b/tests/phpunit/includes/RevisionTest.php
@@ -0,0 +1,506 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+class RevisionTest extends MediaWikiTestCase {
+ protected function setUp() {
+ global $wgContLang;
+
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgContLang' => Language::factory( 'en' ),
+ 'wgLanguageCode' => 'en',
+ 'wgLegacyEncoding' => false,
+ 'wgCompressRevisions' => false,
+
+ 'wgContentHandlerTextFallback' => 'ignore',
+ ) );
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgExtraNamespaces',
+ array(
+ 12312 => 'Dummy',
+ 12313 => 'Dummy_talk',
+ )
+ );
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgNamespaceContentModels',
+ array(
+ 12312 => 'testing',
+ )
+ );
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgContentHandlers',
+ array(
+ 'testing' => 'DummyContentHandlerForTesting',
+ 'RevisionTestModifyableContent' => 'RevisionTestModifyableContentHandler',
+ )
+ );
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ function tearDown() {
+ global $wgContLang;
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+
+ parent::tearDown();
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public 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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public function testGetRevisionTextGzip() {
+ $this->checkPHPExtension( 'zlib' );
+
+ $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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public 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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public 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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public function testGetRevisionTextUtf8NativeGzip() {
+ $this->checkPHPExtension( 'zlib' );
+
+ $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 ) );
+ }
+
+ /**
+ * @covers Revision::getRevisionText
+ */
+ public function testGetRevisionTextUtf8LegacyGzip() {
+ $this->checkPHPExtension( 'zlib' );
+
+ $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 ) );
+ }
+
+ /**
+ * @covers Revision::compressRevisionText
+ */
+ public 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" );
+ }
+
+ /**
+ * @covers Revision::compressRevisionText
+ */
+ public function testCompressRevisionTextUtf8Gzip() {
+ $this->checkPHPExtension( 'zlib' );
+ $this->setMwGlobals( '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" );
+ }
+
+ # =========================================================================
+
+ /**
+ * @param string $text
+ * @param string $title
+ * @param string $model
+ * @param string $format
+ *
+ * @return Revision
+ */
+ function newTestRevision( $text, $title = "Test",
+ $model = CONTENT_MODEL_WIKITEXT, $format = null
+ ) {
+ if ( is_string( $title ) ) {
+ $title = Title::newFromText( $title );
+ }
+
+ $content = ContentHandler::makeContent( $text, $title, $model, $format );
+
+ $rev = new Revision(
+ array(
+ 'id' => 42,
+ 'page' => 23,
+ 'title' => $title,
+
+ 'content' => $content,
+ 'length' => $content->getSize(),
+ 'comment' => "testing",
+ 'minor_edit' => false,
+
+ 'content_format' => $format,
+ )
+ );
+
+ return $rev;
+ }
+
+ function dataGetContentModel() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ),
+ array( 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ),
+ array( serialize( 'hello world' ), 'Dummy:Hello', null, null, "testing" ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetContentModel
+ * @covers Revision::getContentModel
+ */
+ public function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedModel, $rev->getContentModel() );
+ }
+
+ function dataGetContentFormat() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ),
+ array( 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ),
+ array( 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ),
+ array( serialize( 'hello world' ), 'Dummy:Hello', null, null, "testing" ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetContentFormat
+ * @covers Revision::getContentFormat
+ */
+ public function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) {
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedFormat, $rev->getContentFormat() );
+ }
+
+ function dataGetContentHandler() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, 'WikitextContentHandler' ),
+ array( 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ),
+ array( serialize( 'hello world' ), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetContentHandler
+ * @covers Revision::getContentHandler
+ */
+ public function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) {
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) );
+ }
+
+ function dataGetContent() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+ array(
+ serialize( 'hello world' ),
+ 'Hello',
+ "testing",
+ null,
+ Revision::FOR_PUBLIC,
+ serialize( 'hello world' )
+ ),
+ array(
+ serialize( 'hello world' ),
+ 'Dummy:Hello',
+ null,
+ null,
+ Revision::FOR_PUBLIC,
+ serialize( 'hello world' )
+ ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetContent
+ * @covers Revision::getContent
+ */
+ public function testGetContent( $text, $title, $model, $format,
+ $audience, $expectedSerialization
+ ) {
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+ $content = $rev->getContent( $audience );
+
+ $this->assertEquals(
+ $expectedSerialization,
+ is_null( $content ) ? null : $content->serialize( $format )
+ );
+ }
+
+ function dataGetText() {
+ //NOTE: we expect the help namespace to always contain wikitext
+ return array(
+ array( 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+ array( serialize( 'hello world' ), 'Hello', "testing", null, Revision::FOR_PUBLIC, null ),
+ array( serialize( 'hello world' ), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, null ),
+ );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetText
+ * @covers Revision::getText
+ */
+ public function testGetText( $text, $title, $model, $format, $audience, $expectedText ) {
+ $this->hideDeprecated( 'Revision::getText' );
+
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedText, $rev->getText( $audience ) );
+ }
+
+ /**
+ * @group Database
+ * @dataProvider dataGetText
+ * @covers Revision::getRawText
+ */
+ public function testGetRawText( $text, $title, $model, $format, $audience, $expectedText ) {
+ $this->hideDeprecated( 'Revision::getRawText' );
+
+ $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+ $this->assertEquals( $expectedText, $rev->getRawText( $audience ) );
+ }
+
+ public function dataGetSize() {
+ return array(
+ array( "hello world.", CONTENT_MODEL_WIKITEXT, 12 ),
+ array( serialize( "hello world." ), "testing", 12 ),
+ );
+ }
+
+ /**
+ * @covers Revision::getSize
+ * @group Database
+ * @dataProvider dataGetSize
+ */
+ public function testGetSize( $text, $model, $expected_size ) {
+ $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model );
+ $this->assertEquals( $expected_size, $rev->getSize() );
+ }
+
+ public function dataGetSha1() {
+ return array(
+ array( "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ),
+ array(
+ serialize( "hello world." ),
+ "testing",
+ Revision::base36Sha1( serialize( "hello world." ) )
+ ),
+ );
+ }
+
+ /**
+ * @covers Revision::getSha1
+ * @group Database
+ * @dataProvider dataGetSha1
+ */
+ public function testGetSha1( $text, $model, $expected_hash ) {
+ $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model );
+ $this->assertEquals( $expected_hash, $rev->getSha1() );
+ }
+
+ /**
+ * @covers Revision::__construct
+ */
+ public function testConstructWithText() {
+ $this->hideDeprecated( "Revision::getText" );
+
+ $rev = new Revision( array(
+ 'text' => 'hello world.',
+ 'content_model' => CONTENT_MODEL_JAVASCRIPT
+ ) );
+
+ $this->assertNotNull( $rev->getText(), 'no content text' );
+ $this->assertNotNull( $rev->getContent(), 'no content object available' );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+ }
+
+ /**
+ * @covers Revision::__construct
+ */
+ public function testConstructWithContent() {
+ $this->hideDeprecated( "Revision::getText" );
+
+ $title = Title::newFromText( 'RevisionTest_testConstructWithContent' );
+
+ $rev = new Revision( array(
+ 'content' => ContentHandler::makeContent( 'hello world.', $title, CONTENT_MODEL_JAVASCRIPT ),
+ ) );
+
+ $this->assertNotNull( $rev->getText(), 'no content text' );
+ $this->assertNotNull( $rev->getContent(), 'no content object available' );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+ }
+
+ /**
+ * Tests whether $rev->getContent() returns a clone when needed.
+ *
+ * @group Database
+ * @covers Revision::getContent
+ */
+ public function testGetContentClone() {
+ $content = new RevisionTestModifyableContent( "foo" );
+
+ $rev = new Revision(
+ array(
+ 'id' => 42,
+ 'page' => 23,
+ 'title' => Title::newFromText( "testGetContentClone_dummy" ),
+
+ 'content' => $content,
+ 'length' => $content->getSize(),
+ 'comment' => "testing",
+ 'minor_edit' => false,
+ )
+ );
+
+ $content = $rev->getContent( Revision::RAW );
+ $content->setText( "bar" );
+
+ $content2 = $rev->getContent( Revision::RAW );
+ // content is mutable, expect clone
+ $this->assertNotSame( $content, $content2, "expected a clone" );
+ // clone should contain the original text
+ $this->assertEquals( "foo", $content2->getText() );
+
+ $content2->setText( "bla bla" );
+ $this->assertEquals( "bar", $content->getText() ); // clones should be independent
+ }
+
+ /**
+ * Tests whether $rev->getContent() returns the same object repeatedly if appropriate.
+ *
+ * @group Database
+ * @covers Revision::getContent
+ */
+ public function testGetContentUncloned() {
+ $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT );
+ $content = $rev->getContent( Revision::RAW );
+ $content2 = $rev->getContent( Revision::RAW );
+
+ // for immutable content like wikitext, this should be the same object
+ $this->assertSame( $content, $content2 );
+ }
+}
+
+class RevisionTestModifyableContent extends TextContent {
+ public function __construct( $text ) {
+ parent::__construct( $text, "RevisionTestModifyableContent" );
+ }
+
+ public function copy() {
+ return new RevisionTestModifyableContent( $this->mText );
+ }
+
+ public function getText() {
+ return $this->mText;
+ }
+
+ public function setText( $text ) {
+ $this->mText = $text;
+ }
+}
+
+class RevisionTestModifyableContentHandler extends TextContentHandler {
+
+ public function __construct() {
+ parent::__construct( "RevisionTestModifyableContent", array( CONTENT_FORMAT_TEXT ) );
+ }
+
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+
+ return new RevisionTestModifyableContent( $text );
+ }
+
+ public function makeEmptyContent() {
+ return new RevisionTestModifyableContent( '' );
+ }
+}
diff --git a/tests/phpunit/includes/SampleTest.php b/tests/phpunit/includes/SampleTest.php
new file mode 100644
index 00000000..25858110
--- /dev/null
+++ b/tests/phpunit/includes/SampleTest.php
@@ -0,0 +1,108 @@
+<?php
+
+class TestSample extends MediaWikiLangTestCase {
+
+ /**
+ * Anything that needs to happen before your tests should go here.
+ */
+ protected function setUp() {
+ // Be sure to do call the parent setup and teardown functions.
+ // This makes sure that all the various cleanup and restorations
+ // happen as they should (including the restoration for setMwGlobals).
+ parent::setUp();
+
+ // This sets the globals and will restore them automatically
+ // after each test.
+ $this->setMwGlobals( array(
+ 'wgContLang' => Language::factory( 'en' ),
+ 'wgLanguageCode' => 'en',
+ 'wgCapitalLinks' => true,
+ ) );
+ }
+
+ /**
+ * Anything cleanup you need to do should go here.
+ */
+ protected function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * Name tests so that PHPUnit can turn them into sentences 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
+ */
+ public function testTitleObjectStringConversion() {
+ $title = Title::newFromText( "text" );
+ $this->assertInstanceOf( 'Title', $title, "Title creation" );
+ $this->assertEquals( "Text", $title, "Automatic string conversion" );
+
+ $title = Title::newFromText( "text", NS_MEDIA );
+ $this->assertEquals( "Media:Text", $title, "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 static 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_FILE, 'File:Photo.jpg' )
+ );
+ }
+
+ /**
+ * @dataProvider provideTitles
+ * @codingStandardsIgnoreStart Ignore long line warning
+ * See http://phpunit.de/manual/3.7/en/appendixes.annotations.html#appendixes.annotations.dataProvider
+ * @codingStandardsIgnoreEnd
+ */
+ 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://phpunit.de/manual/3.7/en/appendixes.annotations.html#appendixes.annotations.depends
+ */
+ public function testCheckMainPageTitleIsConsideredLocal( $title ) {
+ $this->assertTrue( $title->isLocal() );
+ }
+
+ // @codingStandardsIgnoreStart Ignore long line warning
+ /**
+ * @expectedException MWException object
+ * See http://phpunit.de/manual/3.7/en/appendixes.annotations.html#appendixes.annotations.expectedException
+ */
+ // @codingStandardsIgnoreEnd
+ public 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..50c1e509
--- /dev/null
+++ b/tests/phpunit/includes/SanitizerTest.php
@@ -0,0 +1,349 @@
+<?php
+
+/**
+ * @todo Tests covering decodeCharReferences can be refactored into a single
+ * method and dataprovider.
+ */
+class SanitizerTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ AutoLoader::loadClass( 'Sanitizer' );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testDecodeNamedEntities() {
+ $this->assertEquals(
+ "\xc3\xa9cole",
+ Sanitizer::decodeCharReferences( '&eacute;cole' ),
+ 'decode named entities'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testDecodeNumericEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole!",
+ Sanitizer::decodeCharReferences( "&#x108;io bonas dans l'&#233;cole!" ),
+ 'decode numeric entities'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public 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'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public 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'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testInvalidAmpersand() {
+ $this->assertEquals(
+ 'a & b',
+ Sanitizer::decodeCharReferences( 'a & b' ),
+ 'Invalid ampersand'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testInvalidEntities() {
+ $this->assertEquals(
+ '&foo;',
+ Sanitizer::decodeCharReferences( '&foo;' ),
+ 'Invalid named entity'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::decodeCharReferences
+ */
+ public function testInvalidNumberedEntities() {
+ $this->assertEquals(
+ UTF8_REPLACEMENT,
+ Sanitizer::decodeCharReferences( "&#88888888888888;" ),
+ 'Invalid numbered entity'
+ );
+ }
+
+ /**
+ * @covers Sanitizer::removeHTMLtags
+ * @dataProvider provideHtml5Tags
+ *
+ * @param string $tag Name of an HTML5 element (ie: 'video')
+ * @param bool $escaped Whether sanitizer let the tag in or escape it (ie: '&lt;video&gt;')
+ */
+ public function testRemovehtmltagsOnHtml5Tags( $tag, $escaped ) {
+ $this->setMwGlobals( array(
+ 'wgUseTidy' => false
+ ) );
+
+ if ( $escaped ) {
+ $this->assertEquals( "&lt;$tag&gt;",
+ Sanitizer::removeHTMLtags( "<$tag>" )
+ );
+ } else {
+ $this->assertEquals( "<$tag></$tag>\n",
+ Sanitizer::removeHTMLtags( "<$tag>" )
+ );
+ }
+ }
+
+ /**
+ * Provide HTML5 tags
+ */
+ public static function provideHtml5Tags() {
+ $ESCAPED = true; # We want tag to be escaped
+ $VERBATIM = false; # We want to keep the tag
+ return array(
+ array( 'data', $VERBATIM ),
+ array( 'mark', $VERBATIM ),
+ array( 'time', $VERBATIM ),
+ array( 'video', $ESCAPED ),
+ );
+ }
+
+ function dataRemoveHTMLtags() {
+ return array(
+ // former testSelfClosingTag
+ array(
+ '<div>Hello world</div />',
+ '<div>Hello world</div>',
+ 'Self-closing closing div'
+ ),
+ // Make sure special nested HTML5 semantics are not broken
+ // http://www.whatwg.org/html/text-level-semantics.html#the-kbd-element
+ array(
+ '<kbd><kbd>Shift</kbd>+<kbd>F3</kbd></kbd>',
+ '<kbd><kbd>Shift</kbd>+<kbd>F3</kbd></kbd>',
+ 'Nested <kbd>.'
+ ),
+ // http://www.whatwg.org/html/text-level-semantics.html#the-sub-and-sup-elements
+ array(
+ '<var>x<sub><var>i</var></sub></var>, <var>y<sub><var>i</var></sub></var>',
+ '<var>x<sub><var>i</var></sub></var>, <var>y<sub><var>i</var></sub></var>',
+ 'Nested <var>.'
+ ),
+ // http://www.whatwg.org/html/text-level-semantics.html#the-dfn-element
+ array(
+ '<dfn><abbr title="Garage Door Opener">GDO</abbr></dfn>',
+ '<dfn><abbr title="Garage Door Opener">GDO</abbr></dfn>',
+ '<abbr> inside <dfn>',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataRemoveHTMLtags
+ * @covers Sanitizer::removeHTMLtags
+ */
+ public function testRemoveHTMLtags( $input, $output, $msg = null ) {
+ $GLOBALS['wgUseTidy'] = false;
+ $this->assertEquals( $output, Sanitizer::removeHTMLtags( $input ), $msg );
+ }
+
+ /**
+ * @dataProvider provideTagAttributesToDecode
+ * @covers Sanitizer::decodeTagAttributes
+ */
+ public function testDecodeTagAttributes( $expected, $attributes, $message = '' ) {
+ $this->assertEquals( $expected,
+ Sanitizer::decodeTagAttributes( $attributes ),
+ $message
+ );
+ }
+
+ public static function provideTagAttributesToDecode() {
+ return array(
+ array( array( 'foo' => 'bar' ), 'foo=bar', 'Unquoted attribute' ),
+ array( array( 'foo' => 'bar' ), ' foo = bar ', 'Spaced attribute' ),
+ array( array( 'foo' => 'bar' ), 'foo="bar"', 'Double-quoted attribute' ),
+ array( array( 'foo' => 'bar' ), 'foo=\'bar\'', 'Single-quoted attribute' ),
+ array(
+ array( 'foo' => 'bar', 'baz' => 'foo' ),
+ 'foo=\'bar\' baz="foo"',
+ 'Several attributes'
+ ),
+ array(
+ array( 'foo' => 'bar', 'baz' => 'foo' ),
+ 'foo=\'bar\' baz="foo"',
+ 'Several attributes'
+ ),
+ array(
+ array( 'foo' => 'bar', 'baz' => 'foo' ),
+ 'foo=\'bar\' baz="foo"',
+ 'Several attributes'
+ ),
+ array( array( ':foo' => 'bar' ), ':foo=\'bar\'', 'Leading :' ),
+ array( array( '_foo' => 'bar' ), '_foo=\'bar\'', 'Leading _' ),
+ array( array( 'foo' => 'bar' ), 'Foo=\'bar\'', 'Leading capital' ),
+ array( array( 'foo' => 'BAR' ), 'FOO=BAR', 'Attribute keys are normalized to lowercase' ),
+
+ # Invalid beginning
+ array( array(), '-foo=bar', 'Leading - is forbidden' ),
+ array( array(), '.foo=bar', 'Leading . is forbidden' ),
+ array( array( 'foo-bar' => 'bar' ), 'foo-bar=bar', 'A - is allowed inside the attribute' ),
+ array( array( 'foo-' => 'bar' ), 'foo-=bar', 'A - is allowed inside the attribute' ),
+ array( array( 'foo.bar' => 'baz' ), 'foo.bar=baz', 'A . is allowed inside the attribute' ),
+ array( array( 'foo.' => 'baz' ), 'foo.=baz', 'A . is allowed as last character' ),
+ array( array( 'foo6' => 'baz' ), 'foo6=baz', 'Numbers are allowed' ),
+
+ # This bit is more relaxed than XML rules, but some extensions use
+ # it, like ProofreadPage (see bug 27539)
+ array( array( '1foo' => 'baz' ), '1foo=baz', 'Leading numbers are allowed' ),
+ array( array(), 'foo$=baz', 'Symbols are not allowed' ),
+ array( array(), 'foo@=baz', 'Symbols are not allowed' ),
+ array( array(), 'foo~=baz', 'Symbols are not allowed' ),
+ array(
+ array( 'foo' => '1[#^`*%w/(' ),
+ 'foo=1[#^`*%w/(',
+ 'All kind of characters are allowed as values'
+ ),
+ array(
+ array( 'foo' => '1[#^`*%\'w/(' ),
+ 'foo="1[#^`*%\'w/("',
+ 'Double quotes are allowed if quoted by single quotes'
+ ),
+ array(
+ array( 'foo' => '1[#^`*%"w/(' ),
+ 'foo=\'1[#^`*%"w/(\'',
+ 'Single quotes are allowed if quoted by double quotes'
+ ),
+ array( array( 'foo' => '&"' ), 'foo=&amp;&quot;', 'Special chars can be provided as entities' ),
+ array( array( 'foo' => '&foobar;' ), 'foo=&foobar;', 'Entity-like items are accepted' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDeprecatedAttributes
+ * @covers Sanitizer::fixTagAttributes
+ */
+ public function testDeprecatedAttributesUnaltered( $inputAttr, $inputEl, $message = '' ) {
+ $this->assertEquals( " $inputAttr",
+ Sanitizer::fixTagAttributes( $inputAttr, $inputEl ),
+ $message
+ );
+ }
+
+ public static function provideDeprecatedAttributes() {
+ /** array( <attribute>, <element>, [message] ) */
+ return array(
+ array( 'clear="left"', 'br' ),
+ array( 'clear="all"', 'br' ),
+ array( 'width="100"', 'td' ),
+ array( 'nowrap="true"', 'td' ),
+ array( 'nowrap=""', 'td' ),
+ array( 'align="right"', 'td' ),
+ array( 'align="center"', 'table' ),
+ array( 'align="left"', 'tr' ),
+ array( 'align="center"', 'div' ),
+ array( 'align="left"', 'h1' ),
+ array( 'align="left"', 'p' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCssCommentsFixtures
+ * @covers Sanitizer::checkCss
+ */
+ public function testCssCommentsChecking( $expected, $css, $message = '' ) {
+ $this->assertEquals( $expected,
+ Sanitizer::checkCss( $css ),
+ $message
+ );
+ }
+
+ public static function provideCssCommentsFixtures() {
+ /** array( <expected>, <css>, [message] ) */
+ return array(
+ // Valid comments spanning entire input
+ array( '/**/', '/**/' ),
+ array( '/* comment */', '/* comment */' ),
+ // Weird stuff
+ array( ' ', '/****/' ),
+ array( ' ', '/* /* */' ),
+ array( 'display: block;', "display:/* foo */block;" ),
+ array( 'display: block;', "display:\\2f\\2a foo \\2a\\2f block;",
+ 'Backslash-escaped comments must be stripped (bug 28450)' ),
+ array( '', '/* unfinished comment structure',
+ 'Remove anything after a comment-start token' ),
+ array( '', "\\2f\\2a unifinished comment'",
+ 'Remove anything after a backslash-escaped comment-start token' ),
+ array(
+ '/* insecure input */',
+ 'filter: progid:DXImageTransform.Microsoft.AlphaImageLoader'
+ . '(src=\'asdf.png\',sizingMethod=\'scale\');'
+ ),
+ array(
+ '/* insecure input */',
+ '-ms-filter: "progid:DXImageTransform.Microsoft.AlphaImageLoader'
+ . '(src=\'asdf.png\',sizingMethod=\'scale\')";'
+ ),
+ array( '/* insecure input */', 'width: expression(1+1);' ),
+ array( '/* insecure input */', 'background-image: image(asdf.png);' ),
+ array( '/* insecure input */', 'background-image: -webkit-image(asdf.png);' ),
+ array( '/* insecure input */', 'background-image: -moz-image(asdf.png);' ),
+ array( '/* insecure input */', 'background-image: image-set("asdf.png" 1x, "asdf.png" 2x);' ),
+ array(
+ '/* insecure input */',
+ 'background-image: -webkit-image-set("asdf.png" 1x, "asdf.png" 2x);'
+ ),
+ array(
+ '/* insecure input */',
+ 'background-image: -moz-image-set("asdf.png" 1x, "asdf.png" 2x);'
+ ),
+ );
+ }
+
+ /**
+ * Test for support or lack of support for specific attributes in the attribute whitelist.
+ */
+ public static function provideAttributeSupport() {
+ /** array( <attributes>, <expected>, <message> ) */
+ return array(
+ array(
+ 'div',
+ ' role="presentation"',
+ ' role="presentation"',
+ 'Support for WAI-ARIA\'s role="presentation".'
+ ),
+ array( 'div', ' role="main"', '', "Other WAI-ARIA roles are currently not supported." ),
+ );
+ }
+
+ /**
+ * @dataProvider provideAttributeSupport
+ * @covers Sanitizer::fixTagAttributes
+ */
+ public function testAttributeSupport( $tag, $attributes, $expected, $message ) {
+ $this->assertEquals( $expected,
+ Sanitizer::fixTagAttributes( $attributes, $tag ),
+ $message
+ );
+ }
+}
diff --git a/tests/phpunit/includes/SanitizerValidateEmailTest.php b/tests/phpunit/includes/SanitizerValidateEmailTest.php
new file mode 100644
index 00000000..14911f04
--- /dev/null
+++ b/tests/phpunit/includes/SanitizerValidateEmailTest.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @covers Sanitizer::validateEmail
+ * @todo all test methods in this class should be refactored and...
+ * use a single test method and a single data provider...
+ */
+class SanitizerValidateEmailTest extends MediaWikiTestCase {
+
+ private function checkEmail( $addr, $expected = true, $msg = '' ) {
+ if ( $msg == '' ) {
+ $msg = "Testing $addr";
+ }
+
+ $this->assertEquals(
+ $expected,
+ Sanitizer::validateEmail( $addr ),
+ $msg
+ );
+ }
+
+ private function valid( $addr, $msg = '' ) {
+ $this->checkEmail( $addr, true, $msg );
+ }
+
+ private function invalid( $addr, $msg = '' ) {
+ $this->checkEmail( $addr, false, $msg );
+ }
+
+ public function testEmailWellKnownUserAtHostDotTldAreValid() {
+ $this->valid( 'user@example.com' );
+ $this->valid( 'user@example.museum' );
+ }
+
+ public function testEmailWithUpperCaseCharactersAreValid() {
+ $this->valid( 'USER@example.com' );
+ $this->valid( 'user@EXAMPLE.COM' );
+ $this->valid( 'user@Example.com' );
+ $this->valid( 'USER@eXAMPLE.com' );
+ }
+
+ public function testEmailWithAPlusInUserName() {
+ $this->valid( 'user+sub@example.com' );
+ $this->valid( 'user+@example.com' );
+ }
+
+ public function testEmailDoesNotNeedATopLevelDomain() {
+ $this->valid( "user@localhost" );
+ $this->valid( "FooBar@localdomain" );
+ $this->valid( "nobody@mycompany" );
+ }
+
+ public function testEmailWithWhiteSpacesBeforeOrAfterAreInvalids() {
+ $this->invalid( " user@host.com" );
+ $this->invalid( "user@host.com " );
+ $this->invalid( "\tuser@host.com" );
+ $this->invalid( "user@host.com\t" );
+ }
+
+ public 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
+ */
+ public function testEmailWithCommasAreInvalids() {
+ $this->invalid( "user,foo@example.org" );
+ $this->invalid( "userfoo@ex,ample.org" );
+ }
+
+ public function testEmailWithHyphens() {
+ $this->valid( "user-foo@example.org" );
+ $this->valid( "userfoo@ex-ample.org" );
+ }
+
+ public function testEmailDomainCanNotBeginWithDot() {
+ $this->invalid( "user@." );
+ $this->invalid( "user@.localdomain" );
+ $this->invalid( "user@localdomain." );
+ $this->valid( "user.@localdomain" );
+ $this->valid( ".@localdomain" );
+ $this->invalid( ".@a............" );
+ }
+
+ public function testEmailWithFunnyCharacters() {
+ $this->valid( "\$user!ex{this}@123.com" );
+ }
+
+ public function testEmailTopLevelDomainCanBeNumerical() {
+ $this->valid( "user@example.1234" );
+ }
+
+ public function testEmailWithoutAtSignIsInvalid() {
+ $this->invalid( 'useràexample.com' );
+ }
+
+ public function testEmailWithOneCharacterDomainIsValid() {
+ $this->valid( 'user@a' );
+ }
+}
diff --git a/tests/phpunit/includes/SiteConfigurationTest.php b/tests/phpunit/includes/SiteConfigurationTest.php
new file mode 100644
index 00000000..6547c873
--- /dev/null
+++ b/tests/phpunit/includes/SiteConfigurationTest.php
@@ -0,0 +1,363 @@
+<?php
+
+class SiteConfigurationTest extends MediaWikiTestCase {
+
+ /**
+ * @var SiteConfiguration
+ */
+ protected $mConf;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->mConf = new SiteConfiguration;
+
+ $this->mConf->suffixes = array( 'wikipedia' => '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' );
+ }
+
+ /**
+ * This function is used as a callback within the tests below
+ */
+ public static function getSiteParamsCallback( $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' ),
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::siteFromDB
+ */
+ public 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)'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::getLocalDatabases
+ */
+ public function testGetLocalDatabases() {
+ $this->assertEquals(
+ array( 'enwiki', 'dewiki', 'frwiki' ),
+ $this->mConf->getLocalDatabases(),
+ 'getLocalDatabases()'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::get
+ */
+ public 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)'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::siteFromDB
+ */
+ public function testSiteFromDbWithCallback() {
+ $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
+
+ $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'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::get
+ */
+ public function testParameterReplacement() {
+ $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
+
+ $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'
+ );
+ }
+
+ /**
+ * @covers SiteConfiguration::getAll
+ */
+ public function testGetAllGlobals() {
+ $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
+
+ $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/SpecialPageTest.php b/tests/phpunit/includes/SpecialPageTest.php
new file mode 100644
index 00000000..245cdffd
--- /dev/null
+++ b/tests/phpunit/includes/SpecialPageTest.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @covers SpecialPage
+ *
+ * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class SpecialPageTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgScript' => '/index.php',
+ 'wgContLang' => Language::factory( 'en' )
+ ) );
+ }
+
+ /**
+ * @dataProvider getTitleForProvider
+ */
+ public function testGetTitleFor( $expectedName, $name ) {
+ $title = SpecialPage::getTitleFor( $name );
+ $expected = Title::makeTitle( NS_SPECIAL, $expectedName );
+ $this->assertEquals( $expected, $title );
+ }
+
+ public function getTitleForProvider() {
+ return array(
+ array( 'UserLogin', 'Userlogin' )
+ );
+ }
+
+ /**
+ * @expectedException PHPUnit_Framework_Error_Notice
+ */
+ public function testInvalidGetTitleFor() {
+ $title = SpecialPage::getTitleFor( 'cat' );
+ $expected = Title::makeTitle( NS_SPECIAL, 'Cat' );
+ $this->assertEquals( $expected, $title );
+ }
+
+ /**
+ * @expectedException PHPUnit_Framework_Error_Notice
+ * @dataProvider getTitleForWithWarningProvider
+ */
+ public function testGetTitleForWithWarning( $expected, $name ) {
+ $title = SpecialPage::getTitleFor( $name );
+ $this->assertEquals( $expected, $title );
+ }
+
+ public function getTitleForWithWarningProvider() {
+ return array(
+ array( Title::makeTitle( NS_SPECIAL, 'UserLogin' ), 'UserLogin' )
+ );
+ }
+
+ /**
+ * @dataProvider requireLoginAnonProvider
+ */
+ public function testRequireLoginAnon( $expected, $reason, $title ) {
+ $specialPage = new SpecialPage( 'Watchlist', 'viewmywatchlist' );
+
+ $user = User::newFromId( 0 );
+ $specialPage->getContext()->setUser( $user );
+ $specialPage->getContext()->setLanguage( Language::factory( 'en' ) );
+
+ $this->setExpectedException( 'UserNotLoggedIn', $expected );
+
+ // $specialPage->requireLogin( [ $reason [, $title ] ] )
+ call_user_func_array(
+ array( $specialPage, 'requireLogin' ),
+ array_filter( array( $reason, $title ) )
+ );
+ }
+
+ public function requireLoginAnonProvider() {
+ $lang = 'en';
+
+ $expected1 = wfMessage( 'exception-nologin-text' )->inLanguage( $lang )->text();
+ $expected2 = wfMessage( 'about' )->inLanguage( $lang )->text();
+
+ return array(
+ array( $expected1, null, null ),
+ array( $expected2, 'about', null ),
+ array( $expected2, 'about', 'about' ),
+ );
+ }
+
+ public function testRequireLoginNotAnon() {
+ $specialPage = new SpecialPage( 'Watchlist', 'viewmywatchlist' );
+
+ $user = User::newFromName( "UTSysop" );
+ $specialPage->getContext()->setUser( $user );
+
+ $specialPage->requireLogin();
+
+ // no exception thrown, logged in use can access special page
+ $this->assertTrue( true );
+ }
+
+}
diff --git a/tests/phpunit/includes/StatusTest.php b/tests/phpunit/includes/StatusTest.php
new file mode 100644
index 00000000..628c59b6
--- /dev/null
+++ b/tests/phpunit/includes/StatusTest.php
@@ -0,0 +1,573 @@
+<?php
+
+/**
+ * @author Adam Shorland
+ */
+class StatusTest extends MediaWikiLangTestCase {
+
+ public function testCanConstruct() {
+ new Status();
+ $this->assertTrue( true );
+ }
+
+ /**
+ * @dataProvider provideValues
+ * @covers Status::newGood
+ */
+ public function testNewGood( $value = null ) {
+ $status = Status::newGood( $value );
+ $this->assertTrue( $status->isGood() );
+ $this->assertTrue( $status->isOK() );
+ $this->assertEquals( $value, $status->getValue() );
+ }
+
+ public static function provideValues() {
+ return array(
+ array(),
+ array( 'foo' ),
+ array( array( 'foo' => 'bar' ) ),
+ array( new Exception() ),
+ array( 1234 ),
+ );
+ }
+
+ /**
+ * @covers Status::newFatal
+ */
+ public function testNewFatalWithMessage() {
+ $message = $this->getMockBuilder( 'Message' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $status = Status::newFatal( $message );
+ $this->assertFalse( $status->isGood() );
+ $this->assertFalse( $status->isOK() );
+ $this->assertEquals( $message, $status->getMessage() );
+ }
+
+ /**
+ * @covers Status::newFatal
+ */
+ public function testNewFatalWithString() {
+ $message = 'foo';
+ $status = Status::newFatal( $message );
+ $this->assertFalse( $status->isGood() );
+ $this->assertFalse( $status->isOK() );
+ $this->assertEquals( $message, $status->getMessage()->getKey() );
+ }
+
+ /**
+ * @dataProvider provideSetResult
+ * @covers Status::setResult
+ */
+ public function testSetResult( $ok, $value = null ) {
+ $status = new Status();
+ $status->setResult( $ok, $value );
+ $this->assertEquals( $ok, $status->isOK() );
+ $this->assertEquals( $value, $status->getValue() );
+ }
+
+ public static function provideSetResult() {
+ return array(
+ array( true ),
+ array( false ),
+ array( true, 'value' ),
+ array( false, 'value' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsOk
+ * @covers Status::isOk
+ */
+ public function testIsOk( $ok ) {
+ $status = new Status();
+ $status->ok = $ok;
+ $this->assertEquals( $ok, $status->isOK() );
+ }
+
+ public static function provideIsOk() {
+ return array(
+ array( true ),
+ array( false ),
+ );
+ }
+
+ /**
+ * @covers Status::getValue
+ */
+ public function testGetValue() {
+ $status = new Status();
+ $status->value = 'foobar';
+ $this->assertEquals( 'foobar', $status->getValue() );
+ }
+
+ /**
+ * @dataProvider provideIsGood
+ * @covers Status::isGood
+ */
+ public function testIsGood( $ok, $errors, $expected ) {
+ $status = new Status();
+ $status->ok = $ok;
+ $status->errors = $errors;
+ $this->assertEquals( $expected, $status->isGood() );
+ }
+
+ public static function provideIsGood() {
+ return array(
+ array( true, array(), true ),
+ array( true, array( 'foo' ), false ),
+ array( false, array(), false ),
+ array( false, array( 'foo' ), false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideMockMessageDetails
+ * @covers Status::warning
+ * @covers Status::getWarningsArray
+ * @covers Status::getStatusArray
+ */
+ public function testWarningWithMessage( $mockDetails ) {
+ $status = new Status();
+ $messages = $this->getMockMessages( $mockDetails );
+
+ foreach ( $messages as $message ) {
+ $status->warning( $message );
+ }
+ $warnings = $status->getWarningsArray();
+
+ $this->assertEquals( count( $messages ), count( $warnings ) );
+ foreach ( $messages as $key => $message ) {
+ $expectedArray = array_merge( array( $message->getKey() ), $message->getParams() );
+ $this->assertEquals( $warnings[$key], $expectedArray );
+ }
+ }
+
+ /**
+ * @dataProvider provideMockMessageDetails
+ * @covers Status::error
+ * @covers Status::getErrorsArray
+ * @covers Status::getStatusArray
+ */
+ public function testErrorWithMessage( $mockDetails ) {
+ $status = new Status();
+ $messages = $this->getMockMessages( $mockDetails );
+
+ foreach ( $messages as $message ) {
+ $status->error( $message );
+ }
+ $errors = $status->getErrorsArray();
+
+ $this->assertEquals( count( $messages ), count( $errors ) );
+ foreach ( $messages as $key => $message ) {
+ $expectedArray = array_merge( array( $message->getKey() ), $message->getParams() );
+ $this->assertEquals( $errors[$key], $expectedArray );
+ }
+ }
+
+ /**
+ * @dataProvider provideMockMessageDetails
+ * @covers Status::fatal
+ * @covers Status::getErrorsArray
+ * @covers Status::getStatusArray
+ */
+ public function testFatalWithMessage( $mockDetails ) {
+ $status = new Status();
+ $messages = $this->getMockMessages( $mockDetails );
+
+ foreach ( $messages as $message ) {
+ $status->fatal( $message );
+ }
+ $errors = $status->getErrorsArray();
+
+ $this->assertEquals( count( $messages ), count( $errors ) );
+ foreach ( $messages as $key => $message ) {
+ $expectedArray = array_merge( array( $message->getKey() ), $message->getParams() );
+ $this->assertEquals( $errors[$key], $expectedArray );
+ }
+ $this->assertFalse( $status->isOK() );
+ }
+
+ protected function getMockMessage( $key = 'key', $params = array() ) {
+ $message = $this->getMockBuilder( 'Message' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $message->expects( $this->atLeastOnce() )
+ ->method( 'getKey' )
+ ->will( $this->returnValue( $key ) );
+ $message->expects( $this->atLeastOnce() )
+ ->method( 'getParams' )
+ ->will( $this->returnValue( $params ) );
+ return $message;
+ }
+
+ /**
+ * @param array $messageDetails E.g. array( 'KEY' => array(/PARAMS/) )
+ * @return Message[]
+ */
+ protected function getMockMessages( $messageDetails ) {
+ $messages = array();
+ foreach ( $messageDetails as $key => $paramsArray ) {
+ $messages[] = $this->getMockMessage( $key, $paramsArray );
+ }
+ return $messages;
+ }
+
+ public static function provideMockMessageDetails() {
+ return array(
+ array( array( 'key1' => array( 'foo' => 'bar' ) ) ),
+ array( array( 'key1' => array( 'foo' => 'bar' ), 'key2' => array( 'foo2' => 'bar2' ) ) ),
+ );
+ }
+
+ /**
+ * @covers Status::merge
+ */
+ public function testMerge() {
+ $status1 = new Status();
+ $status2 = new Status();
+ $message1 = $this->getMockMessage( 'warn1' );
+ $message2 = $this->getMockMessage( 'error2' );
+ $status1->warning( $message1 );
+ $status2->error( $message2 );
+
+ $status1->merge( $status2 );
+ $this->assertEquals(
+ 2,
+ count( $status1->getWarningsArray() ) + count( $status1->getErrorsArray() )
+ );
+ }
+
+ /**
+ * @covers Status::merge
+ */
+ public function testMergeWithOverwriteValue() {
+ $status1 = new Status();
+ $status2 = new Status();
+ $message1 = $this->getMockMessage( 'warn1' );
+ $message2 = $this->getMockMessage( 'error2' );
+ $status1->warning( $message1 );
+ $status2->error( $message2 );
+ $status2->value = 'FooValue';
+
+ $status1->merge( $status2, true );
+ $this->assertEquals(
+ 2,
+ count( $status1->getWarningsArray() ) + count( $status1->getErrorsArray() )
+ );
+ $this->assertEquals( 'FooValue', $status1->getValue() );
+ }
+
+ /**
+ * @covers Status::hasMessage
+ */
+ public function testHasMessage() {
+ $status = new Status();
+ $status->fatal( 'bad' );
+ $status->fatal( wfMessage( 'bad-msg' ) );
+ $this->assertTrue( $status->hasMessage( 'bad' ) );
+ $this->assertTrue( $status->hasMessage( 'bad-msg' ) );
+ $this->assertTrue( $status->hasMessage( wfMessage( 'bad-msg' ) ) );
+ $this->assertFalse( $status->hasMessage( 'good' ) );
+ }
+
+ /**
+ * @dataProvider provideCleanParams
+ * @covers Status::cleanParams
+ */
+ public function testCleanParams( $cleanCallback, $params, $expected ) {
+ $method = new ReflectionMethod( 'Status', 'cleanParams' );
+ $method->setAccessible( true );
+ $status = new Status();
+ $status->cleanCallback = $cleanCallback;
+
+ $this->assertEquals( $expected, $method->invoke( $status, $params ) );
+ }
+
+ public static function provideCleanParams() {
+ $cleanCallback = function ( $value ) {
+ return '-' . $value . '-';
+ };
+
+ return array(
+ array( false, array( 'foo' => 'bar' ), array( 'foo' => 'bar' ) ),
+ array( $cleanCallback, array( 'foo' => 'bar' ), array( 'foo' => '-bar-' ) ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetWikiTextAndHtml
+ * @covers Status::getWikiText
+ * @todo test long and short context messages generated through this method
+ * this can not really be done now due to use of wfMessage()->plain()
+ * It is possible to mock such methods but only if namespaces are used
+ */
+ public function testGetWikiText( Status $status, $wikitext, $html ) {
+ $this->assertEquals( $wikitext, $status->getWikiText() );
+ }
+
+ /**
+ * @dataProvider provideGetWikiTextAndHtml
+ * @covers Status::getHtml
+ * @todo test long and short context messages generated through this method
+ * this can not really be done now due to use of $this->getWikiText using
+ * wfMessage()->plain(). It is possible to mock such methods but only if
+ * namespaces are used.
+ */
+ public function testGetHtml( Status $status, $wikitext, $html ) {
+ $this->assertEquals( $html, $status->getHTML() );
+ }
+
+ /**
+ * @return array Array of arrays with values;
+ * 0 => status object
+ * 1 => expected string (with no context)
+ */
+ public static function provideGetWikiTextAndHtml() {
+ $testCases = array();
+
+ $testCases['GoodStatus'] = array(
+ new Status(),
+ "Internal error: Status::getWikiText called for a good result, this is incorrect\n",
+ "<p>Internal error: Status::getWikiText called for a good result, this is incorrect\n</p>",
+ );
+
+ $status = new Status();
+ $status->ok = false;
+ $testCases['GoodButNoError'] = array(
+ $status,
+ "Internal error: Status::getWikiText: Invalid result object: no error text but not OK\n",
+ "<p>Internal error: Status::getWikiText: Invalid result object: no error text but not OK\n</p>",
+ );
+
+ $status = new Status();
+ $status->warning( 'fooBar!' );
+ $testCases['1StringWarning'] = array(
+ $status,
+ "<fooBar!>",
+ "<p>&lt;fooBar!&gt;\n</p>",
+ );
+
+ $status = new Status();
+ $status->warning( 'fooBar!' );
+ $status->warning( 'fooBar2!' );
+ $testCases['2StringWarnings'] = array(
+ $status,
+ "* <fooBar!>\n* <fooBar2!>\n",
+ "<ul><li> &lt;fooBar!&gt;</li>\n<li> &lt;fooBar2!&gt;</li></ul>\n",
+ );
+
+ $status = new Status();
+ $status->warning( new Message( 'fooBar!', array( 'foo', 'bar' ) ) );
+ $testCases['1MessageWarning'] = array(
+ $status,
+ "<fooBar!>",
+ "<p>&lt;fooBar!&gt;\n</p>",
+ );
+
+ $status = new Status();
+ $status->warning( new Message( 'fooBar!', array( 'foo', 'bar' ) ) );
+ $status->warning( new Message( 'fooBar2!' ) );
+ $testCases['2MessageWarnings'] = array(
+ $status,
+ "* <fooBar!>\n* <fooBar2!>\n",
+ "<ul><li> &lt;fooBar!&gt;</li>\n<li> &lt;fooBar2!&gt;</li></ul>\n",
+ );
+
+ return $testCases;
+ }
+
+ /**
+ * @dataProvider provideGetMessage
+ * @covers Status::getMessage
+ * @todo test long and short context messages generated through this method
+ */
+ public function testGetMessage( Status $status, $expectedParams = array(), $expectedKey ) {
+ $message = $status->getMessage();
+ $this->assertInstanceOf( 'Message', $message );
+ $this->assertEquals( $expectedParams, $message->getParams(), 'Message::getParams' );
+ $this->assertEquals( $expectedKey, $message->getKey(), 'Message::getKey' );
+ }
+
+ /**
+ * @return array Array of arrays with values;
+ * 0 => status object
+ * 1 => expected Message parameters (with no context)
+ * 2 => expected Message key
+ */
+ public static function provideGetMessage() {
+ $testCases = array();
+
+ $testCases['GoodStatus'] = array(
+ new Status(),
+ array( "Status::getMessage called for a good result, this is incorrect\n" ),
+ 'internalerror_info'
+ );
+
+ $status = new Status();
+ $status->ok = false;
+ $testCases['GoodButNoError'] = array(
+ $status,
+ array( "Status::getMessage: Invalid result object: no error text but not OK\n" ),
+ 'internalerror_info'
+ );
+
+ $status = new Status();
+ $status->warning( 'fooBar!' );
+ $testCases['1StringWarning'] = array(
+ $status,
+ array(),
+ 'fooBar!'
+ );
+
+ // FIXME: Assertion tries to compare a StubUserLang with a Language object, because
+ // "data providers are executed before both the call to the setUpBeforeClass static method
+ // and the first call to the setUp method. Because of that you can't access any variables
+ // you create there from within a data provider."
+ // http://phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html
+// $status = new Status();
+// $status->warning( 'fooBar!' );
+// $status->warning( 'fooBar2!' );
+// $testCases[ '2StringWarnings' ] = array(
+// $status,
+// array( new Message( 'fooBar!' ), new Message( 'fooBar2!' ) ),
+// "* \$1\n* \$2"
+// );
+
+ $status = new Status();
+ $status->warning( new Message( 'fooBar!', array( 'foo', 'bar' ) ) );
+ $testCases['1MessageWarning'] = array(
+ $status,
+ array( 'foo', 'bar' ),
+ 'fooBar!'
+ );
+
+ $status = new Status();
+ $status->warning( new Message( 'fooBar!', array( 'foo', 'bar' ) ) );
+ $status->warning( new Message( 'fooBar2!' ) );
+ $testCases['2MessageWarnings'] = array(
+ $status,
+ array( new Message( 'fooBar!', array( 'foo', 'bar' ) ), new Message( 'fooBar2!' ) ),
+ "* \$1\n* \$2"
+ );
+
+ return $testCases;
+ }
+
+ /**
+ * @covers Status::replaceMessage
+ */
+ public function testReplaceMessage() {
+ $status = new Status();
+ $message = new Message( 'key1', array( 'foo1', 'bar1' ) );
+ $status->error( $message );
+ $newMessage = new Message( 'key2', array( 'foo2', 'bar2' ) );
+
+ $status->replaceMessage( $message, $newMessage );
+
+ $this->assertEquals( $newMessage, $status->errors[0]['message'] );
+ }
+
+ /**
+ * @covers Status::getErrorMessage
+ */
+ public function testGetErrorMessage() {
+ $method = new ReflectionMethod( 'Status', 'getErrorMessage' );
+ $method->setAccessible( true );
+ $status = new Status();
+ $key = 'foo';
+ $params = array( 'bar' );
+
+ /** @var Message $message */
+ $message = $method->invoke( $status, array_merge( array( $key ), $params ) );
+ $this->assertInstanceOf( 'Message', $message );
+ $this->assertEquals( $key, $message->getKey() );
+ $this->assertEquals( $params, $message->getParams() );
+ }
+
+ /**
+ * @covers Status::getErrorMessageArray
+ */
+ public function testGetErrorMessageArray() {
+ $method = new ReflectionMethod( 'Status', 'getErrorMessageArray' );
+ $method->setAccessible( true );
+ $status = new Status();
+ $key = 'foo';
+ $params = array( 'bar' );
+
+ /** @var Message[] $messageArray */
+ $messageArray = $method->invoke(
+ $status,
+ array(
+ array_merge( array( $key ), $params ),
+ array_merge( array( $key ), $params )
+ )
+ );
+
+ $this->assertInternalType( 'array', $messageArray );
+ $this->assertCount( 2, $messageArray );
+ foreach ( $messageArray as $message ) {
+ $this->assertInstanceOf( 'Message', $message );
+ $this->assertEquals( $key, $message->getKey() );
+ $this->assertEquals( $params, $message->getParams() );
+ }
+ }
+
+ /**
+ * @covers Status::getErrorsByType
+ */
+ public function testGetErrorsByType() {
+ $status = new Status();
+ $warning = new Message( 'warning111' );
+ $error = new Message( 'error111' );
+ $status->warning( $warning );
+ $status->error( $error );
+
+ $warnings = $status->getErrorsByType( 'warning' );
+ $errors = $status->getErrorsByType( 'error' );
+
+ $this->assertCount( 1, $warnings );
+ $this->assertCount( 1, $errors );
+ $this->assertEquals( $warning, $warnings[0]['message'] );
+ $this->assertEquals( $error, $errors[0]['message'] );
+ }
+
+ /**
+ * @covers Status::__wakeup
+ */
+ public function testWakeUpSanitizesCallback() {
+ $status = new Status();
+ $status->cleanCallback = function ( $value ) {
+ return '-' . $value . '-';
+ };
+ $status->__wakeup();
+ $this->assertEquals( false, $status->cleanCallback );
+ }
+
+ /**
+ * @dataProvider provideNonObjectMessages
+ * @covers Status::getStatusArray
+ */
+ public function testGetStatusArrayWithNonObjectMessages( $nonObjMsg ) {
+ $status = new Status();
+ if ( !array_key_exists( 1, $nonObjMsg ) ) {
+ $status->warning( $nonObjMsg[0] );
+ } else {
+ $status->warning( $nonObjMsg[0], $nonObjMsg[1] );
+ }
+
+ $array = $status->getWarningsArray(); // We use getWarningsArray to access getStatusArray
+
+ $this->assertEquals( 1, count( $array ) );
+ $this->assertEquals( $nonObjMsg, $array[0] );
+ }
+
+ public static function provideNonObjectMessages() {
+ return array(
+ array( array( 'ImaString', array( 'param1' => 'value1' ) ) ),
+ array( array( 'ImaString' ) ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/TemplateCategoriesTest.php b/tests/phpunit/includes/TemplateCategoriesTest.php
new file mode 100644
index 00000000..b0d17267
--- /dev/null
+++ b/tests/phpunit/includes/TemplateCategoriesTest.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @group Database
+ */
+require __DIR__ . "/../../../maintenance/runJobs.php";
+
+class TemplateCategoriesTest extends MediaWikiLangTestCase {
+
+ /**
+ * @covers Title::getParentCategories
+ */
+ public function testTemplateCategories() {
+ $user = new User();
+ $user->mRights = array( 'createpage', 'edit', 'purge', 'delete' );
+
+ $title = Title::newFromText( "Categorized from template" );
+ $page = WikiPage::factory( $title );
+ $page->doEditContent(
+ new WikitextContent( '{{Categorising template}}' ),
+ 'Create a page with a template',
+ 0,
+ false,
+ $user
+ );
+
+ $this->assertEquals(
+ array(),
+ $title->getParentCategories(),
+ 'Verify that the category doesn\'t contain the page before the template is created'
+ );
+
+ // Create template
+ $template = WikiPage::factory( Title::newFromText( 'Template:Categorising template' ) );
+ $template->doEditContent(
+ new WikitextContent( '[[Category:Solved bugs]]' ),
+ 'Add a category through a template',
+ 0,
+ false,
+ $user
+ );
+
+ // Run the job queue
+ JobQueueGroup::destroySingletons();
+ $jobs = new RunJobs;
+ $jobs->loadParamsAndArgs( null, array( 'quiet' => true ), null );
+ $jobs->execute();
+
+ // Make sure page is in the category
+ $this->assertEquals(
+ array( 'Category:Solved_bugs' => $title->getPrefixedText() ),
+ $title->getParentCategories(),
+ 'Verify that the page is in the category after the template is created'
+ );
+
+ // Edit the template
+ $template->doEditContent(
+ new WikitextContent( '[[Category:Solved bugs 2]]' ),
+ 'Change the category added by the template',
+ 0,
+ false,
+ $user
+ );
+
+ // Run the job queue
+ JobQueueGroup::destroySingletons();
+ $jobs = new RunJobs;
+ $jobs->loadParamsAndArgs( null, array( 'quiet' => true ), null );
+ $jobs->execute();
+
+ // Make sure page is in the right category
+ $this->assertEquals(
+ array( 'Category:Solved_bugs_2' => $title->getPrefixedText() ),
+ $title->getParentCategories(),
+ 'Verify that the page is in the right category after the template is edited'
+ );
+
+ // Now delete the template
+ $error = '';
+ $template->doDeleteArticleReal( 'Delete the template', false, 0, true, $error, $user );
+
+ // Run the job queue
+ JobQueueGroup::destroySingletons();
+ $jobs = new RunJobs;
+ $jobs->loadParamsAndArgs( null, array( 'quiet' => true ), null );
+ $jobs->execute();
+
+ // Make sure the page is no longer in the category
+ $this->assertEquals(
+ array(),
+ $title->getParentCategories(),
+ 'Verify that the page is no longer in the category after template deletion'
+ );
+
+ }
+}
diff --git a/tests/phpunit/includes/TestUser.php b/tests/phpunit/includes/TestUser.php
new file mode 100644
index 00000000..610a6acd
--- /dev/null
+++ b/tests/phpunit/includes/TestUser.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Wraps the user object, so we can also retain full access to properties
+ * like password if we log in via the API.
+ */
+class TestUser {
+ public $username;
+ public $password;
+ public $email;
+ public $groups;
+ public $user;
+
+ public function __construct( $username, $realname = 'Real Name',
+ $email = 'sample@example.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 );
+
+ // Adjust groups by adding any missing ones and removing any extras
+ $currentGroups = $this->user->getGroups();
+ foreach ( array_diff( $this->groups, $currentGroups ) as $group ) {
+ $this->user->addGroup( $group );
+ }
+ foreach ( array_diff( $currentGroups, $this->groups ) as $group ) {
+ $this->user->removeGroup( $group );
+ }
+ $this->user->saveSettings();
+ }
+}
diff --git a/tests/phpunit/includes/TimeAdjustTest.php b/tests/phpunit/includes/TimeAdjustTest.php
new file mode 100644
index 00000000..ae82bc40
--- /dev/null
+++ b/tests/phpunit/includes/TimeAdjustTest.php
@@ -0,0 +1,39 @@
+<?php
+
+class TimeAdjustTest extends MediaWikiLangTestCase {
+ protected function setUp() {
+ parent::setUp();
+ }
+
+ /**
+ * Test offset usage for a given Language::userAdjust
+ * @dataProvider dataUserAdjust
+ * @covers Language::userAdjust
+ */
+ public function testUserAdjust( $date, $localTZoffset, $expected ) {
+ global $wgContLang;
+
+ $this->setMwGlobals( 'wgLocalTZoffset', $localTZoffset );
+
+ $this->assertEquals(
+ $expected,
+ strval( $wgContLang->userAdjust( $date, '' ) ),
+ "User adjust {$date} by {$localTZoffset} minutes should give {$expected}"
+ );
+ }
+
+ public static function dataUserAdjust() {
+ return 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' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/TitleArrayFromResultTest.php b/tests/phpunit/includes/TitleArrayFromResultTest.php
new file mode 100644
index 00000000..0f7069ae
--- /dev/null
+++ b/tests/phpunit/includes/TitleArrayFromResultTest.php
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * @author Adam Shorland
+ * @covers TitleArrayFromResult
+ */
+class TitleArrayFromResultTest extends MediaWikiTestCase {
+
+ private function getMockResultWrapper( $row = null, $numRows = 1 ) {
+ $resultWrapper = $this->getMockBuilder( 'ResultWrapper' )
+ ->disableOriginalConstructor();
+
+ $resultWrapper = $resultWrapper->getMock();
+ $resultWrapper->expects( $this->atLeastOnce() )
+ ->method( 'current' )
+ ->will( $this->returnValue( $row ) );
+ $resultWrapper->expects( $this->any() )
+ ->method( 'numRows' )
+ ->will( $this->returnValue( $numRows ) );
+
+ return $resultWrapper;
+ }
+
+ private function getRowWithTitle( $namespace = 3, $title = 'foo' ) {
+ $row = new stdClass();
+ $row->page_namespace = $namespace;
+ $row->page_title = $title;
+ return $row;
+ }
+
+ private function getTitleArrayFromResult( $resultWrapper ) {
+ return new TitleArrayFromResult( $resultWrapper );
+ }
+
+ /**
+ * @covers TitleArrayFromResult::__construct
+ */
+ public function testConstructionWithFalseRow() {
+ $row = false;
+ $resultWrapper = $this->getMockResultWrapper( $row );
+
+ $object = $this->getTitleArrayFromResult( $resultWrapper );
+
+ $this->assertEquals( $resultWrapper, $object->res );
+ $this->assertSame( 0, $object->key );
+ $this->assertEquals( $row, $object->current );
+ }
+
+ /**
+ * @covers TitleArrayFromResult::__construct
+ */
+ public function testConstructionWithRow() {
+ $namespace = 0;
+ $title = 'foo';
+ $row = $this->getRowWithTitle( $namespace, $title );
+ $resultWrapper = $this->getMockResultWrapper( $row );
+
+ $object = $this->getTitleArrayFromResult( $resultWrapper );
+
+ $this->assertEquals( $resultWrapper, $object->res );
+ $this->assertSame( 0, $object->key );
+ $this->assertInstanceOf( 'Title', $object->current );
+ $this->assertEquals( $namespace, $object->current->mNamespace );
+ $this->assertEquals( $title, $object->current->mTextform );
+ }
+
+ public static function provideNumberOfRows() {
+ return array(
+ array( 0 ),
+ array( 1 ),
+ array( 122 ),
+ );
+ }
+
+ /**
+ * @dataProvider provideNumberOfRows
+ * @covers TitleArrayFromResult::count
+ */
+ public function testCountWithVaryingValues( $numRows ) {
+ $object = $this->getTitleArrayFromResult( $this->getMockResultWrapper(
+ $this->getRowWithTitle(),
+ $numRows
+ ) );
+ $this->assertEquals( $numRows, $object->count() );
+ }
+
+ /**
+ * @covers TitleArrayFromResult::current
+ */
+ public function testCurrentAfterConstruction() {
+ $namespace = 0;
+ $title = 'foo';
+ $row = $this->getRowWithTitle( $namespace, $title );
+ $object = $this->getTitleArrayFromResult( $this->getMockResultWrapper( $row ) );
+ $this->assertInstanceOf( 'Title', $object->current() );
+ $this->assertEquals( $namespace, $object->current->mNamespace );
+ $this->assertEquals( $title, $object->current->mTextform );
+ }
+
+ public function provideTestValid() {
+ return array(
+ array( $this->getRowWithTitle(), true ),
+ array( false, false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideTestValid
+ * @covers TitleArrayFromResult::valid
+ */
+ public function testValid( $input, $expected ) {
+ $object = $this->getTitleArrayFromResult( $this->getMockResultWrapper( $input ) );
+ $this->assertEquals( $expected, $object->valid() );
+ }
+
+ //@todo unit test for key()
+ //@todo unit test for next()
+ //@todo unit test for rewind()
+}
diff --git a/tests/phpunit/includes/TitleMethodsTest.php b/tests/phpunit/includes/TitleMethodsTest.php
new file mode 100644
index 00000000..5904facd
--- /dev/null
+++ b/tests/phpunit/includes/TitleMethodsTest.php
@@ -0,0 +1,300 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ *
+ * @note We don't make assumptions about the main namespace.
+ * But we do expect the Help namespace to contain Wikitext.
+ */
+class TitleMethodsTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ global $wgContLang;
+
+ parent::setUp();
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgExtraNamespaces',
+ array(
+ 12302 => 'TEST-JS',
+ 12303 => 'TEST-JS_TALK',
+ )
+ );
+
+ $this->mergeMwGlobalArrayValue(
+ 'wgNamespaceContentModels',
+ array(
+ 12302 => CONTENT_MODEL_JAVASCRIPT,
+ )
+ );
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ protected function tearDown() {
+ global $wgContLang;
+
+ parent::tearDown();
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ public static function provideEquals() {
+ return array(
+ array( 'Main Page', 'Main Page', true ),
+ array( 'Main Page', 'Not The Main Page', false ),
+ array( 'Main Page', 'Project:Main Page', false ),
+ array( 'File:Example.png', 'Image:Example.png', true ),
+ array( 'Special:Version', 'Special:Version', true ),
+ array( 'Special:Version', 'Special:Recentchanges', false ),
+ array( 'Special:Version', 'Main Page', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideEquals
+ * @covers Title::equals
+ */
+ public function testEquals( $titleA, $titleB, $expectedBool ) {
+ $titleA = Title::newFromText( $titleA );
+ $titleB = Title::newFromText( $titleB );
+
+ $this->assertEquals( $expectedBool, $titleA->equals( $titleB ) );
+ $this->assertEquals( $expectedBool, $titleB->equals( $titleA ) );
+ }
+
+ public static function provideInNamespace() {
+ return array(
+ array( 'Main Page', NS_MAIN, true ),
+ array( 'Main Page', NS_TALK, false ),
+ array( 'Main Page', NS_USER, false ),
+ array( 'User:Foo', NS_USER, true ),
+ array( 'User:Foo', NS_USER_TALK, false ),
+ array( 'User:Foo', NS_TEMPLATE, false ),
+ array( 'User_talk:Foo', NS_USER_TALK, true ),
+ array( 'User_talk:Foo', NS_USER, false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideInNamespace
+ * @covers Title::inNamespace
+ */
+ public function testInNamespace( $title, $ns, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->inNamespace( $ns ) );
+ }
+
+ /**
+ * @covers Title::inNamespaces
+ */
+ public function testInNamespaces() {
+ $mainpage = Title::newFromText( 'Main Page' );
+ $this->assertTrue( $mainpage->inNamespaces( NS_MAIN, NS_USER ) );
+ $this->assertTrue( $mainpage->inNamespaces( array( NS_MAIN, NS_USER ) ) );
+ $this->assertTrue( $mainpage->inNamespaces( array( NS_USER, NS_MAIN ) ) );
+ $this->assertFalse( $mainpage->inNamespaces( array( NS_PROJECT, NS_TEMPLATE ) ) );
+ }
+
+ public static function provideHasSubjectNamespace() {
+ return array(
+ array( 'Main Page', NS_MAIN, true ),
+ array( 'Main Page', NS_TALK, true ),
+ array( 'Main Page', NS_USER, false ),
+ array( 'User:Foo', NS_USER, true ),
+ array( 'User:Foo', NS_USER_TALK, true ),
+ array( 'User:Foo', NS_TEMPLATE, false ),
+ array( 'User_talk:Foo', NS_USER_TALK, true ),
+ array( 'User_talk:Foo', NS_USER, true ),
+ );
+ }
+
+ /**
+ * @dataProvider provideHasSubjectNamespace
+ * @covers Title::hasSubjectNamespace
+ */
+ public function testHasSubjectNamespace( $title, $ns, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) );
+ }
+
+ public function dataGetContentModel() {
+ return array(
+ array( 'Help:Foo', CONTENT_MODEL_WIKITEXT ),
+ array( 'Help:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'Help:Foo/bar.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ),
+ array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ),
+ array( 'MediaWiki:Foo/bar.css', CONTENT_MODEL_CSS ),
+ array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'TEST-JS:Foo', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'TEST-JS:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'TEST-JS:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'TEST-JS_TALK:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetContentModel
+ * @covers Title::getContentModel
+ */
+ public function testGetContentModel( $title, $expectedModelId ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedModelId, $title->getContentModel() );
+ }
+
+ /**
+ * @dataProvider dataGetContentModel
+ * @covers Title::hasContentModel
+ */
+ public function testHasContentModel( $title, $expectedModelId ) {
+ $title = Title::newFromText( $title );
+ $this->assertTrue( $title->hasContentModel( $expectedModelId ) );
+ }
+
+ public static function provideIsCssOrJsPage() {
+ return array(
+ array( 'Help:Foo', false ),
+ array( 'Help:Foo.js', false ),
+ array( 'Help:Foo/bar.js', false ),
+ array( 'User:Foo', false ),
+ array( 'User:Foo.js', false ),
+ array( 'User:Foo/bar.js', false ),
+ array( 'User:Foo/bar.css', false ),
+ array( 'User talk:Foo/bar.css', false ),
+ array( 'User:Foo/bar.js.xxx', false ),
+ array( 'User:Foo/bar.xxx', false ),
+ array( 'MediaWiki:Foo.js', true ),
+ array( 'MediaWiki:Foo.css', true ),
+ array( 'MediaWiki:Foo.JS', false ),
+ array( 'MediaWiki:Foo.CSS', false ),
+ array( 'MediaWiki:Foo.css.xxx', false ),
+ array( 'TEST-JS:Foo', false ),
+ array( 'TEST-JS:Foo.js', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsCssOrJsPage
+ * @covers Title::isCssOrJsPage
+ */
+ public function testIsCssOrJsPage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isCssOrJsPage() );
+ }
+
+ public static function provideIsCssJsSubpage() {
+ return array(
+ array( 'Help:Foo', false ),
+ array( 'Help:Foo.js', false ),
+ array( 'Help:Foo/bar.js', false ),
+ array( 'User:Foo', false ),
+ array( 'User:Foo.js', false ),
+ array( 'User:Foo/bar.js', true ),
+ array( 'User:Foo/bar.css', true ),
+ array( 'User talk:Foo/bar.css', false ),
+ array( 'User:Foo/bar.js.xxx', false ),
+ array( 'User:Foo/bar.xxx', false ),
+ array( 'MediaWiki:Foo.js', false ),
+ array( 'User:Foo/bar.JS', false ),
+ array( 'User:Foo/bar.CSS', false ),
+ array( 'TEST-JS:Foo', false ),
+ array( 'TEST-JS:Foo.js', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsCssJsSubpage
+ * @covers Title::isCssJsSubpage
+ */
+ public function testIsCssJsSubpage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isCssJsSubpage() );
+ }
+
+ public static function provideIsCssSubpage() {
+ return array(
+ array( 'Help:Foo', false ),
+ array( 'Help:Foo.css', false ),
+ array( 'User:Foo', false ),
+ array( 'User:Foo.js', false ),
+ array( 'User:Foo.css', false ),
+ array( 'User:Foo/bar.js', false ),
+ array( 'User:Foo/bar.css', true ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsCssSubpage
+ * @covers Title::isCssSubpage
+ */
+ public function testIsCssSubpage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isCssSubpage() );
+ }
+
+ public static function provideIsJsSubpage() {
+ return array(
+ array( 'Help:Foo', false ),
+ array( 'Help:Foo.css', false ),
+ array( 'User:Foo', false ),
+ array( 'User:Foo.js', false ),
+ array( 'User:Foo.css', false ),
+ array( 'User:Foo/bar.js', true ),
+ array( 'User:Foo/bar.css', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsJsSubpage
+ * @covers Title::isJsSubpage
+ */
+ public function testIsJsSubpage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isJsSubpage() );
+ }
+
+ public static function provideIsWikitextPage() {
+ return array(
+ array( 'Help:Foo', true ),
+ array( 'Help:Foo.js', true ),
+ array( 'Help:Foo/bar.js', true ),
+ array( 'User:Foo', true ),
+ array( 'User:Foo.js', true ),
+ array( 'User:Foo/bar.js', false ),
+ array( 'User:Foo/bar.css', false ),
+ array( 'User talk:Foo/bar.css', true ),
+ array( 'User:Foo/bar.js.xxx', true ),
+ array( 'User:Foo/bar.xxx', true ),
+ array( 'MediaWiki:Foo.js', false ),
+ array( 'MediaWiki:Foo.css', false ),
+ array( 'MediaWiki:Foo/bar.css', false ),
+ array( 'User:Foo/bar.JS', true ),
+ array( 'User:Foo/bar.CSS', true ),
+ array( 'TEST-JS:Foo', false ),
+ array( 'TEST-JS:Foo.js', false ),
+ array( 'TEST-JS_TALK:Foo.js', true ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsWikitextPage
+ * @covers Title::isWikitextPage
+ */
+ public function testIsWikitextPage( $title, $expectedBool ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedBool, $title->isWikitextPage() );
+ }
+}
diff --git a/tests/phpunit/includes/TitlePermissionTest.php b/tests/phpunit/includes/TitlePermissionTest.php
new file mode 100644
index 00000000..d2400b3f
--- /dev/null
+++ b/tests/phpunit/includes/TitlePermissionTest.php
@@ -0,0 +1,770 @@
+<?php
+
+/**
+ * @group Database
+ *
+ * @covers Title::getUserPermissionsErrors
+ * @covers Title::getUserPermissionsErrorsInternal
+ */
+class TitlePermissionTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var string
+ */
+ protected $userName, $altUserName;
+
+ /**
+ * @var Title
+ */
+ protected $title;
+
+ /**
+ * @var User
+ */
+ protected $user, $anonUser, $userUser, $altUser;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $langObj = Language::factory( 'en' );
+ $localZone = 'UTC';
+ $localOffset = date( 'Z' ) / 60;
+
+ $this->setMwGlobals( array(
+ 'wgMemc' => new EmptyBagOStuff,
+ 'wgContLang' => $langObj,
+ 'wgLanguageCode' => 'en',
+ 'wgLang' => $langObj,
+ 'wgLocaltimezone' => $localZone,
+ 'wgLocalTZoffset' => $localOffset,
+ 'wgNamespaceProtection' => array(
+ NS_MEDIAWIKI => 'editinterface',
+ ),
+ ) );
+ // Without this testUserBlock will use a non-English context on non-English MediaWiki
+ // installations (because of how Title::checkUserBlock is implemented) and fail.
+ RequestContext::resetMain();
+
+ $this->userName = 'Useruser';
+ $this->altUserName = 'Altuseruser';
+ date_default_timezone_set( $localZone );
+
+ $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;
+ }
+ }
+
+ protected function setUserPerm( $perm ) {
+ // Setting member variables is evil!!!
+
+ if ( is_array( $perm ) ) {
+ $this->user->mRights = $perm;
+ } else {
+ $this->user->mRights = array( $perm );
+ }
+ }
+
+ protected function setTitle( $ns, $title = "Main_Page" ) {
+ $this->title = Title::makeTitle( $ns, $title );
+ }
+
+ protected 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;
+ }
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public 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' ) )
+ );
+
+ if ( $this->isWikitextNS( NS_MAIN ) ) {
+ //NOTE: some content models don't allow moving
+ // @todo find a Wikitext namespace for testing
+
+ $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 )
+ );
+
+ 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, $this->user, true ) );
+ $this->assertEquals( $check[$action][3],
+ $this->title->quickUserCan( $action, $this->user ) );
+ # count( User::getGroupsWithPermissions( $action ) ) < 1
+ }
+ }
+
+ protected 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 );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public function testSpecialsAndNSPermissions() {
+ global $wgNamespaceProtection;
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_SPECIAL );
+
+ $this->assertEquals( array( array( 'badaccess-group0' ), array( 'ns-specialprotected' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $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 ) );
+
+ $wgNamespaceProtection[NS_USER] = array( 'bogus' );
+
+ $this->setTitle( NS_USER );
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ), array( 'namespaceprotected', 'User', 'bogus' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( array( 'protectedinterface', 'bogus' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( array( 'protectedinterface', 'bogus' ) ),
+ $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->user ) );
+
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'bogus', $this->user ) );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public function testCssAndJavascriptPermissions() {
+ $this->setUser( $this->userName );
+
+ $this->setTitle( NS_USER, $this->userName . '/test.js' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'mycustomjsprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ), array( 'mycustomjsprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'mycustomjsprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ) )
+ );
+
+ $this->setTitle( NS_USER, $this->userName . '/test.css' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'mycustomcssprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'mycustomcssprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'mycustomcssprotected', 'bogus' ) )
+ );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/test.js' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'customjsprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ), array( 'customjsprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ), array( 'customjsprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ), array( 'customjsprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ) )
+ );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/test.css' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'customcssprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ), array( 'customcssprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ), array( 'customcssprotected', 'bogus' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'customcssprotected', 'bogus' ) )
+ );
+
+ $this->setTitle( NS_USER, $this->altUserName . '/tempo' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) )
+ );
+ }
+
+ protected function runCSSandJSPermissions( $result0, $result1, $result2, $result3, $result4 ) {
+ $this->setUserPerm( '' );
+ $this->assertEquals( $result0,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'editmyusercss' );
+ $this->assertEquals( $result1,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'editmyuserjs' );
+ $this->assertEquals( $result2,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'editusercss' );
+ $this->assertEquals( $result3,
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+
+ $this->setUserPerm( 'edituserjs' );
+ $this->assertEquals( $result4,
+ $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 ) );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public function testPageRestrictions() {
+ global $wgContLang;
+
+ $prefix = $wgContLang->getFormattedNsText( NS_PROJECT );
+
+ $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', $this->user ) );
+ $this->title->mRestrictions = array( "edit" => array( 'bogus', "sysop", "protect", "" ),
+ "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus', 'bogus' ),
+ array( 'protectedpagetext', 'editprotected', 'bogus' ),
+ array( 'protectedpagetext', 'protect', 'bogus' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus', 'edit' ),
+ array( 'protectedpagetext', 'editprotected', 'edit' ),
+ array( 'protectedpagetext', 'protect', 'edit' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ $this->setUserPerm( "" );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus', 'bogus' ),
+ array( 'protectedpagetext', 'editprotected', 'bogus' ),
+ array( 'protectedpagetext', 'protect', 'bogus' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ),
+ array( 'protectedpagetext', 'bogus', 'edit' ),
+ array( 'protectedpagetext', 'editprotected', 'edit' ),
+ array( 'protectedpagetext', 'protect', 'edit' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ $this->setUserPerm( array( "edit", "editprotected" ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus', 'bogus' ),
+ array( 'protectedpagetext', 'protect', 'bogus' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array(
+ array( 'protectedpagetext', 'bogus', 'edit' ),
+ array( 'protectedpagetext', 'protect', 'edit' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+
+ $this->title->mCascadeRestriction = true;
+ $this->setUserPerm( "edit" );
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'bogus', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'edit', $this->user ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus', 'bogus' ),
+ array( 'protectedpagetext', 'editprotected', 'bogus' ),
+ array( 'protectedpagetext', 'protect', 'bogus' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus', 'edit' ),
+ array( 'protectedpagetext', 'editprotected', 'edit' ),
+ array( 'protectedpagetext', 'protect', 'edit' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+
+ $this->setUserPerm( array( "edit", "editprotected" ) );
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'bogus', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->quickUserCan( 'edit', $this->user ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus', 'bogus' ),
+ array( 'protectedpagetext', 'protect', 'bogus' ),
+ array( 'protectedpagetext', 'protect', 'bogus' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus',
+ $this->user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus', 'edit' ),
+ array( 'protectedpagetext', 'protect', 'edit' ),
+ array( 'protectedpagetext', 'protect', 'edit' ) ),
+ $this->title->getUserPermissionsErrors( 'edit',
+ $this->user ) );
+ }
+
+ public function testCascadingSourcesRestrictions() {
+ $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->user ) );
+ $this->assertEquals( array( array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ),
+ array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ),
+ array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ) ),
+ $this->title->getUserPermissionsErrors( 'bogus', $this->user ) );
+
+ $this->assertEquals( true,
+ $this->title->userCan( 'edit', $this->user ) );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
+ }
+
+ /**
+ * @todo This test method should be split up into separate test methods and
+ * data providers
+ */
+ public function testActionPermissions() {
+ $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'] = wfGetDB( DB_SLAVE )->getInfinity();
+ $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->user ) );
+
+ $this->title->mTitleProtection['pt_create_perm'] = 'sysop';
+ $this->setUserPerm( array( 'createpage', 'protect' ) );
+ $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ),
+ $this->title->getUserPermissionsErrors( 'create', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'create', $this->user ) );
+
+ $this->setUserPerm( array( 'createpage', 'editprotected' ) );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'create', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'create', $this->user ) );
+
+ $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->user ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->setUserPerm( array( "move" ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move', $this->user ) );
+ $this->assertEquals( array( array( 'immobile-source-namespace', 'Media' ) ),
+ $this->title->getUserPermissionsErrors( 'move', $this->user ) );
+
+ $this->setTitle( NS_HELP, "test page" );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'move', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'move', $this->user ) );
+
+ $this->title->mInterwiki = "no";
+ $this->assertEquals( array( array( 'immobile-source-page' ) ),
+ $this->title->getUserPermissionsErrors( 'move', $this->user ) );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move', $this->user ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->assertEquals( false,
+ $this->title->userCan( 'move-target', $this->user ) );
+ $this->assertEquals( array( array( 'immobile-target-namespace', 'Media' ) ),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+
+ $this->setTitle( NS_HELP, "test page" );
+ $this->assertEquals( array(),
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+ $this->assertEquals( true,
+ $this->title->userCan( 'move-target', $this->user ) );
+
+ $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', $this->user ) );
+ }
+
+ public function testUserBlock() {
+ global $wgEmailConfirmToEdit, $wgEmailAuthentication;
+ $wgEmailConfirmToEdit = true;
+ $wgEmailAuthentication = true;
+
+ $this->setUserPerm( array( "createpage", "move" ) );
+ $this->setTitle( NS_HELP, "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', $this->user ) );
+
+ # $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', 0, $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', $this->user ) );
+ // quickUserCan should ignore user blocks
+ $this->assertEquals( true, $this->title->quickUserCan( 'move-target', $this->user ) );
+
+ global $wgLocalTZoffset;
+ $wgLocalTZoffset = -60;
+ $this->user->mBlockedby = $this->user->getName();
+ $this->user->mBlock = new Block( '127.0.8.1', 0, $this->user->getId(),
+ '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..fb58381f
--- /dev/null
+++ b/tests/phpunit/includes/TitleTest.php
@@ -0,0 +1,650 @@
+<?php
+
+/**
+ * @group Title
+ */
+class TitleTest extends MediaWikiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => Language::factory( 'en' ),
+ // User language
+ 'wgLang' => Language::factory( 'en' ),
+ 'wgAllowUserJs' => false,
+ 'wgDefaultLanguageVariant' => false,
+ ) );
+ }
+
+ /**
+ * @covers Title::legalChars
+ */
+ public 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"
+ );
+ }
+ }
+ }
+
+ public static function provideValidSecureAndSplit() {
+ return array(
+ array( 'Sandbox' ),
+ array( 'A "B"' ),
+ array( 'A \'B\'' ),
+ array( '.com' ),
+ array( '~' ),
+ array( '#' ),
+ array( '"' ),
+ array( '\'' ),
+ array( 'Talk:Sandbox' ),
+ array( 'Talk:Foo:Sandbox' ),
+ array( 'File:Example.svg' ),
+ array( 'File_talk:Example.svg' ),
+ array( 'Foo/.../Sandbox' ),
+ array( 'Sandbox/...' ),
+ array( 'A~~' ),
+ array( ':A' ),
+ // Length is 256 total, but only title part matters
+ array( 'Category:' . str_repeat( 'x', 248 ) ),
+ array( str_repeat( 'x', 252 ) ),
+ // interwiki prefix
+ array( 'localtestiw: #anchor' ),
+ array( 'localtestiw:' ),
+ array( 'localtestiw:foo' ),
+ array( 'localtestiw: foo # anchor' ),
+ array( 'localtestiw: Talk: Sandbox # anchor' ),
+ array( 'remotetestiw:' ),
+ array( 'remotetestiw: Talk: # anchor' ),
+ array( 'remotetestiw: #bar' ),
+ array( 'remotetestiw: Talk:' ),
+ array( 'remotetestiw: Talk: Foo' ),
+ array( 'localtestiw:remotetestiw:' ),
+ array( 'localtestiw:remotetestiw:foo' )
+ );
+ }
+
+ public static function provideInvalidSecureAndSplit() {
+ return array(
+ array( '' ),
+ array( ':' ),
+ array( '__ __' ),
+ array( ' __ ' ),
+ // Bad characters forbidden regardless of wgLegalTitleChars
+ array( 'A [ B' ),
+ array( 'A ] B' ),
+ array( 'A { B' ),
+ array( 'A } B' ),
+ array( 'A < B' ),
+ array( 'A > B' ),
+ array( 'A | B' ),
+ // URL encoding
+ array( 'A%20B' ),
+ array( 'A%23B' ),
+ array( 'A%2523B' ),
+ // XML/HTML character entity references
+ // Note: Commented out because they are not marked invalid by the PHP test as
+ // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first.
+ //'A &eacute; B',
+ //'A &#233; B',
+ //'A &#x00E9; B',
+ // Subject of NS_TALK does not roundtrip to NS_MAIN
+ array( 'Talk:File:Example.svg' ),
+ // Directory navigation
+ array( '.' ),
+ array( '..' ),
+ array( './Sandbox' ),
+ array( '../Sandbox' ),
+ array( 'Foo/./Sandbox' ),
+ array( 'Foo/../Sandbox' ),
+ array( 'Sandbox/.' ),
+ array( 'Sandbox/..' ),
+ // Tilde
+ array( 'A ~~~ Name' ),
+ array( 'A ~~~~ Signature' ),
+ array( 'A ~~~~~ Timestamp' ),
+ array( str_repeat( 'x', 256 ) ),
+ // Namespace prefix without actual title
+ array( 'Talk:' ),
+ array( 'Talk:#' ),
+ array( 'Category: ' ),
+ array( 'Category: #bar' ),
+ // interwiki prefix
+ array( 'localtestiw: Talk: # anchor' ),
+ array( 'localtestiw: Talk:' )
+ );
+ }
+
+ private function secureAndSplitGlobals() {
+ $this->setMwGlobals( array(
+ 'wgLocalInterwikis' => array( 'localtestiw' ),
+ 'wgHooks' => array(
+ 'InterwikiLoadPrefix' => array(
+ function ( $prefix, &$data ) {
+ if ( $prefix === 'localtestiw' ) {
+ $data = array( 'iw_url' => 'localtestiw' );
+ } elseif ( $prefix === 'remotetestiw' ) {
+ $data = array( 'iw_url' => 'remotetestiw' );
+ }
+ return false;
+ }
+ )
+ )
+ ));
+ }
+
+ /**
+ * See also mediawiki.Title.test.js
+ * @covers Title::secureAndSplit
+ * @dataProvider provideValidSecureAndSplit
+ * @note This mainly tests MediaWikiTitleCodec::parseTitle().
+ */
+ public function testSecureAndSplitValid( $text ) {
+ $this->secureAndSplitGlobals();
+ $this->assertInstanceOf( 'Title', Title::newFromText( $text ), "Valid: $text" );
+ }
+
+ /**
+ * See also mediawiki.Title.test.js
+ * @covers Title::secureAndSplit
+ * @dataProvider provideInvalidSecureAndSplit
+ * @note This mainly tests MediaWikiTitleCodec::parseTitle().
+ */
+ public function testSecureAndSplitInvalid( $text ) {
+ $this->secureAndSplitGlobals();
+ $this->assertNull( Title::newFromText( $text ), "Invalid: $text" );
+ }
+
+ public static function provideConvertByteClassToUnicodeClass() {
+ return array(
+ array(
+ ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+',
+ ' %!"$&\'()*,\\-./0-9:;=?@A-Z\\\\\\^_`a-z~+\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTYf-\\xFF+',
+ 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTY\\x66-\\xFD+',
+ 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTYf-y+',
+ 'QWERTYf-y+',
+ ),
+ array(
+ 'QWERTYf-\\x80+',
+ 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTY\\x66-\\x80+\\x23',
+ 'QWERTYf-\\x7F+#\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTY\\x66-\\x80+\\xD3',
+ 'QWERTYf-\\x7F+\\u0080-\\uFFFF',
+ ),
+ array(
+ '\\\\\\x99',
+ '\\\\\\u0080-\\uFFFF',
+ ),
+ array(
+ '-\\x99',
+ '\\-\\u0080-\\uFFFF',
+ ),
+ array(
+ 'QWERTY\\-\\x99',
+ 'QWERTY\\-\\u0080-\\uFFFF',
+ ),
+ array(
+ '\\\\x99',
+ '\\\\x99',
+ ),
+ array(
+ 'A-\\x9F',
+ 'A-\\x7F\\u0080-\\uFFFF',
+ ),
+ array(
+ '\\x66-\\x77QWERTY\\x88-\\x91FXZ',
+ 'f-wQWERTYFXZ\\u0080-\\uFFFF',
+ ),
+ array(
+ '\\x66-\\x99QWERTY\\xAA-\\xEEFXZ',
+ 'f-\\x7FQWERTYFXZ\\u0080-\\uFFFF',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideConvertByteClassToUnicodeClass
+ * @covers Title::convertByteClassToUnicodeClass
+ */
+ public function testConvertByteClassToUnicodeClass( $byteClass, $unicodeClass ) {
+ $this->assertEquals( $unicodeClass, Title::convertByteClassToUnicodeClass( $byteClass ) );
+ }
+
+ /**
+ * @dataProvider provideSpecialNamesWithAndWithoutParameter
+ * @covers Title::fixSpecialName
+ */
+ public function testFixSpecialNameRetainsParameter( $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"
+ );
+ }
+
+ public static function provideSpecialNamesWithAndWithoutParameter() {
+ return array(
+ array( 'Special:Version', null ),
+ array( 'Special:Version/', '' ),
+ array( 'Special:Version/param', 'param' ),
+ );
+ }
+
+ /**
+ * Auth-less test of Title::isValidMoveOperation
+ *
+ * @group Database
+ * @param string $source
+ * @param string $target
+ * @param array|string|bool $expected Required error
+ * @dataProvider provideTestIsValidMoveOperation
+ * @covers Title::isValidMoveOperation
+ * @covers Title::validateFileMoveOperation
+ */
+ public function testIsValidMoveOperation( $source, $target, $expected ) {
+ $this->setMwGlobals( 'wgContentHandlerUseDB', false );
+ $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 );
+ }
+ }
+ }
+
+ public static function provideTestIsValidMoveOperation() {
+ return array(
+ // for Title::isValidMoveOperation
+ array( 'Some page', '', 'badtitletext' ),
+ array( 'Test', 'Test', 'selfmove' ),
+ array( 'Special:FooBar', 'Test', 'immobile-source-namespace' ),
+ array( 'Test', 'Special:FooBar', 'immobile-target-namespace' ),
+ array( 'MediaWiki:Common.js', 'Help:Some wikitext page', 'bad-target-model' ),
+ array( 'Page', 'File:Test.jpg', 'nonfile-cannot-move-to-file' ),
+ // for Title::validateFileMoveOperation
+ array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ),
+ );
+ }
+
+ /**
+ * Auth-less test of Title::userCan
+ *
+ * @param array $whitelistRegexp
+ * @param string $source
+ * @param string $action
+ * @param array|string|bool $expected Required error
+ *
+ * @covers Title::checkReadPermissions
+ * @dataProvider dataWgWhitelistReadRegexp
+ */
+ public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) {
+ // $wgWhitelistReadRegexp must be an array. Since the provided test cases
+ // usually have only one regex, it is more concise to write the lonely regex
+ // as a string. Thus we cast to an array() to honor $wgWhitelistReadRegexp
+ // type requisite.
+ if ( is_string( $whitelistRegexp ) ) {
+ $whitelistRegexp = array( $whitelistRegexp );
+ }
+
+ $title = Title::newFromDBkey( $source );
+
+ global $wgGroupPermissions;
+ $oldPermissions = $wgGroupPermissions;
+ // Disallow all so we can ensure our regex works
+ $wgGroupPermissions = array();
+ $wgGroupPermissions['*']['read'] = false;
+
+ global $wgWhitelistRead;
+ $oldWhitelist = $wgWhitelistRead;
+ // Undo any LocalSettings explicite whitelists so they won't cause a
+ // failing test to succeed. Set it to some random non sense just
+ // to make sure we properly test Title::checkReadPermissions()
+ $wgWhitelistRead = array( 'some random non sense title' );
+
+ global $wgWhitelistReadRegexp;
+ $oldWhitelistRegexp = $wgWhitelistReadRegexp;
+ $wgWhitelistReadRegexp = $whitelistRegexp;
+
+ // Just use $wgUser which in test is a user object for '127.0.0.1'
+ global $wgUser;
+ // Invalidate user rights cache to take in account $wgGroupPermissions
+ // change above.
+ $wgUser->clearInstanceCache();
+ $errors = $title->userCan( $action, $wgUser );
+
+ // Restore globals
+ $wgGroupPermissions = $oldPermissions;
+ $wgWhitelistRead = $oldWhitelist;
+ $wgWhitelistReadRegexp = $oldWhitelistRegexp;
+
+ if ( is_bool( $expected ) ) {
+ # Forge the assertion message depending on the assertion expectation
+ $allowableness = $expected
+ ? " should be allowed"
+ : " should NOT be allowed";
+ $this->assertEquals(
+ $expected,
+ $errors,
+ "User action '$action' on [[$source]] $allowableness."
+ );
+ } else {
+ $errors = $this->flattenErrorsArray( $errors );
+ foreach ( (array)$expected as $error ) {
+ $this->assertContains( $error, $errors );
+ }
+ }
+ }
+
+ /**
+ * Provides test parameter values for testWgWhitelistReadRegexp()
+ */
+ public function dataWgWhitelistReadRegexp() {
+ $ALLOWED = true;
+ $DISALLOWED = false;
+
+ return array(
+ // Everything, if this doesn't work, we're really in trouble
+ array( '/.*/', 'Main_Page', 'read', $ALLOWED ),
+ array( '/.*/', 'Main_Page', 'edit', $DISALLOWED ),
+
+ // We validate against the title name, not the db key
+ array( '/^Main_Page$/', 'Main_Page', 'read', $DISALLOWED ),
+ // Main page
+ array( '/^Main/', 'Main_Page', 'read', $ALLOWED ),
+ array( '/^Main.*/', 'Main_Page', 'read', $ALLOWED ),
+ // With spaces
+ array( '/Mic\sCheck/', 'Mic Check', 'read', $ALLOWED ),
+ // Unicode multibyte
+ // ...without unicode modifier
+ array( '/Unicode Test . Yes/', 'Unicode Test Ñ Yes', 'read', $DISALLOWED ),
+ // ...with unicode modifier
+ array( '/Unicode Test . Yes/u', 'Unicode Test Ñ Yes', 'read', $ALLOWED ),
+ // Case insensitive
+ array( '/MiC ChEcK/', 'mic check', 'read', $DISALLOWED ),
+ array( '/MiC ChEcK/i', 'mic check', 'read', $ALLOWED ),
+
+ // From DefaultSettings.php:
+ array( "@^UsEr.*@i", 'User is banned', 'read', $ALLOWED ),
+ array( "@^UsEr.*@i", 'User:John Doe', 'read', $ALLOWED ),
+
+ // With namespaces:
+ array( '/^Special:NewPages$/', 'Special:NewPages', 'read', $ALLOWED ),
+ array( null, 'Special:Newpages', 'read', $DISALLOWED ),
+
+ );
+ }
+
+ public function flattenErrorsArray( $errors ) {
+ $result = array();
+ foreach ( $errors as $error ) {
+ $result[] = $error[0];
+ }
+
+ return $result;
+ }
+
+ /**
+ * @dataProvider provideGetPageViewLanguage
+ * @covers Title::getPageViewLanguage
+ */
+ public function testGetPageViewLanguage( $expected, $titleText, $contLang,
+ $lang, $variant, $msg = ''
+ ) {
+ global $wgLanguageCode, $wgContLang, $wgLang, $wgDefaultLanguageVariant, $wgAllowUserJs;
+
+ // Setup environnement for this test
+ $wgLanguageCode = $contLang;
+ $wgContLang = Language::factory( $contLang );
+ $wgLang = Language::factory( $lang );
+ $wgDefaultLanguageVariant = $variant;
+ $wgAllowUserJs = true;
+
+ $title = Title::newFromText( $titleText );
+ $this->assertInstanceOf( 'Title', $title,
+ "Test must be passed a valid title text, you gave '$titleText'"
+ );
+ $this->assertEquals( $expected,
+ $title->getPageViewLanguage()->getCode(),
+ $msg
+ );
+ }
+
+ public static function provideGetPageViewLanguage() {
+ # Format:
+ # - expected
+ # - Title name
+ # - wgContLang (expected in most case)
+ # - wgLang (on some specific pages)
+ # - wgDefaultLanguageVariant
+ # - Optional message
+ return array(
+ array( 'fr', 'Help:I_need_somebody', 'fr', 'fr', false ),
+ array( 'es', 'Help:I_need_somebody', 'es', 'zh-tw', false ),
+ array( 'zh', 'Help:I_need_somebody', 'zh', 'zh-tw', false ),
+
+ array( 'es', 'Help:I_need_somebody', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'es', 'MediaWiki:About', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'es', 'MediaWiki:About/', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'de', 'MediaWiki:About/de', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'MediaWiki:Common.js', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'MediaWiki:Common.css', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'User:JohnDoe/Common.js', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'User:JohnDoe/Monobook.css', 'es', 'zh-tw', 'zh-cn' ),
+
+ array( 'zh-cn', 'Help:I_need_somebody', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'zh', 'MediaWiki:About', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'zh', 'MediaWiki:About/', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'de', 'MediaWiki:About/de', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'zh-cn', 'MediaWiki:About/zh-cn', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'zh-tw', 'MediaWiki:About/zh-tw', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'MediaWiki:Common.js', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'MediaWiki:Common.css', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'User:JohnDoe/Common.js', 'zh', 'zh-tw', 'zh-cn' ),
+ array( 'en', 'User:JohnDoe/Monobook.css', 'zh', 'zh-tw', 'zh-cn' ),
+
+ array( 'zh-tw', 'Special:NewPages', 'es', 'zh-tw', 'zh-cn' ),
+ array( 'zh-tw', 'Special:NewPages', 'zh', 'zh-tw', 'zh-cn' ),
+
+ );
+ }
+
+ /**
+ * @dataProvider provideBaseTitleCases
+ * @covers Title::getBaseText
+ */
+ public function testGetBaseText( $title, $expected, $msg = '' ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expected,
+ $title->getBaseText(),
+ $msg
+ );
+ }
+
+ public static function provideBaseTitleCases() {
+ return array(
+ # Title, expected base, optional message
+ array( 'User:John_Doe/subOne/subTwo', 'John Doe/subOne' ),
+ array( 'User:Foo/Bar/Baz', 'Foo/Bar' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideRootTitleCases
+ * @covers Title::getRootText
+ */
+ public function testGetRootText( $title, $expected, $msg = '' ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expected,
+ $title->getRootText(),
+ $msg
+ );
+ }
+
+ public static function provideRootTitleCases() {
+ return array(
+ # Title, expected base, optional message
+ array( 'User:John_Doe/subOne/subTwo', 'John Doe' ),
+ array( 'User:Foo/Bar/Baz', 'Foo' ),
+ );
+ }
+
+ /**
+ * @todo Handle $wgNamespacesWithSubpages cases
+ * @dataProvider provideSubpageTitleCases
+ * @covers Title::getSubpageText
+ */
+ public function testGetSubpageText( $title, $expected, $msg = '' ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expected,
+ $title->getSubpageText(),
+ $msg
+ );
+ }
+
+ public static function provideSubpageTitleCases() {
+ return array(
+ # Title, expected base, optional message
+ array( 'User:John_Doe/subOne/subTwo', 'subTwo' ),
+ array( 'User:John_Doe/subOne', 'subOne' ),
+ );
+ }
+
+ public static function provideNewFromTitleValue() {
+ return array(
+ array( new TitleValue( NS_MAIN, 'Foo' ) ),
+ array( new TitleValue( NS_MAIN, 'Foo', 'bar' ) ),
+ array( new TitleValue( NS_USER, 'Hansi_Maier' ) ),
+ );
+ }
+
+ /**
+ * @dataProvider provideNewFromTitleValue
+ */
+ public function testNewFromTitleValue( TitleValue $value ) {
+ $title = Title::newFromTitleValue( $value );
+
+ $dbkey = str_replace( ' ', '_', $value->getText() );
+ $this->assertEquals( $dbkey, $title->getDBkey() );
+ $this->assertEquals( $value->getNamespace(), $title->getNamespace() );
+ $this->assertEquals( $value->getFragment(), $title->getFragment() );
+ }
+
+ public static function provideGetTitleValue() {
+ return array(
+ array( 'Foo' ),
+ array( 'Foo#bar' ),
+ array( 'User:Hansi_Maier' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetTitleValue
+ */
+ public function testGetTitleValue( $text ) {
+ $title = Title::newFromText( $text );
+ $value = $title->getTitleValue();
+
+ $dbkey = str_replace( ' ', '_', $value->getText() );
+ $this->assertEquals( $title->getDBkey(), $dbkey );
+ $this->assertEquals( $title->getNamespace(), $value->getNamespace() );
+ $this->assertEquals( $title->getFragment(), $value->getFragment() );
+ }
+
+ public static function provideGetFragment() {
+ return array(
+ array( 'Foo', '' ),
+ array( 'Foo#bar', 'bar' ),
+ array( 'Foo#bär', 'bär' ),
+
+ // Inner whitespace is normalized
+ array( 'Foo#bar_bar', 'bar bar' ),
+ array( 'Foo#bar bar', 'bar bar' ),
+ array( 'Foo#bar bar', 'bar bar' ),
+
+ // Leading whitespace is kept, trailing whitespace is trimmed.
+ // XXX: Is this really want we want?
+ array( 'Foo#_bar_bar_', ' bar bar' ),
+ array( 'Foo# bar bar ', ' bar bar' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetFragment
+ *
+ * @param string $full
+ * @param string $fragment
+ */
+ public function testGetFragment( $full, $fragment ) {
+ $title = Title::newFromText( $full );
+ $this->assertEquals( $fragment, $title->getFragment() );
+ }
+
+ /**
+ * @covers Title::isAlwaysKnown
+ * @dataProvider provideIsAlwaysKnown
+ * @param string $page
+ * @param bool $isKnown
+ */
+ public function testIsAlwaysKnown( $page, $isKnown ) {
+ $title = Title::newFromText( $page );
+ $this->assertEquals( $isKnown, $title->isAlwaysKnown() );
+ }
+
+ public static function provideIsAlwaysKnown() {
+ return array(
+ array( 'Some nonexistent page', false ),
+ array( 'UTPage', false ),
+ array( '#test', true ),
+ array( 'Special:BlankPage', true ),
+ array( 'Special:SomeNonexistentSpecialPage', false ),
+ array( 'MediaWiki:Parentheses', true ),
+ array( 'MediaWiki:Some nonexistent message', false ),
+ );
+ }
+
+ /**
+ * @covers Title::isAlwaysKnown
+ */
+ public function testIsAlwaysKnownOnInterwiki() {
+ $title = Title::makeTitle( NS_MAIN, 'Interwiki link', '', 'externalwiki' );
+ $this->assertTrue( $title->isAlwaysKnown() );
+ }
+}
diff --git a/tests/phpunit/includes/UserArrayFromResultTest.php b/tests/phpunit/includes/UserArrayFromResultTest.php
new file mode 100644
index 00000000..62989faa
--- /dev/null
+++ b/tests/phpunit/includes/UserArrayFromResultTest.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * @author Adam Shorland
+ * @covers UserArrayFromResult
+ */
+class UserArrayFromResultTest extends MediaWikiTestCase {
+
+ private function getMockResultWrapper( $row = null, $numRows = 1 ) {
+ $resultWrapper = $this->getMockBuilder( 'ResultWrapper' )
+ ->disableOriginalConstructor();
+
+ $resultWrapper = $resultWrapper->getMock();
+ $resultWrapper->expects( $this->atLeastOnce() )
+ ->method( 'current' )
+ ->will( $this->returnValue( $row ) );
+ $resultWrapper->expects( $this->any() )
+ ->method( 'numRows' )
+ ->will( $this->returnValue( $numRows ) );
+
+ return $resultWrapper;
+ }
+
+ private function getRowWithUsername( $username = 'fooUser' ) {
+ $row = new stdClass();
+ $row->user_name = $username;
+ return $row;
+ }
+
+ private function getUserArrayFromResult( $resultWrapper ) {
+ return new UserArrayFromResult( $resultWrapper );
+ }
+
+ /**
+ * @covers UserArrayFromResult::__construct
+ */
+ public function testConstructionWithFalseRow() {
+ $row = false;
+ $resultWrapper = $this->getMockResultWrapper( $row );
+
+ $object = $this->getUserArrayFromResult( $resultWrapper );
+
+ $this->assertEquals( $resultWrapper, $object->res );
+ $this->assertSame( 0, $object->key );
+ $this->assertEquals( $row, $object->current );
+ }
+
+ /**
+ * @covers UserArrayFromResult::__construct
+ */
+ public function testConstructionWithRow() {
+ $username = 'addshore';
+ $row = $this->getRowWithUsername( $username );
+ $resultWrapper = $this->getMockResultWrapper( $row );
+
+ $object = $this->getUserArrayFromResult( $resultWrapper );
+
+ $this->assertEquals( $resultWrapper, $object->res );
+ $this->assertSame( 0, $object->key );
+ $this->assertInstanceOf( 'User', $object->current );
+ $this->assertEquals( $username, $object->current->mName );
+ }
+
+ public static function provideNumberOfRows() {
+ return array(
+ array( 0 ),
+ array( 1 ),
+ array( 122 ),
+ );
+ }
+
+ /**
+ * @dataProvider provideNumberOfRows
+ * @covers UserArrayFromResult::count
+ */
+ public function testCountWithVaryingValues( $numRows ) {
+ $object = $this->getUserArrayFromResult( $this->getMockResultWrapper(
+ $this->getRowWithUsername(),
+ $numRows
+ ) );
+ $this->assertEquals( $numRows, $object->count() );
+ }
+
+ /**
+ * @covers UserArrayFromResult::current
+ */
+ public function testCurrentAfterConstruction() {
+ $username = 'addshore';
+ $userRow = $this->getRowWithUsername( $username );
+ $object = $this->getUserArrayFromResult( $this->getMockResultWrapper( $userRow ) );
+ $this->assertInstanceOf( 'User', $object->current() );
+ $this->assertEquals( $username, $object->current()->mName );
+ }
+
+ public function provideTestValid() {
+ return array(
+ array( $this->getRowWithUsername(), true ),
+ array( false, false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideTestValid
+ * @covers UserArrayFromResult::valid
+ */
+ public function testValid( $input, $expected ) {
+ $object = $this->getUserArrayFromResult( $this->getMockResultWrapper( $input ) );
+ $this->assertEquals( $expected, $object->valid() );
+ }
+
+ //@todo unit test for key()
+ //@todo unit test for next()
+ //@todo unit test for rewind()
+}
diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/UserTest.php
new file mode 100644
index 00000000..af95a721
--- /dev/null
+++ b/tests/phpunit/includes/UserTest.php
@@ -0,0 +1,369 @@
+<?php
+
+define( 'NS_UNITTEST', 5600 );
+define( 'NS_UNITTEST_TALK', 5601 );
+
+/**
+ * @group Database
+ */
+class UserTest extends MediaWikiTestCase {
+ /**
+ * @var User
+ */
+ protected $user;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgGroupPermissions' => array(),
+ 'wgRevokePermissions' => array(),
+ ) );
+
+ $this->setUpPermissionGlobals();
+
+ $this->user = new User;
+ $this->user->addGroup( 'unittesters' );
+ }
+
+ private function setUpPermissionGlobals() {
+ global $wgGroupPermissions, $wgRevokePermissions;
+
+ # Data for regular $wgGroupPermissions test
+ $wgGroupPermissions['unittesters'] = array(
+ 'test' => true,
+ 'runtest' => true,
+ 'writetest' => false,
+ 'nukeworld' => false,
+ );
+ $wgGroupPermissions['testwriters'] = array(
+ 'test' => true,
+ 'writetest' => true,
+ 'modifytest' => true,
+ );
+
+ # Data for regular $wgRevokePermissions test
+ $wgRevokePermissions['formertesters'] = array(
+ 'runtest' => true,
+ );
+
+ # For the options test
+ $wgGroupPermissions['*'] = array(
+ 'editmyoptions' => true,
+ );
+ }
+
+ /**
+ * @covers User::getGroupPermissions
+ */
+ 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 );
+ }
+
+ /**
+ * @covers User::getGroupPermissions
+ */
+ 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 );
+ }
+
+ /**
+ * @covers User::getRights
+ */
+ public function testUserPermissions() {
+ $rights = $this->user->getRights();
+ $this->assertContains( 'runtest', $rights );
+ $this->assertNotContains( 'writetest', $rights );
+ $this->assertNotContains( 'modifytest', $rights );
+ $this->assertNotContains( 'nukeworld', $rights );
+ }
+
+ /**
+ * @dataProvider provideGetGroupsWithPermission
+ * @covers User::getGroupsWithPermission
+ */
+ public function testGetGroupsWithPermission( $expected, $right ) {
+ $result = User::getGroupsWithPermission( $right );
+ sort( $result );
+ sort( $expected );
+
+ $this->assertEquals( $expected, $result, "Groups with permission $right" );
+ }
+
+ public static function provideGetGroupsWithPermission() {
+ return array(
+ array(
+ array( 'unittesters', 'testwriters' ),
+ 'test'
+ ),
+ array(
+ array( 'unittesters' ),
+ 'runtest'
+ ),
+ array(
+ array( 'testwriters' ),
+ 'writetest'
+ ),
+ array(
+ array( 'testwriters' ),
+ 'modifytest'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIPs
+ * @covers User::isIP
+ */
+ public function testIsIP( $value, $result, $message ) {
+ $this->assertEquals( $this->user->isIP( $value ), $result, $message );
+ }
+
+ public static function provideIPs() {
+ return array(
+ array( '', false, 'Empty string' ),
+ array( ' ', false, 'Blank space' ),
+ array( '10.0.0.0', true, 'IPv4 private 10/8' ),
+ array( '10.255.255.255', true, 'IPv4 private 10/8' ),
+ array( '192.168.1.1', true, 'IPv4 private 192.168/16' ),
+ array( '203.0.113.0', true, 'IPv4 example' ),
+ array( '2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff', true, 'IPv6 example' ),
+ // Not valid IPs but classified as such by MediaWiki for negated asserting
+ // of whether this might be the identifier of a logged-out user or whether
+ // to allow usernames like it.
+ array( '300.300.300.300', true, 'Looks too much like an IPv4 address' ),
+ array( '203.0.113.xxx', true, 'Assigned by UseMod to cloaked logged-out users' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUserNames
+ * @covers User::isValidUserName
+ */
+ public function testIsValidUserName( $username, $result, $message ) {
+ $this->assertEquals( $this->user->isValidUserName( $username ), $result, $message );
+ }
+
+ public static function provideUserNames() {
+ return array(
+ array( '', false, 'Empty string' ),
+ array( ' ', false, 'Blank space' ),
+ array( 'abcd', false, 'Starts with small letter' ),
+ array( 'Ab/cd', false, 'Contains slash' ),
+ array( 'Ab cd', true, 'Whitespace' ),
+ array( '192.168.1.1', false, 'IP' ),
+ array( 'User:Abcd', false, 'Reserved Namespace' ),
+ array( '12abcd232', true, 'Starts with Numbers' ),
+ array( '?abcd', true, 'Start with ? mark' ),
+ array( '#abcd', false, 'Start with #' ),
+ array( 'Abcdകഖഗഘ', true, ' Mixed scripts' ),
+ array( 'ജോസ്‌തോമസ്', false, 'ZWNJ- Format control character' ),
+ array( 'Ab cd', false, ' Ideographic space' ),
+ array( '300.300.300.300', false, 'Looks too much like an IPv4 address' ),
+ array( '302.113.311.900', false, 'Looks too much like an IPv4 address' ),
+ array( '203.0.113.xxx', false, 'Reserved for usage by UseMod for cloaked logged-out users' ),
+ );
+ }
+
+ /**
+ * Test, if for all rights a right- message exist,
+ * which is used on Special:ListGroupRights as help text
+ * Extensions and core
+ */
+ public function testAllRightsWithMessage() {
+ // Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights
+ $allRights = User::getAllRights();
+ $allMessageKeys = Language::getMessageKeysFor( 'en' );
+
+ $rightsWithMessage = array();
+ foreach ( $allMessageKeys as $message ) {
+ // === 0: must be at beginning of string (position 0)
+ if ( strpos( $message, 'right-' ) === 0 ) {
+ $rightsWithMessage[] = substr( $message, strlen( 'right-' ) );
+ }
+ }
+
+ sort( $allRights );
+ sort( $rightsWithMessage );
+
+ $this->assertEquals(
+ $allRights,
+ $rightsWithMessage,
+ 'Each user rights (core/extensions) has a corresponding right- message.'
+ );
+ }
+
+ /**
+ * Test User::editCount
+ * @group medium
+ * @covers User::getEditCount
+ */
+ public function testEditCount() {
+ $user = User::newFromName( 'UnitTestUser' );
+ $user->loadDefaults();
+ $user->addToDatabase();
+
+ // let the user have a few (3) edits
+ $page = WikiPage::factory( Title::newFromText( 'Help:UserTest_EditCount' ) );
+ for ( $i = 0; $i < 3; $i++ ) {
+ $page->doEdit( (string)$i, 'test', 0, false, $user );
+ }
+
+ $user->clearInstanceCache();
+ $this->assertEquals(
+ 3,
+ $user->getEditCount(),
+ 'After three edits, the user edit count should be 3'
+ );
+
+ // increase the edit count and clear the cache
+ $user->incEditCount();
+
+ $user->clearInstanceCache();
+ $this->assertEquals(
+ 4,
+ $user->getEditCount(),
+ 'After increasing the edit count manually, the user edit count should be 4'
+ );
+ }
+
+ /**
+ * Test changing user options.
+ * @covers User::setOption
+ * @covers User::getOption
+ */
+ public function testOptions() {
+ $user = User::newFromName( 'UnitTestUser' );
+ $user->addToDatabase();
+
+ $user->setOption( 'someoption', 'test' );
+ $user->setOption( 'cols', 200 );
+ $user->saveSettings();
+
+ $user = User::newFromName( 'UnitTestUser' );
+ $this->assertEquals( 'test', $user->getOption( 'someoption' ) );
+ $this->assertEquals( 200, $user->getOption( 'cols' ) );
+ }
+
+ /**
+ * Bug 37963
+ * Make sure defaults are loaded when setOption is called.
+ * @covers User::loadOptions
+ */
+ public function testAnonOptions() {
+ global $wgDefaultUserOptions;
+ $this->user->setOption( 'someoption', 'test' );
+ $this->assertEquals( $wgDefaultUserOptions['cols'], $this->user->getOption( 'cols' ) );
+ $this->assertEquals( 'test', $this->user->getOption( 'someoption' ) );
+ }
+
+ /**
+ * Test password expiration.
+ * @covers User::getPasswordExpired()
+ */
+ public function testPasswordExpire() {
+ global $wgPasswordExpireGrace;
+ $wgTemp = $wgPasswordExpireGrace;
+ $wgPasswordExpireGrace = 3600 * 24 * 7; // 7 days
+
+ $user = User::newFromName( 'UnitTestUser' );
+ $user->loadDefaults();
+ $this->assertEquals( false, $user->getPasswordExpired() );
+
+ $ts = time() - ( 3600 * 24 * 1 ); // 1 day ago
+ $user->expirePassword( $ts );
+ $this->assertEquals( 'soft', $user->getPasswordExpired() );
+
+ $ts = time() - ( 3600 * 24 * 10 ); // 10 days ago
+ $user->expirePassword( $ts );
+ $this->assertEquals( 'hard', $user->getPasswordExpired() );
+
+ $wgPasswordExpireGrace = $wgTemp;
+ }
+
+ /**
+ * Test password validity checks. There are 3 checks in core,
+ * - ensure the password meets the minimal length
+ * - ensure the password is not the same as the username
+ * - ensure the username/password combo isn't forbidden
+ * @covers User::checkPasswordValidity()
+ * @covers User::getPasswordValidity()
+ * @covers User::isValidPassword()
+ */
+ public function testCheckPasswordValidity() {
+ $this->setMwGlobals( array(
+ 'wgMinimalPasswordLength' => 6,
+ 'wgMaximalPasswordLength' => 30,
+ ) );
+ $user = User::newFromName( 'Useruser' );
+ // Sanity
+ $this->assertTrue( $user->isValidPassword( 'Password1234' ) );
+
+ // Minimum length
+ $this->assertFalse( $user->isValidPassword( 'a' ) );
+ $this->assertFalse( $user->checkPasswordValidity( 'a' )->isGood() );
+ $this->assertTrue( $user->checkPasswordValidity( 'a' )->isOK() );
+ $this->assertEquals( 'passwordtooshort', $user->getPasswordValidity( 'a' ) );
+
+ // Maximum length
+ $longPass = str_repeat( 'a', 31 );
+ $this->assertFalse( $user->isValidPassword( $longPass ) );
+ $this->assertFalse( $user->checkPasswordValidity( $longPass )->isGood() );
+ $this->assertFalse( $user->checkPasswordValidity( $longPass )->isOK() );
+ $this->assertEquals( 'passwordtoolong', $user->getPasswordValidity( $longPass ) );
+
+ // Matches username
+ $this->assertFalse( $user->checkPasswordValidity( 'Useruser' )->isGood() );
+ $this->assertTrue( $user->checkPasswordValidity( 'Useruser' )->isOK() );
+ $this->assertEquals( 'password-name-match', $user->getPasswordValidity( 'Useruser' ) );
+
+ // On the forbidden list
+ $this->assertFalse( $user->checkPasswordValidity( 'Passpass' )->isGood() );
+ $this->assertEquals( 'password-login-forbidden', $user->getPasswordValidity( 'Passpass' ) );
+ }
+
+ /**
+ * @covers User::getCanonicalName()
+ * @dataProvider provideGetCanonicalName
+ */
+ public function testGetCanonicalName( $name, $expectedArray, $msg ) {
+ foreach ( $expectedArray as $validate => $expected ) {
+ $this->assertEquals(
+ User::getCanonicalName( $name, $validate === 'false' ? false : $validate ),
+ $expected,
+ $msg . ' (' . $validate . ')'
+ );
+ }
+ }
+
+ public static function provideGetCanonicalName() {
+ return array(
+ array( ' trailing space ', array( 'creatable' => 'Trailing space' ), 'Trailing spaces' ),
+ // @todo FIXME: Maybe the createable name should be 'Talk:Username' or false to reject?
+ array( 'Talk:Username', array( 'creatable' => 'Username', 'usable' => 'Username',
+ 'valid' => 'Username', 'false' => 'Talk:Username' ), 'Namespace prefix' ),
+ array( ' name with # hash', array( 'creatable' => false, 'usable' => false ), 'With hash' ),
+ array( 'Multi spaces', array( 'creatable' => 'Multi spaces',
+ 'usable' => 'Multi spaces' ), 'Multi spaces' ),
+ array( 'lowercase', array( 'creatable' => 'Lowercase' ), 'Lowercase' ),
+ array( 'in[]valid', array( 'creatable' => false, 'usable' => false, 'valid' => false,
+ 'false' => 'In[]valid' ), 'Invalid' ),
+ array( 'with / slash', array( 'creatable' => false, 'usable' => false, 'valid' => false,
+ 'false' => 'With / slash' ), 'With slash' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/WebRequestTest.php b/tests/phpunit/includes/WebRequestTest.php
new file mode 100644
index 00000000..12d7d2a3
--- /dev/null
+++ b/tests/phpunit/includes/WebRequestTest.php
@@ -0,0 +1,358 @@
+<?php
+
+/**
+ * @group WebRequest
+ */
+class WebRequestTest extends MediaWikiTestCase {
+ protected $oldServer;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->oldServer = $_SERVER;
+ IP::clearCaches();
+ }
+
+ protected function tearDown() {
+ $_SERVER = $this->oldServer;
+ IP::clearCaches();
+
+ parent::tearDown();
+ }
+
+ /**
+ * @dataProvider provideDetectServer
+ * @covers WebRequest::detectServer
+ */
+ public function testDetectServer( $expected, $input, $description ) {
+ $_SERVER = $input;
+ $result = WebRequest::detectServer();
+ $this->assertEquals( $expected, $result, $description );
+ }
+
+ public static 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',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetIP
+ * @covers WebRequest::getIP
+ */
+ public function testGetIP( $expected, $input, $squid, $xffList, $private, $description ) {
+ $_SERVER = $input;
+ $this->setMwGlobals( array(
+ 'wgSquidServersNoPurge' => $squid,
+ 'wgUsePrivateIPs' => $private,
+ 'wgHooks' => array(
+ 'IsTrustedProxy' => array(
+ function ( &$ip, &$trusted ) use ( $xffList ) {
+ $trusted = $trusted || in_array( $ip, $xffList );
+ return true;
+ }
+ )
+ )
+ ) );
+
+ $request = new WebRequest();
+ $result = $request->getIP();
+ $this->assertEquals( $expected, $result, $description );
+ }
+
+ public static function provideGetIP() {
+ return array(
+ array(
+ '127.0.0.1',
+ array(
+ 'REMOTE_ADDR' => '127.0.0.1'
+ ),
+ array(),
+ array(),
+ false,
+ 'Simple IPv4'
+ ),
+ array(
+ '::1',
+ array(
+ 'REMOTE_ADDR' => '::1'
+ ),
+ array(),
+ array(),
+ false,
+ 'Simple IPv6'
+ ),
+ array(
+ '12.0.0.1',
+ array(
+ 'REMOTE_ADDR' => 'abcd:0001:002:03:4:555:6666:7777',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.1, abcd:0001:002:03:4:555:6666:7777',
+ ),
+ array( 'ABCD:1:2:3:4:555:6666:7777' ),
+ array(),
+ false,
+ 'IPv6 normalisation'
+ ),
+ array(
+ '12.0.0.3',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2' ),
+ array(),
+ false,
+ 'With X-Forwaded-For'
+ ),
+ array(
+ '12.0.0.1',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array(),
+ array(),
+ false,
+ 'With X-Forwaded-For and disallowed server'
+ ),
+ array(
+ '12.0.0.2',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1' ),
+ array(),
+ false,
+ 'With multiple X-Forwaded-For and only one allowed server'
+ ),
+ array(
+ '10.0.0.3',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2' ),
+ array(),
+ false,
+ 'With X-Forwaded-For and private IP (from cache proxy)'
+ ),
+ array(
+ '10.0.0.4',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2', '10.0.0.3' ),
+ array(),
+ true,
+ 'With X-Forwaded-For and private IP (allowed)'
+ ),
+ array(
+ '10.0.0.4',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2' ),
+ array( '10.0.0.3' ),
+ true,
+ 'With X-Forwaded-For and private IP (allowed)'
+ ),
+ array(
+ '10.0.0.3',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.1', '12.0.0.2' ),
+ array( '10.0.0.3' ),
+ false,
+ 'With X-Forwaded-For and private IP (disallowed)'
+ ),
+ array(
+ '12.0.0.3',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array(),
+ array( '12.0.0.1', '12.0.0.2' ),
+ false,
+ 'With X-Forwaded-For'
+ ),
+ array(
+ '12.0.0.2',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array(),
+ array( '12.0.0.1' ),
+ false,
+ 'With multiple X-Forwaded-For and only one allowed server'
+ ),
+ array(
+ '12.0.0.2',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.2',
+ 'HTTP_X_FORWARDED_FOR' => '10.0.0.3, 12.0.0.2'
+ ),
+ array(),
+ array( '12.0.0.2' ),
+ false,
+ 'With X-Forwaded-For and private IP and hook (disallowed)'
+ ),
+ array(
+ '12.0.0.1',
+ array(
+ 'REMOTE_ADDR' => 'abcd:0001:002:03:4:555:6666:7777',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.1, abcd:0001:002:03:4:555:6666:7777',
+ ),
+ array( 'ABCD:1:2:3::/64' ),
+ array(),
+ false,
+ 'IPv6 CIDR'
+ ),
+ array(
+ '12.0.0.3',
+ array(
+ 'REMOTE_ADDR' => '12.0.0.1',
+ 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2'
+ ),
+ array( '12.0.0.0/24' ),
+ array(),
+ false,
+ 'IPv4 CIDR'
+ ),
+ );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers WebRequest::getIP
+ */
+ public function testGetIpLackOfRemoteAddrThrowAnException() {
+ // ensure that local install state doesn't interfere with test
+ $this->setMwGlobals( array(
+ 'wgSquidServersNoPurge' => array(),
+ 'wgSquidServers' => array(),
+ 'wgUsePrivateIPs' => false,
+ 'wgHooks' => array(),
+ ) );
+
+ $request = new WebRequest();
+ # Next call throw an exception about lacking an IP
+ $request->getIP();
+ }
+
+ public static function provideLanguageData() {
+ return array(
+ array( '', array(), 'Empty Accept-Language header' ),
+ array( 'en', array( 'en' => 1 ), 'One language' ),
+ array( 'en, ar', array( 'en' => 1, 'ar' => 1 ), 'Two languages listed in appearance order.' ),
+ array(
+ 'zh-cn,zh-tw',
+ array( 'zh-cn' => 1, 'zh-tw' => 1 ),
+ 'Two equally prefered languages, listed in appearance order per rfc3282. Checks c9119'
+ ),
+ array(
+ 'es, en; q=0.5',
+ array( 'es' => 1, 'en' => '0.5' ),
+ 'Spanish as first language and English and second'
+ ),
+ array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Less prefered language first' ),
+ array( 'fr, en; q=0.5, es', array( 'fr' => 1, 'es' => 1, 'en' => '0.5' ), 'Three languages' ),
+ array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Two languages' ),
+ array( 'en, zh;q=0', array( 'en' => 1 ), "It's Chinese to me" ),
+ array(
+ 'es; q=1, pt;q=0.7, it; q=0.6, de; q=0.1, ru;q=0',
+ array( 'es' => '1', 'pt' => '0.7', 'it' => '0.6', 'de' => '0.1' ),
+ 'Preference for Romance languages'
+ ),
+ array(
+ 'en-gb, en-us; q=1',
+ array( 'en-gb' => 1, 'en-us' => '1' ),
+ 'Two equally prefered English variants'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideLanguageData
+ * @covers WebRequest::getAcceptLang
+ */
+ public function testAcceptLang( $acceptLanguageHeader, $expectedLanguages, $description ) {
+ $_SERVER = array( 'HTTP_ACCEPT_LANGUAGE' => $acceptLanguageHeader );
+ $request = new WebRequest();
+ $this->assertSame( $request->getAcceptLang(), $expectedLanguages, $description );
+ }
+}
diff --git a/tests/phpunit/includes/WikiPageTest.php b/tests/phpunit/includes/WikiPageTest.php
new file mode 100644
index 00000000..7f7945b8
--- /dev/null
+++ b/tests/phpunit/includes/WikiPageTest.php
@@ -0,0 +1,1301 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ * @group medium
+ **/
+class WikiPageTest extends MediaWikiLangTestCase {
+
+ protected $pages_to_delete;
+
+ function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed = array_merge(
+ $this->tablesUsed,
+ array( 'page',
+ 'revision',
+ 'text',
+
+ 'recentchanges',
+ 'logging',
+
+ 'page_props',
+ 'pagelinks',
+ 'categorylinks',
+ 'langlinks',
+ 'externallinks',
+ 'imagelinks',
+ 'templatelinks',
+ 'iwlinks' ) );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ $this->pages_to_delete = array();
+
+ LinkCache::singleton()->clear(); # avoid cached redirect status, etc
+ }
+
+ protected function tearDown() {
+ foreach ( $this->pages_to_delete as $p ) {
+ /* @var $p WikiPage */
+
+ try {
+ if ( $p->exists() ) {
+ $p->doDeleteArticle( "testing done." );
+ }
+ } catch ( MWException $ex ) {
+ // fail silently
+ }
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * @param Title $title
+ * @param string $model
+ * @return WikiPage
+ */
+ protected function newPage( $title, $model = null ) {
+ if ( is_string( $title ) ) {
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( $title, $ns );
+ }
+
+ $p = new WikiPage( $title );
+
+ $this->pages_to_delete[] = $p;
+
+ return $p;
+ }
+
+ /**
+ * @param string|Title|WikiPage $page
+ * @param string $text
+ * @param int $model
+ *
+ * @return WikiPage
+ */
+ protected function createPage( $page, $text, $model = null ) {
+ if ( is_string( $page ) || $page instanceof Title ) {
+ $page = $this->newPage( $page, $model );
+ }
+
+ $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+ $page->doEditContent( $content, "testing", EDIT_NEW );
+
+ return $page;
+ }
+
+ /**
+ * @covers WikiPage::doEditContent
+ */
+ public function testDoEditContent() {
+ $page = $this->newPage( "WikiPageTest_testDoEditContent" );
+ $title = $page->getTitle();
+
+ $content = ContentHandler::makeContent(
+ "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
+ . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
+ $title,
+ CONTENT_MODEL_WIKITEXT
+ );
+
+ $page->doEditContent( $content, "[[testing]] 1" );
+
+ $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
+ $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
+ $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
+ $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
+
+ $id = $page->getId();
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
+
+ # ------------------------
+ $page = new WikiPage( $title );
+
+ $retrieved = $page->getContent();
+ $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+
+ # ------------------------
+ $content = ContentHandler::makeContent(
+ "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
+ . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.",
+ $title,
+ CONTENT_MODEL_WIKITEXT
+ );
+
+ $page->doEditContent( $content, "testing 2" );
+
+ # ------------------------
+ $page = new WikiPage( $title );
+
+ $retrieved = $page->getContent();
+ $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
+ }
+
+ /**
+ * @covers WikiPage::doEdit
+ */
+ public function testDoEdit() {
+ $this->hideDeprecated( "WikiPage::doEdit" );
+ $this->hideDeprecated( "WikiPage::getText" );
+ $this->hideDeprecated( "Revision::getText" );
+
+ //NOTE: assume help namespace will default to wikitext
+ $title = Title::newFromText( "Help:WikiPageTest_testDoEdit" );
+
+ $page = $this->newPage( $title );
+
+ $text = "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
+ . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.";
+
+ $page->doEdit( $text, "[[testing]] 1" );
+
+ $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
+ $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
+ $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
+ $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
+
+ $id = $page->getId();
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
+
+ # ------------------------
+ $page = new WikiPage( $title );
+
+ $retrieved = $page->getText();
+ $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' );
+
+ # ------------------------
+ $text = "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
+ . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.";
+
+ $page->doEdit( $text, "testing 2" );
+
+ # ------------------------
+ $page = new WikiPage( $title );
+
+ $retrieved = $page->getText();
+ $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' );
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
+ }
+
+ /**
+ * @covers WikiPage::doQuickEdit
+ */
+ public function testDoQuickEdit() {
+ global $wgUser;
+
+ $this->hideDeprecated( "WikiPage::doQuickEdit" );
+
+ //NOTE: assume help namespace will default to wikitext
+ $page = $this->createPage( "Help:WikiPageTest_testDoQuickEdit", "original text" );
+
+ $text = "quick text";
+ $page->doQuickEdit( $text, $wgUser, "testing q" );
+
+ # ---------------------
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( $text, $page->getText() );
+ }
+
+ /**
+ * @covers WikiPage::doQuickEditContent
+ */
+ public function testDoQuickEditContent() {
+ global $wgUser;
+
+ $page = $this->createPage(
+ "WikiPageTest_testDoQuickEditContent",
+ "original text",
+ CONTENT_MODEL_WIKITEXT
+ );
+
+ $content = ContentHandler::makeContent(
+ "quick text",
+ $page->getTitle(),
+ CONTENT_MODEL_WIKITEXT
+ );
+ $page->doQuickEditContent( $content, $wgUser, "testing q" );
+
+ # ---------------------
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertTrue( $content->equals( $page->getContent() ) );
+ }
+
+ /**
+ * @covers WikiPage::doDeleteArticle
+ */
+ public function testDoDeleteArticle() {
+ $page = $this->createPage(
+ "WikiPageTest_testDoDeleteArticle",
+ "[[original text]] foo",
+ CONTENT_MODEL_WIKITEXT
+ );
+ $id = $page->getId();
+
+ $page->doDeleteArticle( "testing deletion" );
+
+ $this->assertFalse(
+ $page->getTitle()->getArticleID() > 0,
+ "Title object should now have page id 0"
+ );
+ $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" );
+ $this->assertFalse(
+ $page->exists(),
+ "WikiPage::exists should return false after page was deleted"
+ );
+ $this->assertNull(
+ $page->getContent(),
+ "WikiPage::getContent should return null after page was deleted"
+ );
+ $this->assertFalse(
+ $page->getText(),
+ "WikiPage::getText should return false after page was deleted"
+ );
+
+ $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
+ $this->assertFalse(
+ $t->exists(),
+ "Title::exists should return false after page was deleted"
+ );
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
+ }
+
+ /**
+ * @covers WikiPage::doDeleteUpdates
+ */
+ public function testDoDeleteUpdates() {
+ $page = $this->createPage(
+ "WikiPageTest_testDoDeleteArticle",
+ "[[original text]] foo",
+ CONTENT_MODEL_WIKITEXT
+ );
+ $id = $page->getId();
+
+ $page->doDeleteUpdates( $id );
+
+ # ------------------------
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+ $n = $res->numRows();
+ $res->free();
+
+ $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
+ }
+
+ /**
+ * @covers WikiPage::getRevision
+ */
+ public function testGetRevision() {
+ $page = $this->newPage( "WikiPageTest_testGetRevision" );
+
+ $rev = $page->getRevision();
+ $this->assertNull( $rev );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+
+ $rev = $page->getRevision();
+
+ $this->assertEquals( $page->getLatest(), $rev->getId() );
+ $this->assertEquals( "some text", $rev->getContent()->getNativeData() );
+ }
+
+ /**
+ * @covers WikiPage::getContent
+ */
+ public function testGetContent() {
+ $page = $this->newPage( "WikiPageTest_testGetContent" );
+
+ $content = $page->getContent();
+ $this->assertNull( $content );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+
+ $content = $page->getContent();
+ $this->assertEquals( "some text", $content->getNativeData() );
+ }
+
+ /**
+ * @covers WikiPage::getText
+ */
+ public function testGetText() {
+ $this->hideDeprecated( "WikiPage::getText" );
+
+ $page = $this->newPage( "WikiPageTest_testGetText" );
+
+ $text = $page->getText();
+ $this->assertFalse( $text );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+
+ $text = $page->getText();
+ $this->assertEquals( "some text", $text );
+ }
+
+ /**
+ * @covers WikiPage::getRawText
+ */
+ public function testGetRawText() {
+ $this->hideDeprecated( "WikiPage::getRawText" );
+
+ $page = $this->newPage( "WikiPageTest_testGetRawText" );
+
+ $text = $page->getRawText();
+ $this->assertFalse( $text );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+
+ $text = $page->getRawText();
+ $this->assertEquals( "some text", $text );
+ }
+
+ /**
+ * @covers WikiPage::getContentModel
+ */
+ public function testGetContentModel() {
+ global $wgContentHandlerUseDB;
+
+ if ( !$wgContentHandlerUseDB ) {
+ $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+ }
+
+ $page = $this->createPage(
+ "WikiPageTest_testGetContentModel",
+ "some text",
+ CONTENT_MODEL_JAVASCRIPT
+ );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() );
+ }
+
+ /**
+ * @covers WikiPage::getContentHandler
+ */
+ public function testGetContentHandler() {
+ global $wgContentHandlerUseDB;
+
+ if ( !$wgContentHandlerUseDB ) {
+ $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+ }
+
+ $page = $this->createPage(
+ "WikiPageTest_testGetContentHandler",
+ "some text",
+ CONTENT_MODEL_JAVASCRIPT
+ );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( 'JavaScriptContentHandler', get_class( $page->getContentHandler() ) );
+ }
+
+ /**
+ * @covers WikiPage::exists
+ */
+ public function testExists() {
+ $page = $this->newPage( "WikiPageTest_testExists" );
+ $this->assertFalse( $page->exists() );
+
+ # -----------------
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+ $this->assertTrue( $page->exists() );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertTrue( $page->exists() );
+
+ # -----------------
+ $page->doDeleteArticle( "done testing" );
+ $this->assertFalse( $page->exists() );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertFalse( $page->exists() );
+ }
+
+ public static function provideHasViewableContent() {
+ return array(
+ array( 'WikiPageTest_testHasViewableContent', false, true ),
+ array( 'Special:WikiPageTest_testHasViewableContent', false ),
+ array( 'MediaWiki:WikiPageTest_testHasViewableContent', false ),
+ array( 'Special:Userlogin', true ),
+ array( 'MediaWiki:help', true ),
+ );
+ }
+
+ /**
+ * @dataProvider provideHasViewableContent
+ * @covers WikiPage::hasViewableContent
+ */
+ public function testHasViewableContent( $title, $viewable, $create = false ) {
+ $page = $this->newPage( $title );
+ $this->assertEquals( $viewable, $page->hasViewableContent() );
+
+ if ( $create ) {
+ $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+ $this->assertTrue( $page->hasViewableContent() );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertTrue( $page->hasViewableContent() );
+ }
+ }
+
+ public static function provideGetRedirectTarget() {
+ return array(
+ array( 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT, "hello world", null ),
+ array(
+ 'WikiPageTest_testGetRedirectTarget_2',
+ CONTENT_MODEL_WIKITEXT,
+ "#REDIRECT [[hello world]]",
+ "Hello world"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetRedirectTarget
+ * @covers WikiPage::getRedirectTarget
+ */
+ public function testGetRedirectTarget( $title, $model, $text, $target ) {
+ $this->setMwGlobals( array(
+ 'wgCapitalLinks' => true,
+ ) );
+
+ $page = $this->createPage( $title, $text, $model );
+
+ # sanity check, because this test seems to fail for no reason for some people.
+ $c = $page->getContent();
+ $this->assertEquals( 'WikitextContent', get_class( $c ) );
+
+ # now, test the actual redirect
+ $t = $page->getRedirectTarget();
+ $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() );
+ }
+
+ /**
+ * @dataProvider provideGetRedirectTarget
+ * @covers WikiPage::isRedirect
+ */
+ public function testIsRedirect( $title, $model, $text, $target ) {
+ $page = $this->createPage( $title, $text, $model );
+ $this->assertEquals( !is_null( $target ), $page->isRedirect() );
+ }
+
+ public static function provideIsCountable() {
+ return array(
+
+ // any
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ '',
+ 'any',
+ true
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo',
+ 'any',
+ true
+ ),
+
+ // comma
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo',
+ 'comma',
+ false
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo, bar',
+ 'comma',
+ true
+ ),
+
+ // link
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo',
+ 'link',
+ false
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo [[bar]]',
+ 'link',
+ true
+ ),
+
+ // redirects
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ '#REDIRECT [[bar]]',
+ 'any',
+ false
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ '#REDIRECT [[bar]]',
+ 'comma',
+ false
+ ),
+ array( 'WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ '#REDIRECT [[bar]]',
+ 'link',
+ false
+ ),
+
+ // not a content namespace
+ array( 'Talk:WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo',
+ 'any',
+ false
+ ),
+ array( 'Talk:WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo, bar',
+ 'comma',
+ false
+ ),
+ array( 'Talk:WikiPageTest_testIsCountable',
+ CONTENT_MODEL_WIKITEXT,
+ 'Foo [[bar]]',
+ 'link',
+ false
+ ),
+
+ // not a content namespace, different model
+ array( 'MediaWiki:WikiPageTest_testIsCountable.js',
+ null,
+ 'Foo',
+ 'any',
+ false
+ ),
+ array( 'MediaWiki:WikiPageTest_testIsCountable.js',
+ null,
+ 'Foo, bar',
+ 'comma',
+ false
+ ),
+ array( 'MediaWiki:WikiPageTest_testIsCountable.js',
+ null,
+ 'Foo [[bar]]',
+ 'link',
+ false
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideIsCountable
+ * @covers WikiPage::isCountable
+ */
+ public function testIsCountable( $title, $model, $text, $mode, $expected ) {
+ global $wgContentHandlerUseDB;
+
+ $this->setMwGlobals( 'wgArticleCountMethod', $mode );
+
+ $title = Title::newFromText( $title );
+
+ if ( !$wgContentHandlerUseDB
+ && $model
+ && ContentHandler::getDefaultModelFor( $title ) != $model
+ ) {
+ $this->markTestSkipped( "Can not use non-default content model $model for "
+ . $title->getPrefixedDBkey() . " with \$wgContentHandlerUseDB disabled." );
+ }
+
+ $page = $this->createPage( $title, $text, $model );
+
+ $editInfo = $page->prepareContentForEdit( $page->getContent() );
+
+ $v = $page->isCountable();
+ $w = $page->isCountable( $editInfo );
+
+ $this->assertEquals(
+ $expected,
+ $v,
+ "isCountable( null ) returned unexpected value " . var_export( $v, true )
+ . " instead of " . var_export( $expected, true )
+ . " in mode `$mode` for text \"$text\""
+ );
+
+ $this->assertEquals(
+ $expected,
+ $w,
+ "isCountable( \$editInfo ) returned unexpected value " . var_export( $v, true )
+ . " instead of " . var_export( $expected, true )
+ . " in mode `$mode` for text \"$text\""
+ );
+ }
+
+ public static function provideGetParserOutput() {
+ return array(
+ array( CONTENT_MODEL_WIKITEXT, "hello ''world''\n", "<p>hello <i>world</i></p>" ),
+ // @todo more...?
+ );
+ }
+
+ /**
+ * @dataProvider provideGetParserOutput
+ * @covers WikiPage::getParserOutput
+ */
+ public function testGetParserOutput( $model, $text, $expectedHtml ) {
+ $page = $this->createPage( 'WikiPageTest_testGetParserOutput', $text, $model );
+
+ $opt = $page->makeParserOptions( 'canonical' );
+ $po = $page->getParserOutput( $opt );
+ $text = $po->getText();
+
+ $text = trim( preg_replace( '/<!--.*?-->/sm', '', $text ) ); # strip injected comments
+ $text = preg_replace( '!\s*(</p>)!sm', '\1', $text ); # don't let tidy confuse us
+
+ $this->assertEquals( $expectedHtml, $text );
+
+ return $po;
+ }
+
+ /**
+ * @covers WikiPage::getParserOutput
+ */
+ public function testGetParserOutput_nonexisting() {
+ static $count = 0;
+ $count++;
+
+ $page = new WikiPage( new Title( "WikiPageTest_testGetParserOutput_nonexisting_$count" ) );
+
+ $opt = new ParserOptions();
+ $po = $page->getParserOutput( $opt );
+
+ $this->assertFalse( $po, "getParserOutput() shall return false for non-existing pages." );
+ }
+
+ /**
+ * @covers WikiPage::getParserOutput
+ */
+ public function testGetParserOutput_badrev() {
+ $page = $this->createPage( 'WikiPageTest_testGetParserOutput', "dummy", CONTENT_MODEL_WIKITEXT );
+
+ $opt = new ParserOptions();
+ $po = $page->getParserOutput( $opt, $page->getLatest() + 1234 );
+
+ // @todo would be neat to also test deleted revision
+
+ $this->assertFalse( $po, "getParserOutput() shall return false for non-existing revisions." );
+ }
+
+ public static $sections =
+
+ "Intro
+
+== stuff ==
+hello world
+
+== test ==
+just a test
+
+== foo ==
+more stuff
+";
+
+ public function dataReplaceSection() {
+ //NOTE: assume the Help namespace to contain wikitext
+ return array(
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "0",
+ "No more",
+ null,
+ trim( preg_replace( '/^Intro/sm', 'No more', WikiPageTest::$sections ) )
+ ),
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "",
+ "No more",
+ null,
+ "No more"
+ ),
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "2",
+ "== TEST ==\nmore fun",
+ null,
+ trim( preg_replace( '/^== test ==.*== foo ==/sm',
+ "== TEST ==\nmore fun\n\n== foo ==",
+ WikiPageTest::$sections ) )
+ ),
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "8",
+ "No more",
+ null,
+ trim( WikiPageTest::$sections )
+ ),
+ array( 'Help:WikiPageTest_testReplaceSection',
+ CONTENT_MODEL_WIKITEXT,
+ WikiPageTest::$sections,
+ "new",
+ "No more",
+ "New",
+ trim( WikiPageTest::$sections ) . "\n\n== New ==\n\nNo more"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataReplaceSection
+ * @covers WikiPage::replaceSection
+ */
+ public function testReplaceSection( $title, $model, $text, $section, $with,
+ $sectionTitle, $expected
+ ) {
+ $this->hideDeprecated( "WikiPage::replaceSection" );
+
+ $page = $this->createPage( $title, $text, $model );
+ $text = $page->replaceSection( $section, $with, $sectionTitle );
+ $text = trim( $text );
+
+ $this->assertEquals( $expected, $text );
+ }
+
+ /**
+ * @dataProvider dataReplaceSection
+ * @covers WikiPage::replaceSectionContent
+ */
+ public function testReplaceSectionContent( $title, $model, $text, $section,
+ $with, $sectionTitle, $expected
+ ) {
+ $page = $this->createPage( $title, $text, $model );
+
+ $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
+ $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
+
+ $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
+ }
+
+ /**
+ * @dataProvider dataReplaceSection
+ * @covers WikiPage::replaceSectionAtRev
+ */
+ public function testReplaceSectionAtRev( $title, $model, $text, $section,
+ $with, $sectionTitle, $expected
+ ) {
+ $page = $this->createPage( $title, $text, $model );
+ $baseRevId = $page->getLatest();
+
+ $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
+ $c = $page->replaceSectionAtRev( $section, $content, $sectionTitle, $baseRevId );
+
+ $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
+ }
+
+ /* @todo FIXME: fix this!
+ public function testGetUndoText() {
+ $this->checkHasDiff3();
+
+ $text = "one";
+ $page = $this->createPage( "WikiPageTest_testGetUndoText", $text );
+ $rev1 = $page->getRevision();
+
+ $text .= "\n\ntwo";
+ $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle() ),
+ "adding section two"
+ );
+ $rev2 = $page->getRevision();
+
+ $text .= "\n\nthree";
+ $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle() ),
+ "adding section three"
+ );
+ $rev3 = $page->getRevision();
+
+ $text .= "\n\nfour";
+ $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle() ),
+ "adding section four"
+ );
+ $rev4 = $page->getRevision();
+
+ $text .= "\n\nfive";
+ $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle() ),
+ "adding section five"
+ );
+ $rev5 = $page->getRevision();
+
+ $text .= "\n\nsix";
+ $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle() ),
+ "adding section six"
+ );
+ $rev6 = $page->getRevision();
+
+ $undo6 = $page->getUndoText( $rev6 );
+ if ( $undo6 === false ) $this->fail( "getUndoText failed for rev6" );
+ $this->assertEquals( "one\n\ntwo\n\nthree\n\nfour\n\nfive", $undo6 );
+
+ $undo3 = $page->getUndoText( $rev4, $rev2 );
+ if ( $undo3 === false ) $this->fail( "getUndoText failed for rev4..rev2" );
+ $this->assertEquals( "one\n\ntwo\n\nfive", $undo3 );
+
+ $undo2 = $page->getUndoText( $rev2 );
+ if ( $undo2 === false ) $this->fail( "getUndoText failed for rev2" );
+ $this->assertEquals( "one\n\nfive", $undo2 );
+ }
+ */
+
+ /**
+ * @todo FIXME: this is a better rollback test than the one below, but it
+ * keeps failing in jenkins for some reason.
+ */
+ public function broken_testDoRollback() {
+ $admin = new User();
+ $admin->setName( "Admin" );
+
+ $text = "one";
+ $page = $this->newPage( "WikiPageTest_testDoRollback" );
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
+ "section one", EDIT_NEW, false, $admin );
+
+ $user1 = new User();
+ $user1->setName( "127.0.1.11" );
+ $text .= "\n\ntwo";
+ $page = new WikiPage( $page->getTitle() );
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
+ "adding section two", 0, false, $user1 );
+
+ $user2 = new User();
+ $user2->setName( "127.0.2.13" );
+ $text .= "\n\nthree";
+ $page = new WikiPage( $page->getTitle() );
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
+ "adding section three", 0, false, $user2 );
+
+ # we are having issues with doRollback spuriously failing. Apparently
+ # the last revision somehow goes missing or not committed under some
+ # circumstances. So, make sure the last revision has the right user name.
+ $dbr = wfGetDB( DB_SLAVE );
+ $this->assertEquals( 3, Revision::countByPageId( $dbr, $page->getId() ) );
+
+ $page = new WikiPage( $page->getTitle() );
+ $rev3 = $page->getRevision();
+ $this->assertEquals( '127.0.2.13', $rev3->getUserText() );
+
+ $rev2 = $rev3->getPrevious();
+ $this->assertEquals( '127.0.1.11', $rev2->getUserText() );
+
+ $rev1 = $rev2->getPrevious();
+ $this->assertEquals( 'Admin', $rev1->getUserText() );
+
+ # now, try the actual rollback
+ $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
+ $token = $admin->getEditToken(
+ array( $page->getTitle()->getPrefixedText(), $user2->getName() ),
+ null
+ );
+ $errors = $page->doRollback(
+ $user2->getName(),
+ "testing revert",
+ $token,
+ false,
+ $details,
+ $admin
+ );
+
+ if ( $errors ) {
+ $this->fail( "Rollback failed:\n" . print_r( $errors, true )
+ . ";\n" . print_r( $details, true ) );
+ }
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(),
+ "rollback did not revert to the correct revision" );
+ $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
+ }
+
+ /**
+ * @todo FIXME: the above rollback test is better, but it keeps failing in jenkins for some reason.
+ * @covers WikiPage::doRollback
+ */
+ public function testDoRollback() {
+ $admin = new User();
+ $admin->setName( "Admin" );
+
+ $text = "one";
+ $page = $this->newPage( "WikiPageTest_testDoRollback" );
+ $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ "section one",
+ EDIT_NEW,
+ false,
+ $admin
+ );
+ $rev1 = $page->getRevision();
+
+ $user1 = new User();
+ $user1->setName( "127.0.1.11" );
+ $text .= "\n\ntwo";
+ $page = new WikiPage( $page->getTitle() );
+ $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ "adding section two",
+ 0,
+ false,
+ $user1
+ );
+
+ # now, try the rollback
+ $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
+ $token = $admin->getEditToken(
+ array( $page->getTitle()->getPrefixedText(), $user1->getName() ),
+ null
+ );
+ $errors = $page->doRollback(
+ $user1->getName(),
+ "testing revert",
+ $token,
+ false,
+ $details,
+ $admin
+ );
+
+ if ( $errors ) {
+ $this->fail( "Rollback failed:\n" . print_r( $errors, true )
+ . ";\n" . print_r( $details, true ) );
+ }
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(),
+ "rollback did not revert to the correct revision" );
+ $this->assertEquals( "one", $page->getContent()->getNativeData() );
+ }
+
+ /**
+ * @covers WikiPage::doRollback
+ */
+ public function testDoRollbackFailureSameContent() {
+ $admin = new User();
+ $admin->setName( "Admin" );
+ $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
+
+ $text = "one";
+ $page = $this->newPage( "WikiPageTest_testDoRollback" );
+ $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ "section one",
+ EDIT_NEW,
+ false,
+ $admin
+ );
+ $rev1 = $page->getRevision();
+
+ $user1 = new User();
+ $user1->setName( "127.0.1.11" );
+ $user1->addGroup( "sysop" ); #XXX: make the test user a sysop...
+ $text .= "\n\ntwo";
+ $page = new WikiPage( $page->getTitle() );
+ $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+ "adding section two",
+ 0,
+ false,
+ $user1
+ );
+
+ # now, do a the rollback from the same user was doing the edit before
+ $resultDetails = array();
+ $token = $user1->getEditToken(
+ array( $page->getTitle()->getPrefixedText(), $user1->getName() ),
+ null
+ );
+ $errors = $page->doRollback(
+ $user1->getName(),
+ "testing revert same user",
+ $token,
+ false,
+ $resultDetails,
+ $admin
+ );
+
+ $this->assertEquals( array(), $errors, "Rollback failed same user" );
+
+ # now, try the rollback
+ $resultDetails = array();
+ $token = $admin->getEditToken(
+ array( $page->getTitle()->getPrefixedText(), $user1->getName() ),
+ null
+ );
+ $errors = $page->doRollback(
+ $user1->getName(),
+ "testing revert",
+ $token,
+ false,
+ $resultDetails,
+ $admin
+ );
+
+ $this->assertEquals( array( array( 'alreadyrolled', 'WikiPageTest testDoRollback',
+ '127.0.1.11', 'Admin' ) ), $errors, "Rollback not failed" );
+
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(),
+ "rollback did not revert to the correct revision" );
+ $this->assertEquals( "one", $page->getContent()->getNativeData() );
+ }
+
+ public static function provideGetAutosummary() {
+ return array(
+ array(
+ 'Hello there, world!',
+ '#REDIRECT [[Foo]]',
+ 0,
+ '/^Redirected page .*Foo/'
+ ),
+
+ array(
+ null,
+ 'Hello world!',
+ EDIT_NEW,
+ '/^Created page .*Hello/'
+ ),
+
+ array(
+ 'Hello there, world!',
+ '',
+ 0,
+ '/^Blanked/'
+ ),
+
+ array(
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
+ eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
+ voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
+ clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.',
+ 'Hello world!',
+ 0,
+ '/^Replaced .*Hello/'
+ ),
+
+ array(
+ 'foo',
+ 'bar',
+ 0,
+ '/^$/'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetAutoSummary
+ * @covers WikiPage::getAutosummary
+ */
+ public function testGetAutosummary( $old, $new, $flags, $expected ) {
+ $this->hideDeprecated( "WikiPage::getAutosummary" );
+
+ $page = $this->newPage( "WikiPageTest_testGetAutosummary" );
+
+ $summary = $page->getAutosummary( $old, $new, $flags );
+
+ $this->assertTrue( (bool)preg_match( $expected, $summary ),
+ "Autosummary didn't match expected pattern $expected: $summary" );
+ }
+
+ public static function provideGetAutoDeleteReason() {
+ return array(
+ array(
+ array(),
+ false,
+ false
+ ),
+
+ array(
+ array(
+ array( "first edit", null ),
+ ),
+ "/first edit.*only contributor/",
+ false
+ ),
+
+ array(
+ array(
+ array( "first edit", null ),
+ array( "second edit", null ),
+ ),
+ "/second edit.*only contributor/",
+ true
+ ),
+
+ array(
+ array(
+ array( "first edit", "127.0.2.22" ),
+ array( "second edit", "127.0.3.33" ),
+ ),
+ "/second edit/",
+ true
+ ),
+
+ array(
+ array(
+ array(
+ "first edit: "
+ . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
+ . " nonumy eirmod tempor invidunt ut labore et dolore magna "
+ . "aliquyam erat, sed diam voluptua. At vero eos et accusam "
+ . "et justo duo dolores et ea rebum. Stet clita kasd gubergren, "
+ . "no sea takimata sanctus est Lorem ipsum dolor sit amet.'",
+ null
+ ),
+ ),
+ '/first edit:.*\.\.\."/',
+ false
+ ),
+
+ array(
+ array(
+ array( "first edit", "127.0.2.22" ),
+ array( "", "127.0.3.33" ),
+ ),
+ "/before blanking.*first edit/",
+ true
+ ),
+
+ );
+ }
+
+ /**
+ * @dataProvider provideGetAutoDeleteReason
+ * @covers WikiPage::getAutoDeleteReason
+ */
+ public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
+ global $wgUser;
+
+ //NOTE: assume Help namespace to contain wikitext
+ $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" );
+
+ $c = 1;
+
+ foreach ( $edits as $edit ) {
+ $user = new User();
+
+ if ( !empty( $edit[1] ) ) {
+ $user->setName( $edit[1] );
+ } else {
+ $user = $wgUser;
+ }
+
+ $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
+
+ $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
+
+ $c += 1;
+ }
+
+ $reason = $page->getAutoDeleteReason( $hasHistory );
+
+ if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) {
+ $this->assertEquals( $expectedResult, $reason );
+ } else {
+ $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
+ "Autosummary didn't match expected pattern $expectedResult: $reason" );
+ }
+
+ $this->assertEquals( $expectedHistory, $hasHistory,
+ "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
+
+ $page->doDeleteArticle( "done" );
+ }
+
+ public static function providePreSaveTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+ ),
+ array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider providePreSaveTransform
+ * @covers WikiPage::preSaveTransform
+ */
+ public function testPreSaveTransform( $text, $expected ) {
+ $this->hideDeprecated( 'WikiPage::preSaveTransform' );
+ $user = new User();
+ $user->setName( "127.0.0.1" );
+
+ //NOTE: assume Help namespace to contain wikitext
+ $page = $this->newPage( "Help:WikiPageTest_testPreloadTransform" );
+ $text = $page->preSaveTransform( $text, $user );
+
+ $this->assertEquals( $expected, $text );
+ }
+
+ /**
+ * @covers WikiPage::factory
+ */
+ public 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/WikiPageTestContentHandlerUseDB.php b/tests/phpunit/includes/WikiPageTestContentHandlerUseDB.php
new file mode 100644
index 00000000..3db76280
--- /dev/null
+++ b/tests/phpunit/includes/WikiPageTestContentHandlerUseDB.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ */
+class WikiPageTestContentHandlerUseDB extends WikiPageTest {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( 'wgContentHandlerUseDB', false );
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $page_table = $dbw->tableName( 'page' );
+ $revision_table = $dbw->tableName( 'revision' );
+ $archive_table = $dbw->tableName( 'archive' );
+
+ if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) {
+ $dbw->query( "alter table $page_table drop column page_content_model" );
+ $dbw->query( "alter table $revision_table drop column rev_content_model" );
+ $dbw->query( "alter table $revision_table drop column rev_content_format" );
+ $dbw->query( "alter table $archive_table drop column ar_content_model" );
+ $dbw->query( "alter table $archive_table drop column ar_content_format" );
+ }
+ }
+
+ /**
+ * @covers WikiPage::getContentModel
+ */
+ public function testGetContentModel() {
+ $page = $this->createPage(
+ "WikiPageTest_testGetContentModel",
+ "some text",
+ CONTENT_MODEL_JAVASCRIPT
+ );
+
+ $page = new WikiPage( $page->getTitle() );
+
+ // NOTE: since the content model is not recorded in the database,
+ // we expect to get the default, namely CONTENT_MODEL_WIKITEXT
+ $this->assertEquals( CONTENT_MODEL_WIKITEXT, $page->getContentModel() );
+ }
+
+ /**
+ * @covers WikiPage::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $page = $this->createPage(
+ "WikiPageTest_testGetContentHandler",
+ "some text",
+ CONTENT_MODEL_JAVASCRIPT
+ );
+
+ // NOTE: since the content model is not recorded in the database,
+ // we expect to get the default, namely CONTENT_MODEL_WIKITEXT
+ $page = new WikiPage( $page->getTitle() );
+ $this->assertEquals( 'WikitextContentHandler', get_class( $page->getContentHandler() ) );
+ }
+}
diff --git a/tests/phpunit/includes/XmlJsTest.php b/tests/phpunit/includes/XmlJsTest.php
new file mode 100644
index 00000000..0dbb0109
--- /dev/null
+++ b/tests/phpunit/includes/XmlJsTest.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @group Xml
+ */
+class XmlJs extends MediaWikiTestCase {
+
+ /**
+ * @covers XmlJsCode::__construct
+ * @dataProvider provideConstruction
+ */
+ public function testConstruction( $value ) {
+ $obj = new XmlJsCode( $value );
+ $this->assertEquals( $value, $obj->value );
+ }
+
+ public static function provideConstruction() {
+ return array(
+ array( null ),
+ array( '' ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/XmlSelectTest.php b/tests/phpunit/includes/XmlSelectTest.php
new file mode 100644
index 00000000..9f154bb7
--- /dev/null
+++ b/tests/phpunit/includes/XmlSelectTest.php
@@ -0,0 +1,185 @@
+<?php
+
+/**
+ * @group Xml
+ */
+class XmlSelectTest extends MediaWikiTestCase {
+
+ /**
+ * @var XmlSelect
+ */
+ protected $select;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( array(
+ 'wgWellFormedXml' => true,
+ ) );
+ $this->select = new XmlSelect();
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ $this->select = null;
+ }
+
+ /**
+ * @covers XmlSelect::__construct
+ */
+ public function testConstructWithoutParameters() {
+ $this->assertEquals( '<select></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * Parameters are $name (false), $id (false), $default (false)
+ * @dataProvider provideConstructionParameters
+ * @covers XmlSelect::__construct
+ */
+ 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 static 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>' ),
+ );
+ }
+
+ /**
+ * @covers XmlSelect::addOption
+ */
+ public function testAddOption() {
+ $this->select->addOption( 'foo' );
+ $this->assertEquals(
+ '<select><option value="foo">foo</option></select>',
+ $this->select->getHTML()
+ );
+ }
+
+ /**
+ * @covers XmlSelect::addOption
+ */
+ public function testAddOptionWithDefault() {
+ $this->select->addOption( 'foo', true );
+ $this->assertEquals(
+ '<select><option value="1">foo</option></select>',
+ $this->select->getHTML()
+ );
+ }
+
+ /**
+ * @covers XmlSelect::addOption
+ */
+ public function testAddOptionWithFalse() {
+ $this->select->addOption( 'foo', false );
+ $this->assertEquals(
+ '<select><option value="foo">foo</option></select>',
+ $this->select->getHTML()
+ );
+ }
+
+ /**
+ * @covers XmlSelect::addOption
+ */
+ public function testAddOptionWithValueZero() {
+ $this->select->addOption( 'foo', 0 );
+ $this->assertEquals(
+ '<select><option value="0">foo</option></select>',
+ $this->select->getHTML()
+ );
+ }
+
+ /**
+ * @covers XmlSelect::setDefault
+ */
+ 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="">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()
+ * @covers XmlSelect::setDefault
+ */
+ 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="">bar1</option>' . "\n" .
+ '<option value="foo2">foo2</option></select>', $this->select->getHTML() );
+ }
+
+ /**
+ * @covers XmlSelect::setAttribute
+ * @covers XmlSelect::getAttribute
+ */
+ 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..e6558819
--- /dev/null
+++ b/tests/phpunit/includes/XmlTest.php
@@ -0,0 +1,411 @@
+<?php
+
+/**
+ * @group Xml
+ */
+class XmlTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $langObj = Language::factory( 'en' );
+ $langObj->setNamespaces( array(
+ -2 => 'Media',
+ -1 => 'Special',
+ 0 => '',
+ 1 => 'Talk',
+ 2 => 'User',
+ 3 => 'User_talk',
+ 4 => 'MyWiki',
+ 5 => 'MyWiki_Talk',
+ 6 => 'File',
+ 7 => 'File_talk',
+ 8 => 'MediaWiki',
+ 9 => 'MediaWiki_talk',
+ 10 => 'Template',
+ 11 => 'Template_talk',
+ 100 => 'Custom',
+ 101 => 'Custom_talk',
+ ) );
+
+ $this->setMwGlobals( array(
+ 'wgLang' => $langObj,
+ 'wgWellFormedXml' => true,
+ ) );
+ }
+
+ /**
+ * @covers Xml::expandAttributes
+ */
+ 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'
+ );
+ }
+
+ /**
+ * @covers Xml::expandAttributes
+ */
+ public function testExpandAttributesException() {
+ $this->setExpectedException( 'MWException' );
+ Xml::expandAttributes( 'string' );
+ }
+
+ /**
+ * @covers Xml::element
+ */
+ public function testElementOpen() {
+ $this->assertEquals(
+ '<element>',
+ Xml::element( 'element', null, null ),
+ 'Opening element with no attributes'
+ );
+ }
+
+ /**
+ * @covers Xml::element
+ */
+ public function testElementEmpty() {
+ $this->assertEquals(
+ '<element />',
+ Xml::element( 'element', null, '' ),
+ 'Terminated empty element'
+ );
+ }
+
+ /**
+ * @covers Xml::input
+ */
+ public function testElementInputCanHaveAValueOfZero() {
+ $this->assertEquals(
+ '<input name="name" value="0" class="mw-ui-input" />',
+ Xml::input( 'name', false, 0 ),
+ 'Input with a value of 0 (bug 23797)'
+ );
+ }
+
+ /**
+ * @covers Xml::element
+ */
+ public 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'
+ );
+ }
+
+ /**
+ * @covers Xml::escapeTagsOnly
+ */
+ public function testEscapeTagsOnly() {
+ $this->assertEquals( '&quot;&gt;&lt;', Xml::escapeTagsOnly( '"><' ),
+ 'replace " > and < with their HTML entitites'
+ );
+ }
+
+ /**
+ * @covers Xml::element
+ */
+ public function testElementAttributes() {
+ $this->assertEquals(
+ '<element key="value" <>="&lt;&gt;">',
+ Xml::element( 'element', array( 'key' => 'value', '<>' => '<>' ), null ),
+ 'Element attributes, keys are not escaped'
+ );
+ }
+
+ /**
+ * @covers Xml::openElement
+ */
+ public function testOpenElement() {
+ $this->assertEquals(
+ '<element k="v">',
+ Xml::openElement( 'element', array( 'k' => 'v' ) ),
+ 'openElement() shortcut'
+ );
+ }
+
+ /**
+ * @covers Xml::closeElement
+ */
+ public function testCloseElement() {
+ $this->assertEquals( '</element>', Xml::closeElement( 'element' ), 'closeElement() shortcut' );
+ }
+
+ /**
+ * @covers Xml::dateMenu
+ */
+ public function testDateMenu() {
+ $curYear = intval( gmdate( 'Y' ) );
+ $prevYear = $curYear - 1;
+
+ $curMonth = intval( gmdate( 'n' ) );
+
+ $nextMonth = $curMonth + 1;
+ if ( $nextMonth == 13 ) {
+ $nextMonth = 1;
+ }
+
+ $this->assertEquals(
+ '<label for="year">From year (and earlier):</label> ' .
+ '<input id="year" maxlength="4" size="7" type="number" value="2011" name="year" class="mw-ui-input" /> ' .
+ '<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="">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 id="year" maxlength="4" size="7" type="number" value="2011" name="year" class="mw-ui-input" /> ' .
+ '<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"
+ );
+
+ $wantedYear = $nextMonth == 1 ? $curYear : $prevYear;
+ $this->assertEquals(
+ Xml::dateMenu( $wantedYear, $nextMonth ),
+ Xml::dateMenu( '', $nextMonth ),
+ "Date menu next month is 11 months ago"
+ );
+
+ $this->assertEquals(
+ '<label for="year">From year (and earlier):</label> ' .
+ '<input id="year" maxlength="4" size="7" type="number" name="year" class="mw-ui-input" /> ' .
+ '<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"
+ );
+ }
+
+ /**
+ * @covers Xml::textarea
+ */
+ public function testTextareaNoContent() {
+ $this->assertEquals(
+ '<textarea name="name" id="name" cols="40" rows="5" class="mw-ui-input"></textarea>',
+ Xml::textarea( 'name', '' ),
+ 'textarea() with not content'
+ );
+ }
+
+ /**
+ * @covers Xml::textarea
+ */
+ public function testTextareaAttribs() {
+ $this->assertEquals(
+ '<textarea name="name" id="name" cols="20" rows="10" class="mw-ui-input">&lt;txt&gt;</textarea>',
+ Xml::textarea( 'name', '<txt>', 20, 10 ),
+ 'textarea() with custom attribs'
+ );
+ }
+
+ /**
+ * @covers Xml::label
+ */
+ public function testLabelCreation() {
+ $this->assertEquals(
+ '<label for="id">name</label>',
+ Xml::label( 'name', 'id' ),
+ 'label() with no attribs'
+ );
+ }
+
+ /**
+ * @covers Xml::label
+ */
+ public 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"'
+ );
+ }
+
+ /**
+ * @covers Xml::languageSelector
+ */
+ public function testLanguageSelector() {
+ $select = Xml::languageSelector( 'en', true, null,
+ array( 'id' => 'testlang' ), wfMessage( 'yourlanguage' ) );
+ $this->assertEquals(
+ '<label for="testlang">Language:</label>',
+ $select[0]
+ );
+ }
+
+ /**
+ * @covers Xml::escapeJsString
+ */
+ public function testEscapeJsStringSpecialChars() {
+ $this->assertEquals(
+ '\\\\\r\n',
+ Xml::escapeJsString( "\\\r\n" ),
+ 'escapeJsString() with special characters'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarBoolean() {
+ $this->assertEquals(
+ 'true',
+ Xml::encodeJsVar( true ),
+ 'encodeJsVar() with boolean'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarNull() {
+ $this->assertEquals(
+ 'null',
+ Xml::encodeJsVar( null ),
+ 'encodeJsVar() with null'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public 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'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarObject() {
+ $this->assertEquals(
+ '{"a":"a","b":1}',
+ Xml::encodeJsVar( (object)array( 'a' => 'a', 'b' => 1 ) ),
+ 'encodeJsVar() with object'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarInt() {
+ $this->assertEquals(
+ '123456',
+ Xml::encodeJsVar( 123456 ),
+ 'encodeJsVar() with int'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarFloat() {
+ $this->assertEquals(
+ '1.23456',
+ Xml::encodeJsVar( 1.23456 ),
+ 'encodeJsVar() with float'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarIntString() {
+ $this->assertEquals(
+ '"123456"',
+ Xml::encodeJsVar( '123456' ),
+ 'encodeJsVar() with int-like string'
+ );
+ }
+
+ /**
+ * @covers Xml::encodeJsVar
+ */
+ public function testEncodeJsVarFloatString() {
+ $this->assertEquals(
+ '"1.23456"',
+ Xml::encodeJsVar( '1.23456' ),
+ 'encodeJsVar() with float-like string'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/XmlTypeCheckTest.php b/tests/phpunit/includes/XmlTypeCheckTest.php
new file mode 100644
index 00000000..6ad97fd4
--- /dev/null
+++ b/tests/phpunit/includes/XmlTypeCheckTest.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * PHPUnit tests for XMLTypeCheck.
+ * @author physikerwelt
+ * @group Xml
+ * @covers XMLTypeCheck
+ */
+class XmlTypeCheckTest extends MediaWikiTestCase {
+ const WELL_FORMED_XML = "<root><child /></root>";
+ const MAL_FORMED_XML = "<root><child /></error>";
+ const XML_WITH_PIH = '<?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="/w/index.php"?><svg><child /></svg>';
+
+ /**
+ * @covers XMLTypeCheck::newFromString
+ * @covers XMLTypeCheck::getRootElement
+ */
+ public function testWellFormedXML() {
+ $testXML = XmlTypeCheck::newFromString( self::WELL_FORMED_XML );
+ $this->assertTrue( $testXML->wellFormed );
+ $this->assertEquals( 'root', $testXML->getRootElement() );
+ }
+
+ /**
+ * @covers XMLTypeCheck::newFromString
+ */
+ public function testMalFormedXML() {
+ $testXML = XmlTypeCheck::newFromString( self::MAL_FORMED_XML );
+ $this->assertFalse( $testXML->wellFormed );
+ }
+
+ /**
+ * @covers XMLTypeCheck::processingInstructionHandler
+ */
+ public function testProcessingInstructionHandler() {
+ $called = false;
+ $testXML = new XmlTypeCheck(
+ self::XML_WITH_PIH,
+ null,
+ false,
+ array(
+ 'processing_instruction_handler' => function() use ( &$called ) {
+ $called = true;
+ }
+ )
+ );
+ $this->assertTrue( $called );
+ }
+
+}
diff --git a/tests/phpunit/includes/actions/ActionTest.php b/tests/phpunit/includes/actions/ActionTest.php
new file mode 100644
index 00000000..cc6fb11a
--- /dev/null
+++ b/tests/phpunit/includes/actions/ActionTest.php
@@ -0,0 +1,199 @@
+<?php
+
+/**
+ * @covers Action
+ *
+ * @licence GNU GPL v2+
+ * @author Thiemo Mättig
+ *
+ * @group Action
+ * @group Database
+ */
+class ActionTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $context = $this->getContext();
+ $this->setMwGlobals( 'wgActions', array(
+ 'null' => null,
+ 'disabled' => false,
+ 'view' => true,
+ 'edit' => true,
+ 'revisiondelete' => true,
+ 'dummy' => true,
+ 'string' => 'NamedDummyAction',
+ 'declared' => 'NonExistingClassName',
+ 'callable' => array( $this, 'dummyActionCallback' ),
+ 'object' => new InstantiatedDummyAction( $context->getWikiPage(), $context ),
+ ) );
+ }
+
+ private function getPage() {
+ return WikiPage::factory( Title::makeTitle( 0, 'Title' ) );
+ }
+
+ private function getContext( $requestedAction = null ) {
+ $request = new FauxRequest( array( 'action' => $requestedAction ) );
+
+ $context = new DerivativeContext( RequestContext::getMain() );
+ $context->setRequest( $request );
+ $context->setWikiPage( $this->getPage() );
+
+ return $context;
+ }
+
+ public function actionProvider() {
+ return array(
+ array( 'dummy', 'DummyAction' ),
+ array( 'string', 'NamedDummyAction' ),
+ array( 'callable', 'CalledDummyAction' ),
+ array( 'object', 'InstantiatedDummyAction' ),
+
+ // Capitalization is ignored
+ array( 'DUMMY', 'DummyAction' ),
+ array( 'STRING', 'NamedDummyAction' ),
+
+ // Null and non-existing values
+ array( 'null', null ),
+ array( 'undeclared', null ),
+ array( '', null ),
+ array( false, null ),
+ );
+ }
+
+ /**
+ * @dataProvider actionProvider
+ * @param string $requestedAction
+ * @param string|null $expected
+ */
+ public function testActionExists( $requestedAction, $expected ) {
+ $exists = Action::exists( $requestedAction );
+
+ $this->assertSame( $expected !== null, $exists );
+ }
+
+ public function testActionExists_doesNotRequireInstantiation() {
+ // The method is not supposed to check if the action can be instantiated.
+ $exists = Action::exists( 'declared' );
+
+ $this->assertTrue( $exists );
+ }
+
+ /**
+ * @dataProvider actionProvider
+ * @param string $requestedAction
+ * @param string|null $expected
+ */
+ public function testGetActionName( $requestedAction, $expected ) {
+ $context = $this->getContext( $requestedAction );
+ $actionName = Action::getActionName( $context );
+
+ $this->assertEquals( $expected ?: 'nosuchaction', $actionName );
+ }
+
+ public function testGetActionName_editredlinkWorkaround() {
+ // See https://bugzilla.wikimedia.org/show_bug.cgi?id=20966
+ $context = $this->getContext( 'editredlink' );
+ $actionName = Action::getActionName( $context );
+
+ $this->assertEquals( 'edit', $actionName );
+ }
+
+ public function testGetActionName_historysubmitWorkaround() {
+ // See https://bugzilla.wikimedia.org/show_bug.cgi?id=20966
+ $context = $this->getContext( 'historysubmit' );
+ $actionName = Action::getActionName( $context );
+
+ $this->assertEquals( 'view', $actionName );
+ }
+
+ public function testGetActionName_revisiondeleteWorkaround() {
+ // See https://bugzilla.wikimedia.org/show_bug.cgi?id=20966
+ $context = $this->getContext( 'historysubmit' );
+ $context->getRequest()->setVal( 'revisiondelete', true );
+ $actionName = Action::getActionName( $context );
+
+ $this->assertEquals( 'revisiondelete', $actionName );
+ }
+
+ /**
+ * @dataProvider actionProvider
+ * @param string $requestedAction
+ * @param string|null $expected
+ */
+ public function testActionFactory( $requestedAction, $expected ) {
+ $context = $this->getContext();
+ $action = Action::factory( $requestedAction, $context->getWikiPage(), $context );
+
+ $this->assertType( $expected ?: 'null', $action );
+ }
+
+ public function testNull_doesNotExist() {
+ $exists = Action::exists( null );
+
+ $this->assertFalse( $exists );
+ }
+
+ public function testNull_defaultsToView() {
+ $context = $this->getContext( null );
+ $actionName = Action::getActionName( $context );
+
+ $this->assertEquals( 'view', $actionName );
+ }
+
+ public function testNull_canNotBeInstantiated() {
+ $page = $this->getPage();
+ $action = Action::factory( null, $page );
+
+ $this->assertNull( $action );
+ }
+
+ public function testDisabledAction_exists() {
+ $exists = Action::exists( 'disabled' );
+
+ $this->assertTrue( $exists );
+ }
+
+ public function testDisabledAction_isNotResolved() {
+ $context = $this->getContext( 'disabled' );
+ $actionName = Action::getActionName( $context );
+
+ $this->assertEquals( 'nosuchaction', $actionName );
+ }
+
+ public function testDisabledAction_factoryReturnsFalse() {
+ $page = $this->getPage();
+ $action = Action::factory( 'disabled', $page );
+
+ $this->assertFalse( $action );
+ }
+
+ public function dummyActionCallback() {
+ $context = $this->getContext();
+ return new CalledDummyAction( $context->getWikiPage(), $context );
+ }
+
+}
+
+class DummyAction extends Action {
+
+ public function getName() {
+ return get_called_class();
+ }
+
+ public function show() {
+ }
+
+ public function execute() {
+ }
+}
+
+class NamedDummyAction extends DummyAction {
+}
+
+class CalledDummyAction extends DummyAction {
+}
+
+class InstantiatedDummyAction extends DummyAction {
+}
diff --git a/tests/phpunit/includes/api/ApiBaseTest.php b/tests/phpunit/includes/api/ApiBaseTest.php
new file mode 100644
index 00000000..a05c4fa8
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiBaseTest.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiBaseTest extends ApiTestCase {
+
+ /**
+ * @covers ApiBase::requireOnlyOneParameter
+ */
+ public function testRequireOnlyOneParameterDefault() {
+ $mock = new MockApi();
+ $mock->requireOnlyOneParameter(
+ array( "filename" => "foo.txt", "enablechunks" => false ),
+ "filename", "enablechunks"
+ );
+ $this->assertTrue( true );
+ }
+
+ /**
+ * @expectedException UsageException
+ * @covers ApiBase::requireOnlyOneParameter
+ */
+ public function testRequireOnlyOneParameterZero() {
+ $mock = new MockApi();
+ $mock->requireOnlyOneParameter(
+ array( "filename" => "foo.txt", "enablechunks" => 0 ),
+ "filename", "enablechunks"
+ );
+ }
+
+ /**
+ * @expectedException UsageException
+ * @covers ApiBase::requireOnlyOneParameter
+ */
+ public function testRequireOnlyOneParameterTrue() {
+ $mock = new MockApi();
+ $mock->requireOnlyOneParameter(
+ array( "filename" => "foo.txt", "enablechunks" => true ),
+ "filename", "enablechunks"
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php
new file mode 100644
index 00000000..d98eec6a
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiBlockTest.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiBlock
+ */
+class ApiBlockTest extends ApiTestCase {
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ protected function getTokens() {
+ return $this->getTokenList( self::$users['sysop'] );
+ }
+
+ function addDBData() {
+ $user = User::newFromName( 'UTApiBlockee' );
+
+ if ( $user->getId() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTApiBlockeePassword' );
+
+ $user->saveSettings();
+ }
+ }
+
+ /**
+ * This test has probably always been broken and use an invalid token
+ * Bug tracking brokenness is https://bugzilla.wikimedia.org/35646
+ *
+ * Root cause is https://gerrit.wikimedia.org/r/3434
+ * Which made the Block/Unblock API to actually verify the token
+ * previously always considered valid (bug 34212).
+ */
+ public function testMakeNormalBlock() {
+ $tokens = $this->getTokens();
+
+ $user = User::newFromName( 'UTApiBlockee' );
+
+ if ( !$user->getId() ) {
+ $this->markTestIncomplete( "The user UTApiBlockee does not exist" );
+ }
+
+ if ( !array_key_exists( 'blocktoken', $tokens ) ) {
+ $this->markTestIncomplete( "No block token found" );
+ }
+
+ $this->doApiRequest( array(
+ 'action' => 'block',
+ 'user' => 'UTApiBlockee',
+ 'reason' => 'Some reason',
+ 'token' => $tokens['blocktoken'] ), null, false, self::$users['sysop']->user );
+
+ $block = Block::newFromTarget( 'UTApiBlockee' );
+
+ $this->assertTrue( !is_null( $block ), 'Block is valid' );
+
+ $this->assertEquals( 'UTApiBlockee', (string)$block->getTarget() );
+ $this->assertEquals( 'Some reason', $block->mReason );
+ $this->assertEquals( 'infinity', $block->mExpiry );
+ }
+
+ /**
+ * @expectedException UsageException
+ * @expectedExceptionMessage The token parameter must be set
+ */
+ public function testBlockingActionWithNoToken( ) {
+ $this->doApiRequest(
+ array(
+ 'action' => 'block',
+ 'user' => 'UTApiBlockee',
+ 'reason' => 'Some reason',
+ ),
+ null,
+ false,
+ self::$users['sysop']->user
+ );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiCreateAccountTest.php b/tests/phpunit/includes/api/ApiCreateAccountTest.php
new file mode 100644
index 00000000..8d134f76
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiCreateAccountTest.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @group Database
+ * @group API
+ * @group medium
+ *
+ * @covers ApiCreateAccount
+ */
+class ApiCreateAccountTest extends ApiTestCase {
+ protected function setUp() {
+ parent::setUp();
+ LoginForm::setCreateaccountToken();
+ $this->setMwGlobals( array( 'wgEnableEmail' => true ) );
+ }
+
+ /**
+ * Test the account creation API with a valid request. Also
+ * make sure the new account can log in and is valid.
+ *
+ * This test does multiple API requests so it might end up being
+ * a bit slow. Raise the default timeout.
+ * @group medium
+ */
+ public function testValid() {
+ global $wgServer;
+
+ if ( !isset( $wgServer ) ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+
+ $password = User::randomPassword();
+
+ $ret = $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'name' => 'Apitestnew',
+ 'password' => $password,
+ 'email' => 'test@domain.test',
+ 'realname' => 'Test Name'
+ ) );
+
+ $result = $ret[0];
+ $this->assertNotInternalType( 'bool', $result );
+ $this->assertNotInternalType( 'null', $result['createaccount'] );
+
+ // Should first ask for token.
+ $a = $result['createaccount'];
+ $this->assertEquals( 'NeedToken', $a['result'] );
+ $token = $a['token'];
+
+ // Finally create the account
+ $ret = $this->doApiRequest(
+ array(
+ 'action' => 'createaccount',
+ 'name' => 'Apitestnew',
+ 'password' => $password,
+ 'token' => $token,
+ 'email' => 'test@domain.test',
+ 'realname' => 'Test Name'
+ ),
+ $ret[2]
+ );
+
+ $result = $ret[0];
+ $this->assertNotInternalType( 'bool', $result );
+ $this->assertEquals( 'Success', $result['createaccount']['result'] );
+
+ // Try logging in with the new user.
+ $ret = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => 'Apitestnew',
+ 'lgpassword' => $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' => 'Apitestnew',
+ 'lgpassword' => $password,
+ ),
+ $ret[2]
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotInternalType( 'bool', $result );
+ $a = $result['login']['result'];
+
+ $this->assertEquals( 'Success', $a );
+
+ // log out to destroy the session
+ $ret = $this->doApiRequest(
+ array(
+ 'action' => 'logout',
+ ),
+ $ret[2]
+ );
+ $this->assertEquals( array(), $ret[0] );
+ }
+
+ /**
+ * Make sure requests with no names are invalid.
+ * @expectedException UsageException
+ */
+ public function testNoName() {
+ $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'token' => LoginForm::getCreateaccountToken(),
+ 'password' => 'password',
+ ) );
+ }
+
+ /**
+ * Make sure requests with no password are invalid.
+ * @expectedException UsageException
+ */
+ public function testNoPassword() {
+ $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'name' => 'testName',
+ 'token' => LoginForm::getCreateaccountToken(),
+ ) );
+ }
+
+ /**
+ * Make sure requests with existing users are invalid.
+ * @expectedException UsageException
+ */
+ public function testExistingUser() {
+ $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'name' => 'Apitestsysop',
+ 'token' => LoginForm::getCreateaccountToken(),
+ 'password' => 'password',
+ 'email' => 'test@domain.test',
+ ) );
+ }
+
+ /**
+ * Make sure requests with invalid emails are invalid.
+ * @expectedException UsageException
+ */
+ public function testInvalidEmail() {
+ $this->doApiRequest( array(
+ 'action' => 'createaccount',
+ 'name' => 'Test User',
+ 'token' => LoginForm::getCreateaccountToken(),
+ 'password' => 'password',
+ 'email' => 'invalid',
+ ) );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiEditPageTest.php b/tests/phpunit/includes/api/ApiEditPageTest.php
new file mode 100644
index 00000000..3179a452
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiEditPageTest.php
@@ -0,0 +1,496 @@
+<?php
+
+/**
+ * Tests for MediaWiki api.php?action=edit.
+ *
+ * @author Daniel Kinzler
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiEditPage
+ */
+class ApiEditPageTest extends ApiTestCase {
+
+ protected function setUp() {
+ global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgExtraNamespaces' => $wgExtraNamespaces,
+ 'wgNamespaceContentModels' => $wgNamespaceContentModels,
+ 'wgContentHandlers' => $wgContentHandlers,
+ 'wgContLang' => $wgContLang,
+ ) );
+
+ $wgExtraNamespaces[12312] = 'Dummy';
+ $wgExtraNamespaces[12313] = 'Dummy_talk';
+
+ $wgNamespaceContentModels[12312] = "testing";
+ $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+
+ $this->doLogin();
+ }
+
+ protected function tearDown() {
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ parent::tearDown();
+ }
+
+ public function testEdit() {
+ $name = 'Help:ApiEditPageTest_testEdit'; // assume Help namespace to default to wikitext
+
+ // -- test new page --------------------------------------------
+ $apiResult = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ ) );
+ $apiResult = $apiResult[0];
+
+ // Validate API result data
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertEquals( 'Success', $apiResult['edit']['result'] );
+
+ $this->assertArrayHasKey( 'new', $apiResult['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
+
+ $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
+
+ // -- test existing page, no change ----------------------------
+ $data = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ ) );
+
+ $this->assertEquals( 'Success', $data[0]['edit']['result'] );
+
+ $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
+ $this->assertArrayHasKey( 'nochange', $data[0]['edit'] );
+
+ // -- test existing page, with change --------------------------
+ $data = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'different text'
+ ) );
+
+ $this->assertEquals( 'Success', $data[0]['edit']['result'] );
+
+ $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] );
+
+ $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] );
+ $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] );
+ $this->assertNotEquals(
+ $data[0]['edit']['newrevid'],
+ $data[0]['edit']['oldrevid'],
+ "revision id should change after edit"
+ );
+ }
+
+ public function testNonTextEdit() {
+ $name = 'Dummy:ApiEditPageTest_testNonTextEdit';
+ $data = serialize( 'some bla bla text' );
+
+ // -- test new page --------------------------------------------
+ $apiResult = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => $data, ) );
+ $apiResult = $apiResult[0];
+
+ // Validate API result data
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertEquals( 'Success', $apiResult['edit']['result'] );
+
+ $this->assertArrayHasKey( 'new', $apiResult['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
+
+ $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
+
+ // validate resulting revision
+ $page = WikiPage::factory( Title::newFromText( $name ) );
+ $this->assertEquals( "testing", $page->getContentModel() );
+ $this->assertEquals( $data, $page->getContent()->serialize() );
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideEditAppend() {
+ return array(
+ array( #0: append
+ 'foo', 'append', 'bar', "foobar"
+ ),
+ array( #1: prepend
+ 'foo', 'prepend', 'bar', "barfoo"
+ ),
+ array( #2: append to empty page
+ '', 'append', 'foo', "foo"
+ ),
+ array( #3: prepend to empty page
+ '', 'prepend', 'foo', "foo"
+ ),
+ array( #4: append to non-existing page
+ null, 'append', 'foo', "foo"
+ ),
+ array( #5: prepend to non-existing page
+ null, 'prepend', 'foo', "foo"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideEditAppend
+ */
+ public function testEditAppend( $text, $op, $append, $expected ) {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditAppend_$count";
+
+ // -- create page (or not) -----------------------------------------
+ if ( $text !== null ) {
+ list( $re ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => $text, ) );
+
+ $this->assertEquals( 'Success', $re['edit']['result'] ); // sanity
+ }
+
+ // -- try append/prepend --------------------------------------------
+ list( $re ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ $op . 'text' => $append, ) );
+
+ $this->assertEquals( 'Success', $re['edit']['result'] );
+
+ // -- validate -----------------------------------------------------
+ $page = new WikiPage( Title::newFromText( $name ) );
+ $content = $page->getContent();
+ $this->assertNotNull( $content, 'Page should have been created' );
+
+ $text = $content->getNativeData();
+
+ $this->assertEquals( $expected, $text );
+ }
+
+ /**
+ * Test editing of sections
+ */
+ public function testEditSection() {
+ $name = 'Help:ApiEditPageTest_testEditSection';
+ $page = WikiPage::factory( Title::newFromText( $name ) );
+ $text = "==section 1==\ncontent 1\n==section 2==\ncontent2";
+ // Preload the page with some text
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), 'summary' );
+
+ list( $re ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => '1',
+ 'text' => "==section 1==\nnew content 1",
+ ) );
+ $this->assertEquals( 'Success', $re['edit']['result'] );
+ $newtext = WikiPage::factory( Title::newFromText( $name ) )
+ ->getContent( Revision::RAW )
+ ->getNativeData();
+ $this->assertEquals( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext );
+
+ // Test that we raise a 'nosuchsection' error
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => '9999',
+ 'text' => 'text',
+ ) );
+ $this->fail( "Should have raised a UsageException" );
+ } catch ( UsageException $e ) {
+ $this->assertEquals( 'nosuchsection', $e->getCodeString() );
+ }
+ }
+
+ /**
+ * Test action=edit&section=new
+ * Run it twice so we test adding a new section on a
+ * page that doesn't exist (bug 52830) and one that
+ * does exist
+ */
+ public function testEditNewSection() {
+ $name = 'Help:ApiEditPageTest_testEditNewSection';
+
+ // Test on a page that does not already exist
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ list( $re ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => 'new',
+ 'text' => 'test',
+ 'summary' => 'header',
+ ));
+
+ $this->assertEquals( 'Success', $re['edit']['result'] );
+ // Check the page text is correct
+ $text = WikiPage::factory( Title::newFromText( $name ) )
+ ->getContent( Revision::RAW )
+ ->getNativeData();
+ $this->assertEquals( "== header ==\n\ntest", $text );
+
+ // Now on one that does
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ list( $re2 ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => 'new',
+ 'text' => 'test',
+ 'summary' => 'header',
+ ));
+
+ $this->assertEquals( 'Success', $re2['edit']['result'] );
+ $text = WikiPage::factory( Title::newFromText( $name ) )
+ ->getContent( Revision::RAW )
+ ->getNativeData();
+ $this->assertEquals( "== header ==\n\ntest\n\n== header ==\n\ntest", $text );
+ }
+
+ /**
+ * Ensure we can edit through a redirect, if adding a section
+ */
+ public function testEdit_redirect() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEdit_redirect_$count";
+ $title = Title::newFromText( $name );
+ $page = WikiPage::factory( $title );
+
+ $rname = "Help:ApiEditPageTest_testEdit_redirect_r$count";
+ $rtitle = Title::newFromText( $rname );
+ $rpage = WikiPage::factory( $rtitle );
+
+ // base edit for content
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // base edit for redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $rpage, '20120101000000' );
+
+ // conflicting edit to redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user );
+ $this->forceRevisionDate( $rpage, '20120101020202' );
+
+ // try to save edit, following the redirect
+ list( $re, , ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $rname,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ 'section' => 'new',
+ 'redirect' => true,
+ ), null, self::$users['sysop']->user );
+
+ $this->assertEquals( 'Success', $re['edit']['result'],
+ "no problems expected when following redirect" );
+ }
+
+ /**
+ * Ensure we cannot edit through a redirect, if attempting to overwrite content
+ */
+ public function testEdit_redirectText() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEdit_redirectText_$count";
+ $title = Title::newFromText( $name );
+ $page = WikiPage::factory( $title );
+
+ $rname = "Help:ApiEditPageTest_testEdit_redirectText_r$count";
+ $rtitle = Title::newFromText( $rname );
+ $rpage = WikiPage::factory( $rtitle );
+
+ // base edit for content
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // base edit for redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $rpage, '20120101000000' );
+
+ // conflicting edit to redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user );
+ $this->forceRevisionDate( $rpage, '20120101020202' );
+
+ // try to save edit, following the redirect but without creating a section
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $rname,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ 'redirect' => true,
+ ), null, self::$users['sysop']->user );
+
+ $this->fail( 'redirect-appendonly error expected' );
+ } catch ( UsageException $ex ) {
+ $this->assertEquals( 'redirect-appendonly', $ex->getCodeString() );
+ }
+ }
+
+ public function testEditConflict() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditConflict_$count";
+ $title = Title::newFromText( $name );
+
+ $page = WikiPage::factory( $title );
+
+ // base edit
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // conflicting edit
+ $page->doEditContent( new WikitextContent( "Foo bar" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user );
+ $this->forceRevisionDate( $page, '20120101020202' );
+
+ // try to save edit, expect conflict
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ ), null, self::$users['sysop']->user );
+
+ $this->fail( 'edit conflict expected' );
+ } catch ( UsageException $ex ) {
+ $this->assertEquals( 'editconflict', $ex->getCodeString() );
+ }
+ }
+
+ /**
+ * Ensure that editing using section=new will prevent simple conflicts
+ */
+ public function testEditConflict_newSection() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditConflict_newSection_$count";
+ $title = Title::newFromText( $name );
+
+ $page = WikiPage::factory( $title );
+
+ // base edit
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // conflicting edit
+ $page->doEditContent( new WikitextContent( "Foo bar" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user );
+ $this->forceRevisionDate( $page, '20120101020202' );
+
+ // try to save edit, expect no conflict
+ list( $re, , ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ 'section' => 'new',
+ ), null, self::$users['sysop']->user );
+
+ $this->assertEquals( 'Success', $re['edit']['result'],
+ "no edit conflict expected here" );
+ }
+
+ public function testEditConflict_bug41990() {
+ static $count = 0;
+ $count++;
+
+ /*
+ * bug 41990: if the target page has a newer revision than the redirect, then editing the
+ * redirect while specifying 'redirect' and *not* specifying 'basetimestamp' erroneously
+ * caused an edit conflict to be detected.
+ */
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_$count";
+ $title = Title::newFromText( $name );
+ $page = WikiPage::factory( $title );
+
+ $rname = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_r$count";
+ $rtitle = Title::newFromText( $rname );
+ $rpage = WikiPage::factory( $rtitle );
+
+ // base edit for content
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $page, '20120101000000' );
+
+ // base edit for redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->user );
+ $this->forceRevisionDate( $rpage, '20120101000000' );
+
+ // new edit to content
+ $page->doEditContent( new WikitextContent( "Foo bar" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user );
+ $this->forceRevisionDate( $rpage, '20120101020202' );
+
+ // try to save edit; should work, following the redirect.
+ list( $re, , ) = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $rname,
+ 'text' => 'nix bar!',
+ 'section' => 'new',
+ 'redirect' => true,
+ ), null, self::$users['sysop']->user );
+
+ $this->assertEquals( 'Success', $re['edit']['result'],
+ "no edit conflict expected here" );
+ }
+
+ /**
+ * @param WikiPage $page
+ * @param string|int $timestamp
+ */
+ protected function forceRevisionDate( WikiPage $page, $timestamp ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->update( 'revision',
+ array( 'rev_timestamp' => $dbw->timestamp( $timestamp ) ),
+ array( 'rev_id' => $page->getLatest() ) );
+
+ $page->clear();
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiLoginTest.php b/tests/phpunit/includes/api/ApiLoginTest.php
new file mode 100644
index 00000000..67a75f36
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiLoginTest.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiLogin
+ */
+class ApiLoginTest extends ApiTestCase {
+
+ /**
+ * Test result of attempted login with an empty username
+ */
+ public function testApiLoginNoName() {
+ $data = $this->doApiRequest( array( 'action' => 'login',
+ 'lgname' => '', 'lgpassword' => self::$users['sysop']->password,
+ ) );
+ $this->assertEquals( 'NoName', $data[0]['login']['result'] );
+ }
+
+ public 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",
+ ),
+ $ret[2]
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotInternalType( "bool", $result );
+ $a = $result["login"]["result"];
+
+ $this->assertEquals( "WrongPass", $a );
+ }
+
+ public 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,
+ ),
+ $ret[2]
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotInternalType( "bool", $result );
+ $a = $result["login"]["result"];
+
+ $this->assertEquals( "Success", $a );
+ }
+
+ /**
+ * @group Broken
+ */
+ public function testApiLoginGotCookie() {
+ $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
+ );
+ }
+
+ public 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[2] );
+
+ $this->assertArrayHasKey( "login", $data[0] );
+ $this->assertArrayHasKey( "result", $data[0]['login'] );
+ $this->assertEquals( "Success", $data[0]['login']['result'] );
+ $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] );
+ }
+
+}
diff --git a/tests/phpunit/includes/api/ApiMainTest.php b/tests/phpunit/includes/api/ApiMainTest.php
new file mode 100644
index 00000000..780cf9ed
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiMainTest.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiMain
+ */
+class ApiMainTest extends ApiTestCase {
+
+ /**
+ * 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
+ */
+ public 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" ) );
+ }
+
+ public static function provideAssert() {
+ $anon = new User();
+ $bot = new User();
+ $bot->setName( 'Bot' );
+ $bot->addToDatabase();
+ $bot->addGroup( 'bot' );
+ $user = new User();
+ $user->setName( 'User' );
+ $user->addToDatabase();
+ return array(
+ array( $anon, 'user', 'assertuserfailed' ),
+ array( $user, 'user', false ),
+ array( $user, 'bot', 'assertbotfailed' ),
+ array( $bot, 'user', false ),
+ array( $bot, 'bot', false ),
+ );
+ }
+
+ /**
+ * Tests the assert={user|bot} functionality
+ *
+ * @covers ApiMain::checkAsserts
+ * @dataProvider provideAssert
+ * @param User $user
+ * @param string $assert
+ * @param string|bool $error False if no error expected
+ */
+ public function testAssert( $user, $assert, $error ) {
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'query',
+ 'assert' => $assert,
+ ), null, null, $user );
+ $this->assertFalse( $error ); // That no error was expected
+ } catch ( UsageException $e ) {
+ $this->assertEquals( $e->getCodeString(), $error );
+ }
+ }
+
+}
diff --git a/tests/phpunit/includes/api/ApiModuleManagerTest.php b/tests/phpunit/includes/api/ApiModuleManagerTest.php
new file mode 100644
index 00000000..dab81e16
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiModuleManagerTest.php
@@ -0,0 +1,318 @@
+<?php
+
+/**
+ * @covers ApiModuleManager
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiModuleManagerTest extends MediaWikiTestCase {
+
+ private function getModuleManager() {
+ $request = new FauxRequest();
+ $main = new ApiMain( $request );
+ return new ApiModuleManager( $main );
+ }
+
+ public function newApiLogin( $main, $action ) {
+ return new ApiLogin( $main, $action );
+ }
+
+ public function addModuleProvider() {
+ return array(
+ 'plain class' => array(
+ 'login',
+ 'action',
+ 'ApiLogin',
+ null,
+ ),
+
+ 'with factory' => array(
+ 'login',
+ 'action',
+ 'ApiLogin',
+ array( $this, 'newApiLogin' ),
+ ),
+
+ 'with closure' => array(
+ 'logout',
+ 'action',
+ 'ApiLogout',
+ function ( ApiMain $main, $action ) {
+ return new ApiLogout( $main, $action );
+ },
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider addModuleProvider
+ */
+ public function testAddModule( $name, $group, $class, $factory = null ) {
+ $moduleManager = $this->getModuleManager();
+ $moduleManager->addModule( $name, $group, $class, $factory );
+
+ $this->assertTrue( $moduleManager->isDefined( $name, $group ), 'isDefined' );
+ $this->assertNotNull( $moduleManager->getModule( $name, $group, true ), 'getModule' );
+ }
+
+ public function addModulesProvider() {
+ return array(
+ 'empty' => array(
+ array(),
+ 'action',
+ ),
+
+ 'simple' => array(
+ array(
+ 'login' => 'ApiLogin',
+ 'logout' => 'ApiLogout',
+ ),
+ 'action',
+ ),
+
+ 'with factories' => array(
+ array(
+ 'login' => array(
+ 'class' => 'ApiLogin',
+ 'factory' => array( $this, 'newApiLogin' ),
+ ),
+ 'logout' => array(
+ 'class' => 'ApiLogout',
+ 'factory' => function ( ApiMain $main, $action ) {
+ return new ApiLogout( $main, $action );
+ },
+ ),
+ ),
+ 'action',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider addModulesProvider
+ */
+ public function testAddModules( array $modules, $group ) {
+ $moduleManager = $this->getModuleManager();
+ $moduleManager->addModules( $modules, $group );
+
+ foreach ( array_keys( $modules ) as $name ) {
+ $this->assertTrue( $moduleManager->isDefined( $name, $group ), 'isDefined' );
+ $this->assertNotNull( $moduleManager->getModule( $name, $group, true ), 'getModule' );
+ }
+
+ $this->assertTrue( true ); // Don't mark the test as risky if $modules is empty
+ }
+
+ public function getModuleProvider() {
+ $modules = array(
+ 'feedrecentchanges' => 'ApiFeedRecentChanges',
+ 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ),
+ 'login' => array(
+ 'class' => 'ApiLogin',
+ 'factory' => array( $this, 'newApiLogin' ),
+ ),
+ 'logout' => array(
+ 'class' => 'ApiLogout',
+ 'factory' => function ( ApiMain $main, $action ) {
+ return new ApiLogout( $main, $action );
+ },
+ ),
+ );
+
+ return array(
+ 'legacy entry' => array(
+ $modules,
+ 'feedrecentchanges',
+ 'ApiFeedRecentChanges',
+ ),
+
+ 'just a class' => array(
+ $modules,
+ 'feedcontributions',
+ 'ApiFeedContributions',
+ ),
+
+ 'with factory' => array(
+ $modules,
+ 'login',
+ 'ApiLogin',
+ ),
+
+ 'with closure' => array(
+ $modules,
+ 'logout',
+ 'ApiLogout',
+ ),
+ );
+ }
+
+ /**
+ * @covers ApiModuleManager::getModule
+ * @dataProvider getModuleProvider
+ */
+ public function testGetModule( $modules, $name, $expectedClass ) {
+ $moduleManager = $this->getModuleManager();
+ $moduleManager->addModules( $modules, 'test' );
+
+ // should return the right module
+ $module1 = $moduleManager->getModule( $name, null, false );
+ $this->assertInstanceOf( $expectedClass, $module1 );
+
+ // should pass group check (with caching disabled)
+ $module2 = $moduleManager->getModule( $name, 'test', true );
+ $this->assertNotNull( $module2 );
+
+ // should use cached instance
+ $module3 = $moduleManager->getModule( $name, null, false );
+ $this->assertSame( $module1, $module3 );
+
+ // should not use cached instance if caching is disabled
+ $module4 = $moduleManager->getModule( $name, null, true );
+ $this->assertNotSame( $module1, $module4 );
+ }
+
+ /**
+ * @covers ApiModuleManager::getModule
+ */
+ public function testGetModule_null() {
+ $modules = array(
+ 'login' => 'ApiLogin',
+ 'logout' => 'ApiLogout',
+ );
+
+ $moduleManager = $this->getModuleManager();
+ $moduleManager->addModules( $modules, 'test' );
+
+ $this->assertNull( $moduleManager->getModule( 'quux' ), 'unknown name' );
+ $this->assertNull( $moduleManager->getModule( 'login', 'bla' ), 'wrong group' );
+ }
+
+ /**
+ * @covers ApiModuleManager::getNames
+ */
+ public function testGetNames() {
+ $fooModules = array(
+ 'login' => 'ApiLogin',
+ 'logout' => 'ApiLogout',
+ );
+
+ $barModules = array(
+ 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ),
+ 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ),
+ );
+
+ $moduleManager = $this->getModuleManager();
+ $moduleManager->addModules( $fooModules, 'foo' );
+ $moduleManager->addModules( $barModules, 'bar' );
+
+ $fooNames = $moduleManager->getNames( 'foo' );
+ $this->assertArrayEquals( array_keys( $fooModules ), $fooNames );
+
+ $allNames = $moduleManager->getNames();
+ $allModules = array_merge( $fooModules, $barModules );
+ $this->assertArrayEquals( array_keys( $allModules ), $allNames );
+ }
+
+ /**
+ * @covers ApiModuleManager::getNamesWithClasses
+ */
+ public function testGetNamesWithClasses() {
+ $fooModules = array(
+ 'login' => 'ApiLogin',
+ 'logout' => 'ApiLogout',
+ );
+
+ $barModules = array(
+ 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ),
+ 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ),
+ );
+
+ $moduleManager = $this->getModuleManager();
+ $moduleManager->addModules( $fooModules, 'foo' );
+ $moduleManager->addModules( $barModules, 'bar' );
+
+ $fooNamesWithClasses = $moduleManager->getNamesWithClasses( 'foo' );
+ $this->assertArrayEquals( $fooModules, $fooNamesWithClasses );
+
+ $allNamesWithClasses = $moduleManager->getNamesWithClasses();
+ $allModules = array_merge( $fooModules, array(
+ 'feedcontributions' => 'ApiFeedContributions',
+ 'feedrecentchanges' => 'ApiFeedRecentChanges',
+ ) );
+ $this->assertArrayEquals( $allModules, $allNamesWithClasses );
+ }
+
+ /**
+ * @covers ApiModuleManager::getModuleGroup
+ */
+ public function testGetModuleGroup() {
+ $fooModules = array(
+ 'login' => 'ApiLogin',
+ 'logout' => 'ApiLogout',
+ );
+
+ $barModules = array(
+ 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ),
+ 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ),
+ );
+
+ $moduleManager = $this->getModuleManager();
+ $moduleManager->addModules( $fooModules, 'foo' );
+ $moduleManager->addModules( $barModules, 'bar' );
+
+ $this->assertEquals( 'foo', $moduleManager->getModuleGroup( 'login' ) );
+ $this->assertEquals( 'bar', $moduleManager->getModuleGroup( 'feedrecentchanges' ) );
+ $this->assertNull( $moduleManager->getModuleGroup( 'quux' ) );
+ }
+
+ /**
+ * @covers ApiModuleManager::getGroups
+ */
+ public function testGetGroups() {
+ $fooModules = array(
+ 'login' => 'ApiLogin',
+ 'logout' => 'ApiLogout',
+ );
+
+ $barModules = array(
+ 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ),
+ 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ),
+ );
+
+ $moduleManager = $this->getModuleManager();
+ $moduleManager->addModules( $fooModules, 'foo' );
+ $moduleManager->addModules( $barModules, 'bar' );
+
+ $groups = $moduleManager->getGroups();
+ $this->assertArrayEquals( array( 'foo', 'bar' ), $groups );
+ }
+
+ /**
+ * @covers ApiModuleManager::getClassName
+ */
+ public function testGetClassName() {
+ $fooModules = array(
+ 'login' => 'ApiLogin',
+ 'logout' => 'ApiLogout',
+ );
+
+ $barModules = array(
+ 'feedcontributions' => array( 'class' => 'ApiFeedContributions' ),
+ 'feedrecentchanges' => array( 'class' => 'ApiFeedRecentChanges' ),
+ );
+
+ $moduleManager = $this->getModuleManager();
+ $moduleManager->addModules( $fooModules, 'foo' );
+ $moduleManager->addModules( $barModules, 'bar' );
+
+ $this->assertEquals( 'ApiLogin', $moduleManager->getClassName( 'login' ) );
+ $this->assertEquals( 'ApiLogout', $moduleManager->getClassName( 'logout' ) );
+ $this->assertEquals( 'ApiFeedContributions', $moduleManager->getClassName( 'feedcontributions' ) );
+ $this->assertEquals( 'ApiFeedRecentChanges', $moduleManager->getClassName( 'feedrecentchanges' ) );
+ $this->assertFalse( $moduleManager->getClassName( 'nonexistentmodule' ) );
+ }
+
+
+}
diff --git a/tests/phpunit/includes/api/ApiOptionsTest.php b/tests/phpunit/includes/api/ApiOptionsTest.php
new file mode 100644
index 00000000..5f955bbc
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiOptionsTest.php
@@ -0,0 +1,459 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiOptions
+ */
+class ApiOptionsTest extends MediaWikiLangTestCase {
+
+ /** @var PHPUnit_Framework_MockObject_MockObject */
+ private $mUserMock;
+ /** @var ApiOptions */
+ private $mTested;
+ private $mSession;
+ /** @var DerivativeContext */
+ private $mContext;
+
+ private $mOldGetPreferencesHooks;
+
+ private static $Success = array( 'options' => 'success' );
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->mUserMock = $this->getMockBuilder( 'User' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ // Set up groups and rights
+ $this->mUserMock->expects( $this->any() )
+ ->method( 'getEffectiveGroups' )->will( $this->returnValue( array( '*', 'user' ) ) );
+ $this->mUserMock->expects( $this->any() )
+ ->method( 'isAllowed' )->will( $this->returnValue( true ) );
+
+ // Set up callback for User::getOptionKinds
+ $this->mUserMock->expects( $this->any() )
+ ->method( 'getOptionKinds' )->will( $this->returnCallback( array( $this, 'getOptionKinds' ) ) );
+
+ // Create a new context
+ $this->mContext = new DerivativeContext( new RequestContext() );
+ $this->mContext->getContext()->setTitle( Title::newFromText( 'Test' ) );
+ $this->mContext->setUser( $this->mUserMock );
+
+ $main = new ApiMain( $this->mContext );
+
+ // Empty session
+ $this->mSession = array();
+
+ $this->mTested = new ApiOptions( $main, 'options' );
+
+ global $wgHooks;
+ if ( !isset( $wgHooks['GetPreferences'] ) ) {
+ $wgHooks['GetPreferences'] = array();
+ }
+ $this->mOldGetPreferencesHooks = $wgHooks['GetPreferences'];
+ $wgHooks['GetPreferences'][] = array( $this, 'hookGetPreferences' );
+ }
+
+ protected function tearDown() {
+ global $wgHooks;
+
+ $wgHooks['GetPreferences'] = $this->mOldGetPreferencesHooks;
+ $this->mOldGetPreferencesHooks = false;
+
+ parent::tearDown();
+ }
+
+ public function hookGetPreferences( $user, &$preferences ) {
+ $preferences = array();
+
+ foreach ( array( 'name', 'willBeNull', 'willBeEmpty', 'willBeHappy' ) as $k ) {
+ $preferences[$k] = array(
+ 'type' => 'text',
+ 'section' => 'test',
+ 'label' => '&#160;',
+ );
+ }
+
+ $preferences['testmultiselect'] = array(
+ 'type' => 'multiselect',
+ 'options' => array(
+ 'Test' => array(
+ '<span dir="auto">Some HTML here for option 1</span>' => 'opt1',
+ '<span dir="auto">Some HTML here for option 2</span>' => 'opt2',
+ '<span dir="auto">Some HTML here for option 3</span>' => 'opt3',
+ '<span dir="auto">Some HTML here for option 4</span>' => 'opt4',
+ ),
+ ),
+ 'section' => 'test',
+ 'label' => '&#160;',
+ 'prefix' => 'testmultiselect-',
+ 'default' => array(),
+ );
+
+ return true;
+ }
+
+ /**
+ * @param IContextSource $context
+ * @param array|null $options
+ *
+ * @return array
+ */
+ public function getOptionKinds( IContextSource $context, $options = null ) {
+ // Match with above.
+ $kinds = array(
+ 'name' => 'registered',
+ 'willBeNull' => 'registered',
+ 'willBeEmpty' => 'registered',
+ 'willBeHappy' => 'registered',
+ 'testmultiselect-opt1' => 'registered-multiselect',
+ 'testmultiselect-opt2' => 'registered-multiselect',
+ 'testmultiselect-opt3' => 'registered-multiselect',
+ 'testmultiselect-opt4' => 'registered-multiselect',
+ 'special' => 'special',
+ );
+
+ if ( $options === null ) {
+ return $kinds;
+ }
+
+ $mapping = array();
+ foreach ( $options as $key => $value ) {
+ if ( isset( $kinds[$key] ) ) {
+ $mapping[$key] = $kinds[$key];
+ } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
+ $mapping[$key] = 'userjs';
+ } else {
+ $mapping[$key] = 'unused';
+ }
+ }
+
+ return $mapping;
+ }
+
+ private function getSampleRequest( $custom = array() ) {
+ $request = array(
+ 'token' => '123ABC',
+ 'change' => null,
+ 'optionname' => null,
+ 'optionvalue' => null,
+ );
+
+ return array_merge( $request, $custom );
+ }
+
+ private function executeQuery( $request ) {
+ $this->mContext->setRequest( new FauxRequest( $request, true, $this->mSession ) );
+ $this->mTested->execute();
+
+ return $this->mTested->getResult()->getData();
+ }
+
+ /**
+ * @expectedException UsageException
+ */
+ public function testNoToken() {
+ $request = $this->getSampleRequest( array( 'token' => null ) );
+
+ $this->executeQuery( $request );
+ }
+
+ public function testAnon() {
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'isAnon' )
+ ->will( $this->returnValue( true ) );
+
+ try {
+ $request = $this->getSampleRequest();
+
+ $this->executeQuery( $request );
+ } catch ( UsageException $e ) {
+ $this->assertEquals( 'notloggedin', $e->getCodeString() );
+ $this->assertEquals( 'Anonymous users cannot change preferences', $e->getMessage() );
+
+ return;
+ }
+ $this->fail( "UsageException was not thrown" );
+ }
+
+ public function testNoOptionname() {
+ try {
+ $request = $this->getSampleRequest( array( 'optionvalue' => '1' ) );
+
+ $this->executeQuery( $request );
+ } catch ( UsageException $e ) {
+ $this->assertEquals( 'nooptionname', $e->getCodeString() );
+ $this->assertEquals( 'The optionname parameter must be set', $e->getMessage() );
+
+ return;
+ }
+ $this->fail( "UsageException was not thrown" );
+ }
+
+ public function testNoChanges() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'setOption' );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'saveSettings' );
+
+ try {
+ $request = $this->getSampleRequest();
+
+ $this->executeQuery( $request );
+ } catch ( UsageException $e ) {
+ $this->assertEquals( 'nochanges', $e->getCodeString() );
+ $this->assertEquals( 'No changes were requested', $e->getMessage() );
+
+ return;
+ }
+ $this->fail( "UsageException was not thrown" );
+ }
+
+ public function testReset() {
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'resetOptions' )
+ ->with( $this->equalTo( array( 'all' ) ) );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'setOption' );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array( 'reset' => '' ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testResetKinds() {
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'resetOptions' )
+ ->with( $this->equalTo( array( 'registered' ) ) );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'setOption' );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array( 'reset' => '', 'resetkinds' => 'registered' ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testOptionWithValue() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array( 'optionname' => 'name', 'optionvalue' => 'value' ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testOptionResetValue() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'name' ), $this->identicalTo( null ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array( 'optionname' => 'name' ) );
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testChange() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->at( 2 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 4 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'willBeNull' ), $this->identicalTo( null ) );
+
+ $this->mUserMock->expects( $this->at( 5 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 6 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) );
+
+ $this->mUserMock->expects( $this->at( 7 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 8 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array(
+ 'change' => 'willBeNull|willBeEmpty=|willBeHappy=Happy'
+ ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testResetChangeOption() {
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->at( 4 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 5 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) );
+
+ $this->mUserMock->expects( $this->at( 6 ) )
+ ->method( 'getOptions' );
+
+ $this->mUserMock->expects( $this->at( 7 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $args = array(
+ 'reset' => '',
+ 'change' => 'willBeHappy=Happy',
+ 'optionname' => 'name',
+ 'optionvalue' => 'value'
+ );
+
+ $response = $this->executeQuery( $this->getSampleRequest( $args ) );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testMultiSelect() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->at( 3 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'testmultiselect-opt1' ), $this->identicalTo( true ) );
+
+ $this->mUserMock->expects( $this->at( 4 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'testmultiselect-opt2' ), $this->identicalTo( null ) );
+
+ $this->mUserMock->expects( $this->at( 5 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'testmultiselect-opt3' ), $this->identicalTo( false ) );
+
+ $this->mUserMock->expects( $this->at( 6 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'testmultiselect-opt4' ), $this->identicalTo( false ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array(
+ 'change' => 'testmultiselect-opt1=1|testmultiselect-opt2|'
+ . 'testmultiselect-opt3=|testmultiselect-opt4=0'
+ ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+
+ public function testSpecialOption() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array(
+ 'change' => 'special=1'
+ ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( array(
+ 'options' => 'success',
+ 'warnings' => array(
+ 'options' => array(
+ '*' => "Validation error for 'special': cannot be set by this module"
+ )
+ )
+ ), $response );
+ }
+
+ public function testUnknownOption() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array(
+ 'change' => 'unknownOption=1'
+ ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( array(
+ 'options' => 'success',
+ 'warnings' => array(
+ 'options' => array(
+ '*' => "Validation error for 'unknownOption': not a valid preference"
+ )
+ )
+ ), $response );
+ }
+
+ public function testUserjsOption() {
+ $this->mUserMock->expects( $this->never() )
+ ->method( 'resetOptions' );
+
+ $this->mUserMock->expects( $this->at( 3 ) )
+ ->method( 'setOption' )
+ ->with( $this->equalTo( 'userjs-option' ), $this->equalTo( '1' ) );
+
+ $this->mUserMock->expects( $this->once() )
+ ->method( 'saveSettings' );
+
+ $request = $this->getSampleRequest( array(
+ 'change' => 'userjs-option=1'
+ ) );
+
+ $response = $this->executeQuery( $request );
+
+ $this->assertEquals( self::$Success, $response );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiParseTest.php b/tests/phpunit/includes/api/ApiParseTest.php
new file mode 100644
index 00000000..d038a4f5
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiParseTest.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiParse
+ */
+class ApiParseTest extends ApiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ public function testParseNonexistentPage() {
+ $somePage = mt_rand();
+
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'parse',
+ 'page' => $somePage ) );
+
+ $this->fail( "API did not return an error when parsing a nonexistent page" );
+ } catch ( UsageException $ex ) {
+ $this->assertEquals(
+ 'missingtitle',
+ $ex->getCodeString(),
+ "Parse request for nonexistent page must give 'missingtitle' error: "
+ . var_export( $ex->getMessageArray(), true )
+ );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiPurgeTest.php b/tests/phpunit/includes/api/ApiPurgeTest.php
new file mode 100644
index 00000000..7fce134a
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiPurgeTest.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiPurge
+ */
+class ApiPurgeTest extends ApiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ /**
+ * @group Broken
+ */
+ public 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],
+ "Must receive a 'purge' result from API" );
+
+ $this->assertEquals(
+ 3,
+ count( $data[0]['purge'] ),
+ "Purge request for three articles should give back three results received: "
+ . var_export( $data[0]['purge'], true ) );
+
+ $pages = array( 'UTPage' => 'purged', $somePage => 'missing', '%5D' => 'invalid' );
+ foreach ( $data[0]['purge'] as $v ) {
+ $this->assertArrayHasKey( $pages[$v['title']], $v );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiQueryAllPagesTest.php b/tests/phpunit/includes/api/ApiQueryAllPagesTest.php
new file mode 100644
index 00000000..124988f3
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiQueryAllPagesTest.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+class ApiQueryAllPagesTest extends ApiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ /**
+ * @todo give this test a real name explaining what is being tested here
+ */
+ public function testBug25702() {
+ $title = Title::newFromText( 'Category:Template:xyz' );
+ $page = WikiPage::factory( $title );
+ $page->doEdit( 'Some text', 'inserting content' );
+
+ $result = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'list' => 'allpages',
+ 'apnamespace' => NS_CATEGORY,
+ 'apprefix' => 'Template:x' ) );
+
+ $this->assertArrayHasKey( 'query', $result[0] );
+ $this->assertArrayHasKey( 'allpages', $result[0]['query'] );
+ $this->assertNotEquals( 0, count( $result[0]['query']['allpages'] ),
+ 'allpages list does not contain page Category:Template:xyz' );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiRevisionDeleteTest.php b/tests/phpunit/includes/api/ApiRevisionDeleteTest.php
new file mode 100644
index 00000000..b03836eb
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiRevisionDeleteTest.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * Tests for action=revisiondelete
+ * @covers APIRevisionDelete
+ * @group API
+ * @group medium
+ * @group Database
+ */
+class ApiRevisionDeleteTest extends ApiTestCase {
+
+ public static $page = 'Help:ApiRevDel_test';
+ public $revs = array();
+
+ protected function setUp() {
+ // Needs to be before setup since this gets cached
+ $this->mergeMwGlobalArrayValue( 'wgGroupPermissions', array( 'sysop' => array( 'deleterevision' => true ) ) );
+ parent::setUp();
+ // Make a few edits for us to play with
+ for ( $i = 1; $i <= 5; $i++ ) {
+ self::editPage( self::$page, MWCryptRand::generateHex( 10 ), 'summary' );
+ $this->revs[] = Title::newFromText( self::$page )->getLatestRevID( Title::GAID_FOR_UPDATE );
+ }
+
+ }
+
+ public function testHidingRevisions() {
+ $user = self::$users['sysop']->user;
+ $revid = array_shift( $this->revs );
+ $out = $this->doApiRequest( array(
+ 'action' => 'revisiondelete',
+ 'type' => 'revision',
+ 'target' => self::$page,
+ 'ids' => $revid,
+ 'hide' => 'content|user|comment',
+ 'token' => $user->getEditToken(),
+ ) );
+ // Check the output
+ $out = $out[0]['revisiondelete'];
+ $this->assertEquals( $out['status'], 'Success' );
+ $this->assertArrayHasKey( 'items', $out );
+ $item = $out['items'][0];
+ $this->assertArrayHasKey( 'userhidden', $item );
+ $this->assertArrayHasKey( 'commenthidden', $item );
+ $this->assertArrayHasKey( 'texthidden', $item );
+ $this->assertEquals( $item['id'], $revid );
+
+ // Now check that that revision was actually hidden
+ $rev = Revision::newFromId( $revid );
+ $this->assertEquals( $rev->getContent( Revision::FOR_PUBLIC ), null );
+ $this->assertEquals( $rev->getComment( Revision::FOR_PUBLIC ), '' );
+ $this->assertEquals( $rev->getUser( Revision::FOR_PUBLIC ), 0 );
+
+ // Now test unhiding!
+ $out2 = $this->doApiRequest( array(
+ 'action' => 'revisiondelete',
+ 'type' => 'revision',
+ 'target' => self::$page,
+ 'ids' => $revid,
+ 'show' => 'content|user|comment',
+ 'token' => $user->getEditToken(),
+ ) );
+
+ // Check the output
+ $out2 = $out2[0]['revisiondelete'];
+ $this->assertEquals( $out2['status'], 'Success' );
+ $this->assertArrayHasKey( 'items', $out2 );
+ $item = $out2['items'][0];
+
+ $this->assertArrayNotHasKey( 'userhidden', $item );
+ $this->assertArrayNotHasKey( 'commenthidden', $item );
+ $this->assertArrayNotHasKey( 'texthidden', $item );
+
+ $this->assertEquals( $item['id'], $revid );
+
+ $rev = Revision::newFromId( $revid );
+ $this->assertNotEquals( $rev->getContent( Revision::FOR_PUBLIC ), null );
+ $this->assertNotEquals( $rev->getComment( Revision::FOR_PUBLIC ), '' );
+ $this->assertNotEquals( $rev->getUser( Revision::FOR_PUBLIC ), 0 );
+ }
+
+ public function testUnhidingOutput() {
+ $user = self::$users['sysop']->user;
+ $revid = array_shift( $this->revs );
+ // Hide revisions
+ $this->doApiRequest( array(
+ 'action' => 'revisiondelete',
+ 'type' => 'revision',
+ 'target' => self::$page,
+ 'ids' => $revid,
+ 'hide' => 'content|user|comment',
+ 'token' => $user->getEditToken(),
+ ) );
+
+ $out = $this->doApiRequest( array(
+ 'action' => 'revisiondelete',
+ 'type' => 'revision',
+ 'target' => self::$page,
+ 'ids' => $revid,
+ 'show' => 'comment',
+ 'token' => $user->getEditToken(),
+ ) );
+ $out = $out[0]['revisiondelete'];
+ $this->assertEquals( $out['status'], 'Success' );
+ $this->assertArrayHasKey( 'items', $out );
+ $item = $out['items'][0];
+ // Check it has userhidden & texthidden keys
+ // but no commenthidden key
+ $this->assertArrayHasKey( 'userhidden', $item );
+ $this->assertArrayNotHasKey( 'commenthidden', $item );
+ $this->assertArrayHasKey( 'texthidden', $item );
+ $this->assertEquals( $item['id'], $revid );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php
new file mode 100644
index 00000000..cd141947
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTestCase.php
@@ -0,0 +1,196 @@
+<?php
+
+abstract class ApiTestCase extends MediaWikiLangTestCase {
+ protected static $apiUrl;
+
+ /**
+ * @var ApiTestContext
+ */
+ protected $apiContext;
+
+ protected function setUp() {
+ global $wgServer;
+
+ parent::setUp();
+ self::$apiUrl = $wgServer . wfScript( 'api' );
+
+ ApiQueryInfo::resetTokenCache(); // tokens are invalid because we cleared the session
+
+ self::$users = array(
+ 'sysop' => new TestUser(
+ 'Apitestsysop',
+ 'Api Test Sysop',
+ 'api_test_sysop@example.com',
+ array( 'sysop' )
+ ),
+ 'uploader' => new TestUser(
+ 'Apitestuser',
+ 'Api Test User',
+ 'api_test_user@example.com',
+ array()
+ )
+ );
+
+ $this->setMwGlobals( array(
+ 'wgMemc' => new EmptyBagOStuff(),
+ 'wgAuth' => new StubObject( 'wgAuth', 'AuthPlugin' ),
+ 'wgRequest' => new FauxRequest( array() ),
+ 'wgUser' => self::$users['sysop']->user,
+ ) );
+
+ $this->apiContext = new ApiTestContext();
+ }
+
+ /**
+ * Edits or creates a page/revision
+ * @param string $pageName Page title
+ * @param string $text Content of the page
+ * @param string $summary Optional summary string for the revision
+ * @param int $defaultNs Optional namespace id
+ * @return array Array as returned by WikiPage::doEditContent()
+ */
+ protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
+ $title = Title::newFromText( $pageName, $defaultNs );
+ $page = WikiPage::factory( $title );
+
+ return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
+ }
+
+ /**
+ * Does the API request and returns the result.
+ *
+ * The returned value is an array containing
+ * - the result data (array)
+ * - the request (WebRequest)
+ * - the session data of the request (array)
+ * - if $appendModule is true, the Api module $module
+ *
+ * @param array $params
+ * @param array|null $session
+ * @param bool $appendModule
+ * @param User|null $user
+ *
+ * @return array
+ */
+ protected function doApiRequest( array $params, array $session = null,
+ $appendModule = false, User $user = null
+ ) {
+ global $wgRequest, $wgUser;
+
+ if ( is_null( $session ) ) {
+ // re-use existing global session by default
+ $session = $wgRequest->getSessionArray();
+ }
+
+ // set up global environment
+ if ( $user ) {
+ $wgUser = $user;
+ }
+
+ $wgRequest = new FauxRequest( $params, true, $session );
+ RequestContext::getMain()->setRequest( $wgRequest );
+
+ // set up local environment
+ $context = $this->apiContext->newTestContext( $wgRequest, $wgUser );
+
+ $module = new ApiMain( $context, true );
+
+ // run it!
+ $module->execute();
+
+ // construct result
+ $results = array(
+ $module->getResultData(),
+ $context->getRequest(),
+ $context->getRequest()->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 array $params Key-value API params
+ * @param array|null $session Session array
+ * @param User|null $user A User object for the context
+ * @return array Result of the API call
+ * @throws Exception In case wsToken is not set in the session
+ */
+ protected function doApiRequestWithToken( array $params, array $session = null,
+ User $user = null
+ ) {
+ global $wgRequest;
+
+ if ( $session === null ) {
+ $session = $wgRequest->getSessionArray();
+ }
+
+ if ( isset( $session['wsToken'] ) && $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, false, $user );
+ } else {
+ throw new Exception( "Session token not available" );
+ }
+ }
+
+ protected function doLogin( $user = 'sysop' ) {
+ if ( !array_key_exists( $user, self::$users ) ) {
+ throw new MWException( "Can not log in to undefined user $user" );
+ }
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => self::$users[$user]->username,
+ 'lgpassword' => self::$users[$user]->password ) );
+
+ $token = $data[0]['login']['token'];
+
+ $data = $this->doApiRequest(
+ array(
+ 'action' => 'login',
+ 'lgtoken' => $token,
+ 'lgname' => self::$users[$user]->username,
+ 'lgpassword' => self::$users[$user]->password,
+ ),
+ $data[2]
+ );
+
+ return $data;
+ }
+
+ protected function getTokenList( $user, $session = null ) {
+ $data = $this->doApiRequest( array(
+ 'action' => 'tokens',
+ 'type' => 'edit|delete|protect|move|block|unblock|watch'
+ ), $session, false, $user->user );
+
+ if ( !array_key_exists( 'tokens', $data[0] ) ) {
+ throw new MWException( 'Api failed to return a token list' );
+ }
+
+ return $data[0]['tokens'];
+ }
+
+ public function testApiTestGroup() {
+ $groups = PHPUnit_Util_Test::getGroups( get_class( $this ) );
+ $constraint = PHPUnit_Framework_Assert::logicalOr(
+ $this->contains( 'medium' ),
+ $this->contains( 'large' )
+ );
+ $this->assertThat( $groups, $constraint,
+ 'ApiTestCase::setUp can be slow, tests must be "medium" or "large"'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiTestCaseUpload.php b/tests/phpunit/includes/api/ApiTestCaseUpload.php
new file mode 100644
index 00000000..7e513394
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTestCaseUpload.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * * Abstract class to support upload tests
+ */
+
+abstract class ApiTestCaseUpload extends ApiTestCase {
+ /**
+ * Fixture -- run before every test
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgEnableUploads' => true,
+ 'wgEnableAPI' => true,
+ ) );
+
+ wfSetupSession();
+
+ $this->clearFakeUploads();
+ }
+
+ protected function tearDown() {
+ $this->clearTempUpload();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Helper function -- remove files and associated articles by Title
+ *
+ * @param Title $title Title to be removed
+ *
+ * @return bool
+ */
+ 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;
+ }
+
+ $page = WikiPage::factory( $title );
+ $page->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 string $fileName Filename to be removed
+ *
+ * @return bool
+ */
+ 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 string $filePath Path to file on the filesystem
+ *
+ * @return bool
+ */
+ public function deleteFileByContent( $filePath ) {
+ $hash = FSFile::getSha1Base36FromPath( $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 string $fieldName Name this would have in the upload form
+ * @param string $fileName Name to title this
+ * @param string $type MIME type
+ * @param string $filePath Path where to find file contents
+ *
+ * @throws Exception
+ * @return bool
+ */
+ 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;
+ }
+
+ function fakeUploadChunk( $fieldName, $fileName, $type, & $chunkData ) {
+ $tmpName = tempnam( wfTempDir(), "" );
+ // copy the chunk data to temp location:
+ if ( !file_put_contents( $tmpName, $chunkData ) ) {
+ throw new Exception( "couldn't copy chunk data 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
+ );
+ }
+
+ function clearTempUpload() {
+ if ( isset( $_FILES['file']['tmp_name'] ) ) {
+ $tmp = $_FILES['file']['tmp_name'];
+ if ( file_exists( $tmp ) ) {
+ unlink( $tmp );
+ }
+ }
+ }
+
+ /**
+ * Remove traces of previous fake uploads
+ */
+ function clearFakeUploads() {
+ $_FILES = array();
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiTestContext.php b/tests/phpunit/includes/api/ApiTestContext.php
new file mode 100644
index 00000000..17dad1fa
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTestContext.php
@@ -0,0 +1,21 @@
+<?php
+
+class ApiTestContext extends RequestContext {
+
+ /**
+ * Returns a DerivativeContext with the request variables in place
+ *
+ * @param WebRequest $request WebRequest request object including parameters and session
+ * @param User|null $user User or null
+ * @return DerivativeContext
+ */
+ public function newTestContext( WebRequest $request, User $user = null ) {
+ $context = new DerivativeContext( $this );
+ $context->setRequest( $request );
+ if ( $user !== null ) {
+ $context->setUser( $user );
+ }
+
+ return $context;
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiTokensTest.php b/tests/phpunit/includes/api/ApiTokensTest.php
new file mode 100644
index 00000000..fbe97893
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiTokensTest.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiTokens
+ */
+class ApiTokensTest extends ApiTestCase {
+
+ public function testGettingToken() {
+ foreach ( self::$users as $user ) {
+ $this->runTokenTest( $user );
+ }
+ }
+
+ protected function runTokenTest( $user ) {
+ $tokens = $this->getTokenList( $user );
+
+ $rights = $user->user->getRights();
+
+ $this->assertArrayHasKey( 'edittoken', $tokens );
+ $this->assertArrayHasKey( 'movetoken', $tokens );
+
+ if ( isset( $rights['delete'] ) ) {
+ $this->assertArrayHasKey( 'deletetoken', $tokens );
+ }
+
+ if ( isset( $rights['block'] ) ) {
+ $this->assertArrayHasKey( 'blocktoken', $tokens );
+ $this->assertArrayHasKey( 'unblocktoken', $tokens );
+ }
+
+ if ( isset( $rights['protect'] ) ) {
+ $this->assertArrayHasKey( 'protecttoken', $tokens );
+ }
+ }
+
+}
diff --git a/tests/phpunit/includes/api/ApiUnblockTest.php b/tests/phpunit/includes/api/ApiUnblockTest.php
new file mode 100644
index 00000000..2c2370a8
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiUnblockTest.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiUnblock
+ */
+class ApiUnblockTest extends ApiTestCase {
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ /**
+ * @expectedException UsageException
+ */
+ public function testWithNoToken( ) {
+ $this->doApiRequest(
+ array(
+ 'action' => 'unblock',
+ 'user' => 'UTApiBlockee',
+ 'reason' => 'Some reason',
+ ),
+ null,
+ false,
+ self::$users['sysop']->user
+ );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php
new file mode 100644
index 00000000..8ea761f8
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiUploadTest.php
@@ -0,0 +1,572 @@
+<?php
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ */
+
+/**
+ * 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
+ * @group Broken
+ * Broken test, reports false errors from time to time.
+ * See https://bugzilla.wikimedia.org/26169
+ *
+ * This is pretty sucky... needs to be prettified.
+ */
+class ApiUploadTest extends ApiTestCaseUpload {
+ /**
+ * Testing login
+ * XXX this is a funny way of getting session context
+ */
+ public 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'] );
+
+ $this->assertNotEmpty( $session, 'API Login must return a session' );
+
+ 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 ) {
+ $exception = false;
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'upload',
+ ), $session, self::$users['uploader']->user );
+ } 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 ) {
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ /** @var array $filePaths */
+ $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,
+ self::$users['uploader']->user );
+ } 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 ) {
+ $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, self::$users['uploader']->user );
+ } 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 ) {
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ $filePaths = $randomImageGenerator->writeImages( 2, $extension, wfTempDir() );
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ // we'll reuse this filename
+ /** @var array $filePaths */
+ $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,
+ self::$users['uploader']->user );
+ } 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,
+ self::$users['uploader']->user ); // FIXME: leaks a temporary file
+ } 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 ) {
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ /** @var array $filePaths */
+ $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, , $session ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } 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 ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user ); // FIXME: leaks a temporary file
+ } 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 ) {
+ $this->setMwGlobals( array(
+ 'wgUser' => self::$users['uploader']->user, // @todo FIXME: still used somewhere
+ ) );
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ /** @var array $filePaths */
+ $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, , $session ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user ); // FIXME: leaks a temporary file
+ } 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 ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception, "No UsageException exception." );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePath );
+ }
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadChunks( $session ) {
+ $this->setMwGlobals( array(
+ // @todo FIXME: still used somewhere
+ 'wgUser' => self::$users['uploader']->user,
+ ) );
+
+ $chunkSize = 1048576;
+ // Download a large image file
+ // ( using RandomImageGenerator for large files is not stable )
+ $mimeType = 'image/jpeg';
+ $url = 'http://upload.wikimedia.org/wikipedia/commons/'
+ . 'e/ed/Oberaargletscher_from_Oberaar%2C_2010_07.JPG';
+ $filePath = wfTempDir() . '/Oberaargletscher_from_Oberaar.jpg';
+ try {
+ // Only download if the file is not avaliable in the temp location:
+ if ( !is_file( $filePath ) ) {
+ copy( $url, $filePath );
+ }
+ } catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $fileSize = filesize( $filePath );
+ $fileName = basename( $filePath );
+
+ $this->deleteFileByFileName( $fileName );
+ $this->deleteFileByContent( $filePath );
+
+ // Base upload params:
+ $params = array(
+ 'action' => 'upload',
+ 'stash' => 1,
+ 'filename' => $fileName,
+ 'filesize' => $fileSize,
+ 'offset' => 0,
+ );
+
+ // Upload chunks
+ $chunkSessionKey = false;
+ $resultOffset = 0;
+ // Open the file:
+ wfSuppressWarnings();
+ $handle = fopen( $filePath, "r" );
+ wfRestoreWarnings();
+
+ if ( $handle === false ) {
+ $this->markTestIncomplete( "could not open file: $filePath" );
+ }
+
+ while ( !feof( $handle ) ) {
+ // Get the current chunk
+ wfSuppressWarnings();
+ $chunkData = fread( $handle, $chunkSize );
+ wfRestoreWarnings();
+
+ // Upload the current chunk into the $_FILE object:
+ $this->fakeUploadChunk( 'chunk', 'blob', $mimeType, $chunkData );
+
+ // Check for chunkSessionKey
+ if ( !$chunkSessionKey ) {
+ // Upload fist chunk ( and get the session key )
+ try {
+ list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } catch ( UsageException $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+ // Make sure we got a valid chunk continue:
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertTrue( isset( $result['upload']['filekey'] ) );
+ // If we don't get a session key mark test incomplete.
+ if ( !isset( $result['upload']['filekey'] ) ) {
+ $this->markTestIncomplete( "no filekey provided" );
+ }
+ $chunkSessionKey = $result['upload']['filekey'];
+ $this->assertEquals( 'Continue', $result['upload']['result'] );
+ // First chunk should have chunkSize == offset
+ $this->assertEquals( $chunkSize, $result['upload']['offset'] );
+ $resultOffset = $result['upload']['offset'];
+ continue;
+ }
+ // Filekey set to chunk session
+ $params['filekey'] = $chunkSessionKey;
+ // Update the offset ( always add chunkSize for subquent chunks
+ // should be in-sync with $result['upload']['offset'] )
+ $params['offset'] += $chunkSize;
+ // Make sure param offset is insync with resultOffset:
+ $this->assertEquals( $resultOffset, $params['offset'] );
+ // Upload current chunk
+ try {
+ list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } catch ( UsageException $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+ // Make sure we got a valid chunk continue:
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertTrue( isset( $result['upload']['filekey'] ) );
+
+ // Check if we were on the last chunk:
+ if ( $params['offset'] + $chunkSize >= $fileSize ) {
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ break;
+ } else {
+ $this->assertEquals( 'Continue', $result['upload']['result'] );
+ // update $resultOffset
+ $resultOffset = $result['upload']['offset'];
+ }
+ }
+ fclose( $handle );
+
+ // Check that we got a valid file result:
+ wfDebug( __METHOD__
+ . " hohoh filesize {$fileSize} info {$result['upload']['imageinfo']['size']}\n\n" );
+ $this->assertEquals( $fileSize, $result['upload']['imageinfo']['size'] );
+ $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
+ $this->assertTrue( isset( $result['upload']['filekey'] ) );
+ $filekey = $result['upload']['filekey'];
+
+ // 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 ) = $this->doApiRequestWithToken( $params, $session,
+ self::$users['uploader']->user );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ // don't remove downloaded temporary file for fast subquent tests.
+ //unlink( $filePath );
+ }
+}
diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php
new file mode 100644
index 00000000..e49c6c0e
--- /dev/null
+++ b/tests/phpunit/includes/api/ApiWatchTest.php
@@ -0,0 +1,157 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @todo This test suite is severly broken and need a full review
+ */
+class ApiWatchTest extends ApiTestCase {
+ protected function setUp() {
+ parent::setUp();
+ $this->doLogin();
+ }
+
+ function getTokens() {
+ return $this->getTokenList( self::$users['sysop'] );
+ }
+
+ /**
+ */
+ public function testWatchEdit() {
+ $tokens = $this->getTokens();
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'edit',
+ 'title' => 'Help:UTPage', // Help namespace is hopefully wikitext
+ 'text' => 'new text',
+ 'token' => $tokens['edittoken'],
+ 'watchlist' => 'watch' ) );
+ $this->assertArrayHasKey( 'edit', $data[0] );
+ $this->assertArrayHasKey( 'result', $data[0]['edit'] );
+ $this->assertEquals( 'Success', $data[0]['edit']['result'] );
+
+ return $data;
+ }
+
+ /**
+ * @depends testWatchEdit
+ */
+ public function testWatchClear() {
+ $tokens = $this->getTokens();
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'wllimit' => 'max',
+ 'list' => 'watchlist' ) );
+
+ 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,
+ 'token' => $tokens['watchtoken'] ) );
+ }
+ }
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'list' => 'watchlist' ), $data );
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'watchlist', $data[0]['query'] );
+ foreach ( $data[0]['query']['watchlist'] as $index => $item ) {
+ // Previous tests may insert an invalid title
+ // like ":ApiEditPageTest testNonTextEdit", which
+ // can't be cleared.
+ if ( strpos( $item['title'], ':' ) === 0 ) {
+ unset( $data[0]['query']['watchlist'][$index] );
+ }
+ }
+ $this->assertEquals( 0, count( $data[0]['query']['watchlist'] ) );
+
+ return $data;
+ }
+
+ /**
+ */
+ public function testWatchProtect() {
+ $tokens = $this->getTokens();
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'protect',
+ 'token' => $tokens['protecttoken'],
+ 'title' => 'Help:UTPage',
+ 'protections' => 'edit=sysop',
+ 'watchlist' => 'unwatch' ) );
+
+ $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] );
+ }
+
+ /**
+ */
+ public function testGetRollbackToken() {
+ $this->getTokens();
+
+ if ( !Title::newFromText( 'Help:UTPage' )->exists() ) {
+ $this->markTestSkipped( "The article [[Help:UTPage]] does not exist" ); //TODO: just create it?
+ }
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'prop' => 'revisions',
+ 'titles' => 'Help:UTPage',
+ 'rvtoken' => 'rollback' ) );
+
+ $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->markTestSkipped( "Target page (Help: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;
+ }
+
+ /**
+ * @group Broken
+ * Broken because there is currently no revision info in the $pageinfo
+ *
+ * @depends testGetRollbackToken
+ */
+ public function testWatchRollback( $data ) {
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key];
+ $revinfo = $pageinfo['revisions'][0];
+
+ try {
+ $data = $this->doApiRequest( array(
+ 'action' => 'rollback',
+ 'title' => 'Help:UTPage',
+ 'user' => $revinfo['user'],
+ 'token' => $pageinfo['rollbacktoken'],
+ 'watchlist' => 'watch' ) );
+
+ $this->assertArrayHasKey( 'rollback', $data[0] );
+ $this->assertArrayHasKey( 'title', $data[0]['rollback'] );
+ } catch ( UsageException $ue ) {
+ if ( $ue->getCodeString() == 'onlyauthor' ) {
+ $this->markTestIncomplete( "Only one author to 'Help:UTPage', cannot test rollback" );
+ } else {
+ $this->fail( "Received error '" . $ue->getCodeString() . "'" );
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/MockApi.php b/tests/phpunit/includes/api/MockApi.php
new file mode 100644
index 00000000..d94aa2cd
--- /dev/null
+++ b/tests/phpunit/includes/api/MockApi.php
@@ -0,0 +1,20 @@
+<?php
+
+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/MockApiQueryBase.php b/tests/phpunit/includes/api/MockApiQueryBase.php
new file mode 100644
index 00000000..4bede519
--- /dev/null
+++ b/tests/phpunit/includes/api/MockApiQueryBase.php
@@ -0,0 +1,11 @@
+<?php
+class MockApiQueryBase extends ApiQueryBase {
+ public function execute() {
+ }
+
+ public function getVersion() {
+ }
+
+ public function __construct() {
+ }
+}
diff --git a/tests/phpunit/includes/api/PrefixUniquenessTest.php b/tests/phpunit/includes/api/PrefixUniquenessTest.php
new file mode 100644
index 00000000..13da33c7
--- /dev/null
+++ b/tests/phpunit/includes/api/PrefixUniquenessTest.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * Checks that all API query modules, core and extensions, have unique prefixes.
+ *
+ * @group API
+ */
+class PrefixUniquenessTest extends MediaWikiTestCase {
+
+ public function testPrefixes() {
+ $main = new ApiMain( new FauxRequest() );
+ $query = new ApiQuery( $main, 'foo', 'bar' );
+ $moduleManager = $query->getModuleManager();
+
+ $modules = $moduleManager->getNames();
+ $prefixes = array();
+
+ foreach ( $modules as $name ) {
+ $module = $moduleManager->getModule( $name );
+ $class = get_class( $module );
+
+ $prefix = $module->getModulePrefix();
+ if ( isset( $prefixes[$prefix] ) ) {
+ $this->fail( "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}" );
+ }
+ $prefixes[$module->getModulePrefix()] = $class;
+ }
+ $this->assertTrue( true ); // dummy call to make this test non-incomplete
+ }
+}
diff --git a/tests/phpunit/includes/api/RandomImageGenerator.php b/tests/phpunit/includes/api/RandomImageGenerator.php
new file mode 100644
index 00000000..6374cfac
--- /dev/null
+++ b/tests/phpunit/includes/api/RandomImageGenerator.php
@@ -0,0 +1,496 @@
+<?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;
+
+ /**
+ * 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',
+ 'maxWidth', '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',
+ __DIR__ . '/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" );
+ }
+ }
+
+ /**
+ * Writes random images with random filenames to disk in the directory you
+ * specify, or current working directory.
+ *
+ * @param int $number Number of filenames to write
+ * @param string $format Optional, must be understood by ImageMagick, such as 'jpg' or 'gif'
+ * @param string $dir 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 string $format (a typical extension like 'svg', 'jpg', etc.)
+ *
+ * @throws Exception
+ * @return string
+ */
+ function getImageWriteMethod( $format ) {
+ global $wgUseImageMagick, $wgImageMagickConvertCommand;
+ if ( $format === 'svg' ) {
+ return 'writeSvg';
+ } else {
+ // figure out how to write images
+ global $wgExiv2Command;
+ if ( class_exists( 'Imagick' ) && $wgExiv2Command && is_executable( $wgExiv2Command ) ) {
+ 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 int $number Number of filenames to generate
+ * @param string $extension Optional, defaults to 'jpg'
+ * @param string $dir Optional, defaults to current working directory
+ * @return array 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 array $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 array $spec Spec describing background and shapes to draw
+ * @param string $format File format to write (which is obviously always svg here)
+ * @param string $filename Filename to write to
+ *
+ * @throws Exception
+ */
+ 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 array $spec Spec describing background and circles to draw
+ * @param string $format File format to write
+ * @param string $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 array $spec Returned by getImageSpec
+ * @param array $matrix 2x2 transformation matrix
+ * @return array 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 array $matrix 2x2 rotation matrix
+ * @param int $x The x-coordinate number
+ * @param int $y The 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 array $spec Spec describing background and shapes to draw
+ * @param string $format File format to write (unused by this method but
+ * kept so it has the same signature as writeImageWithApi).
+ * @param string $filename Filename to write to
+ *
+ * @return bool
+ */
+ 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 int $number Number of pairs
+ * @return array 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 int $number_desired Number of lines desired
+ *
+ * @throws Exception
+ * @return array 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/UserWrapper.php b/tests/phpunit/includes/api/UserWrapper.php
new file mode 100644
index 00000000..f8da0ff4
--- /dev/null
+++ b/tests/phpunit/includes/api/UserWrapper.php
@@ -0,0 +1,25 @@
+<?php
+
+class UserWrapper {
+ public $userName;
+ public $password;
+ public $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();
+ }
+}
diff --git a/tests/phpunit/includes/api/format/ApiFormatJsonTest.php b/tests/phpunit/includes/api/format/ApiFormatJsonTest.php
new file mode 100644
index 00000000..fc1f9021
--- /dev/null
+++ b/tests/phpunit/includes/api/format/ApiFormatJsonTest.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiFormatJson
+ */
+class ApiFormatJsonTest extends ApiFormatTestBase {
+
+ public function testValidSyntax( ) {
+ $data = $this->apiRequest( 'json', array( 'action' => 'query', 'meta' => 'siteinfo' ) );
+
+ $this->assertInternalType( 'array', json_decode( $data, true ) );
+ $this->assertGreaterThan( 0, count( (array)$data ) );
+ }
+
+ public function testJsonpInjection( ) {
+ $data = $this->apiRequest( 'json', array( 'action' => 'query', 'meta' => 'siteinfo', 'callback' => 'myCallback' ) );
+ $this->assertEquals( '/**/myCallback(', substr( $data, 0, 15 ) );
+ }
+}
diff --git a/tests/phpunit/includes/api/format/ApiFormatNoneTest.php b/tests/phpunit/includes/api/format/ApiFormatNoneTest.php
new file mode 100644
index 00000000..cabd750b
--- /dev/null
+++ b/tests/phpunit/includes/api/format/ApiFormatNoneTest.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiFormatNone
+ */
+class ApiFormatNoneTest extends ApiFormatTestBase {
+
+ public function testValidSyntax( ) {
+ $data = $this->apiRequest( 'none', array( 'action' => 'query', 'meta' => 'siteinfo' ) );
+
+ $this->assertEquals( '', $data ); // No output!
+ }
+}
diff --git a/tests/phpunit/includes/api/format/ApiFormatPhpTest.php b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
new file mode 100644
index 00000000..54f447a9
--- /dev/null
+++ b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiFormatPhp
+ */
+class ApiFormatPhpTest extends ApiFormatTestBase {
+
+ public function testValidSyntax( ) {
+ $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..5f6d53ce
--- /dev/null
+++ b/tests/phpunit/includes/api/format/ApiFormatTestBase.php
@@ -0,0 +1,32 @@
+<?php
+
+abstract class ApiFormatTestBase extends ApiTestCase {
+
+ /**
+ * @param string $format
+ * @param array $params
+ * @param array $data
+ *
+ * @return string
+ */
+ protected function apiRequest( $format, $params, $data = null ) {
+ $data = parent::doApiRequest( $params, $data, true );
+
+ /** @var ApiMain $module */
+ $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/format/ApiFormatWddxTest.php b/tests/phpunit/includes/api/format/ApiFormatWddxTest.php
new file mode 100644
index 00000000..d075f547
--- /dev/null
+++ b/tests/phpunit/includes/api/format/ApiFormatWddxTest.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiFormatWddx
+ */
+class ApiFormatWddxTest extends ApiFormatTestBase {
+
+ /**
+ * @requires function wddx_deserialize
+ */
+ public function testValidSyntax( ) {
+ $data = $this->apiRequest( 'wddx', array( 'action' => 'query', 'meta' => 'siteinfo' ) );
+
+ $this->assertInternalType( 'array', wddx_deserialize( $data ) );
+ $this->assertGreaterThan( 0, count( (array)$data ) );
+ }
+}
diff --git a/tests/phpunit/includes/api/generateRandomImages.php b/tests/phpunit/includes/api/generateRandomImages.php
new file mode 100644
index 00000000..87f5c4c0
--- /dev/null
+++ b/tests/phpunit/includes/api/generateRandomImages.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Bootstrapping for test image file generation
+ *
+ * @file
+ */
+
+// Start up MediaWiki in command-line mode
+require_once __DIR__ . "/../../../../maintenance/Maintenance.php";
+require __DIR__ . "/RandomImageGenerator.php";
+
+class GenerateRandomImages extends Maintenance {
+
+ public function getDbType() {
+ return Maintenance::DB_NONE;
+ }
+
+ 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/query/ApiQueryBasicTest.php b/tests/phpunit/includes/api/query/ApiQueryBasicTest.php
new file mode 100644
index 00000000..e486c4f4
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryBasicTest.php
@@ -0,0 +1,353 @@
+<?php
+/**
+ *
+ * Created on Feb 6, 2013
+ *
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * 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
+ */
+
+require_once 'ApiQueryTestBase.php';
+
+/**
+ * These tests validate basic functionality of the api query module
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiQuery
+ */
+class ApiQueryBasicTest extends ApiQueryTestBase {
+ protected $exceptionFromAddDBData;
+
+ /**
+ * Create a set of pages. These must not change, otherwise the tests might give wrong results.
+ * @see MediaWikiTestCase::addDBData()
+ */
+ function addDBData() {
+ try {
+ if ( Title::newFromText( 'AQBT-All' )->exists() ) {
+ return;
+ }
+
+ // Ordering is important, as it will be returned in the same order as stored in the index
+ $this->editPage( 'AQBT-All', '[[Category:AQBT-Cat]] [[AQBT-Links]] {{AQBT-T}}' );
+ $this->editPage( 'AQBT-Categories', '[[Category:AQBT-Cat]]' );
+ $this->editPage( 'AQBT-Links', '[[AQBT-All]] [[AQBT-Categories]] [[AQBT-Templates]]' );
+ $this->editPage( 'AQBT-Templates', '{{AQBT-T}}' );
+ $this->editPage( 'AQBT-T', 'Content', '', NS_TEMPLATE );
+
+ // Refresh due to the bug with listing transclusions as links if they don't exist
+ $this->editPage( 'AQBT-All', '[[Category:AQBT-Cat]] [[AQBT-Links]] {{AQBT-T}}' );
+ $this->editPage( 'AQBT-Templates', '{{AQBT-T}}' );
+ } catch ( Exception $e ) {
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ private static $links = array(
+ array( 'prop' => 'links', 'titles' => 'AQBT-All' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All',
+ 'links' => array(
+ array( 'ns' => 0, 'title' => 'AQBT-Links' ),
+ )
+ )
+ ) )
+ );
+
+ private static $templates = array(
+ array( 'prop' => 'templates', 'titles' => 'AQBT-All' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All',
+ 'templates' => array(
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ),
+ )
+ )
+ ) )
+ );
+
+ private static $categories = array(
+ array( 'prop' => 'categories', 'titles' => 'AQBT-All' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All',
+ 'categories' => array(
+ array( 'ns' => 14, 'title' => 'Category:AQBT-Cat' ),
+ )
+ )
+ ) )
+ );
+
+ private static $allpages = array(
+ array( 'list' => 'allpages', 'apprefix' => 'AQBT-' ),
+ array( 'allpages' => array(
+ array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ),
+ array( 'pageid' => 2, 'ns' => 0, 'title' => 'AQBT-Categories' ),
+ array( 'pageid' => 3, 'ns' => 0, 'title' => 'AQBT-Links' ),
+ array( 'pageid' => 4, 'ns' => 0, 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $alllinks = array(
+ array( 'list' => 'alllinks', 'alprefix' => 'AQBT-' ),
+ array( 'alllinks' => array(
+ array( 'ns' => 0, 'title' => 'AQBT-All' ),
+ array( 'ns' => 0, 'title' => 'AQBT-Categories' ),
+ array( 'ns' => 0, 'title' => 'AQBT-Links' ),
+ array( 'ns' => 0, 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $alltransclusions = array(
+ array( 'list' => 'alltransclusions', 'atprefix' => 'AQBT-' ),
+ array( 'alltransclusions' => array(
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ),
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ),
+ ) )
+ );
+
+ // Although this appears to have no use it is used by testLists()
+ private static $allcategories = array(
+ array( 'list' => 'allcategories', 'acprefix' => 'AQBT-' ),
+ array( 'allcategories' => array(
+ array( '*' => 'AQBT-Cat' ),
+ ) )
+ );
+
+ private static $backlinks = array(
+ array( 'list' => 'backlinks', 'bltitle' => 'AQBT-Links' ),
+ array( 'backlinks' => array(
+ array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ),
+ ) )
+ );
+
+ private static $embeddedin = array(
+ array( 'list' => 'embeddedin', 'eititle' => 'Template:AQBT-T' ),
+ array( 'embeddedin' => array(
+ array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ),
+ array( 'pageid' => 4, 'ns' => 0, 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $categorymembers = array(
+ array( 'list' => 'categorymembers', 'cmtitle' => 'Category:AQBT-Cat' ),
+ array( 'categorymembers' => array(
+ array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ),
+ array( 'pageid' => 2, 'ns' => 0, 'title' => 'AQBT-Categories' ),
+ ) )
+ );
+
+ private static $generatorAllpages = array(
+ array( 'generator' => 'allpages', 'gapprefix' => 'AQBT-' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All' ),
+ '2' => array(
+ 'pageid' => 2,
+ 'ns' => 0,
+ 'title' => 'AQBT-Categories' ),
+ '3' => array(
+ 'pageid' => 3,
+ 'ns' => 0,
+ 'title' => 'AQBT-Links' ),
+ '4' => array(
+ 'pageid' => 4,
+ 'ns' => 0,
+ 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $generatorLinks = array(
+ array( 'generator' => 'links', 'titles' => 'AQBT-Links' ),
+ array( 'pages' => array(
+ '1' => array(
+ 'pageid' => 1,
+ 'ns' => 0,
+ 'title' => 'AQBT-All' ),
+ '2' => array(
+ 'pageid' => 2,
+ 'ns' => 0,
+ 'title' => 'AQBT-Categories' ),
+ '4' => array(
+ 'pageid' => 4,
+ 'ns' => 0,
+ 'title' => 'AQBT-Templates' ),
+ ) )
+ );
+
+ private static $generatorLinksPropLinks = array(
+ array( 'prop' => 'links' ),
+ array( 'pages' => array(
+ '1' => array( 'links' => array(
+ array( 'ns' => 0, 'title' => 'AQBT-Links' ),
+ ) )
+ ) )
+ );
+
+ private static $generatorLinksPropTemplates = array(
+ array( 'prop' => 'templates' ),
+ array( 'pages' => array(
+ '1' => array( 'templates' => array(
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ) ) ),
+ '4' => array( 'templates' => array(
+ array( 'ns' => 10, 'title' => 'Template:AQBT-T' ) ) ),
+ ) )
+ );
+
+ /**
+ * Test basic props
+ */
+ public function testProps() {
+ $this->check( self::$links );
+ $this->check( self::$templates );
+ $this->check( self::$categories );
+ }
+
+ /**
+ * Test basic lists
+ */
+ public function testLists() {
+ $this->check( self::$allpages );
+ $this->check( self::$alllinks );
+ $this->check( self::$alltransclusions );
+ // This test is temporarily disabled until a sqlite bug is fixed
+ // Confirmed still broken 15-nov-2013
+ // $this->check( self::$allcategories );
+ $this->check( self::$backlinks );
+ $this->check( self::$embeddedin );
+ $this->check( self::$categorymembers );
+ }
+
+ /**
+ * Test basic lists
+ */
+ public function testAllTogether() {
+
+ // All props together
+ $this->check( $this->merge(
+ self::$links,
+ self::$templates,
+ self::$categories
+ ) );
+
+ // All lists together
+ $this->check( $this->merge(
+ self::$allpages,
+ self::$alllinks,
+ self::$alltransclusions,
+ // This test is temporarily disabled until a sqlite bug is fixed
+ // self::$allcategories,
+ self::$backlinks,
+ self::$embeddedin,
+ self::$categorymembers
+ ) );
+
+ // All props+lists together
+ $this->check( $this->merge(
+ self::$links,
+ self::$templates,
+ self::$categories,
+ self::$allpages,
+ self::$alllinks,
+ self::$alltransclusions,
+ // This test is temporarily disabled until a sqlite bug is fixed
+ // self::$allcategories,
+ self::$backlinks,
+ self::$embeddedin,
+ self::$categorymembers
+ ) );
+ }
+
+ /**
+ * Test basic lists
+ */
+ public function testGenerator() {
+ // generator=allpages
+ $this->check( self::$generatorAllpages );
+ // generator=allpages & list=allpages
+ $this->check( $this->merge(
+ self::$generatorAllpages,
+ self::$allpages ) );
+ // generator=links
+ $this->check( self::$generatorLinks );
+ // generator=links & prop=links
+ $this->check( $this->merge(
+ self::$generatorLinks,
+ self::$generatorLinksPropLinks ) );
+ // generator=links & prop=templates
+ $this->check( $this->merge(
+ self::$generatorLinks,
+ self::$generatorLinksPropTemplates ) );
+ // generator=links & prop=links|templates
+ $this->check( $this->merge(
+ self::$generatorLinks,
+ self::$generatorLinksPropLinks,
+ self::$generatorLinksPropTemplates ) );
+ // generator=links & prop=links|templates & list=allpages|...
+ $this->check( $this->merge(
+ self::$generatorLinks,
+ self::$generatorLinksPropLinks,
+ self::$generatorLinksPropTemplates,
+ self::$allpages,
+ self::$alllinks,
+ self::$alltransclusions,
+ // This test is temporarily disabled until a sqlite bug is fixed
+ // self::$allcategories,
+ self::$backlinks,
+ self::$embeddedin,
+ self::$categorymembers ) );
+ }
+
+ /**
+ * Test bug 51821
+ */
+ public function testGeneratorRedirects() {
+ $this->editPage( 'AQBT-Target', 'test' );
+ $this->editPage( 'AQBT-Redir', '#REDIRECT [[AQBT-Target]]' );
+ $this->check( array(
+ array( 'generator' => 'backlinks', 'gbltitle' => 'AQBT-Target', 'redirects' => '1' ),
+ array(
+ 'redirects' => array(
+ array(
+ 'from' => 'AQBT-Redir',
+ 'to' => 'AQBT-Target',
+ )
+ ),
+ 'pages' => array(
+ '6' => array(
+ 'pageid' => 6,
+ 'ns' => 0,
+ 'title' => 'AQBT-Target',
+ )
+ ),
+ )
+ ) );
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php b/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php
new file mode 100644
index 00000000..347cd6f8
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * 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 3 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
+ */
+
+require_once 'ApiQueryContinueTestBase.php';
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiQuery
+ */
+class ApiQueryContinue2Test extends ApiQueryContinueTestBase {
+ protected $exceptionFromAddDBData;
+
+ /**
+ * Create a set of pages. These must not change, otherwise the tests might give wrong results.
+ * @see MediaWikiTestCase::addDBData()
+ */
+ function addDBData() {
+ try {
+ $this->editPage( 'AQCT73462-A', '**AQCT73462-A** [[AQCT73462-B]] [[AQCT73462-C]]' );
+ $this->editPage( 'AQCT73462-B', '[[AQCT73462-A]] **AQCT73462-B** [[AQCT73462-C]]' );
+ $this->editPage( 'AQCT73462-C', '[[AQCT73462-A]] [[AQCT73462-B]] **AQCT73462-C**' );
+ $this->editPage( 'AQCT73462-A', '**AQCT73462-A** [[AQCT73462-B]] [[AQCT73462-C]]' );
+ $this->editPage( 'AQCT73462-B', '[[AQCT73462-A]] **AQCT73462-B** [[AQCT73462-C]]' );
+ $this->editPage( 'AQCT73462-C', '[[AQCT73462-A]] [[AQCT73462-B]] **AQCT73462-C**' );
+ } catch ( Exception $e ) {
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ /**
+ * @medium
+ */
+ public function testA() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p, $gDir ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT73462-',
+ 'prop' => 'links',
+ 'gaplimit' => "$g",
+ 'pllimit' => "$p",
+ 'gapdir' => $gDir ? "ascending" : "descending",
+ );
+ };
+ // generator + 1 prop + 1 list
+ $data = $this->query( $mk( 99, 99, true ), 1, 'g1p', false );
+ $this->checkC( $data, $mk( 1, 1, true ), 6, 'g1p-11t' );
+ $this->checkC( $data, $mk( 2, 2, true ), 3, 'g1p-22t' );
+ $this->checkC( $data, $mk( 1, 1, false ), 6, 'g1p-11f' );
+ $this->checkC( $data, $mk( 2, 2, false ), 3, 'g1p-22f' );
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryContinueTest.php b/tests/phpunit/includes/api/query/ApiQueryContinueTest.php
new file mode 100644
index 00000000..03797901
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryContinueTest.php
@@ -0,0 +1,316 @@
+<?php
+/**
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * 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
+ */
+
+require_once 'ApiQueryContinueTestBase.php';
+
+/**
+ * These tests validate the new continue functionality of the api query module by
+ * doing multiple requests with varying parameters, merging the results, and checking
+ * that the result matches the full data received in one no-limits call.
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiQuery
+ */
+class ApiQueryContinueTest extends ApiQueryContinueTestBase {
+ protected $exceptionFromAddDBData;
+
+ /**
+ * Create a set of pages. These must not change, otherwise the tests might give wrong results.
+ * @see MediaWikiTestCase::addDBData()
+ */
+ function addDBData() {
+ try {
+ $this->editPage( 'Template:AQCT-T1', '**Template:AQCT-T1**' );
+ $this->editPage( 'Template:AQCT-T2', '**Template:AQCT-T2**' );
+ $this->editPage( 'Template:AQCT-T3', '**Template:AQCT-T3**' );
+ $this->editPage( 'Template:AQCT-T4', '**Template:AQCT-T4**' );
+ $this->editPage( 'Template:AQCT-T5', '**Template:AQCT-T5**' );
+
+ $this->editPage( 'AQCT-1', '**AQCT-1** {{AQCT-T2}} {{AQCT-T3}} {{AQCT-T4}} {{AQCT-T5}}' );
+ $this->editPage( 'AQCT-2', '[[AQCT-1]] **AQCT-2** {{AQCT-T3}} {{AQCT-T4}} {{AQCT-T5}}' );
+ $this->editPage( 'AQCT-3', '[[AQCT-1]] [[AQCT-2]] **AQCT-3** {{AQCT-T4}} {{AQCT-T5}}' );
+ $this->editPage( 'AQCT-4', '[[AQCT-1]] [[AQCT-2]] [[AQCT-3]] **AQCT-4** {{AQCT-T5}}' );
+ $this->editPage( 'AQCT-5', '[[AQCT-1]] [[AQCT-2]] [[AQCT-3]] [[AQCT-4]] **AQCT-5**' );
+ } catch ( Exception $e ) {
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ /**
+ * Test smart continue - list=allpages
+ * @medium
+ */
+ public function test1List() {
+ $this->mVerbose = false;
+ $mk = function ( $l ) {
+ return array(
+ 'list' => 'allpages',
+ 'apprefix' => 'AQCT-',
+ 'aplimit' => "$l",
+ );
+ };
+ $data = $this->query( $mk( 99 ), 1, '1L', false );
+
+ // 1 list
+ $this->checkC( $data, $mk( 1 ), 5, '1L-1' );
+ $this->checkC( $data, $mk( 2 ), 3, '1L-2' );
+ $this->checkC( $data, $mk( 3 ), 2, '1L-3' );
+ $this->checkC( $data, $mk( 4 ), 2, '1L-4' );
+ $this->checkC( $data, $mk( 5 ), 1, '1L-5' );
+ }
+
+ /**
+ * Test smart continue - list=allpages|alltransclusions
+ * @medium
+ */
+ public function test2Lists() {
+ $this->mVerbose = false;
+ $mk = function ( $l1, $l2 ) {
+ return array(
+ 'list' => 'allpages|alltransclusions',
+ 'apprefix' => 'AQCT-',
+ 'atprefix' => 'AQCT-',
+ 'atunique' => '',
+ 'aplimit' => "$l1",
+ 'atlimit' => "$l2",
+ );
+ };
+ // 2 lists
+ $data = $this->query( $mk( 99, 99 ), 1, '2L', false );
+ $this->checkC( $data, $mk( 1, 1 ), 5, '2L-11' );
+ $this->checkC( $data, $mk( 2, 2 ), 3, '2L-22' );
+ $this->checkC( $data, $mk( 3, 3 ), 2, '2L-33' );
+ $this->checkC( $data, $mk( 4, 4 ), 2, '2L-44' );
+ $this->checkC( $data, $mk( 5, 5 ), 1, '2L-55' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, prop=links
+ * @medium
+ */
+ public function testGen1Prop() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'prop' => 'links',
+ 'pllimit' => "$p",
+ );
+ };
+ // generator + 1 prop
+ $data = $this->query( $mk( 99, 99 ), 1, 'G1P', false );
+ $this->checkC( $data, $mk( 1, 1 ), 11, 'G1P-11' );
+ $this->checkC( $data, $mk( 2, 2 ), 6, 'G1P-22' );
+ $this->checkC( $data, $mk( 3, 3 ), 4, 'G1P-33' );
+ $this->checkC( $data, $mk( 4, 4 ), 3, 'G1P-44' );
+ $this->checkC( $data, $mk( 5, 5 ), 2, 'G1P-55' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, prop=links|templates
+ * @medium
+ */
+ public function testGen2Prop() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p1, $p2 ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'prop' => 'links|templates',
+ 'pllimit' => "$p1",
+ 'tllimit' => "$p2",
+ );
+ };
+ // generator + 2 props
+ $data = $this->query( $mk( 99, 99, 99 ), 1, 'G2P', false );
+ $this->checkC( $data, $mk( 1, 1, 1 ), 16, 'G2P-111' );
+ $this->checkC( $data, $mk( 2, 2, 2 ), 9, 'G2P-222' );
+ $this->checkC( $data, $mk( 3, 3, 3 ), 6, 'G2P-333' );
+ $this->checkC( $data, $mk( 4, 4, 4 ), 4, 'G2P-444' );
+ $this->checkC( $data, $mk( 5, 5, 5 ), 2, 'G2P-555' );
+ $this->checkC( $data, $mk( 5, 1, 1 ), 10, 'G2P-511' );
+ $this->checkC( $data, $mk( 4, 2, 2 ), 7, 'G2P-422' );
+ $this->checkC( $data, $mk( 2, 3, 3 ), 7, 'G2P-233' );
+ $this->checkC( $data, $mk( 2, 4, 4 ), 5, 'G2P-244' );
+ $this->checkC( $data, $mk( 1, 5, 5 ), 5, 'G2P-155' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, prop=links, list=alltransclusions
+ * @medium
+ */
+ public function testGen1Prop1List() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p, $l ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'prop' => 'links',
+ 'pllimit' => "$p",
+ 'list' => 'alltransclusions',
+ 'atprefix' => 'AQCT-',
+ 'atunique' => '',
+ 'atlimit' => "$l",
+ );
+ };
+ // generator + 1 prop + 1 list
+ $data = $this->query( $mk( 99, 99, 99 ), 1, 'G1P1L', false );
+ $this->checkC( $data, $mk( 1, 1, 1 ), 11, 'G1P1L-111' );
+ $this->checkC( $data, $mk( 2, 2, 2 ), 6, 'G1P1L-222' );
+ $this->checkC( $data, $mk( 3, 3, 3 ), 4, 'G1P1L-333' );
+ $this->checkC( $data, $mk( 4, 4, 4 ), 3, 'G1P1L-444' );
+ $this->checkC( $data, $mk( 5, 5, 5 ), 2, 'G1P1L-555' );
+ $this->checkC( $data, $mk( 5, 5, 1 ), 4, 'G1P1L-551' );
+ $this->checkC( $data, $mk( 5, 5, 2 ), 2, 'G1P1L-552' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, prop=links|templates,
+ * list=alllinks|alltransclusions, meta=siteinfo
+ * @medium
+ */
+ public function testGen2Prop2List1Meta() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $p1, $p2, $l1, $l2 ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'prop' => 'links|templates',
+ 'pllimit' => "$p1",
+ 'tllimit' => "$p2",
+ 'list' => 'alllinks|alltransclusions',
+ 'alprefix' => 'AQCT-',
+ 'alunique' => '',
+ 'allimit' => "$l1",
+ 'atprefix' => 'AQCT-',
+ 'atunique' => '',
+ 'atlimit' => "$l2",
+ 'meta' => 'siteinfo',
+ 'siprop' => 'namespaces',
+ );
+ };
+ // generator + 1 prop + 1 list
+ $data = $this->query( $mk( 99, 99, 99, 99, 99 ), 1, 'G2P2L1M', false );
+ $this->checkC( $data, $mk( 1, 1, 1, 1, 1 ), 16, 'G2P2L1M-11111' );
+ $this->checkC( $data, $mk( 2, 2, 2, 2, 2 ), 9, 'G2P2L1M-22222' );
+ $this->checkC( $data, $mk( 3, 3, 3, 3, 3 ), 6, 'G2P2L1M-33333' );
+ $this->checkC( $data, $mk( 4, 4, 4, 4, 4 ), 4, 'G2P2L1M-44444' );
+ $this->checkC( $data, $mk( 5, 5, 5, 5, 5 ), 2, 'G2P2L1M-55555' );
+ $this->checkC( $data, $mk( 5, 5, 5, 1, 1 ), 4, 'G2P2L1M-55511' );
+ $this->checkC( $data, $mk( 5, 5, 5, 2, 2 ), 2, 'G2P2L1M-55522' );
+ $this->checkC( $data, $mk( 5, 1, 1, 5, 5 ), 10, 'G2P2L1M-51155' );
+ $this->checkC( $data, $mk( 5, 2, 2, 5, 5 ), 5, 'G2P2L1M-52255' );
+ }
+
+ /**
+ * Test smart continue - generator=templates, prop=templates
+ * @medium
+ */
+ public function testSameGenAndProp() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $gDir, $p, $pDir ) {
+ return array(
+ 'titles' => 'AQCT-1',
+ 'generator' => 'templates',
+ 'gtllimit' => "$g",
+ 'gtldir' => $gDir ? 'ascending' : 'descending',
+ 'prop' => 'templates',
+ 'tllimit' => "$p",
+ 'tldir' => $pDir ? 'ascending' : 'descending',
+ );
+ };
+ // generator + 1 prop
+ $data = $this->query( $mk( 99, true, 99, true ), 1, 'G=P', false );
+
+ $this->checkC( $data, $mk( 1, true, 1, true ), 4, 'G=P-1t1t' );
+ $this->checkC( $data, $mk( 2, true, 2, true ), 2, 'G=P-2t2t' );
+ $this->checkC( $data, $mk( 3, true, 3, true ), 2, 'G=P-3t3t' );
+ $this->checkC( $data, $mk( 1, true, 3, true ), 4, 'G=P-1t3t' );
+ $this->checkC( $data, $mk( 3, true, 1, true ), 2, 'G=P-3t1t' );
+
+ $this->checkC( $data, $mk( 1, true, 1, false ), 4, 'G=P-1t1f' );
+ $this->checkC( $data, $mk( 2, true, 2, false ), 2, 'G=P-2t2f' );
+ $this->checkC( $data, $mk( 3, true, 3, false ), 2, 'G=P-3t3f' );
+ $this->checkC( $data, $mk( 1, true, 3, false ), 4, 'G=P-1t3f' );
+ $this->checkC( $data, $mk( 3, true, 1, false ), 2, 'G=P-3t1f' );
+
+ $this->checkC( $data, $mk( 1, false, 1, true ), 4, 'G=P-1f1t' );
+ $this->checkC( $data, $mk( 2, false, 2, true ), 2, 'G=P-2f2t' );
+ $this->checkC( $data, $mk( 3, false, 3, true ), 2, 'G=P-3f3t' );
+ $this->checkC( $data, $mk( 1, false, 3, true ), 4, 'G=P-1f3t' );
+ $this->checkC( $data, $mk( 3, false, 1, true ), 2, 'G=P-3f1t' );
+
+ $this->checkC( $data, $mk( 1, false, 1, false ), 4, 'G=P-1f1f' );
+ $this->checkC( $data, $mk( 2, false, 2, false ), 2, 'G=P-2f2f' );
+ $this->checkC( $data, $mk( 3, false, 3, false ), 2, 'G=P-3f3f' );
+ $this->checkC( $data, $mk( 1, false, 3, false ), 4, 'G=P-1f3f' );
+ $this->checkC( $data, $mk( 3, false, 1, false ), 2, 'G=P-3f1f' );
+ }
+
+ /**
+ * Test smart continue - generator=allpages, list=allpages
+ * @medium
+ */
+ public function testSameGenList() {
+ $this->mVerbose = false;
+ $mk = function ( $g, $gDir, $l, $pDir ) {
+ return array(
+ 'generator' => 'allpages',
+ 'gapprefix' => 'AQCT-',
+ 'gaplimit' => "$g",
+ 'gapdir' => $gDir ? 'ascending' : 'descending',
+ 'list' => 'allpages',
+ 'apprefix' => 'AQCT-',
+ 'aplimit' => "$l",
+ 'apdir' => $pDir ? 'ascending' : 'descending',
+ );
+ };
+ // generator + 1 list
+ $data = $this->query( $mk( 99, true, 99, true ), 1, 'G=L', false );
+
+ $this->checkC( $data, $mk( 1, true, 1, true ), 5, 'G=L-1t1t' );
+ $this->checkC( $data, $mk( 2, true, 2, true ), 3, 'G=L-2t2t' );
+ $this->checkC( $data, $mk( 3, true, 3, true ), 2, 'G=L-3t3t' );
+ $this->checkC( $data, $mk( 1, true, 3, true ), 5, 'G=L-1t3t' );
+ $this->checkC( $data, $mk( 3, true, 1, true ), 5, 'G=L-3t1t' );
+ $this->checkC( $data, $mk( 1, true, 1, false ), 5, 'G=L-1t1f' );
+ $this->checkC( $data, $mk( 2, true, 2, false ), 3, 'G=L-2t2f' );
+ $this->checkC( $data, $mk( 3, true, 3, false ), 2, 'G=L-3t3f' );
+ $this->checkC( $data, $mk( 1, true, 3, false ), 5, 'G=L-1t3f' );
+ $this->checkC( $data, $mk( 3, true, 1, false ), 5, 'G=L-3t1f' );
+ $this->checkC( $data, $mk( 1, false, 1, true ), 5, 'G=L-1f1t' );
+ $this->checkC( $data, $mk( 2, false, 2, true ), 3, 'G=L-2f2t' );
+ $this->checkC( $data, $mk( 3, false, 3, true ), 2, 'G=L-3f3t' );
+ $this->checkC( $data, $mk( 1, false, 3, true ), 5, 'G=L-1f3t' );
+ $this->checkC( $data, $mk( 3, false, 1, true ), 5, 'G=L-3f1t' );
+ $this->checkC( $data, $mk( 1, false, 1, false ), 5, 'G=L-1f1f' );
+ $this->checkC( $data, $mk( 2, false, 2, false ), 3, 'G=L-2f2f' );
+ $this->checkC( $data, $mk( 3, false, 3, false ), 2, 'G=L-3f3f' );
+ $this->checkC( $data, $mk( 1, false, 3, false ), 5, 'G=L-1f3f' );
+ $this->checkC( $data, $mk( 3, false, 1, false ), 5, 'G=L-3f1f' );
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php b/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php
new file mode 100644
index 00000000..bce62685
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php
@@ -0,0 +1,218 @@
+<?php
+/**
+ * Created on Jan 1, 2013
+ *
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * 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
+ */
+
+require_once 'ApiQueryTestBase.php';
+
+abstract class ApiQueryContinueTestBase extends ApiQueryTestBase {
+
+ /**
+ * Enable to print in-depth debugging info during the test run
+ */
+ protected $mVerbose = false;
+
+ /**
+ * Run query() and compare against expected values
+ * @param array $expected
+ * @param array $params Api parameters
+ * @param int $expectedCount Max number of iterations
+ * @param string $id Unit test id
+ * @param bool $continue True to use smart continue
+ * @return array Merged results data array
+ */
+ protected function checkC( $expected, $params, $expectedCount, $id, $continue = true ) {
+ $result = $this->query( $params, $expectedCount, $id, $continue );
+ $this->assertResult( $expected, $result, $id );
+ }
+
+ /**
+ * Run query in a loop until no more values are available
+ * @param array $params Api parameters
+ * @param int $expectedCount Max number of iterations
+ * @param string $id Unit test id
+ * @param bool $useContinue True to use smart continue
+ * @return array Merged results data array
+ * @throws Exception
+ */
+ protected function query( $params, $expectedCount, $id, $useContinue = true ) {
+ if ( isset( $params['action'] ) ) {
+ $this->assertEquals( 'query', $params['action'], 'Invalid query action' );
+ } else {
+ $params['action'] = 'query';
+ }
+ if ( $useContinue && !isset( $params['continue'] ) ) {
+ $params['continue'] = '';
+ }
+ $count = 0;
+ $result = array();
+ $continue = array();
+ do {
+ $request = array_merge( $params, $continue );
+ uksort( $request, function ( $a, $b ) {
+ // put 'continue' params at the end - lazy method
+ $a = strpos( $a, 'continue' ) !== false ? 'zzz ' . $a : $a;
+ $b = strpos( $b, 'continue' ) !== false ? 'zzz ' . $b : $b;
+
+ return strcmp( $a, $b );
+ } );
+ $reqStr = http_build_query( $request );
+ //$reqStr = str_replace( '&', ' & ', $reqStr );
+ $this->assertLessThan( $expectedCount, $count, "$id more data: $reqStr" );
+ if ( $this->mVerbose ) {
+ print "$id (#$count): $reqStr\n";
+ }
+ try {
+ $data = $this->doApiRequest( $request );
+ } catch ( Exception $e ) {
+ throw new Exception( "$id on $count", 0, $e );
+ }
+ $data = $data[0];
+ if ( isset( $data['warnings'] ) ) {
+ $warnings = json_encode( $data['warnings'] );
+ $this->fail( "$id Warnings on #$count in $reqStr\n$warnings" );
+ }
+ $this->assertArrayHasKey( 'query', $data, "$id no 'query' on #$count in $reqStr" );
+ if ( isset( $data['continue'] ) ) {
+ $continue = $data['continue'];
+ unset( $data['continue'] );
+ } else {
+ $continue = array();
+ }
+ if ( $this->mVerbose ) {
+ $this->printResult( $data );
+ }
+ $this->mergeResult( $result, $data );
+ $count++;
+ if ( empty( $continue ) ) {
+ $this->assertEquals( $expectedCount, $count, "$id finished early" );
+
+ return $result;
+ } elseif ( !$useContinue ) {
+ $this->assertFalse( 'Non-smart query must be requested all at once' );
+ }
+ } while ( true );
+ }
+
+ /**
+ * @param array $data
+ */
+ private function printResult( $data ) {
+ $q = $data['query'];
+ $print = array();
+ if ( isset( $q['pages'] ) ) {
+ foreach ( $q['pages'] as $p ) {
+ $m = $p['title'];
+ if ( isset( $p['links'] ) ) {
+ $m .= '/[' . implode( ',', array_map(
+ function ( $v ) {
+ return $v['title'];
+ },
+ $p['links'] ) ) . ']';
+ }
+ if ( isset( $p['categories'] ) ) {
+ $m .= '/(' . implode( ',', array_map(
+ function ( $v ) {
+ return str_replace( 'Category:', '', $v['title'] );
+ },
+ $p['categories'] ) ) . ')';
+ }
+ $print[] = $m;
+ }
+ }
+ if ( isset( $q['allcategories'] ) ) {
+ $print[] = '*Cats/(' . implode( ',', array_map(
+ function ( $v ) {
+ return $v['*'];
+ },
+ $q['allcategories'] ) ) . ')';
+ }
+ self::GetItems( $q, 'allpages', 'Pages', $print );
+ self::GetItems( $q, 'alllinks', 'Links', $print );
+ self::GetItems( $q, 'alltransclusions', 'Trnscl', $print );
+ print ' ' . implode( ' ', $print ) . "\n";
+ }
+
+ private static function GetItems( $q, $moduleName, $name, &$print ) {
+ if ( isset( $q[$moduleName] ) ) {
+ $print[] = "*$name/[" . implode( ',',
+ array_map(
+ function ( $v ) {
+ return $v['title'];
+ },
+ $q[$moduleName] ) ) . ']';
+ }
+ }
+
+ /**
+ * Recursively merge the new result returned from the query to the previous results.
+ * @param mixed $results
+ * @param mixed $newResult
+ * @param bool $numericIds If true, treat keys as ids to be merged instead of appending
+ */
+ protected function mergeResult( &$results, $newResult, $numericIds = false ) {
+ $this->assertEquals(
+ is_array( $results ),
+ is_array( $newResult ),
+ 'Type of result and data do not match'
+ );
+ if ( !is_array( $results ) ) {
+ $this->assertEquals( $results, $newResult, 'Repeated result must be the same as before' );
+ } else {
+ $sort = null;
+ foreach ( $newResult as $key => $value ) {
+ if ( !$numericIds && $sort === null ) {
+ if ( !is_array( $value ) ) {
+ $sort = false;
+ } elseif ( array_key_exists( 'title', $value ) ) {
+ $sort = function ( $a, $b ) {
+ return strcmp( $a['title'], $b['title'] );
+ };
+ } else {
+ $sort = false;
+ }
+ }
+ $keyExists = array_key_exists( $key, $results );
+ if ( is_numeric( $key ) ) {
+ if ( $numericIds ) {
+ if ( !$keyExists ) {
+ $results[$key] = $value;
+ } else {
+ $this->mergeResult( $results[$key], $value );
+ }
+ } else {
+ $results[] = $value;
+ }
+ } elseif ( !$keyExists ) {
+ $results[$key] = $value;
+ } else {
+ $this->mergeResult( $results[$key], $value, $key === 'pages' );
+ }
+ }
+ if ( $numericIds ) {
+ ksort( $results, SORT_NUMERIC );
+ } elseif ( $sort !== null && $sort !== false ) {
+ usort( $results, $sort );
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php b/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php
new file mode 100644
index 00000000..74ceff90
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiQueryRevisions
+ */
+class ApiQueryRevisionsTest extends ApiTestCase {
+
+ /**
+ * @group medium
+ */
+ public function testContentComesWithContentModelAndFormat() {
+ $pageName = 'Help:' . __METHOD__;
+ $title = Title::newFromText( $pageName );
+ $page = WikiPage::factory( $title );
+ $page->doEdit( 'Some text', 'inserting content' );
+
+ $apiResult = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'prop' => 'revisions',
+ 'titles' => $pageName,
+ 'rvprop' => 'content',
+ ) );
+ $this->assertArrayHasKey( 'query', $apiResult[0] );
+ $this->assertArrayHasKey( 'pages', $apiResult[0]['query'] );
+ foreach ( $apiResult[0]['query']['pages'] as $page ) {
+ $this->assertArrayHasKey( 'revisions', $page );
+ foreach ( $page['revisions'] as $revision ) {
+ $this->assertArrayHasKey( 'contentformat', $revision,
+ 'contentformat should be included when asking content so client knows how to interpret it'
+ );
+ $this->assertArrayHasKey( 'contentmodel', $revision,
+ 'contentmodel should be included when asking content so client knows how to interpret it'
+ );
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryTest.php b/tests/phpunit/includes/api/query/ApiQueryTest.php
new file mode 100644
index 00000000..bba22c77
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryTest.php
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiQuery
+ */
+class ApiQueryTest extends ApiTestCase {
+ /**
+ * @var array Storage for $wgHooks
+ */
+ protected $hooks;
+
+ protected function setUp() {
+ global $wgHooks;
+
+ parent::setUp();
+ $this->doLogin();
+
+ // Setup en: as interwiki prefix
+ $this->hooks = $wgHooks;
+ $wgHooks['InterwikiLoadPrefix'][] = function ( $prefix, &$data ) {
+ if ( $prefix == 'apiquerytestiw' ) {
+ $data = array( 'iw_url' => 'wikipedia' );
+ }
+ return false;
+ };
+ }
+
+ protected function tearDown() {
+ global $wgHooks;
+ $wgHooks = $this->hooks;
+
+ parent::tearDown();
+ }
+
+ public function testTitlesGetNormalized() {
+ global $wgMetaNamespace;
+
+ $this->setMwGlobals( array(
+ 'wgCapitalLinks' => true,
+ ) );
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'titles' => 'Project:articleA|article_B' ) );
+
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'normalized', $data[0]['query'] );
+
+ // Forge a normalized title
+ $to = Title::newFromText( $wgMetaNamespace . ':ArticleA' );
+
+ $this->assertEquals(
+ array(
+ 'from' => 'Project:articleA',
+ 'to' => $to->getPrefixedText(),
+ ),
+ $data[0]['query']['normalized'][0]
+ );
+
+ $this->assertEquals(
+ array(
+ 'from' => 'article_B',
+ 'to' => 'Article B'
+ ),
+ $data[0]['query']['normalized'][1]
+ );
+ }
+
+ public 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] );
+ }
+
+ /**
+ * Test the ApiBase::titlePartToKey function
+ *
+ * @param string $titlePart
+ * @param int $namespace
+ * @param string $expected
+ * @param string $expectException
+ * @dataProvider provideTestTitlePartToKey
+ */
+ function testTitlePartToKey( $titlePart, $namespace, $expected, $expectException ) {
+ $this->setMwGlobals( array(
+ 'wgCapitalLinks' => true,
+ ) );
+
+ $api = new MockApiQueryBase();
+ $exceptionCaught = false;
+ try {
+ $this->assertEquals( $expected, $api->titlePartToKey( $titlePart, $namespace ) );
+ } catch ( UsageException $e ) {
+ $exceptionCaught = true;
+ }
+ $this->assertEquals( $expectException, $exceptionCaught,
+ 'UsageException thrown by titlePartToKey' );
+ }
+
+ function provideTestTitlePartToKey() {
+ return array(
+ array( 'a b c', NS_MAIN, 'A_b_c', false ),
+ array( 'x', NS_MAIN, 'X', false ),
+ array( 'y ', NS_MAIN, 'Y_', false ),
+ array( 'template:foo', NS_CATEGORY, 'Template:foo', false ),
+ array( 'apiquerytestiw:foo', NS_CATEGORY, 'Apiquerytestiw:foo', false ),
+ array( "\xF7", NS_MAIN, null, true ),
+ array( 'template:foo', NS_MAIN, null, true ),
+ array( 'apiquerytestiw:foo', NS_MAIN, null, true ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/api/query/ApiQueryTestBase.php b/tests/phpunit/includes/api/query/ApiQueryTestBase.php
new file mode 100644
index 00000000..56c15b23
--- /dev/null
+++ b/tests/phpunit/includes/api/query/ApiQueryTestBase.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * Created on Feb 10, 2013
+ *
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * 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 3 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
+ */
+
+/** This class has some common functionality for testing query module
+ */
+abstract class ApiQueryTestBase extends ApiTestCase {
+
+ const PARAM_ASSERT = <<<STR
+Each parameter must be an array of two elements,
+first - an array of params to the API call,
+and the second array - expected results as returned by the API
+STR;
+
+ /**
+ * Merges all requests parameter + expected values into one
+ * @param array $v,... List of arrays, each of which contains exactly two
+ * @return array
+ */
+ protected function merge( /*...*/ ) {
+ $request = array();
+ $expected = array();
+ foreach ( func_get_args() as $v ) {
+ list( $req, $exp ) = $this->validateRequestExpectedPair( $v );
+ $request = array_merge_recursive( $request, $req );
+ $this->mergeExpected( $expected, $exp );
+ }
+
+ return array( $request, $expected );
+ }
+
+ /**
+ * Check that the parameter is a valid two element array,
+ * with the first element being API request and the second - expected result
+ * @param array $v
+ * @return array
+ */
+ private function validateRequestExpectedPair( $v ) {
+ $this->assertType( 'array', $v, self::PARAM_ASSERT );
+ $this->assertEquals( 2, count( $v ), self::PARAM_ASSERT );
+ $this->assertArrayHasKey( 0, $v, self::PARAM_ASSERT );
+ $this->assertArrayHasKey( 1, $v, self::PARAM_ASSERT );
+ $this->assertType( 'array', $v[0], self::PARAM_ASSERT );
+ $this->assertType( 'array', $v[1], self::PARAM_ASSERT );
+
+ return $v;
+ }
+
+ /**
+ * Recursively merges the expected values in the $item into the $all
+ * @param array &$all
+ * @param array $item
+ */
+ private function mergeExpected( &$all, $item ) {
+ foreach ( $item as $k => $v ) {
+ if ( array_key_exists( $k, $all ) ) {
+ if ( is_array( $all[$k] ) ) {
+ $this->mergeExpected( $all[$k], $v );
+ } else {
+ $this->assertEquals( $all[$k], $v );
+ }
+ } else {
+ $all[$k] = $v;
+ }
+ }
+ }
+
+ /**
+ * Checks that the request's result matches the expected results.
+ * @param array $values Array is a two element array( request, expected_results )
+ * @throws Exception
+ */
+ protected function check( $values ) {
+ list( $req, $exp ) = $this->validateRequestExpectedPair( $values );
+ if ( !array_key_exists( 'action', $req ) ) {
+ $req['action'] = 'query';
+ }
+ foreach ( $req as &$val ) {
+ if ( is_array( $val ) ) {
+ $val = implode( '|', array_unique( $val ) );
+ }
+ }
+ $result = $this->doApiRequest( $req );
+ $this->assertResult( array( 'query' => $exp ), $result[0], $req );
+ }
+
+ protected function assertResult( $exp, $result, $message = '' ) {
+ try {
+ $exp = self::sanitizeResultArray( $exp );
+ $result = self::sanitizeResultArray( $result );
+ $this->assertEquals( $exp, $result );
+ } catch ( PHPUnit_Framework_ExpectationFailedException $e ) {
+ if ( is_array( $message ) ) {
+ $message = http_build_query( $message );
+ }
+ throw new PHPUnit_Framework_ExpectationFailedException(
+ $e->getMessage() . "\nRequest: $message",
+ new PHPUnit_Framework_ComparisonFailure(
+ $exp,
+ $result,
+ print_r( $exp, true ),
+ print_r( $result, true ),
+ false,
+ $e->getComparisonFailure()->getMessage() . "\nRequest: $message"
+ )
+ );
+ }
+ }
+
+ /**
+ * Recursively ksorts a result array and removes any 'pageid' keys.
+ * @param array $result
+ * @return array
+ */
+ private static function sanitizeResultArray( $result ) {
+ unset( $result['pageid'] );
+ foreach ( $result as $key => $value ) {
+ if ( is_array( $value ) ) {
+ $result[$key] = self::sanitizeResultArray( $value );
+ }
+ }
+
+ // Sort the result by keys, then take advantage of how array_merge will
+ // renumber numeric keys while leaving others alone.
+ ksort( $result );
+ return array_merge( $result );
+ }
+}
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/cache/GenderCacheTest.php b/tests/phpunit/includes/cache/GenderCacheTest.php
new file mode 100644
index 00000000..ce2db5d7
--- /dev/null
+++ b/tests/phpunit/includes/cache/GenderCacheTest.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @group Database
+ * @group Cache
+ */
+class GenderCacheTest extends MediaWikiLangTestCase {
+
+ protected function setUp() {
+ global $wgDefaultUserOptions;
+ parent::setUp();
+ //ensure the correct default gender
+ $wgDefaultUserOptions['gender'] = 'unknown';
+ }
+
+ function addDBData() {
+ $user = User::newFromName( 'UTMale' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTMalePassword' );
+ }
+ //ensure the right gender
+ $user->setOption( 'gender', 'male' );
+ $user->saveSettings();
+
+ $user = User::newFromName( 'UTFemale' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTFemalePassword' );
+ }
+ //ensure the right gender
+ $user->setOption( 'gender', 'female' );
+ $user->saveSettings();
+
+ $user = User::newFromName( 'UTDefaultGender' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ $user->setPassword( 'UTDefaultGenderPassword' );
+ }
+ //ensure the default gender
+ $user->setOption( 'gender', null );
+ $user->saveSettings();
+ }
+
+ /**
+ * test usernames
+ *
+ * @dataProvider provideUserGenders
+ * @covers GenderCache::getGenderOf
+ */
+ public function testUserName( $username, $expectedGender ) {
+ $genderCache = GenderCache::singleton();
+ $gender = $genderCache->getGenderOf( $username );
+ $this->assertEquals( $gender, $expectedGender, "GenderCache normal" );
+ }
+
+ /**
+ * genderCache should work with user objects, too
+ *
+ * @dataProvider provideUserGenders
+ * @covers GenderCache::getGenderOf
+ */
+ public function testUserObjects( $username, $expectedGender ) {
+ $genderCache = GenderCache::singleton();
+ $user = User::newFromName( $username );
+ $gender = $genderCache->getGenderOf( $user );
+ $this->assertEquals( $gender, $expectedGender, "GenderCache normal" );
+ }
+
+ public static function provideUserGenders() {
+ return array(
+ array( 'UTMale', 'male' ),
+ array( 'UTFemale', 'female' ),
+ array( 'UTDefaultGender', 'unknown' ),
+ array( 'UTNotExist', 'unknown' ),
+ //some not valid user
+ array( '127.0.0.1', 'unknown' ),
+ array( 'user@test', 'unknown' ),
+ );
+ }
+
+ /**
+ * test strip of subpages to avoid unnecessary queries
+ * against the never existing username
+ *
+ * @dataProvider provideStripSubpages
+ * @covers GenderCache::getGenderOf
+ */
+ public function testStripSubpages( $pageWithSubpage, $expectedGender ) {
+ $genderCache = GenderCache::singleton();
+ $gender = $genderCache->getGenderOf( $pageWithSubpage );
+ $this->assertEquals( $gender, $expectedGender, "GenderCache must strip of subpages" );
+ }
+
+ public static function provideStripSubpages() {
+ return array(
+ array( 'UTMale/subpage', 'male' ),
+ array( 'UTFemale/subpage', 'female' ),
+ array( 'UTDefaultGender/subpage', 'unknown' ),
+ array( 'UTNotExist/subpage', 'unknown' ),
+ array( '127.0.0.1/subpage', 'unknown' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/cache/LocalisationCacheTest.php b/tests/phpunit/includes/cache/LocalisationCacheTest.php
new file mode 100644
index 00000000..fc06a501
--- /dev/null
+++ b/tests/phpunit/includes/cache/LocalisationCacheTest.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * @group Database
+ * @group Cache
+ * @covers LocalisationCache
+ * @author Niklas Laxström
+ */
+class LocalisationCacheTest extends MediaWikiTestCase {
+ protected function setUp() {
+ global $IP;
+
+ parent::setUp();
+ $this->setMwGlobals( array(
+ 'wgMessagesDirs' => array( "$IP/tests/phpunit/data/localisationcache" ),
+ 'wgExtensionMessagesFiles' => array(),
+ 'wgHooks' => array(),
+ ) );
+ }
+
+ public function testPuralRulesFallback() {
+ $cache = new LocalisationCache( array( 'store' => 'detect' ) );
+
+ $this->assertEquals(
+ $cache->getItem( 'ar', 'pluralRules' ),
+ $cache->getItem( 'arz', 'pluralRules' ),
+ 'arz plural rules (undefined) fallback to ar (defined)'
+ );
+
+ $this->assertEquals(
+ $cache->getItem( 'ar', 'compiledPluralRules' ),
+ $cache->getItem( 'arz', 'compiledPluralRules' ),
+ 'arz compiled plural rules (undefined) fallback to ar (defined)'
+ );
+
+ $this->assertNotEquals(
+ $cache->getItem( 'ksh', 'pluralRules' ),
+ $cache->getItem( 'de', 'pluralRules' ),
+ 'ksh plural rules (defined) dont fallback to de (defined)'
+ );
+
+ $this->assertNotEquals(
+ $cache->getItem( 'ksh', 'compiledPluralRules' ),
+ $cache->getItem( 'de', 'compiledPluralRules' ),
+ 'ksh compiled plural rules (defined) dont fallback to de (defined)'
+ );
+ }
+
+ public function testRecacheFallbacks() {
+ $lc = new LocalisationCache( array( 'store' => 'detect' ) );
+ $lc->recache( 'uk' );
+ $this->assertEquals(
+ array(
+ 'present-uk' => 'uk',
+ 'present-ru' => 'ru',
+ 'present-en' => 'en',
+ ),
+ $lc->getItem( 'uk', 'messages' ),
+ 'Fallbacks are only used to fill missing data'
+ );
+ }
+
+ public function testRecacheFallbacksWithHooks() {
+ global $wgHooks;
+
+ // Use hook to provide updates for messages. This is what the
+ // LocalisationUpdate extension does. See bug 68781.
+ $wgHooks['LocalisationCacheRecacheFallback'][] = function (
+ LocalisationCache $lc,
+ $code,
+ array &$cache
+ ) {
+ if ( $code === 'ru' ) {
+ $cache['messages']['present-uk'] = 'ru-override';
+ $cache['messages']['present-ru'] = 'ru-override';
+ $cache['messages']['present-en'] = 'ru-override';
+ }
+ };
+
+ $lc = new LocalisationCache( array( 'store' => 'detect' ) );
+ $lc->recache( 'uk' );
+ $this->assertEquals(
+ array(
+ 'present-uk' => 'uk',
+ 'present-ru' => 'ru-override',
+ 'present-en' => 'ru-override',
+ ),
+ $lc->getItem( 'uk', 'messages' ),
+ 'Updates provided by hooks follow the normal fallback order.'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/cache/MessageCacheTest.php b/tests/phpunit/includes/cache/MessageCacheTest.php
new file mode 100644
index 00000000..442e9f9f
--- /dev/null
+++ b/tests/phpunit/includes/cache/MessageCacheTest.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @group Database
+ * @group Cache
+ * @covers MessageCache
+ */
+class MessageCacheTest extends MediaWikiLangTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->configureLanguages();
+ MessageCache::singleton()->enable();
+ }
+
+ /**
+ * Helper function -- setup site language for testing
+ */
+ protected function configureLanguages() {
+ // for the test, we need the content language to be anything but English,
+ // let's choose e.g. German (de)
+ $langCode = 'de';
+ $langObj = Language::factory( $langCode );
+
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => $langCode,
+ 'wgLang' => $langObj,
+ 'wgContLang' => $langObj,
+ ) );
+ }
+
+ function addDBData() {
+ $this->configureLanguages();
+
+ // Set up messages and fallbacks ab -> ru -> de
+ $this->makePage( 'FallbackLanguageTest-Full', 'ab' );
+ $this->makePage( 'FallbackLanguageTest-Full', 'ru' );
+ $this->makePage( 'FallbackLanguageTest-Full', 'de' );
+
+ // Fallbacks where ab does not exist
+ $this->makePage( 'FallbackLanguageTest-Partial', 'ru' );
+ $this->makePage( 'FallbackLanguageTest-Partial', 'de' );
+
+ // Fallback to the content language
+ $this->makePage( 'FallbackLanguageTest-ContLang', 'de' );
+
+ // Add customizations for an existing message.
+ $this->makePage( 'sunday', 'ru' );
+
+ // Full key tests -- always want russian
+ $this->makePage( 'MessageCacheTest-FullKeyTest', 'ab' );
+ $this->makePage( 'MessageCacheTest-FullKeyTest', 'ru' );
+
+ // In content language -- get base if no derivative
+ $this->makePage( 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none', false );
+ }
+
+ /**
+ * Helper function for addDBData -- adds a simple page to the database
+ *
+ * @param string $title Title of page to be created
+ * @param string $lang Language and content of the created page
+ * @param string|null $content Content of the created page, or null for a generic string
+ * @param bool $createSubPage Set to false if a root page should be created
+ */
+ protected function makePage( $title, $lang, $content = null, $createSubPage = true ) {
+ global $wgContLang;
+
+ if ( $content === null ) {
+ $content = $lang;
+ }
+ if ( $lang !== $wgContLang->getCode() || $createSubPage ) {
+ $title = "$title/$lang";
+ }
+
+ $title = Title::newFromText( $title, NS_MEDIAWIKI );
+ $wikiPage = new WikiPage( $title );
+ $contentHandler = ContentHandler::makeContent( $content, $title );
+ $wikiPage->doEditContent( $contentHandler, "$lang translation test case" );
+ }
+
+ /**
+ * Test message fallbacks, bug #1495
+ *
+ * @dataProvider provideMessagesForFallback
+ */
+ public function testMessageFallbacks( $message, $lang, $expectedContent ) {
+ $result = MessageCache::singleton()->get( $message, true, $lang );
+ $this->assertEquals( $expectedContent, $result, "Message fallback failed." );
+ }
+
+ function provideMessagesForFallback() {
+ return array(
+ array( 'FallbackLanguageTest-Full', 'ab', 'ab' ),
+ array( 'FallbackLanguageTest-Partial', 'ab', 'ru' ),
+ array( 'FallbackLanguageTest-ContLang', 'ab', 'de' ),
+ array( 'FallbackLanguageTest-None', 'ab', false ),
+
+ // Existing message with customizations on the fallbacks
+ array( 'sunday', 'ab', 'амҽыш' ),
+
+ // bug 46579
+ array( 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none' ),
+ // UI language different from content language should only use de/none as last option
+ array( 'FallbackLanguageTest-NoDervContLang', 'fit', 'de/none' ),
+ );
+ }
+
+ /**
+ * There's a fallback case where the message key is given as fully qualified -- this
+ * should ignore the passed $lang and use the language from the key
+ *
+ * @dataProvider provideMessagesForFullKeys
+ */
+ public function testFullKeyBehaviour( $message, $lang, $expectedContent ) {
+ $result = MessageCache::singleton()->get( $message, true, $lang, true );
+ $this->assertEquals( $expectedContent, $result, "Full key message fallback failed." );
+ }
+
+ function provideMessagesForFullKeys() {
+ return array(
+ array( 'MessageCacheTest-FullKeyTest/ru', 'ru', 'ru' ),
+ array( 'MessageCacheTest-FullKeyTest/ru', 'ab', 'ru' ),
+ array( 'MessageCacheTest-FullKeyTest/ru/foo', 'ru', false ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/cache/RedisBloomCacheTest.php b/tests/phpunit/includes/cache/RedisBloomCacheTest.php
new file mode 100644
index 00000000..3d491e90
--- /dev/null
+++ b/tests/phpunit/includes/cache/RedisBloomCacheTest.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * Test for BloomCacheRedis class.
+ *
+ * @TODO: some generic base "redis test server conf" for all testing?
+ *
+ * @covers BloomCacheRedis
+ * @group Cache
+ */
+class BloomCacheRedisTest extends MediaWikiTestCase {
+ private static $suffix;
+
+ protected function setUp() {
+ parent::setUp();
+
+ self::$suffix = self::$suffix ? : mt_rand();
+
+ $fcache = BloomCache::get( 'main' );
+ if ( $fcache instanceof BloomCacheRedis ) {
+ $fcache->delete( "unit-testing-" . self::$suffix );
+ } else {
+ $this->markTestSkipped( 'The main bloom cache is not redis.' );
+ }
+ }
+
+ public function testBloomCache() {
+ $key = "unit-testing-" . self::$suffix;
+ $fcache = BloomCache::get( 'main' );
+ $count = 1500;
+
+ $this->assertTrue( $fcache->delete( $key ), "OK delete of filter '$key'." );
+ $this->assertTrue( $fcache->init( $key, $count, .001 ), "OK init of filter '$key'." );
+
+ $members = array();
+ for ( $i = 0; $i < $count; ++$i ) {
+ $members[] = "$i-value-$i";
+ }
+ $this->assertTrue( $fcache->add( $key, $members ), "Addition of members to '$key' OK." );
+
+ for ( $i = 0; $i < $count; ++$i ) {
+ $this->assertTrue( $fcache->isHit( $key, "$i-value-$i" ), "Hit on member '$i-value-$i'." );
+ }
+
+ $falsePositives = array();
+ for ( $i = $count; $i < 2 * $count; ++$i ) {
+ if ( $fcache->isHit( $key, "value$i" ) ) {
+ $falsePositives[] = "value$i";
+ }
+ }
+
+ $eFalsePositives = array(
+ 'value1763',
+ 'value2245',
+ 'value2353',
+ 'value2791',
+ 'value2898',
+ 'value2975'
+ );
+ $this->assertEquals( $eFalsePositives, $falsePositives, "Correct number of false positives found." );
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+
+ $fcache = BloomCache::get( 'main' );
+ if ( $fcache instanceof BloomCacheRedis ) {
+ $fcache->delete( "unit-testing-" . self::$suffix );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/changes/EnhancedChangesListTest.php b/tests/phpunit/includes/changes/EnhancedChangesListTest.php
new file mode 100644
index 00000000..40a11d2d
--- /dev/null
+++ b/tests/phpunit/includes/changes/EnhancedChangesListTest.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * @covers EnhancedChangesList
+ *
+ * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class EnhancedChangesListTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var TestRecentChangesHelper
+ */
+ private $testRecentChangesHelper;
+
+ public function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->testRecentChangesHelper = new TestRecentChangesHelper();
+ }
+
+ public function testBeginRecentChangesList_styleModules() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $enhancedChangesList->beginRecentChangesList();
+
+ $styleModules = $enhancedChangesList->getOutput()->getModuleStyles();
+
+ $this->assertContains(
+ 'mediawiki.special.changeslist',
+ $styleModules,
+ 'has mediawiki.special.changeslist'
+ );
+
+ $this->assertContains(
+ 'mediawiki.special.changeslist.enhanced',
+ $styleModules,
+ 'has mediawiki.special.changeslist.enhanced'
+ );
+ }
+
+ public function testBeginRecentChangesList_jsModules() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $enhancedChangesList->beginRecentChangesList();
+
+ $modules = $enhancedChangesList->getOutput()->getModules();
+
+ $this->assertContains( 'jquery.makeCollapsible', $modules, 'has jquery.makeCollapsible' );
+ $this->assertContains( 'mediawiki.icon', $modules, 'has mediawiki.icon' );
+ }
+
+ public function testBeginRecentChangesList_html() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $html = $enhancedChangesList->beginRecentChangesList();
+
+ $this->assertEquals( '<div class="mw-changeslist">', $html );
+ }
+
+ /**
+ * @todo more tests
+ */
+ public function testRecentChangesLine() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $enhancedChangesList->beginRecentChangesList();
+
+ $recentChange = $this->getEditChange( '20131103092153' );
+ $html = $enhancedChangesList->recentChangesLine( $recentChange, false );
+
+ $this->assertInternalType( 'string', $html );
+
+ $recentChange2 = $this->getEditChange( '20131103092253' );
+ $html = $enhancedChangesList->recentChangesLine( $recentChange2, false );
+
+ $this->assertEquals( '', $html );
+ }
+
+ /**
+ * @todo more tests for actual formatting, this is more of a smoke test
+ */
+ public function testEndRecentChangesList() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $enhancedChangesList->beginRecentChangesList();
+
+ $recentChange = $this->getEditChange( '20131103092153' );
+ $enhancedChangesList->recentChangesLine( $recentChange, false );
+
+ $recentChange2 = $this->getEditChange( '20131103092253' );
+ $enhancedChangesList->recentChangesLine( $recentChange2, false );
+
+ $html = $enhancedChangesList->endRecentChangesList();
+
+ preg_match_all( '/td class="mw-enhanced-rc-nested"/', $html, $matches );
+ $this->assertCount( 2, $matches[0] );
+ }
+
+ /**
+ * @return EnhancedChangesList
+ */
+ private function newEnhancedChangesList() {
+ $user = User::newFromId( 0 );
+ $context = $this->testRecentChangesHelper->getTestContext( $user );
+
+ return new EnhancedChangesList( $context );
+ }
+
+ /**
+ * @return RecentChange
+ */
+ private function getEditChange( $timestamp ) {
+ $user = $this->getTestUser();
+ $recentChange = $this->testRecentChangesHelper->makeEditRecentChange(
+ $user, 'Cat', $timestamp, 5, 191, 190, 0, 0
+ );
+
+ return $recentChange;
+ }
+
+ /**
+ * @return User
+ */
+ private function getTestUser() {
+ $user = User::newFromName( 'TestRecentChangesUser' );
+
+ if ( !$user->getId() ) {
+ $user->addToDatabase();
+ }
+
+ return $user;
+ }
+
+}
diff --git a/tests/phpunit/includes/changes/OldChangesListTest.php b/tests/phpunit/includes/changes/OldChangesListTest.php
new file mode 100644
index 00000000..2ea9f33e
--- /dev/null
+++ b/tests/phpunit/includes/changes/OldChangesListTest.php
@@ -0,0 +1,187 @@
+<?php
+
+/**
+ * @covers OldChangesList
+ *
+ * @todo add tests to cover article link, timestamp, character difference,
+ * log entry, user tool links, direction marks, tags, rollback,
+ * watching users, and date header.
+ *
+ * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class OldChangesListTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var TestRecentChangesHelper
+ */
+ private $testRecentChangesHelper;
+
+ public function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->testRecentChangesHelper = new TestRecentChangesHelper();
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgArticlePath' => '/wiki/$1',
+ 'wgLang' => Language::factory( 'qqx' )
+ ) );
+ }
+
+ /**
+ * @dataProvider recentChangesLine_CssForLineNumberProvider
+ */
+ public function testRecentChangesLine_CssForLineNumber( $expected, $linenumber, $message ) {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, $linenumber );
+
+ $this->assertRegExp( $expected, $line, $message );
+ }
+
+ public function recentChangesLine_CssForLineNumberProvider() {
+ return array(
+ array( '/mw-line-odd/', 1, 'odd line number' ),
+ array( '/mw-line-even/', 2, 'even line number' )
+ );
+ }
+
+ public function testRecentChangesLine_NotWatchedCssClass() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertRegExp( '/mw-changeslist-line-not-watched/', $line );
+ }
+
+ public function testRecentChangesLine_WatchedCssClass() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, true, 1 );
+
+ $this->assertRegExp( '/mw-changeslist-line-watched/', $line );
+ }
+
+ public function testRecentChangesLine_LogTitle() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getLogChange( 'delete', 'delete' );
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertRegExp( '/href="\/wiki\/Special:Log\/delete/', $line, 'link has href attribute' );
+ $this->assertRegExp( '/title="Special:Log\/delete/', $line, 'link has title attribute' );
+ $this->assertRegExp( "/dellogpage/", $line, 'link text' );
+ }
+
+ public function testRecentChangesLine_DiffHistLinks() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertRegExp(
+ '/title=Cat&amp;curid=20131103212153&amp;diff=5&amp;oldid=191/',
+ $line,
+ 'assert diff link'
+ );
+
+ $this->assertRegExp( '/tabindex="0"/', $line, 'assert tab index' );
+ $this->assertRegExp(
+ '/title=Cat&amp;curid=20131103212153&amp;action=history"/',
+ $line,
+ 'assert history link'
+ );
+ }
+
+ public function testRecentChangesLine_Flags() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getNewBotEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertContains(
+ "<abbr class='newpage' title='(recentchanges-label-newpage)'>(newpageletter)</abbr>",
+ $line,
+ 'new page flag'
+ );
+
+ $this->assertContains(
+ "<abbr class='botedit' title='(recentchanges-label-bot)'>(boteditletter)</abbr>",
+ $line,
+ 'bot flag'
+ );
+ }
+
+ public function testRecentChangesLine_Tags() {
+ $recentChange = $this->getEditChange();
+ $recentChange->mAttribs['ts_tags'] = 'vandalism,newbie';
+
+ $oldChangesList = $this->getOldChangesList();
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertRegExp( '/<li class="[\w\s-]*mw-tag-vandalism[\w\s-]*">/', $line );
+ $this->assertRegExp( '/<li class="[\w\s-]*mw-tag-newbie[\w\s-]*">/', $line );
+ }
+
+ private function getNewBotEditChange() {
+ $user = $this->getTestUser();
+
+ $recentChange = $this->testRecentChangesHelper->makeNewBotEditRecentChange(
+ $user, 'Abc', '20131103212153', 5, 191, 190, 0, 0
+ );
+
+ return $recentChange;
+ }
+
+ private function getLogChange( $logType, $logAction ) {
+ $user = $this->getTestUser();
+
+ $recentChange = $this->testRecentChangesHelper->makeLogRecentChange(
+ $logType, $logAction, $user, 'Abc', '20131103212153', 0, 0
+ );
+
+ return $recentChange;
+ }
+
+ private function getEditChange() {
+ $user = $this->getTestUser();
+ $recentChange = $this->testRecentChangesHelper->makeEditRecentChange(
+ $user, 'Cat', '20131103212153', 5, 191, 190, 0, 0
+ );
+
+ return $recentChange;
+ }
+
+ private function getOldChangesList() {
+ $context = $this->getContext();
+ return new OldChangesList( $context );
+ }
+
+ private function getTestUser() {
+ $user = User::newFromName( 'TestRecentChangesUser' );
+
+ if ( !$user->getId() ) {
+ $user->addToDatabase();
+ }
+
+ return $user;
+ }
+
+ private function getContext() {
+ $user = $this->getTestUser();
+ $context = $this->testRecentChangesHelper->getTestContext( $user );
+ $context->setLanguage( Language::factory( 'qqx' ) );
+
+ return $context;
+ }
+
+}
diff --git a/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php b/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php
new file mode 100644
index 00000000..ee1a4d0e
--- /dev/null
+++ b/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php
@@ -0,0 +1,331 @@
+<?php
+
+/**
+ * @covers RCCacheEntryFactory
+ *
+ * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class RCCacheEntryFactoryTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var TestRecentChangesHelper
+ */
+ private $testRecentChangesHelper;
+
+ public function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->testRecentChangesHelper = new TestRecentChangesHelper();
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgArticlePath' => '/wiki/$1'
+ ) );
+ }
+
+ /**
+ * @dataProvider editChangeProvider
+ */
+ public function testNewFromRecentChange( $expected, $context, $messages,
+ $recentChange, $watched
+ ) {
+ $cacheEntryFactory = new RCCacheEntryFactory( $context, $messages );
+ $cacheEntry = $cacheEntryFactory->newFromRecentChange( $recentChange, $watched );
+
+ $this->assertInstanceOf( 'RCCacheEntry', $cacheEntry );
+
+ $this->assertEquals( $watched, $cacheEntry->watched, 'watched' );
+ $this->assertEquals( $expected['timestamp'], $cacheEntry->timestamp, 'timestamp' );
+ $this->assertEquals(
+ $expected['numberofWatchingusers'], $cacheEntry->numberofWatchingusers,
+ 'watching users'
+ );
+ $this->assertEquals( $expected['unpatrolled'], $cacheEntry->unpatrolled, 'unpatrolled' );
+
+ $this->assertUserLinks( 'TestRecentChangesUser', $cacheEntry );
+ $this->assertTitleLink( 'Xyz', $cacheEntry );
+
+ $this->assertQueryLink( 'cur', $expected['cur'], $cacheEntry->curlink, 'cur link' );
+ $this->assertQueryLink( 'prev', $expected['diff'], $cacheEntry->lastlink, 'prev link' );
+ $this->assertQueryLink( 'diff', $expected['diff'], $cacheEntry->difflink, 'diff link' );
+ }
+
+ public function editChangeProvider() {
+ return array(
+ array(
+ array(
+ 'title' => 'Xyz',
+ 'user' => 'TestRecentChangesUser',
+ 'diff' => array( 'curid' => 5, 'diff' => 191, 'oldid' => 190 ),
+ 'cur' => array( 'curid' => 5, 'diff' => 0, 'oldid' => 191 ),
+ 'timestamp' => '21:21',
+ 'numberofWatchingusers' => 0,
+ 'unpatrolled' => false
+ ),
+ $this->getContext(),
+ $this->getMessages(),
+ $this->testRecentChangesHelper->makeEditRecentChange(
+ $this->getTestUser(),
+ 'Xyz',
+ 5, // curid
+ 191, // thisid
+ 190, // lastid
+ '20131103212153',
+ 0, // counter
+ 0 // number of watching users
+ ),
+ false
+ )
+ );
+ }
+
+ /**
+ * @dataProvider deleteChangeProvider
+ */
+ public function testNewForDeleteChange( $expected, $context, $messages, $recentChange, $watched ) {
+ $cacheEntryFactory = new RCCacheEntryFactory( $context, $messages );
+ $cacheEntry = $cacheEntryFactory->newFromRecentChange( $recentChange, $watched );
+
+ $this->assertInstanceOf( 'RCCacheEntry', $cacheEntry );
+
+ $this->assertEquals( $watched, $cacheEntry->watched, 'watched' );
+ $this->assertEquals( $expected['timestamp'], $cacheEntry->timestamp, 'timestamp' );
+ $this->assertEquals(
+ $expected['numberofWatchingusers'],
+ $cacheEntry->numberofWatchingusers, 'watching users'
+ );
+ $this->assertEquals( $expected['unpatrolled'], $cacheEntry->unpatrolled, 'unpatrolled' );
+
+ $this->assertDeleteLogLink( $cacheEntry );
+ $this->assertUserLinks( 'TestRecentChangesUser', $cacheEntry );
+
+ $this->assertEquals( 'cur', $cacheEntry->curlink, 'cur link for delete log or rev' );
+ $this->assertEquals( 'diff', $cacheEntry->difflink, 'diff link for delete log or rev' );
+ $this->assertEquals( 'prev', $cacheEntry->lastlink, 'pref link for delete log or rev' );
+ }
+
+ public function deleteChangeProvider() {
+ return array(
+ array(
+ array(
+ 'title' => 'Abc',
+ 'user' => 'TestRecentChangesUser',
+ 'timestamp' => '21:21',
+ 'numberofWatchingusers' => 0,
+ 'unpatrolled' => false
+ ),
+ $this->getContext(),
+ $this->getMessages(),
+ $this->testRecentChangesHelper->makeLogRecentChange(
+ 'delete',
+ 'delete',
+ $this->getTestUser(),
+ 'Abc',
+ '20131103212153',
+ 0, // counter
+ 0 // number of watching users
+ ),
+ false
+ )
+ );
+ }
+
+ /**
+ * @dataProvider revUserDeleteProvider
+ */
+ public function testNewForRevUserDeleteChange( $expected, $context, $messages,
+ $recentChange, $watched
+ ) {
+ $cacheEntryFactory = new RCCacheEntryFactory( $context, $messages );
+ $cacheEntry = $cacheEntryFactory->newFromRecentChange( $recentChange, $watched );
+
+ $this->assertInstanceOf( 'RCCacheEntry', $cacheEntry );
+
+ $this->assertEquals( $watched, $cacheEntry->watched, 'watched' );
+ $this->assertEquals( $expected['timestamp'], $cacheEntry->timestamp, 'timestamp' );
+ $this->assertEquals(
+ $expected['numberofWatchingusers'],
+ $cacheEntry->numberofWatchingusers, 'watching users'
+ );
+ $this->assertEquals( $expected['unpatrolled'], $cacheEntry->unpatrolled, 'unpatrolled' );
+
+ $this->assertRevDel( $cacheEntry );
+ $this->assertTitleLink( 'Zzz', $cacheEntry );
+
+ $this->assertEquals( 'cur', $cacheEntry->curlink, 'cur link for delete log or rev' );
+ $this->assertEquals( 'diff', $cacheEntry->difflink, 'diff link for delete log or rev' );
+ $this->assertEquals( 'prev', $cacheEntry->lastlink, 'pref link for delete log or rev' );
+ }
+
+ public function revUserDeleteProvider() {
+ return array(
+ array(
+ array(
+ 'title' => 'Zzz',
+ 'user' => 'TestRecentChangesUser',
+ 'diff' => '',
+ 'cur' => '',
+ 'timestamp' => '21:21',
+ 'numberofWatchingusers' => 0,
+ 'unpatrolled' => false
+ ),
+ $this->getContext(),
+ $this->getMessages(),
+ $this->testRecentChangesHelper->makeDeletedEditRecentChange(
+ $this->getTestUser(),
+ 'Zzz',
+ '20131103212153',
+ 191, // thisid
+ 190, // lastid
+ '20131103212153',
+ 0, // counter
+ 0 // number of watching users
+ ),
+ false
+ )
+ );
+ }
+
+ private function assertUserLinks( $user, $cacheEntry ) {
+ $this->assertTag(
+ array(
+ 'tag' => 'a',
+ 'attributes' => array(
+ 'class' => 'new mw-userlink'
+ ),
+ 'content' => $user
+ ),
+ $cacheEntry->userlink,
+ 'verify user link'
+ );
+
+ $this->assertTag(
+ array(
+ 'tag' => 'span',
+ 'attributes' => array(
+ 'class' => 'mw-usertoollinks'
+ ),
+ 'child' => array(
+ 'tag' => 'a',
+ 'content' => 'Talk',
+ )
+ ),
+ $cacheEntry->usertalklink,
+ 'verify user talk link'
+ );
+
+ $this->assertTag(
+ array(
+ 'tag' => 'span',
+ 'attributes' => array(
+ 'class' => 'mw-usertoollinks'
+ ),
+ 'child' => array(
+ 'tag' => 'a',
+ 'content' => 'contribs',
+ )
+ ),
+ $cacheEntry->usertalklink,
+ 'verify user tool links'
+ );
+ }
+
+ private function assertDeleteLogLink( $cacheEntry ) {
+ $this->assertTag(
+ array(
+ 'tag' => 'a',
+ 'attributes' => array(
+ 'href' => '/wiki/Special:Log/delete',
+ 'title' => 'Special:Log/delete'
+ ),
+ 'content' => 'Deletion log'
+ ),
+ $cacheEntry->link,
+ 'verify deletion log link'
+ );
+ }
+
+ private function assertRevDel( $cacheEntry ) {
+ $this->assertTag(
+ array(
+ 'tag' => 'span',
+ 'attributes' => array(
+ 'class' => 'history-deleted'
+ ),
+ 'content' => '(username removed)'
+ ),
+ $cacheEntry->userlink,
+ 'verify user link for change with deleted revision and user'
+ );
+ }
+
+ private function assertTitleLink( $title, $cacheEntry ) {
+ $this->assertTag(
+ array(
+ 'tag' => 'a',
+ 'attributes' => array(
+ 'href' => '/wiki/' . $title,
+ 'title' => $title
+ ),
+ 'content' => $title
+ ),
+ $cacheEntry->link,
+ 'verify title link'
+ );
+ }
+
+ private function assertQueryLink( $content, $params, $link ) {
+ $this->assertTag(
+ array(
+ 'tag' => 'a',
+ 'content' => $content
+ ),
+ $link,
+ 'assert query link element'
+ );
+
+ foreach ( $params as $key => $value ) {
+ $this->assertRegExp( '/' . $key . '=' . $value . '/', $link, "verify $key link params" );
+ }
+ }
+
+ private function getMessages() {
+ return array(
+ 'cur' => 'cur',
+ 'diff' => 'diff',
+ 'hist' => 'hist',
+ 'enhancedrc-history' => 'history',
+ 'last' => 'prev',
+ 'blocklink' => 'block',
+ 'history' => 'Page history',
+ 'semicolon-separator' => '; ',
+ 'pipe-separator' => ' | '
+ );
+ }
+
+ private function getTestUser() {
+ $user = User::newFromName( 'TestRecentChangesUser' );
+
+ if ( !$user->getId() ) {
+ $user->addToDatabase();
+ }
+
+ return $user;
+ }
+
+ private function getContext() {
+ $user = $this->getTestUser();
+ $context = $this->testRecentChangesHelper->getTestContext( $user );
+
+ $title = Title::newFromText( 'RecentChanges', NS_SPECIAL );
+ $context->setTitle( $title );
+
+ return $context;
+ }
+}
diff --git a/tests/phpunit/includes/changes/RecentChangeTest.php b/tests/phpunit/includes/changes/RecentChangeTest.php
new file mode 100644
index 00000000..98903f1e
--- /dev/null
+++ b/tests/phpunit/includes/changes/RecentChangeTest.php
@@ -0,0 +1,286 @@
+<?php
+
+/**
+ * @group Database
+ */
+class RecentChangeTest extends MediaWikiTestCase {
+ protected $title;
+ protected $target;
+ protected $user;
+ protected $user_comment;
+ protected $context;
+
+ public function __construct() {
+ parent::__construct();
+
+ $this->title = Title::newFromText( 'SomeTitle' );
+ $this->target = Title::newFromText( 'TestTarget' );
+ $this->user = User::newFromName( 'UserName' );
+
+ $this->user_comment = '<User comment about action>';
+ $this->context = RequestContext::newExtraneousContext( $this->title );
+ }
+
+ /**
+ * The testIrcMsgForAction* tests are supposed to cover the hacky
+ * LogFormatter::getIRCActionText / bug 34508
+ *
+ * Third parties bots listen to those messages. They are clever enough
+ * to fetch the i18n messages from the wiki and then analyze the IRC feed
+ * to reverse engineer the $1, $2 messages.
+ * One thing bots can not detect is when MediaWiki change the meaning of
+ * a message like what happened when we deployed 1.19. $1 became the user
+ * performing the action which broke basically all bots around.
+ *
+ * Should cover the following log actions (which are most commonly used by bots):
+ * - block/block
+ * - block/unblock
+ * - delete/delete
+ * - delete/restore
+ * - newusers/create
+ * - newusers/create2
+ * - newusers/autocreate
+ * - move/move
+ * - move/move_redir
+ * - protect/protect
+ * - protect/modifyprotect
+ * - protect/unprotect
+ * - upload/upload
+ *
+ * As well as the following Auto Edit Summaries:
+ * - blank
+ * - replace
+ * - rollback
+ * - undo
+ */
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeBlock() {
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # block/block
+ $this->assertIRCComment(
+ $this->context->msg( 'blocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'block', 'block',
+ array(),
+ $this->user_comment
+ );
+ # block/unblock
+ $this->assertIRCComment(
+ $this->context->msg( 'unblocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'block', 'unblock',
+ array(),
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeDelete() {
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # delete/delete
+ $this->assertIRCComment(
+ $this->context->msg( 'deletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'delete', 'delete',
+ array(),
+ $this->user_comment
+ );
+
+ # delete/restore
+ $this->assertIRCComment(
+ $this->context->msg( 'undeletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'delete', 'restore',
+ array(),
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeNewusers() {
+ $this->assertIRCComment(
+ 'New user account',
+ 'newusers', 'newusers',
+ array()
+ );
+ $this->assertIRCComment(
+ 'New user account',
+ 'newusers', 'create',
+ array()
+ );
+ $this->assertIRCComment(
+ 'created new account SomeTitle',
+ 'newusers', 'create2',
+ array()
+ );
+ $this->assertIRCComment(
+ 'Account created automatically',
+ 'newusers', 'autocreate',
+ array()
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeMove() {
+ $move_params = array(
+ '4::target' => $this->target->getPrefixedText(),
+ '5::noredir' => 0,
+ );
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # move/move
+ $this->assertIRCComment(
+ $this->context->msg( '1movedto2', 'SomeTitle', 'TestTarget' )
+ ->plain() . $sep . $this->user_comment,
+ 'move', 'move',
+ $move_params,
+ $this->user_comment
+ );
+
+ # move/move_redir
+ $this->assertIRCComment(
+ $this->context->msg( '1movedto2_redir', 'SomeTitle', 'TestTarget' )
+ ->plain() . $sep . $this->user_comment,
+ 'move', 'move_redir',
+ $move_params,
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypePatrol() {
+ # patrol/patrol
+ $this->assertIRCComment(
+ $this->context->msg( 'patrol-log-line', 'revision 777', '[[SomeTitle]]', '' )->plain(),
+ 'patrol', 'patrol',
+ array(
+ '4::curid' => '777',
+ '5::previd' => '666',
+ '6::auto' => 0,
+ )
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeProtect() {
+ $protectParams = array(
+ '[edit=sysop] (indefinite) ‎[move=sysop] (indefinite)'
+ );
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # protect/protect
+ $this->assertIRCComment(
+ $this->context->msg( 'protectedarticle', 'SomeTitle ' . $protectParams[0] )
+ ->plain() . $sep . $this->user_comment,
+ 'protect', 'protect',
+ $protectParams,
+ $this->user_comment
+ );
+
+ # protect/unprotect
+ $this->assertIRCComment(
+ $this->context->msg( 'unprotectedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'protect', 'unprotect',
+ array(),
+ $this->user_comment
+ );
+
+ # protect/modify
+ $this->assertIRCComment(
+ $this->context->msg( 'modifiedarticleprotection', 'SomeTitle ' . $protectParams[0] )
+ ->plain() . $sep . $this->user_comment,
+ 'protect', 'modify',
+ $protectParams,
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @covers LogFormatter::getIRCActionText
+ */
+ public function testIrcMsgForLogTypeUpload() {
+ $sep = $this->context->msg( 'colon-separator' )->text();
+
+ # upload/upload
+ $this->assertIRCComment(
+ $this->context->msg( 'uploadedimage', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'upload', 'upload',
+ array(),
+ $this->user_comment
+ );
+
+ # upload/overwrite
+ $this->assertIRCComment(
+ $this->context->msg( 'overwroteimage', 'SomeTitle' )->plain() . $sep . $this->user_comment,
+ 'upload', 'overwrite',
+ array(),
+ $this->user_comment
+ );
+ }
+
+ /**
+ * @todo Emulate these edits somehow and extract
+ * raw edit summary from RecentChange object
+ * --
+ */
+ /*
+ public function testIrcMsgForBlankingAES() {
+ // $this->context->msg( 'autosumm-blank', .. );
+ }
+
+ public function testIrcMsgForReplaceAES() {
+ // $this->context->msg( 'autosumm-replace', .. );
+ }
+
+ public function testIrcMsgForRollbackAES() {
+ // $this->context->msg( 'revertpage', .. );
+ }
+
+ public function testIrcMsgForUndoAES() {
+ // $this->context->msg( 'undo-summary', .. );
+ }
+ */
+
+ /**
+ * @param string $expected Expected IRC text without colors codes
+ * @param string $type Log type (move, delete, suppress, patrol ...)
+ * @param string $action A log type action
+ * @param array $params
+ * @param string $comment (optional) A comment for the log action
+ * @param string $msg (optional) A message for PHPUnit :-)
+ */
+ protected function assertIRCComment( $expected, $type, $action, $params,
+ $comment = null, $msg = ''
+ ) {
+ $logEntry = new ManualLogEntry( $type, $action );
+ $logEntry->setPerformer( $this->user );
+ $logEntry->setTarget( $this->title );
+ if ( $comment !== null ) {
+ $logEntry->setComment( $comment );
+ }
+ $logEntry->setParameters( $params );
+
+ $formatter = LogFormatter::newFromEntry( $logEntry );
+ $formatter->setContext( $this->context );
+
+ // Apply the same transformation as done in IRCColourfulRCFeedFormatter::getLine for rc_comment
+ $ircRcComment = IRCColourfulRCFeedFormatter::cleanupForIRC( $formatter->getIRCActionComment() );
+
+ $this->assertEquals(
+ $expected,
+ $ircRcComment,
+ $msg
+ );
+ }
+}
diff --git a/tests/phpunit/includes/changes/TestRecentChangesHelper.php b/tests/phpunit/includes/changes/TestRecentChangesHelper.php
new file mode 100644
index 00000000..ad643274
--- /dev/null
+++ b/tests/phpunit/includes/changes/TestRecentChangesHelper.php
@@ -0,0 +1,137 @@
+<?php
+
+/**
+ * Helper for generating test recent changes entries.
+ *
+ * @licence GNU GPL v2+
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class TestRecentChangesHelper {
+
+ public function makeEditRecentChange( User $user, $titleText, $curid, $thisid, $lastid,
+ $timestamp, $counter, $watchingUsers
+ ) {
+
+ $attribs = array_merge(
+ $this->getDefaultAttributes( $titleText, $timestamp ),
+ array(
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_this_oldid' => $thisid,
+ 'rc_last_oldid' => $lastid,
+ 'rc_cur_id' => $curid
+ )
+ );
+
+ return $this->makeRecentChange( $attribs, $counter, $watchingUsers );
+ }
+
+ public function makeLogRecentChange( $logType, $logAction, User $user, $titleText, $timestamp, $counter,
+ $watchingUsers
+ ) {
+ $attribs = array_merge(
+ $this->getDefaultAttributes( $titleText, $timestamp ),
+ array(
+ 'rc_cur_id' => 0,
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_this_oldid' => 0,
+ 'rc_last_oldid' => 0,
+ 'rc_old_len' => null,
+ 'rc_new_len' => null,
+ 'rc_type' => 3,
+ 'rc_logid' => 25,
+ 'rc_log_type' => $logType,
+ 'rc_log_action' => $logAction,
+ 'rc_source' => 'mw.log'
+ )
+ );
+
+ return $this->makeRecentChange( $attribs, $counter, $watchingUsers );
+ }
+
+ public function makeDeletedEditRecentChange( User $user, $titleText, $timestamp, $curid,
+ $thisid, $lastid, $counter, $watchingUsers
+ ) {
+ $attribs = array_merge(
+ $this->getDefaultAttributes( $titleText, $timestamp ),
+ array(
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_deleted' => 5,
+ 'rc_cur_id' => $curid,
+ 'rc_this_oldid' => $thisid,
+ 'rc_last_oldid' => $lastid
+ )
+ );
+
+ return $this->makeRecentChange( $attribs, $counter, $watchingUsers );
+ }
+
+ public function makeNewBotEditRecentChange( User $user, $titleText, $curid, $thisid, $lastid,
+ $timestamp, $counter, $watchingUsers
+ ) {
+
+ $attribs = array_merge(
+ $this->getDefaultAttributes( $titleText, $timestamp ),
+ array(
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_this_oldid' => $thisid,
+ 'rc_last_oldid' => $lastid,
+ 'rc_cur_id' => $curid,
+ 'rc_type' => 1,
+ 'rc_bot' => 1,
+ 'rc_source' => 'mw.new'
+ )
+ );
+
+ return $this->makeRecentChange( $attribs, $counter, $watchingUsers );
+ }
+
+ private function makeRecentChange( $attribs, $counter, $watchingUsers ) {
+ $change = new RecentChange();
+ $change->setAttribs( $attribs );
+ $change->counter = $counter;
+ $change->numberofWatchingusers = $watchingUsers;
+
+ return $change;
+ }
+
+ private function getDefaultAttributes( $titleText, $timestamp ) {
+ return array(
+ 'rc_id' => 545,
+ 'rc_user' => 0,
+ 'rc_user_text' => '127.0.0.1',
+ 'rc_ip' => '127.0.0.1',
+ 'rc_title' => $titleText,
+ 'rc_namespace' => 0,
+ 'rc_timestamp' => $timestamp,
+ 'rc_old_len' => 212,
+ 'rc_new_len' => 188,
+ 'rc_comment' => '',
+ 'rc_minor' => 0,
+ 'rc_bot' => 0,
+ 'rc_type' => 0,
+ 'rc_patrolled' => 1,
+ 'rc_deleted' => 0,
+ 'rc_logid' => 0,
+ 'rc_log_type' => null,
+ 'rc_log_action' => '',
+ 'rc_params' => '',
+ 'rc_source' => 'mw.edit'
+ );
+ }
+
+ public function getTestContext( User $user ) {
+ $context = new RequestContext();
+ $context->setLanguage( Language::factory( 'en' ) );
+
+ $context->setUser( $user );
+
+ $title = Title::newFromText( 'RecentChanges', NS_SPECIAL );
+ $context->setTitle( $title );
+
+ return $context;
+ }
+}
diff --git a/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php b/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php
new file mode 100644
index 00000000..3f887dc0
--- /dev/null
+++ b/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @covers ComposerVersionNormalizer
+ *
+ * @group ComposerHooks
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class ComposerVersionNormalizerTest extends PHPUnit_Framework_TestCase {
+
+ /**
+ * @dataProvider nonStringProvider
+ */
+ public function testGivenNonString_normalizeThrowsInvalidArgumentException( $nonString ) {
+ $normalizer = new ComposerVersionNormalizer();
+
+ $this->setExpectedException( 'InvalidArgumentException' );
+ $normalizer->normalizeSuffix( $nonString );
+ }
+
+ public function nonStringProvider() {
+ return array(
+ array( null ),
+ array( 42 ),
+ array( array() ),
+ array( new stdClass() ),
+ array( true ),
+ );
+ }
+
+ /**
+ * @dataProvider simpleVersionProvider
+ */
+ public function testGivenSimpleVersion_normalizeSuffixReturnsAsIs( $simpleVersion ) {
+ $this->assertRemainsUnchanged( $simpleVersion );
+ }
+
+ protected function assertRemainsUnchanged( $version ) {
+ $normalizer = new ComposerVersionNormalizer();
+
+ $this->assertEquals(
+ $version,
+ $normalizer->normalizeSuffix( $version )
+ );
+ }
+
+ public function simpleVersionProvider() {
+ return array(
+ array( '1.22.0' ),
+ array( '1.19.2' ),
+ array( '1.19.2.0' ),
+ array( '1.9' ),
+ array( '123.321.456.654' ),
+ );
+ }
+
+ /**
+ * @dataProvider complexVersionProvider
+ */
+ public function testGivenComplexVersionWithoutDash_normalizeSuffixAddsDash(
+ $withoutDash, $withDash
+ ) {
+ $normalizer = new ComposerVersionNormalizer();
+
+ $this->assertEquals(
+ $withDash,
+ $normalizer->normalizeSuffix( $withoutDash )
+ );
+ }
+
+ public function complexVersionProvider() {
+ return array(
+ array( '1.22.0alpha', '1.22.0-alpha' ),
+ array( '1.22.0RC', '1.22.0-RC' ),
+ array( '1.19beta', '1.19-beta' ),
+ array( '1.9RC4', '1.9-RC4' ),
+ array( '1.9.1.2RC4', '1.9.1.2-RC4' ),
+ array( '1.9.1.2RC', '1.9.1.2-RC' ),
+ array( '123.321.456.654RC9001', '123.321.456.654-RC9001' ),
+ );
+ }
+
+ /**
+ * @dataProvider complexVersionProvider
+ */
+ public function testGivenComplexVersionWithDash_normalizeSuffixReturnsAsIs(
+ $withoutDash, $withDash
+ ) {
+ $this->assertRemainsUnchanged( $withDash );
+ }
+
+ /**
+ * @dataProvider fourLevelVersionsProvider
+ */
+ public function testGivenFourLevels_levelCountNormalizationDoesNothing( $version ) {
+ $normalizer = new ComposerVersionNormalizer();
+
+ $this->assertEquals(
+ $version,
+ $normalizer->normalizeLevelCount( $version )
+ );
+ }
+
+ public function fourLevelVersionsProvider() {
+ return array(
+ array( '1.22.0.0' ),
+ array( '1.19.2.4' ),
+ array( '1.19.2.0' ),
+ array( '1.9.0.1' ),
+ array( '123.321.456.654' ),
+ array( '123.321.456.654RC4' ),
+ array( '123.321.456.654-RC4' ),
+ );
+ }
+
+ /**
+ * @dataProvider levelNormalizationProvider
+ */
+ public function testGivenFewerLevels_levelCountNormalizationEnsuresFourLevels(
+ $expected, $version
+ ) {
+ $normalizer = new ComposerVersionNormalizer();
+
+ $this->assertEquals(
+ $expected,
+ $normalizer->normalizeLevelCount( $version )
+ );
+ }
+
+ public function levelNormalizationProvider() {
+ return array(
+ array( '1.22.0.0', '1.22' ),
+ array( '1.22.0.0', '1.22.0' ),
+ array( '1.19.2.0', '1.19.2' ),
+ array( '12345.0.0.0', '12345' ),
+ array( '12345.0.0.0-RC4', '12345-RC4' ),
+ array( '12345.0.0.0-alpha', '12345-alpha' ),
+ );
+ }
+
+ /**
+ * @dataProvider invalidVersionProvider
+ */
+ public function testGivenInvalidVersion_normalizeSuffixReturnsAsIs( $invalidVersion ) {
+ $this->assertRemainsUnchanged( $invalidVersion );
+ }
+
+ public function invalidVersionProvider() {
+ return array(
+ array( '1.221-a' ),
+ array( '1.221-' ),
+ array( '1.22rc4a' ),
+ array( 'a1.22rc' ),
+ array( '.1.22rc' ),
+ array( 'a' ),
+ array( 'alpha42' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/config/ConfigFactoryTest.php b/tests/phpunit/includes/config/ConfigFactoryTest.php
new file mode 100644
index 00000000..3902858d
--- /dev/null
+++ b/tests/phpunit/includes/config/ConfigFactoryTest.php
@@ -0,0 +1,70 @@
+<?php
+
+class ConfigFactoryTest extends MediaWikiTestCase {
+
+ public function tearDown() {
+ // Reset this since we mess with it a bit
+ ConfigFactory::destroyDefaultInstance();
+ parent::tearDown();
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegister() {
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+ $this->assertTrue( true ); // No exception thrown
+ $this->setExpectedException( 'InvalidArgumentException' );
+ $factory->register( 'invalid', 'Invalid callback' );
+ }
+
+ /**
+ * @covers ConfigFactory::makeConfig
+ */
+ public function testMakeConfig() {
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+ $conf = $factory->makeConfig( 'unittest' );
+ $this->assertInstanceOf( 'Config', $conf );
+ }
+
+ /**
+ * @covers ConfigFactory::makeConfig
+ */
+ public function testMakeConfigWithNoBuilders() {
+ $factory = new ConfigFactory();
+ $this->setExpectedException( 'ConfigException' );
+ $factory->makeConfig( 'nobuilderregistered' );
+ }
+
+ /**
+ * @covers ConfigFactory::makeConfig
+ */
+ public function testMakeConfigWithInvalidCallback() {
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', function () {
+ return true; // Not a Config object
+ } );
+ $this->setExpectedException( 'UnexpectedValueException' );
+ $factory->makeConfig( 'unittest' );
+ }
+
+ /**
+ * @covers ConfigFactory::getDefaultInstance
+ */
+ public function testGetDefaultInstance() {
+ // Set $wgConfigRegistry, and check the default
+ // instance read from it
+ $this->setMwGlobals( 'wgConfigRegistry', array(
+ 'conf1' => 'GlobalVarConfig::newInstance',
+ 'conf2' => 'GlobalVarConfig::newInstance',
+ ) );
+ ConfigFactory::destroyDefaultInstance();
+ $factory = ConfigFactory::getDefaultInstance();
+ $this->assertInstanceOf( 'Config', $factory->makeConfig( 'conf1' ) );
+ $this->assertInstanceOf( 'Config', $factory->makeConfig( 'conf2' ) );
+ $this->setExpectedException( 'ConfigException' );
+ $factory->makeConfig( 'conf3' );
+ }
+}
diff --git a/tests/phpunit/includes/config/GlobalVarConfigTest.php b/tests/phpunit/includes/config/GlobalVarConfigTest.php
new file mode 100644
index 00000000..70b9e684
--- /dev/null
+++ b/tests/phpunit/includes/config/GlobalVarConfigTest.php
@@ -0,0 +1,120 @@
+<?php
+
+class GlobalVarConfigTest extends MediaWikiTestCase {
+
+ /**
+ * @covers GlobalVarConfig::newInstance
+ */
+ public function testNewInstance() {
+ $config = GlobalVarConfig::newInstance();
+ $this->assertInstanceOf( 'GlobalVarConfig', $config );
+ $this->maybeStashGlobal( 'wgBaz' );
+ $GLOBALS['wgBaz'] = 'somevalue';
+ // Check prefix is set to 'wg'
+ $this->assertEquals( 'somevalue', $config->get( 'Baz' ) );
+ }
+
+ /**
+ * @covers GlobalVarConfig::__construct
+ * @dataProvider provideConstructor
+ */
+ public function testConstructor( $prefix ) {
+ $var = $prefix . 'GlobalVarConfigTest';
+ $rand = wfRandomString();
+ $this->maybeStashGlobal( $var );
+ $GLOBALS[$var] = $rand;
+ $config = new GlobalVarConfig( $prefix );
+ $this->assertInstanceOf( 'GlobalVarConfig', $config );
+ $this->assertEquals( $rand, $config->get( 'GlobalVarConfigTest' ) );
+ }
+
+ public static function provideConstructor() {
+ return array(
+ array( 'wg' ),
+ array( 'ef' ),
+ array( 'smw' ),
+ array( 'blahblahblahblah' ),
+ array( '' ),
+ );
+ }
+
+ /**
+ * @covers GlobalVarConfig::has
+ */
+ public function testHas() {
+ $this->maybeStashGlobal( 'wgGlobalVarConfigTestHas' );
+ $GLOBALS['wgGlobalVarConfigTestHas'] = wfRandomString();
+ $this->maybeStashGlobal( 'wgGlobalVarConfigTestNotHas' );
+ $config = new GlobalVarConfig();
+ $this->assertTrue( $config->has( 'GlobalVarConfigTestHas' ) );
+ $this->assertFalse( $config->has( 'GlobalVarConfigTestNotHas' ) );
+ }
+
+ public static function provideGet() {
+ $set = array(
+ 'wgSomething' => 'default1',
+ 'wgFoo' => 'default2',
+ 'efVariable' => 'default3',
+ 'BAR' => 'default4',
+ );
+
+ foreach ( $set as $var => $value ) {
+ $GLOBALS[$var] = $value;
+ }
+
+ return array(
+ array( 'Something', 'wg', 'default1' ),
+ array( 'Foo', 'wg', 'default2' ),
+ array( 'Variable', 'ef', 'default3' ),
+ array( 'BAR', '', 'default4' ),
+ array( 'ThisGlobalWasNotSetAbove', 'wg', false )
+ );
+ }
+
+ /**
+ * @param string $name
+ * @param string $prefix
+ * @param string $expected
+ * @dataProvider provideGet
+ * @covers GlobalVarConfig::get
+ * @covers GlobalVarConfig::getWithPrefix
+ */
+ public function testGet( $name, $prefix, $expected ) {
+ $config = new GlobalVarConfig( $prefix );
+ if ( $expected === false ) {
+ $this->setExpectedException( 'ConfigException', 'GlobalVarConfig::get: undefined option:' );
+ }
+ $this->assertEquals( $config->get( $name ), $expected );
+ }
+
+ public static function provideSet() {
+ return array(
+ array( 'Foo', 'wg', 'wgFoo' ),
+ array( 'SomethingRandom', 'wg', 'wgSomethingRandom' ),
+ array( 'FromAnExtension', 'eg', 'egFromAnExtension' ),
+ array( 'NoPrefixHere', '', 'NoPrefixHere' ),
+ );
+ }
+
+ private function maybeStashGlobal( $var ) {
+ if ( array_key_exists( $var, $GLOBALS ) ) {
+ // Will be reset after this test is over
+ $this->stashMwGlobals( $var );
+ }
+ }
+
+ /**
+ * @dataProvider provideSet
+ * @covers GlobalVarConfig::set
+ * @covers GlobalVarConfig::setWithPrefix
+ */
+ public function testSet( $name, $prefix, $var ) {
+ $this->hideDeprecated( 'GlobalVarConfig::set' );
+ $this->maybeStashGlobal( $var );
+ $config = new GlobalVarConfig( $prefix );
+ $random = wfRandomString();
+ $config->set( $name, $random );
+ $this->assertArrayHasKey( $var, $GLOBALS );
+ $this->assertEquals( $random, $GLOBALS[$var] );
+ }
+}
diff --git a/tests/phpunit/includes/config/HashConfigTest.php b/tests/phpunit/includes/config/HashConfigTest.php
new file mode 100644
index 00000000..3ad3bfbd
--- /dev/null
+++ b/tests/phpunit/includes/config/HashConfigTest.php
@@ -0,0 +1,63 @@
+<?php
+
+class HashConfigTest extends MediaWikiTestCase {
+
+ /**
+ * @covers HashConfig::newInstance
+ */
+ public function testNewInstance() {
+ $conf = HashConfig::newInstance();
+ $this->assertInstanceOf( 'HashConfig', $conf );
+ }
+
+ /**
+ * @covers HashConfig::__construct
+ */
+ public function testConstructor() {
+ $conf = new HashConfig();
+ $this->assertInstanceOf( 'HashConfig', $conf );
+
+ // Test passing arguments to the constructor
+ $conf2 = new HashConfig( array(
+ 'one' => '1',
+ ) );
+ $this->assertEquals( '1', $conf2->get( 'one' ) );
+ }
+
+ /**
+ * @covers HashConfig::get
+ */
+ public function testGet() {
+ $conf = new HashConfig( array(
+ 'one' => '1',
+ ));
+ $this->assertEquals( '1', $conf->get( 'one' ) );
+ $this->setExpectedException( 'ConfigException', 'HashConfig::get: undefined option' );
+ $conf->get( 'two' );
+ }
+
+ /**
+ * @covers HashConfig::has
+ */
+ public function testHas() {
+ $conf = new HashConfig( array(
+ 'one' => '1',
+ ) );
+ $this->assertTrue( $conf->has( 'one' ) );
+ $this->assertFalse( $conf->has( 'two' ) );
+ }
+
+ /**
+ * @covers HashConfig::set
+ */
+ public function testSet() {
+ $conf = new HashConfig( array(
+ 'one' => '1',
+ ) );
+ $conf->set( 'two', '2' );
+ $this->assertEquals( '2', $conf->get( 'two' ) );
+ // Check that set overwrites
+ $conf->set( 'one', '3' );
+ $this->assertEquals( '3', $conf->get( 'one' ) );
+ }
+} \ No newline at end of file
diff --git a/tests/phpunit/includes/config/MultiConfigTest.php b/tests/phpunit/includes/config/MultiConfigTest.php
new file mode 100644
index 00000000..158da466
--- /dev/null
+++ b/tests/phpunit/includes/config/MultiConfigTest.php
@@ -0,0 +1,38 @@
+<?php
+
+class MultiConfigTest extends MediaWikiTestCase {
+
+ /**
+ * Tests that settings are fetched in the right order
+ *
+ * @covers MultiConfig::get
+ */
+ public function testGet() {
+ $multi = new MultiConfig( array(
+ new HashConfig( array( 'foo' => 'bar' ) ),
+ new HashConfig( array( 'foo' => 'baz', 'bar' => 'foo' ) ),
+ new HashConfig( array( 'bar' => 'baz' ) ),
+ ) );
+
+ $this->assertEquals( 'bar', $multi->get( 'foo' ) );
+ $this->assertEquals( 'foo', $multi->get( 'bar' ) );
+ $this->setExpectedException( 'ConfigException', 'MultiConfig::get: undefined option:' );
+ $multi->get( 'notset' );
+ }
+
+ /**
+ * @covers MultiConfig::has
+ */
+ public function testHas() {
+ $conf = new MultiConfig( array(
+ new HashConfig( array( 'foo' => 'foo' ) ),
+ new HashConfig( array( 'something' => 'bleh' ) ),
+ new HashConfig( array( 'meh' => 'eh' ) ),
+ ) );
+
+ $this->assertTrue( $conf->has( 'foo' ) );
+ $this->assertTrue( $conf->has( 'something' ) );
+ $this->assertTrue( $conf->has( 'meh' ) );
+ $this->assertFalse( $conf->has( 'what' ) );
+ }
+}
diff --git a/tests/phpunit/includes/content/ContentHandlerTest.php b/tests/phpunit/includes/content/ContentHandlerTest.php
new file mode 100644
index 00000000..f7449734
--- /dev/null
+++ b/tests/phpunit/includes/content/ContentHandlerTest.php
@@ -0,0 +1,525 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ *
+ * @note Declare that we are using the database, because otherwise we'll fail in
+ * the "databaseless" test run. This is because the LinkHolderArray used by the
+ * parser needs database access.
+ */
+class ContentHandlerTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ global $wgContLang;
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgExtraNamespaces' => array(
+ 12312 => 'Dummy',
+ 12313 => 'Dummy_talk',
+ ),
+ // The below tests assume that namespaces not mentioned here (Help, User, MediaWiki, ..)
+ // default to CONTENT_MODEL_WIKITEXT.
+ 'wgNamespaceContentModels' => array(
+ 12312 => 'testing',
+ ),
+ 'wgContentHandlers' => array(
+ CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler',
+ CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler',
+ CONTENT_MODEL_CSS => 'CssContentHandler',
+ CONTENT_MODEL_TEXT => 'TextContentHandler',
+ 'testing' => 'DummyContentHandlerForTesting',
+ ),
+ ) );
+
+ // Reset namespace cache
+ MWNamespace::getCanonicalNamespaces( true );
+ $wgContLang->resetNamespaces();
+ }
+
+ protected function tearDown() {
+ global $wgContLang;
+
+ // Reset namespace cache
+ MWNamespace::getCanonicalNamespaces( true );
+ $wgContLang->resetNamespaces();
+
+ parent::tearDown();
+ }
+
+ public static function dataGetDefaultModelFor() {
+ return array(
+ array( 'Help:Foo', CONTENT_MODEL_WIKITEXT ),
+ array( 'Help:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'Help:Foo/bar.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ),
+ array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
+ array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ),
+ array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ),
+ array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetDefaultModelFor
+ * @covers ContentHandler::getDefaultModelFor
+ */
+ public function testGetDefaultModelFor( $title, $expectedModelId ) {
+ $title = Title::newFromText( $title );
+ $this->assertEquals( $expectedModelId, ContentHandler::getDefaultModelFor( $title ) );
+ }
+
+ /**
+ * @dataProvider dataGetDefaultModelFor
+ * @covers ContentHandler::getForTitle
+ */
+ public function testGetForTitle( $title, $expectedContentModel ) {
+ $title = Title::newFromText( $title );
+ $handler = ContentHandler::getForTitle( $title );
+ $this->assertEquals( $expectedContentModel, $handler->getModelID() );
+ }
+
+ public static function dataGetLocalizedName() {
+ return array(
+ array( null, null ),
+ array( "xyzzy", null ),
+
+ // XXX: depends on content language
+ array( CONTENT_MODEL_JAVASCRIPT, '/javascript/i' ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetLocalizedName
+ * @covers ContentHandler::getLocalizedName
+ */
+ public function testGetLocalizedName( $id, $expected ) {
+ $name = ContentHandler::getLocalizedName( $id );
+
+ if ( $expected ) {
+ $this->assertNotNull( $name, "no name found for content model $id" );
+ $this->assertTrue( preg_match( $expected, $name ) > 0,
+ "content model name for #$id did not match pattern $expected"
+ );
+ } else {
+ $this->assertEquals( $id, $name, "localization of unknown model $id should have "
+ . "fallen back to use the model id directly."
+ );
+ }
+ }
+
+ public static function dataGetPageLanguage() {
+ global $wgLanguageCode;
+
+ return array(
+ array( "Main", $wgLanguageCode ),
+ array( "Dummy:Foo", $wgLanguageCode ),
+ array( "MediaWiki:common.js", 'en' ),
+ array( "User:Foo/common.js", 'en' ),
+ array( "MediaWiki:common.css", 'en' ),
+ array( "User:Foo/common.css", 'en' ),
+ array( "User:Foo", $wgLanguageCode ),
+
+ array( CONTENT_MODEL_JAVASCRIPT, 'javascript' ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetPageLanguage
+ * @covers ContentHandler::getPageLanguage
+ */
+ public function testGetPageLanguage( $title, $expected ) {
+ if ( is_string( $title ) ) {
+ $title = Title::newFromText( $title );
+ }
+
+ $expected = wfGetLangObj( $expected );
+
+ $handler = ContentHandler::getForTitle( $title );
+ $lang = $handler->getPageLanguage( $title );
+
+ $this->assertEquals( $expected->getCode(), $lang->getCode() );
+ }
+
+ public static function dataGetContentText_Null() {
+ return array(
+ array( 'fail' ),
+ array( 'serialize' ),
+ array( 'ignore' ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetContentText_Null
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_Null( $contentHandlerTextFallback ) {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', $contentHandlerTextFallback );
+
+ $content = null;
+
+ $text = ContentHandler::getContentText( $content );
+ $this->assertEquals( '', $text );
+ }
+
+ public static function dataGetContentText_TextContent() {
+ return array(
+ array( 'fail' ),
+ array( 'serialize' ),
+ array( 'ignore' ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetContentText_TextContent
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_TextContent( $contentHandlerTextFallback ) {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', $contentHandlerTextFallback );
+
+ $content = new WikitextContent( "hello world" );
+
+ $text = ContentHandler::getContentText( $content );
+ $this->assertEquals( $content->getNativeData(), $text );
+ }
+
+ /**
+ * ContentHandler::getContentText should have thrown an exception for non-text Content object
+ * @expectedException MWException
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_NonTextContent_fail() {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', 'fail' );
+
+ $content = new DummyContentForTesting( "hello world" );
+
+ ContentHandler::getContentText( $content );
+ }
+
+ /**
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_NonTextContent_serialize() {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', 'serialize' );
+
+ $content = new DummyContentForTesting( "hello world" );
+
+ $text = ContentHandler::getContentText( $content );
+ $this->assertEquals( $content->serialize(), $text );
+ }
+
+ /**
+ * @covers ContentHandler::getContentText
+ */
+ public function testGetContentText_NonTextContent_ignore() {
+ $this->setMwGlobals( 'wgContentHandlerTextFallback', 'ignore' );
+
+ $content = new DummyContentForTesting( "hello world" );
+
+ $text = ContentHandler::getContentText( $content );
+ $this->assertNull( $text );
+ }
+
+ /*
+ public static function makeContent( $text, Title $title, $modelId = null, $format = null ) {}
+ */
+
+ public static function dataMakeContent() {
+ return array(
+ array( 'hallo', 'Help:Test', null, null, CONTENT_MODEL_WIKITEXT, 'hallo', false ),
+ array( 'hallo', 'MediaWiki:Test.js', null, null, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ),
+ array( serialize( 'hallo' ), 'Dummy:Test', null, null, "testing", 'hallo', false ),
+
+ array(
+ 'hallo',
+ 'Help:Test',
+ null,
+ CONTENT_FORMAT_WIKITEXT,
+ CONTENT_MODEL_WIKITEXT,
+ 'hallo',
+ false
+ ),
+ array(
+ 'hallo',
+ 'MediaWiki:Test.js',
+ null,
+ CONTENT_FORMAT_JAVASCRIPT,
+ CONTENT_MODEL_JAVASCRIPT,
+ 'hallo',
+ false
+ ),
+ array( serialize( 'hallo' ), 'Dummy:Test', null, "testing", "testing", 'hallo', false ),
+
+ array( 'hallo', 'Help:Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ),
+ array(
+ 'hallo',
+ 'MediaWiki:Test.js',
+ CONTENT_MODEL_CSS,
+ null,
+ CONTENT_MODEL_CSS,
+ 'hallo',
+ false
+ ),
+ array(
+ serialize( 'hallo' ),
+ 'Dummy:Test',
+ CONTENT_MODEL_CSS,
+ null,
+ CONTENT_MODEL_CSS,
+ serialize( 'hallo' ),
+ false
+ ),
+
+ array( 'hallo', 'Help:Test', CONTENT_MODEL_WIKITEXT, "testing", null, null, true ),
+ array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, "testing", null, null, true ),
+ array( 'hallo', 'Dummy:Test', CONTENT_MODEL_JAVASCRIPT, "testing", null, null, true ),
+ );
+ }
+
+ /**
+ * @dataProvider dataMakeContent
+ * @covers ContentHandler::makeContent
+ */
+ public function testMakeContent( $data, $title, $modelId, $format,
+ $expectedModelId, $expectedNativeData, $shouldFail
+ ) {
+ $title = Title::newFromText( $title );
+
+ try {
+ $content = ContentHandler::makeContent( $data, $title, $modelId, $format );
+
+ if ( $shouldFail ) {
+ $this->fail( "ContentHandler::makeContent should have failed!" );
+ }
+
+ $this->assertEquals( $expectedModelId, $content->getModel(), 'bad model id' );
+ $this->assertEquals( $expectedNativeData, $content->getNativeData(), 'bads native data' );
+ } catch ( MWException $ex ) {
+ if ( !$shouldFail ) {
+ $this->fail( "ContentHandler::makeContent failed unexpectedly: " . $ex->getMessage() );
+ } else {
+ // dummy, so we don't get the "test did not perform any assertions" message.
+ $this->assertTrue( true );
+ }
+ }
+ }
+
+ /*
+ * Test if we become a "Created blank page" summary from getAutoSummary if no Content added to
+ * page.
+ */
+ public function testGetAutosummary() {
+ $content = new DummyContentHandlerForTesting( CONTENT_MODEL_WIKITEXT );
+ $title = Title::newFromText( 'Help:Test' );
+ // Create a new content object with no content
+ $newContent = ContentHandler::makeContent( '', $title, null, null, CONTENT_MODEL_WIKITEXT );
+ // first check, if we become a blank page created summary with the right bitmask
+ $autoSummary = $content->getAutosummary( null, $newContent, 97 );
+ $this->assertEquals( $autoSummary, 'Created blank page' );
+ // now check, what we become with another bitmask
+ $autoSummary = $content->getAutosummary( null, $newContent, 92 );
+ $this->assertEquals( $autoSummary, '' );
+ }
+
+ /*
+ public function testSupportsSections() {
+ $this->markTestIncomplete( "not yet implemented" );
+ }
+ */
+
+ /**
+ * @covers ContentHandler::runLegacyHooks
+ */
+ public function testRunLegacyHooks() {
+ Hooks::register( 'testRunLegacyHooks', __CLASS__ . '::dummyHookHandler' );
+
+ $content = new WikitextContent( 'test text' );
+ $ok = ContentHandler::runLegacyHooks(
+ 'testRunLegacyHooks',
+ array( 'foo', &$content, 'bar' ),
+ false
+ );
+
+ $this->assertTrue( $ok, "runLegacyHooks should have returned true" );
+ $this->assertEquals( "TEST TEXT", $content->getNativeData() );
+ }
+
+ public static function dummyHookHandler( $foo, &$text, $bar ) {
+ if ( $text === null || $text === false ) {
+ return false;
+ }
+
+ $text = strtoupper( $text );
+
+ return true;
+ }
+}
+
+class DummyContentHandlerForTesting extends ContentHandler {
+
+ public function __construct( $dataModel ) {
+ parent::__construct( $dataModel, array( "testing" ) );
+ }
+
+ /**
+ * @see ContentHandler::serializeContent
+ *
+ * @param Content $content
+ * @param string $format
+ *
+ * @return string
+ */
+ public function serializeContent( Content $content, $format = null ) {
+ return $content->serialize();
+ }
+
+ /**
+ * @see ContentHandler::unserializeContent
+ *
+ * @param string $blob
+ * @param string $format Unused.
+ *
+ * @return Content
+ */
+ public function unserializeContent( $blob, $format = null ) {
+ $d = unserialize( $blob );
+
+ return new DummyContentForTesting( $d );
+ }
+
+ /**
+ * Creates an empty Content object of the type supported by this ContentHandler.
+ *
+ */
+ public function makeEmptyContent() {
+ return new DummyContentForTesting( '' );
+ }
+}
+
+class DummyContentForTesting extends AbstractContent {
+
+ public function __construct( $data ) {
+ parent::__construct( "testing" );
+
+ $this->data = $data;
+ }
+
+ public function serialize( $format = null ) {
+ return serialize( $this->data );
+ }
+
+ /**
+ * @return string A string representing the content in a way useful for
+ * building a full text search index. If no useful representation exists,
+ * this method returns an empty string.
+ */
+ public function getTextForSearchIndex() {
+ return '';
+ }
+
+ /**
+ * @return string|bool The wikitext to include when another page includes this content,
+ * or false if the content is not includable in a wikitext page.
+ */
+ public function getWikitextForTransclusion() {
+ return false;
+ }
+
+ /**
+ * Returns a textual representation of the content suitable for use in edit
+ * summaries and log messages.
+ *
+ * @param int $maxlength Maximum length of the summary text.
+ * @return string The summary text.
+ */
+ public function getTextForSummary( $maxlength = 250 ) {
+ return '';
+ }
+
+ /**
+ * Returns native represenation of the data. Interpretation depends on the data model used,
+ * as given by getDataModel().
+ *
+ * @return mixed The native representation of the content. Could be a string, a nested array
+ * structure, an object, a binary blob... anything, really.
+ */
+ public function getNativeData() {
+ return $this->data;
+ }
+
+ /**
+ * returns the content's nominal size in bogo-bytes.
+ *
+ * @return int
+ */
+ public function getSize() {
+ return strlen( $this->data );
+ }
+
+ /**
+ * Return a copy of this Content object. The following must be true for the object returned
+ * if $copy = $original->copy()
+ *
+ * * get_class($original) === get_class($copy)
+ * * $original->getModel() === $copy->getModel()
+ * * $original->equals( $copy )
+ *
+ * If and only if the Content object is imutable, the copy() method can and should
+ * return $this. That is, $copy === $original may be true, but only for imutable content
+ * objects.
+ *
+ * @return Content A copy of this object
+ */
+ public function copy() {
+ return $this;
+ }
+
+ /**
+ * Returns true if this content is countable as a "real" wiki page, provided
+ * that it's also in a countable location (e.g. a current revision in the main namespace).
+ *
+ * @param bool $hasLinks If it is known whether this content contains links,
+ * provide this information here, to avoid redundant parsing to find out.
+ * @return bool
+ */
+ public function isCountable( $hasLinks = null ) {
+ return false;
+ }
+
+ /**
+ * @param Title $title
+ * @param int $revId Unused.
+ * @param null|ParserOptions $options
+ * @param bool $generateHtml Whether to generate Html (default: true). If false, the result
+ * of calling getText() on the ParserOutput object returned by this method is undefined.
+ *
+ * @return ParserOutput
+ */
+ public function getParserOutput( Title $title, $revId = null,
+ ParserOptions $options = null, $generateHtml = true
+ ) {
+ return new ParserOutput( $this->getNativeData() );
+ }
+
+ /**
+ * @see AbstractContent::fillParserOutput()
+ *
+ * @param Title $title Context title for parsing
+ * @param int|null $revId Revision ID (for {{REVISIONID}})
+ * @param ParserOptions $options Parser options
+ * @param bool $generateHtml Whether or not to generate HTML
+ * @param ParserOutput &$output The output object to fill (reference).
+ */
+ protected function fillParserOutput( Title $title, $revId,
+ ParserOptions $options, $generateHtml, ParserOutput &$output ) {
+ $output = new ParserOutput( $this->getNativeData() );
+ }
+}
diff --git a/tests/phpunit/includes/content/CssContentTest.php b/tests/phpunit/includes/content/CssContentTest.php
new file mode 100644
index 00000000..40484d3a
--- /dev/null
+++ b/tests/phpunit/includes/content/CssContentTest.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- needed, because we do need the database to test link updates
+ */
+class CssContentTest extends JavaScriptContentTest {
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Anon user
+ $user = new User();
+ $user->setName( '127.0.0.1' );
+
+ $this->setMwGlobals( array(
+ 'wgUser' => $user,
+ 'wgTextModelsToParse' => array(
+ CONTENT_MODEL_CSS,
+ )
+ ) );
+ }
+
+ public function newContent( $text ) {
+ return new CssContent( $text );
+ }
+
+ public static function dataGetParserOutput() {
+ return array(
+ array(
+ 'MediaWiki:Test.css',
+ null,
+ "hello <world>\n",
+ "<pre class=\"mw-code mw-css\" dir=\"ltr\">\nhello &lt;world&gt;\n\n</pre>"
+ ),
+ array(
+ 'MediaWiki:Test.css',
+ null,
+ "/* hello [[world]] */\n",
+ "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n/* hello [[world]] */\n\n</pre>",
+ array(
+ 'Links' => array(
+ array( 'World' => 0 )
+ )
+ )
+ ),
+
+ // TODO: more...?
+ );
+ }
+
+ /**
+ * @covers CssContent::getModel
+ */
+ public function testGetModel() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( CONTENT_MODEL_CSS, $content->getModel() );
+ }
+
+ /**
+ * @covers CssContent::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( CONTENT_MODEL_CSS, $content->getContentHandler()->getModelID() );
+ }
+
+ public static function dataEquals() {
+ return array(
+ array( new CssContent( 'hallo' ), null, false ),
+ array( new CssContent( 'hallo' ), new CssContent( 'hallo' ), true ),
+ array( new CssContent( 'hallo' ), new WikitextContent( 'hallo' ), false ),
+ array( new CssContent( 'hallo' ), new CssContent( 'HALLO' ), false ),
+ );
+ }
+
+ /**
+ * @dataProvider dataEquals
+ * @covers CssContent::equals
+ */
+ public function testEquals( Content $a, Content $b = null, $equal = false ) {
+ $this->assertEquals( $equal, $a->equals( $b ) );
+ }
+}
diff --git a/tests/phpunit/includes/content/JavaScriptContentTest.php b/tests/phpunit/includes/content/JavaScriptContentTest.php
new file mode 100644
index 00000000..7193ec9f
--- /dev/null
+++ b/tests/phpunit/includes/content/JavaScriptContentTest.php
@@ -0,0 +1,293 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- needed, because we do need the database to test link updates
+ */
+class JavaScriptContentTest extends TextContentTest {
+
+ public function newContent( $text ) {
+ return new JavaScriptContent( $text );
+ }
+
+ public static function dataGetParserOutput() {
+ return array(
+ array(
+ 'MediaWiki:Test.js',
+ null,
+ "hello <world>\n",
+ "<pre class=\"mw-code mw-js\" dir=\"ltr\">\nhello &lt;world&gt;\n\n</pre>"
+ ),
+ array(
+ 'MediaWiki:Test.js',
+ null,
+ "hello(); // [[world]]\n",
+ "<pre class=\"mw-code mw-js\" dir=\"ltr\">\nhello(); // [[world]]\n\n</pre>",
+ array(
+ 'Links' => array(
+ array( 'World' => 0 )
+ )
+ )
+ ),
+
+ // TODO: more...?
+ );
+ }
+
+ // XXX: Unused function
+ public static function dataGetSection() {
+ return array(
+ array( WikitextContentTest::$sections,
+ '0',
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '2',
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '8',
+ null
+ ),
+ );
+ }
+
+ // XXX: Unused function
+ public static function dataReplaceSection() {
+ return array(
+ array( WikitextContentTest::$sections,
+ '0',
+ 'No more',
+ null,
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '',
+ 'No more',
+ null,
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '2',
+ "== TEST ==\nmore fun",
+ null,
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ '8',
+ 'No more',
+ null,
+ null
+ ),
+ array( WikitextContentTest::$sections,
+ 'new',
+ 'No more',
+ 'New',
+ null
+ ),
+ );
+ }
+
+ /**
+ * @covers JavaScriptContent::addSectionHeader
+ */
+ public function testAddSectionHeader() {
+ $content = $this->newContent( 'hello world' );
+ $c = $content->addSectionHeader( 'test' );
+
+ $this->assertTrue( $content->equals( $c ) );
+ }
+
+ // XXX: currently, preSaveTransform is applied to scripts. this may change or become optional.
+ public static function dataPreSaveTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+ ),
+ array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ ),
+ array( " Foo \n ",
+ " Foo",
+ ),
+ );
+ }
+
+ public static function dataPreloadTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ 'hello this is ~~~',
+ ),
+ array( 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
+ 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
+ ),
+ );
+ }
+
+ public static function dataGetRedirectTarget() {
+ return array(
+ array( '#REDIRECT [[Test]]',
+ null,
+ ),
+ array( '#REDIRECT Test',
+ null,
+ ),
+ array( '* #REDIRECT [[Test]]',
+ null,
+ ),
+ );
+ }
+
+ /**
+ * @todo Test needs database!
+ */
+ /*
+ public function getRedirectChain() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectArray( $text );
+ }
+ */
+
+ /**
+ * @todo Test needs database!
+ */
+ /*
+ public function getUltimateRedirectTarget() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectRecurse( $text );
+ }
+ */
+
+ public static function dataIsCountable() {
+ return array(
+ array( '',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'comma',
+ false
+ ),
+ array( 'Foo, bar',
+ null,
+ 'comma',
+ false
+ ),
+ array( 'Foo',
+ null,
+ 'link',
+ false
+ ),
+ array( 'Foo [[bar]]',
+ null,
+ 'link',
+ false
+ ),
+ array( 'Foo',
+ true,
+ 'link',
+ false
+ ),
+ array( 'Foo [[bar]]',
+ false,
+ 'link',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'any',
+ true
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'comma',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'link',
+ false
+ ),
+ );
+ }
+
+ public static function dataGetTextForSummary() {
+ return array(
+ array( "hello\nworld.",
+ 16,
+ 'hello world.',
+ ),
+ array( 'hello world.',
+ 8,
+ 'hello...',
+ ),
+ array( '[[hello world]].',
+ 8,
+ '[[hel...',
+ ),
+ );
+ }
+
+ /**
+ * @covers JavaScriptContent::matchMagicWord
+ */
+ public function testMatchMagicWord() {
+ $mw = MagicWord::get( "staticredirect" );
+
+ $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" );
+ $this->assertFalse(
+ $content->matchMagicWord( $mw ),
+ "should not have matched magic word, since it's not wikitext"
+ );
+ }
+
+ /**
+ * @covers JavaScriptContent::updateRedirect
+ */
+ public function testUpdateRedirect() {
+ $target = Title::newFromText( "testUpdateRedirect_target" );
+
+ $content = $this->newContent( "#REDIRECT [[Someplace]]" );
+ $newContent = $content->updateRedirect( $target );
+
+ $this->assertTrue(
+ $content->equals( $newContent ),
+ "content should be unchanged since it's not wikitext"
+ );
+ }
+
+ /**
+ * @covers JavaScriptContent::getModel
+ */
+ public function testGetModel() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getModel() );
+ }
+
+ /**
+ * @covers JavaScriptContent::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getContentHandler()->getModelID() );
+ }
+
+ public static function dataEquals() {
+ return array(
+ array( new JavaScriptContent( "hallo" ), null, false ),
+ array( new JavaScriptContent( "hallo" ), new JavaScriptContent( "hallo" ), true ),
+ array( new JavaScriptContent( "hallo" ), new CssContent( "hallo" ), false ),
+ array( new JavaScriptContent( "hallo" ), new JavaScriptContent( "HALLO" ), false ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/content/JsonContentTest.php b/tests/phpunit/includes/content/JsonContentTest.php
new file mode 100644
index 00000000..77b542f4
--- /dev/null
+++ b/tests/phpunit/includes/content/JsonContentTest.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * @author Adam Shorland
+ * @covers JsonContent
+ */
+class JsonContentTest extends MediaWikiLangTestCase {
+
+ /**
+ * @dataProvider provideValidConstruction
+ */
+ public function testValidConstruct( $text, $modelId, $isValid, $expected ) {
+ $obj = new JsonContent( $text, $modelId );
+ $this->assertEquals( $isValid, $obj->isValid() );
+ $this->assertEquals( $expected, $obj->getJsonData() );
+ }
+
+ public static function provideValidConstruction() {
+ return array(
+ array( 'foo', CONTENT_MODEL_JSON, false, null ),
+ array( FormatJson::encode( array() ), CONTENT_MODEL_JSON, true, array() ),
+ array( FormatJson::encode( array( 'foo' ) ), CONTENT_MODEL_JSON, true, array( 'foo' ) ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDataToEncode
+ */
+ public function testBeautifyUsesFormatJson( $data ) {
+ $obj = new JsonContent( FormatJson::encode( $data ) );
+ $this->assertEquals( FormatJson::encode( $data, true ), $obj->beautifyJSON() );
+ }
+
+ public static function provideDataToEncode() {
+ return array(
+ array( array() ),
+ array( array( 'foo' ) ),
+ array( array( 'foo', 'bar' ) ),
+ array( array( 'baz' => 'foo', 'bar' ) ),
+ array( array( 'baz' => 1000, 'bar' ) ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDataToEncode
+ */
+ public function testPreSaveTransform( $data ) {
+ $obj = new JsonContent( FormatJson::encode( $data ) );
+ $newObj = $obj->preSaveTransform( $this->getMockTitle(), $this->getMockUser(), $this->getMockParserOptions() );
+ $this->assertTrue( $newObj->equals( new JsonContent( FormatJson::encode( $data, true ) ) ) );
+ }
+
+ private function getMockTitle() {
+ return $this->getMockBuilder( 'Title' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ private function getMockUser() {
+ return $this->getMockBuilder( 'User' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+ private function getMockParserOptions() {
+ return $this->getMockBuilder( 'ParserOptions' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ /**
+ * @dataProvider provideDataAndParserText
+ */
+ public function testFillParserOutput( $data, $expected ) {
+ $obj = new JsonContent( FormatJson::encode( $data ) );
+ $parserOutput = $obj->getParserOutput( $this->getMockTitle(), null, null, true );
+ $this->assertInstanceOf( 'ParserOutput', $parserOutput );
+ $this->assertEquals( $expected, $parserOutput->getText() );
+ }
+
+ public static function provideDataAndParserText() {
+ return array(
+ array(
+ array(),
+ '<table class="mw-json"><tbody></tbody></table>'
+ ),
+ array(
+ array( 'foo' ),
+ '<table class="mw-json"><tbody><tr><th>0</th><td class="value">&quot;foo&quot;</td></tr></tbody></table>'
+ ),
+ array(
+ array( 'foo', 'bar' ),
+ '<table class="mw-json"><tbody><tr><th>0</th><td class="value">&quot;foo&quot;</td></tr>' .
+ "\n" .
+ '<tr><th>1</th><td class="value">&quot;bar&quot;</td></tr></tbody></table>'
+ ),
+ array(
+ array( 'baz' => 'foo', 'bar' ),
+ '<table class="mw-json"><tbody><tr><th>baz</th><td class="value">&quot;foo&quot;</td></tr>' .
+ "\n" .
+ '<tr><th>0</th><td class="value">&quot;bar&quot;</td></tr></tbody></table>'
+ ),
+ array(
+ array( 'baz' => 1000, 'bar' ),
+ '<table class="mw-json"><tbody><tr><th>baz</th><td class="value">1000</td></tr>' .
+ "\n" .
+ '<tr><th>0</th><td class="value">&quot;bar&quot;</td></tr></tbody></table>'
+ ),
+ array(
+ array( '<script>alert("evil!")</script>'),
+ '<table class="mw-json"><tbody><tr><th>0</th><td class="value">&quot;&lt;script&gt;alert(&quot;evil!&quot;)&lt;/script&gt;&quot;</td></tr></tbody></table>',
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/content/TextContentTest.php b/tests/phpunit/includes/content/TextContentTest.php
new file mode 100644
index 00000000..2f811094
--- /dev/null
+++ b/tests/phpunit/includes/content/TextContentTest.php
@@ -0,0 +1,490 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- needed, because we do need the database to test link updates
+ */
+class TextContentTest extends MediaWikiLangTestCase {
+ protected $context;
+ protected $savedContentGetParserOutput;
+
+ protected function setUp() {
+ global $wgHooks;
+
+ parent::setUp();
+
+ // Anon user
+ $user = new User();
+ $user->setName( '127.0.0.1' );
+
+ $this->context = new RequestContext( new FauxRequest() );
+ $this->context->setTitle( Title::newFromText( 'Test' ) );
+ $this->context->setUser( $user );
+
+ $this->setMwGlobals( array(
+ 'wgUser' => $user,
+ 'wgTextModelsToParse' => array(
+ CONTENT_MODEL_WIKITEXT,
+ CONTENT_MODEL_CSS,
+ CONTENT_MODEL_JAVASCRIPT,
+ ),
+ 'wgUseTidy' => false,
+ 'wgAlwaysUseTidy' => false,
+ 'wgCapitalLinks' => true,
+ ) );
+
+ // bypass hooks that force custom rendering
+ if ( isset( $wgHooks['ContentGetParserOutput'] ) ) {
+ $this->savedContentGetParserOutput = $wgHooks['ContentGetParserOutput'];
+ unset( $wgHooks['ContentGetParserOutput'] );
+ }
+ }
+
+ public function teardown() {
+ global $wgHooks;
+
+ // restore hooks that force custom rendering
+ if ( $this->savedContentGetParserOutput !== null ) {
+ $wgHooks['ContentGetParserOutput'] = $this->savedContentGetParserOutput;
+ }
+
+ parent::teardown();
+ }
+
+ public function newContent( $text ) {
+ return new TextContent( $text );
+ }
+
+ public static function dataGetParserOutput() {
+ return array(
+ array(
+ 'TextContentTest_testGetParserOutput',
+ CONTENT_MODEL_TEXT,
+ "hello ''world'' & [[stuff]]\n", "hello ''world'' &amp; [[stuff]]",
+ array(
+ 'Links' => array()
+ )
+ ),
+ // TODO: more...?
+ );
+ }
+
+ /**
+ * @dataProvider dataGetParserOutput
+ * @covers TextContent::getParserOutput
+ */
+ public function testGetParserOutput( $title, $model, $text, $expectedHtml,
+ $expectedFields = null
+ ) {
+ $title = Title::newFromText( $title );
+ $content = ContentHandler::makeContent( $text, $title, $model );
+
+ $po = $content->getParserOutput( $title );
+
+ $html = $po->getText();
+ $html = preg_replace( '#<!--.*?-->#sm', '', $html ); // strip comments
+
+ $this->assertEquals( $expectedHtml, trim( $html ) );
+
+ if ( $expectedFields ) {
+ foreach ( $expectedFields as $field => $exp ) {
+ $f = 'get' . ucfirst( $field );
+ $v = call_user_func( array( $po, $f ) );
+
+ if ( is_array( $exp ) ) {
+ $this->assertArrayEquals( $exp, $v );
+ } else {
+ $this->assertEquals( $exp, $v );
+ }
+ }
+ }
+
+ // TODO: assert more properties
+ }
+
+ public static function dataPreSaveTransform() {
+ return array(
+ array(
+ #0: no signature resolution
+ 'hello this is ~~~',
+ 'hello this is ~~~',
+ ),
+ array(
+ #1: rtrim
+ " Foo \n ",
+ ' Foo',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataPreSaveTransform
+ * @covers TextContent::preSaveTransform
+ */
+ public function testPreSaveTransform( $text, $expected ) {
+ global $wgContLang;
+
+ $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang );
+
+ $content = $this->newContent( $text );
+ $content = $content->preSaveTransform(
+ $this->context->getTitle(),
+ $this->context->getUser(),
+ $options
+ );
+
+ $this->assertEquals( $expected, $content->getNativeData() );
+ }
+
+ public static function dataPreloadTransform() {
+ return array(
+ array(
+ 'hello this is ~~~',
+ 'hello this is ~~~',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataPreloadTransform
+ * @covers TextContent::preloadTransform
+ */
+ public function testPreloadTransform( $text, $expected ) {
+ global $wgContLang;
+ $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang );
+
+ $content = $this->newContent( $text );
+ $content = $content->preloadTransform( $this->context->getTitle(), $options );
+
+ $this->assertEquals( $expected, $content->getNativeData() );
+ }
+
+ public static function dataGetRedirectTarget() {
+ return array(
+ array( '#REDIRECT [[Test]]',
+ null,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetRedirectTarget
+ * @covers TextContent::getRedirectTarget
+ */
+ public function testGetRedirectTarget( $text, $expected ) {
+ $content = $this->newContent( $text );
+ $t = $content->getRedirectTarget();
+
+ if ( is_null( $expected ) ) {
+ $this->assertNull( $t, "text should not have generated a redirect target: $text" );
+ } else {
+ $this->assertEquals( $expected, $t->getPrefixedText() );
+ }
+ }
+
+ /**
+ * @dataProvider dataGetRedirectTarget
+ * @covers TextContent::isRedirect
+ */
+ public function testIsRedirect( $text, $expected ) {
+ $content = $this->newContent( $text );
+
+ $this->assertEquals( !is_null( $expected ), $content->isRedirect() );
+ }
+
+ /**
+ * @todo Test needs database! Should be done by a test class in the Database group.
+ */
+ /*
+ public function getRedirectChain() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectArray( $text );
+ }
+ */
+
+ /**
+ * @todo Test needs database! Should be done by a test class in the Database group.
+ */
+ /*
+ public function getUltimateRedirectTarget() {
+ $text = $this->getNativeData();
+ return Title::newFromRedirectRecurse( $text );
+ }
+ */
+
+ public static function dataIsCountable() {
+ return array(
+ array( '',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'comma',
+ false
+ ),
+ array( 'Foo, bar',
+ null,
+ 'comma',
+ false
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataIsCountable
+ * @group Database
+ * @covers TextContent::isCountable
+ */
+ public function testIsCountable( $text, $hasLinks, $mode, $expected ) {
+ $this->setMwGlobals( 'wgArticleCountMethod', $mode );
+
+ $content = $this->newContent( $text );
+
+ $v = $content->isCountable( $hasLinks, $this->context->getTitle() );
+
+ $this->assertEquals(
+ $expected,
+ $v,
+ 'isCountable() returned unexpected value ' . var_export( $v, true )
+ . ' instead of ' . var_export( $expected, true )
+ . " in mode `$mode` for text \"$text\""
+ );
+ }
+
+ public static function dataGetTextForSummary() {
+ return array(
+ array( "hello\nworld.",
+ 16,
+ 'hello world.',
+ ),
+ array( 'hello world.',
+ 8,
+ 'hello...',
+ ),
+ array( '[[hello world]].',
+ 8,
+ '[[hel...',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetTextForSummary
+ * @covers TextContent::getTextForSummary
+ */
+ public function testGetTextForSummary( $text, $maxlength, $expected ) {
+ $content = $this->newContent( $text );
+
+ $this->assertEquals( $expected, $content->getTextForSummary( $maxlength ) );
+ }
+
+ /**
+ * @covers TextContent::getTextForSearchIndex
+ */
+ public function testGetTextForSearchIndex() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 'hello world.', $content->getTextForSearchIndex() );
+ }
+
+ /**
+ * @covers TextContent::copy
+ */
+ public function testCopy() {
+ $content = $this->newContent( 'hello world.' );
+ $copy = $content->copy();
+
+ $this->assertTrue( $content->equals( $copy ), 'copy must be equal to original' );
+ $this->assertEquals( 'hello world.', $copy->getNativeData() );
+ }
+
+ /**
+ * @covers TextContent::getSize
+ */
+ public function testGetSize() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 12, $content->getSize() );
+ }
+
+ /**
+ * @covers TextContent::getNativeData
+ */
+ public function testGetNativeData() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 'hello world.', $content->getNativeData() );
+ }
+
+ /**
+ * @covers TextContent::getWikitextForTransclusion
+ */
+ public function testGetWikitextForTransclusion() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 'hello world.', $content->getWikitextForTransclusion() );
+ }
+
+ /**
+ * @covers TextContent::getModel
+ */
+ public function testGetModel() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_TEXT, $content->getModel() );
+ }
+
+ /**
+ * @covers TextContent::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_TEXT, $content->getContentHandler()->getModelID() );
+ }
+
+ public static function dataIsEmpty() {
+ return array(
+ array( '', true ),
+ array( ' ', false ),
+ array( '0', false ),
+ array( 'hallo welt.', false ),
+ );
+ }
+
+ /**
+ * @dataProvider dataIsEmpty
+ * @covers TextContent::isEmpty
+ */
+ public function testIsEmpty( $text, $empty ) {
+ $content = $this->newContent( $text );
+
+ $this->assertEquals( $empty, $content->isEmpty() );
+ }
+
+ public static function dataEquals() {
+ return array(
+ array( new TextContent( "hallo" ), null, false ),
+ array( new TextContent( "hallo" ), new TextContent( "hallo" ), true ),
+ array( new TextContent( "hallo" ), new JavaScriptContent( "hallo" ), false ),
+ array( new TextContent( "hallo" ), new WikitextContent( "hallo" ), false ),
+ array( new TextContent( "hallo" ), new TextContent( "HALLO" ), false ),
+ );
+ }
+
+ /**
+ * @dataProvider dataEquals
+ * @covers TextContent::equals
+ */
+ public function testEquals( Content $a, Content $b = null, $equal = false ) {
+ $this->assertEquals( $equal, $a->equals( $b ) );
+ }
+
+ public static function dataGetDeletionUpdates() {
+ return array(
+ array( "TextContentTest_testGetSecondaryDataUpdates_1",
+ CONTENT_MODEL_TEXT, "hello ''world''\n",
+ array()
+ ),
+ array( "TextContentTest_testGetSecondaryDataUpdates_2",
+ CONTENT_MODEL_TEXT, "hello [[world test 21344]]\n",
+ array()
+ ),
+ // TODO: more...?
+ );
+ }
+
+ /**
+ * @dataProvider dataGetDeletionUpdates
+ * @covers TextContent::getDeletionUpdates
+ */
+ public function testDeletionUpdates( $title, $model, $text, $expectedStuff ) {
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( $title, $ns );
+
+ $content = ContentHandler::makeContent( $text, $title, $model );
+
+ $page = WikiPage::factory( $title );
+ $page->doEditContent( $content, '' );
+
+ $updates = $content->getDeletionUpdates( $page );
+
+ // make updates accessible by class name
+ foreach ( $updates as $update ) {
+ $class = get_class( $update );
+ $updates[$class] = $update;
+ }
+
+ if ( !$expectedStuff ) {
+ $this->assertTrue( true ); // make phpunit happy
+ return;
+ }
+
+ foreach ( $expectedStuff as $class => $fieldValues ) {
+ $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" );
+
+ $update = $updates[$class];
+
+ foreach ( $fieldValues as $field => $value ) {
+ $v = $update->$field; #if the field doesn't exist, just crash and burn
+ $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" );
+ }
+ }
+
+ $page->doDeleteArticle( '' );
+ }
+
+ public static function provideConvert() {
+ return array(
+ array( // #0
+ 'Hallo Welt',
+ CONTENT_MODEL_WIKITEXT,
+ 'lossless',
+ 'Hallo Welt'
+ ),
+ array( // #1
+ 'Hallo Welt',
+ CONTENT_MODEL_WIKITEXT,
+ 'lossless',
+ 'Hallo Welt'
+ ),
+ array( // #1
+ 'Hallo Welt',
+ CONTENT_MODEL_CSS,
+ 'lossless',
+ 'Hallo Welt'
+ ),
+ array( // #1
+ 'Hallo Welt',
+ CONTENT_MODEL_JAVASCRIPT,
+ 'lossless',
+ 'Hallo Welt'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideConvert
+ * @covers TextContent::convert
+ */
+ public function testConvert( $text, $model, $lossy, $expectedNative ) {
+ $content = $this->newContent( $text );
+
+ $converted = $content->convert( $model, $lossy );
+
+ if ( $expectedNative === false ) {
+ $this->assertFalse( $converted, "conversion to $model was expected to fail!" );
+ } else {
+ $this->assertInstanceOf( 'Content', $converted );
+ $this->assertEquals( $expectedNative, $converted->getNativeData() );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/content/WikitextContentHandlerTest.php b/tests/phpunit/includes/content/WikitextContentHandlerTest.php
new file mode 100644
index 00000000..38fb5733
--- /dev/null
+++ b/tests/phpunit/includes/content/WikitextContentHandlerTest.php
@@ -0,0 +1,241 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+class WikitextContentHandlerTest extends MediaWikiLangTestCase {
+ /**
+ * @var ContentHandler
+ */
+ private $handler;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
+ }
+
+ /**
+ * @covers WikitextContentHandler::serializeContent
+ */
+ public function testSerializeContent() {
+ $content = new WikitextContent( 'hello world' );
+
+ $this->assertEquals( 'hello world', $this->handler->serializeContent( $content ) );
+ $this->assertEquals(
+ 'hello world',
+ $this->handler->serializeContent( $content, CONTENT_FORMAT_WIKITEXT )
+ );
+
+ try {
+ $this->handler->serializeContent( $content, 'dummy/foo' );
+ $this->fail( "serializeContent() should have failed on unknown format" );
+ } catch ( MWException $e ) {
+ // ok, as expected
+ }
+ }
+
+ /**
+ * @covers WikitextContentHandler::unserializeContent
+ */
+ public function testUnserializeContent() {
+ $content = $this->handler->unserializeContent( 'hello world' );
+ $this->assertEquals( 'hello world', $content->getNativeData() );
+
+ $content = $this->handler->unserializeContent( 'hello world', CONTENT_FORMAT_WIKITEXT );
+ $this->assertEquals( 'hello world', $content->getNativeData() );
+
+ try {
+ $this->handler->unserializeContent( 'hello world', 'dummy/foo' );
+ $this->fail( "unserializeContent() should have failed on unknown format" );
+ } catch ( MWException $e ) {
+ // ok, as expected
+ }
+ }
+
+ /**
+ * @covers WikitextContentHandler::makeEmptyContent
+ */
+ public function testMakeEmptyContent() {
+ $content = $this->handler->makeEmptyContent();
+
+ $this->assertTrue( $content->isEmpty() );
+ $this->assertEquals( '', $content->getNativeData() );
+ }
+
+ public static function dataIsSupportedFormat() {
+ return array(
+ array( null, true ),
+ array( CONTENT_FORMAT_WIKITEXT, true ),
+ array( 99887766, false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideMakeRedirectContent
+ * @param Title|string $title Title object or string for Title::newFromText()
+ * @param string $expected Serialized form of the content object built
+ * @covers WikitextContentHandler::makeRedirectContent
+ */
+ public function testMakeRedirectContent( $title, $expected ) {
+ global $wgContLang;
+ $wgContLang->resetNamespaces();
+
+ MagicWord::clearCache();
+
+ if ( is_string( $title ) ) {
+ $title = Title::newFromText( $title );
+ }
+ $content = $this->handler->makeRedirectContent( $title );
+ $this->assertEquals( $expected, $content->serialize() );
+ }
+
+ public static function provideMakeRedirectContent() {
+ return array(
+ array( 'Hello', '#REDIRECT [[Hello]]' ),
+ array( 'Template:Hello', '#REDIRECT [[Template:Hello]]' ),
+ array( 'Hello#section', '#REDIRECT [[Hello#section]]' ),
+ array( 'user:john_doe#section', '#REDIRECT [[User:John doe#section]]' ),
+ array( 'MEDIAWIKI:FOOBAR', '#REDIRECT [[MediaWiki:FOOBAR]]' ),
+ array( 'Category:Foo', '#REDIRECT [[:Category:Foo]]' ),
+ array( Title::makeTitle( NS_MAIN, 'en:Foo' ), '#REDIRECT [[en:Foo]]' ),
+ array( Title::makeTitle( NS_MAIN, 'Foo', '', 'en' ), '#REDIRECT [[:en:Foo]]' ),
+ array(
+ Title::makeTitle( NS_MAIN, 'Bar', 'fragment', 'google' ),
+ '#REDIRECT [[google:Bar#fragment]]'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataIsSupportedFormat
+ * @covers WikitextContentHandler::isSupportedFormat
+ */
+ public function testIsSupportedFormat( $format, $supported ) {
+ $this->assertEquals( $supported, $this->handler->isSupportedFormat( $format ) );
+ }
+
+ public static function dataMerge3() {
+ return array(
+ array(
+ "first paragraph
+
+ second paragraph\n",
+
+ "FIRST paragraph
+
+ second paragraph\n",
+
+ "first paragraph
+
+ SECOND paragraph\n",
+
+ "FIRST paragraph
+
+ SECOND paragraph\n",
+ ),
+
+ array( "first paragraph
+ second paragraph\n",
+
+ "Bla bla\n",
+
+ "Blubberdibla\n",
+
+ false,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataMerge3
+ * @covers WikitextContentHandler::merge3
+ */
+ public function testMerge3( $old, $mine, $yours, $expected ) {
+ $this->checkHasDiff3();
+
+ // test merge
+ $oldContent = new WikitextContent( $old );
+ $myContent = new WikitextContent( $mine );
+ $yourContent = new WikitextContent( $yours );
+
+ $merged = $this->handler->merge3( $oldContent, $myContent, $yourContent );
+
+ $this->assertEquals( $expected, $merged ? $merged->getNativeData() : $merged );
+ }
+
+ public static function dataGetAutosummary() {
+ return array(
+ array(
+ 'Hello there, world!',
+ '#REDIRECT [[Foo]]',
+ 0,
+ '/^Redirected page .*Foo/'
+ ),
+
+ array(
+ null,
+ 'Hello world!',
+ EDIT_NEW,
+ '/^Created page .*Hello/'
+ ),
+
+ array(
+ 'Hello there, world!',
+ '',
+ 0,
+ '/^Blanked/'
+ ),
+
+ array(
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
+ eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
+ voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
+ clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.',
+ 'Hello world!',
+ 0,
+ '/^Replaced .*Hello/'
+ ),
+
+ array(
+ 'foo',
+ 'bar',
+ 0,
+ '/^$/'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetAutosummary
+ * @covers WikitextContentHandler::getAutosummary
+ */
+ public function testGetAutosummary( $old, $new, $flags, $expected ) {
+ $oldContent = is_null( $old ) ? null : new WikitextContent( $old );
+ $newContent = is_null( $new ) ? null : new WikitextContent( $new );
+
+ $summary = $this->handler->getAutosummary( $oldContent, $newContent, $flags );
+
+ $this->assertTrue(
+ (bool)preg_match( $expected, $summary ),
+ "Autosummary didn't match expected pattern $expected: $summary"
+ );
+ }
+
+ /**
+ * @todo Text case requires database, should be done by a test class in the Database group
+ */
+ /*
+ public function testGetAutoDeleteReason( Title $title, &$hasHistory ) {}
+ */
+
+ /**
+ * @todo Text case requires database, should be done by a test class in the Database group
+ */
+ /*
+ public function testGetUndoContent( Revision $current, Revision $undo,
+ Revision $undoafter = null
+ ) {
+ }
+ */
+}
diff --git a/tests/phpunit/includes/content/WikitextContentTest.php b/tests/phpunit/includes/content/WikitextContentTest.php
new file mode 100644
index 00000000..7becd6f4
--- /dev/null
+++ b/tests/phpunit/includes/content/WikitextContentTest.php
@@ -0,0 +1,433 @@
+<?php
+
+/**
+ * @group ContentHandler
+ *
+ * @group Database
+ * ^--- needed, because we do need the database to test link updates
+ */
+class WikitextContentTest extends TextContentTest {
+ public static $sections = "Intro
+
+== stuff ==
+hello world
+
+== test ==
+just a test
+
+== foo ==
+more stuff
+";
+
+ public function newContent( $text ) {
+ return new WikitextContent( $text );
+ }
+
+ public static function dataGetParserOutput() {
+ return array(
+ array(
+ "WikitextContentTest_testGetParserOutput",
+ CONTENT_MODEL_WIKITEXT,
+ "hello ''world''\n",
+ "<p>hello <i>world</i>\n</p>"
+ ),
+ // TODO: more...?
+ );
+ }
+
+ public static function dataGetSecondaryDataUpdates() {
+ return array(
+ array( "WikitextContentTest_testGetSecondaryDataUpdates_1",
+ CONTENT_MODEL_WIKITEXT, "hello ''world''\n",
+ array(
+ 'LinksUpdate' => array(
+ 'mRecursive' => true,
+ 'mLinks' => array()
+ )
+ )
+ ),
+ array( "WikitextContentTest_testGetSecondaryDataUpdates_2",
+ CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n",
+ array(
+ 'LinksUpdate' => array(
+ 'mRecursive' => true,
+ 'mLinks' => array(
+ array( 'World_test_21344' => 0 )
+ )
+ )
+ )
+ ),
+ // TODO: more...?
+ );
+ }
+
+ /**
+ * @dataProvider dataGetSecondaryDataUpdates
+ * @group Database
+ * @covers WikitextContent::getSecondaryDataUpdates
+ */
+ public function testGetSecondaryDataUpdates( $title, $model, $text, $expectedStuff ) {
+ $ns = $this->getDefaultWikitextNS();
+ $title = Title::newFromText( $title, $ns );
+
+ $content = ContentHandler::makeContent( $text, $title, $model );
+
+ $page = WikiPage::factory( $title );
+ $page->doEditContent( $content, '' );
+
+ $updates = $content->getSecondaryDataUpdates( $title );
+
+ // make updates accessible by class name
+ foreach ( $updates as $update ) {
+ $class = get_class( $update );
+ $updates[$class] = $update;
+ }
+
+ foreach ( $expectedStuff as $class => $fieldValues ) {
+ $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" );
+
+ $update = $updates[$class];
+
+ foreach ( $fieldValues as $field => $value ) {
+ $v = $update->$field; #if the field doesn't exist, just crash and burn
+ $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" );
+ }
+ }
+
+ $page->doDeleteArticle( '' );
+ }
+
+ public static function dataGetSection() {
+ return array(
+ array( WikitextContentTest::$sections,
+ "0",
+ "Intro"
+ ),
+ array( WikitextContentTest::$sections,
+ "2",
+ "== test ==
+just a test"
+ ),
+ array( WikitextContentTest::$sections,
+ "8",
+ false
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataGetSection
+ * @covers WikitextContent::getSection
+ */
+ public function testGetSection( $text, $sectionId, $expectedText ) {
+ $content = $this->newContent( $text );
+
+ $sectionContent = $content->getSection( $sectionId );
+ if ( is_object( $sectionContent ) ) {
+ $sectionText = $sectionContent->getNativeData();
+ } else {
+ $sectionText = $sectionContent;
+ }
+
+ $this->assertEquals( $expectedText, $sectionText );
+ }
+
+ public static function dataReplaceSection() {
+ return array(
+ array( WikitextContentTest::$sections,
+ "0",
+ "No more",
+ null,
+ trim( preg_replace( '/^Intro/sm', 'No more', WikitextContentTest::$sections ) )
+ ),
+ array( WikitextContentTest::$sections,
+ "",
+ "No more",
+ null,
+ "No more"
+ ),
+ array( WikitextContentTest::$sections,
+ "2",
+ "== TEST ==\nmore fun",
+ null,
+ trim( preg_replace(
+ '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==",
+ WikitextContentTest::$sections
+ ) )
+ ),
+ array( WikitextContentTest::$sections,
+ "8",
+ "No more",
+ null,
+ WikitextContentTest::$sections
+ ),
+ array( WikitextContentTest::$sections,
+ "new",
+ "No more",
+ "New",
+ trim( WikitextContentTest::$sections ) . "\n\n\n== New ==\n\nNo more"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider dataReplaceSection
+ * @covers WikitextContent::replaceSection
+ */
+ public function testReplaceSection( $text, $section, $with, $sectionTitle, $expected ) {
+ $content = $this->newContent( $text );
+ $c = $content->replaceSection( $section, $this->newContent( $with ), $sectionTitle );
+
+ $this->assertEquals( $expected, is_null( $c ) ? null : $c->getNativeData() );
+ }
+
+ /**
+ * @covers WikitextContent::addSectionHeader
+ */
+ public function testAddSectionHeader() {
+ $content = $this->newContent( 'hello world' );
+ $content = $content->addSectionHeader( 'test' );
+
+ $this->assertEquals( "== test ==\n\nhello world", $content->getNativeData() );
+ }
+
+ public static function dataPreSaveTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+ ),
+ array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ ),
+ array( // rtrim
+ " Foo \n ",
+ " Foo",
+ ),
+ );
+ }
+
+ public static function dataPreloadTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is ~~~",
+ ),
+ array( 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
+ 'hello \'\'this\'\' is bar',
+ ),
+ );
+ }
+
+ public static function dataGetRedirectTarget() {
+ return array(
+ array( '#REDIRECT [[Test]]',
+ 'Test',
+ ),
+ array( '#REDIRECT Test',
+ null,
+ ),
+ array( '* #REDIRECT [[Test]]',
+ null,
+ ),
+ );
+ }
+
+ public static function dataGetTextForSummary() {
+ return array(
+ array( "hello\nworld.",
+ 16,
+ 'hello world.',
+ ),
+ array( 'hello world.',
+ 8,
+ 'hello...',
+ ),
+ array( '[[hello world]].',
+ 8,
+ 'hel...',
+ ),
+ );
+ }
+
+ public static function dataIsCountable() {
+ return array(
+ array( '',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'any',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'comma',
+ false
+ ),
+ array( 'Foo, bar',
+ null,
+ 'comma',
+ true
+ ),
+ array( 'Foo',
+ null,
+ 'link',
+ false
+ ),
+ array( 'Foo [[bar]]',
+ null,
+ 'link',
+ true
+ ),
+ array( 'Foo',
+ true,
+ 'link',
+ true
+ ),
+ array( 'Foo [[bar]]',
+ false,
+ 'link',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'any',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'comma',
+ false
+ ),
+ array( '#REDIRECT [[bar]]',
+ true,
+ 'link',
+ false
+ ),
+ );
+ }
+
+ /**
+ * @covers WikitextContent::matchMagicWord
+ */
+ public function testMatchMagicWord() {
+ $mw = MagicWord::get( "staticredirect" );
+
+ $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" );
+ $this->assertTrue( $content->matchMagicWord( $mw ), "should have matched magic word" );
+
+ $content = $this->newContent( "#REDIRECT [[FOO]]" );
+ $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word" );
+ }
+
+ /**
+ * @covers WikitextContent::updateRedirect
+ */
+ public function testUpdateRedirect() {
+ $target = Title::newFromText( "testUpdateRedirect_target" );
+
+ // test with non-redirect page
+ $content = $this->newContent( "hello world." );
+ $newContent = $content->updateRedirect( $target );
+
+ $this->assertTrue( $content->equals( $newContent ), "content should be unchanged" );
+
+ // test with actual redirect
+ $content = $this->newContent( "#REDIRECT [[Someplace]]" );
+ $newContent = $content->updateRedirect( $target );
+
+ $this->assertFalse( $content->equals( $newContent ), "content should have changed" );
+ $this->assertTrue( $newContent->isRedirect(), "new content should be a redirect" );
+
+ $this->assertEquals( $target->getFullText(), $newContent->getRedirectTarget()->getFullText() );
+ }
+
+ /**
+ * @covers WikitextContent::getModel
+ */
+ public function testGetModel() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getModel() );
+ }
+
+ /**
+ * @covers WikitextContent::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $content = $this->newContent( "hello world." );
+
+ $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelID() );
+ }
+
+ public function testRedirectParserOption() {
+ $title = Title::newFromText( 'testRedirectParserOption' );
+
+ // Set up hook and its reporting variables
+ $wikitext = null;
+ $redirectTarget = null;
+ $this->mergeMwGlobalArrayValue( 'wgHooks', array(
+ 'InternalParseBeforeLinks' => array(
+ function ( &$parser, &$text, &$stripState ) use ( &$wikitext, &$redirectTarget ) {
+ $wikitext = $text;
+ $redirectTarget = $parser->getOptions()->getRedirectTarget();
+ }
+ )
+ ) );
+
+ // Test with non-redirect page
+ $wikitext = false;
+ $redirectTarget = false;
+ $content = $this->newContent( 'hello world.' );
+ $options = $content->getContentHandler()->makeParserOptions( 'canonical' );
+ $options->setRedirectTarget( $title );
+ $content->getParserOutput( $title, null, $options );
+ $this->assertEquals( 'hello world.', $wikitext,
+ 'Wikitext passed to hook was not as expected'
+ );
+ $this->assertEquals( null, $redirectTarget, 'Redirect seen in hook was not null' );
+ $this->assertEquals( $title, $options->getRedirectTarget(),
+ 'ParserOptions\' redirectTarget was changed'
+ );
+
+ // Test with a redirect page
+ $wikitext = false;
+ $redirectTarget = false;
+ $content = $this->newContent( "#REDIRECT [[TestRedirectParserOption/redir]]\nhello redirect." );
+ $options = $content->getContentHandler()->makeParserOptions( 'canonical' );
+ $content->getParserOutput( $title, null, $options );
+ $this->assertEquals( 'hello redirect.', $wikitext, 'Wikitext passed to hook was not as expected' );
+ $this->assertNotEquals( null, $redirectTarget, 'Redirect seen in hook was null' );
+ $this->assertEquals( 'TestRedirectParserOption/redir', $redirectTarget->getFullText(),
+ 'Redirect seen in hook was not the expected title'
+ );
+ $this->assertEquals( null, $options->getRedirectTarget(),
+ 'ParserOptions\' redirectTarget was changed'
+ );
+ }
+
+ public static function dataEquals() {
+ return array(
+ array( new WikitextContent( "hallo" ), null, false ),
+ array( new WikitextContent( "hallo" ), new WikitextContent( "hallo" ), true ),
+ array( new WikitextContent( "hallo" ), new JavaScriptContent( "hallo" ), false ),
+ array( new WikitextContent( "hallo" ), new TextContent( "hallo" ), false ),
+ array( new WikitextContent( "hallo" ), new WikitextContent( "HALLO" ), false ),
+ );
+ }
+
+ public static function dataGetDeletionUpdates() {
+ return array(
+ array( "WikitextContentTest_testGetSecondaryDataUpdates_1",
+ CONTENT_MODEL_WIKITEXT, "hello ''world''\n",
+ array( 'LinksDeletionUpdate' => array() )
+ ),
+ array( "WikitextContentTest_testGetSecondaryDataUpdates_2",
+ CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n",
+ array( 'LinksDeletionUpdate' => array() )
+ ),
+ // @todo more...?
+ );
+ }
+}
diff --git a/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php b/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php
new file mode 100644
index 00000000..55e48d13
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php
@@ -0,0 +1,247 @@
+<?php
+/**
+ * Holds tests for DatabaseMysqlBase MediaWiki class.
+ *
+ * @section LICENSE
+ * 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
+ * @author Antoine Musso
+ * @author Bryan Davis
+ * @copyright © 2013 Antoine Musso
+ * @copyright © 2013 Bryan Davis
+ * @copyright © 2013 Wikimedia Foundation Inc.
+ */
+
+/**
+ * Fake class around abstract class so we can call concrete methods.
+ */
+class FakeDatabaseMysqlBase extends DatabaseMysqlBase {
+ // From DatabaseBase
+ function __construct() {
+ }
+
+ protected function closeConnection() {
+ }
+
+ protected function doQuery( $sql ) {
+ }
+
+ // From DatabaseMysql
+ protected function mysqlConnect( $realServer ) {
+ }
+
+ protected function mysqlSetCharset( $charset ) {
+ }
+
+ protected function mysqlFreeResult( $res ) {
+ }
+
+ protected function mysqlFetchObject( $res ) {
+ }
+
+ protected function mysqlFetchArray( $res ) {
+ }
+
+ protected function mysqlNumRows( $res ) {
+ }
+
+ protected function mysqlNumFields( $res ) {
+ }
+
+ protected function mysqlFieldName( $res, $n ) {
+ }
+
+ protected function mysqlFieldType( $res, $n ) {
+ }
+
+ protected function mysqlDataSeek( $res, $row ) {
+ }
+
+ protected function mysqlError( $conn = null ) {
+ }
+
+ protected function mysqlFetchField( $res, $n ) {
+ }
+
+ protected function mysqlPing() {
+ }
+
+ // From interface DatabaseType
+ function insertId() {
+ }
+
+ function lastErrno() {
+ }
+
+ function affectedRows() {
+ }
+
+ function getServerVersion() {
+ }
+}
+
+class DatabaseMysqlBaseTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider provideDiapers
+ * @covers DatabaseMysqlBase::addIdentifierQuotes
+ */
+ public function testAddIdentifierQuotes( $expected, $in ) {
+ $db = new FakeDatabaseMysqlBase();
+ $quoted = $db->addIdentifierQuotes( $in );
+ $this->assertEquals( $expected, $quoted );
+ }
+
+ /**
+ * Feeds testAddIdentifierQuotes
+ *
+ * Named per bug 20281 convention.
+ */
+ function provideDiapers() {
+ return array(
+ // Format: expected, input
+ array( '``', '' ),
+
+ // Yeah I really hate loosely typed PHP idiocies nowadays
+ array( '``', null ),
+
+ // Dear codereviewer, guess what addIdentifierQuotes()
+ // will return with thoses:
+ array( '``', false ),
+ array( '`1`', true ),
+
+ // We never know what could happen
+ array( '`0`', 0 ),
+ array( '`1`', 1 ),
+
+ // Whatchout! Should probably use something more meaningful
+ array( "`'`", "'" ), # single quote
+ array( '`"`', '"' ), # double quote
+ array( '````', '`' ), # backtick
+ array( '`’`', '’' ), # apostrophe (look at your encyclopedia)
+
+ // sneaky NUL bytes are lurking everywhere
+ array( '``', "\0" ),
+ array( '`xyzzy`', "\0x\0y\0z\0z\0y\0" ),
+
+ // unicode chars
+ array(
+ self::createUnicodeString( '`\u0001a\uFFFFb`' ),
+ self::createUnicodeString( '\u0001a\uFFFFb' )
+ ),
+ array(
+ self::createUnicodeString( '`\u0001\uFFFF`' ),
+ self::createUnicodeString( '\u0001\u0000\uFFFF\u0000' )
+ ),
+ array( '`☃`', '☃' ),
+ array( '`メインページ`', 'メインページ' ),
+ array( '`Басты_бет`', 'Басты_бет' ),
+
+ // Real world:
+ array( '`Alix`', 'Alix' ), # while( ! $recovered ) { sleep(); }
+ array( '`Backtick: ```', 'Backtick: `' ),
+ array( '`This is a test`', 'This is a test' ),
+ );
+ }
+
+ private static function createUnicodeString( $str ) {
+ return json_decode( '"' . $str . '"' );
+ }
+
+ function getMockForViews() {
+ $db = $this->getMockBuilder( 'DatabaseMysql' )
+ ->disableOriginalConstructor()
+ ->setMethods( array( 'fetchRow', 'query' ) )
+ ->getMock();
+
+ $db->expects( $this->any() )
+ ->method( 'query' )
+ ->with( $this->anything() )
+ ->will(
+ $this->returnValue( null )
+ );
+
+ $db->expects( $this->any() )
+ ->method( 'fetchRow' )
+ ->with( $this->anything() )
+ ->will( $this->onConsecutiveCalls(
+ array( 'Tables_in_' => 'view1' ),
+ array( 'Tables_in_' => 'view2' ),
+ array( 'Tables_in_' => 'myview' ),
+ false # no more rows
+ ));
+ return $db;
+ }
+ /**
+ * @covers DatabaseMysqlBase::listViews
+ */
+ function testListviews() {
+ $db = $this->getMockForViews();
+
+ // The first call populate an internal cache of views
+ $this->assertEquals( array( 'view1', 'view2', 'myview' ),
+ $db->listViews() );
+ $this->assertEquals( array( 'view1', 'view2', 'myview' ),
+ $db->listViews() );
+
+ // Prefix filtering
+ $this->assertEquals( array( 'view1', 'view2' ),
+ $db->listViews( 'view' ) );
+ $this->assertEquals( array( 'myview' ),
+ $db->listViews( 'my' ) );
+ $this->assertEquals( array(),
+ $db->listViews( 'UNUSED_PREFIX' ) );
+ $this->assertEquals( array( 'view1', 'view2', 'myview' ),
+ $db->listViews( '' ) );
+ }
+
+ /**
+ * @covers DatabaseMysqlBase::isView
+ * @dataProvider provideViewExistanceChecks
+ */
+ function testIsView( $isView, $viewName ) {
+ $db = $this->getMockForViews();
+
+ switch ( $isView ) {
+ case true:
+ $this->assertTrue( $db->isView( $viewName ),
+ "$viewName should be considered a view" );
+ break;
+
+ case false:
+ $this->assertFalse( $db->isView( $viewName ),
+ "$viewName has not been defined as a view" );
+ break;
+ }
+
+ }
+
+ function provideViewExistanceChecks() {
+ return array(
+ // format: whether it is a view, view name
+ array( true, 'view1' ),
+ array( true, 'view2' ),
+ array( true, 'myview' ),
+
+ array( false, 'user' ),
+
+ array( false, 'view10' ),
+ array( false, 'my' ),
+ array( false, 'OH_MY_GOD' ), # they killed kenny!
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/db/DatabaseSQLTest.php b/tests/phpunit/includes/db/DatabaseSQLTest.php
new file mode 100644
index 00000000..5c2d4b70
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseSQLTest.php
@@ -0,0 +1,725 @@
+<?php
+
+/**
+ * Test the abstract database layer
+ * This is a non DBMS depending test.
+ */
+class DatabaseSQLTest extends MediaWikiTestCase {
+
+ /**
+ * @var DatabaseTestHelper
+ */
+ private $database;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->database = new DatabaseTestHelper( __CLASS__ );
+ }
+
+ protected function assertLastSql( $sqlText ) {
+ $this->assertEquals(
+ $this->database->getLastSqls(),
+ $sqlText
+ );
+ }
+
+ /**
+ * @dataProvider provideSelect
+ * @covers DatabaseBase::select
+ */
+ public function testSelect( $sql, $sqlText ) {
+ $this->database->select(
+ $sql['tables'],
+ $sql['fields'],
+ isset( $sql['conds'] ) ? $sql['conds'] : array(),
+ __METHOD__,
+ isset( $sql['options'] ) ? $sql['options'] : array(),
+ isset( $sql['join_conds'] ) ? $sql['join_conds'] : array()
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideSelect() {
+ return array(
+ array(
+ array(
+ 'tables' => 'table',
+ 'fields' => array( 'field', 'alias' => 'field2' ),
+ 'conds' => array( 'alias' => 'text' ),
+ ),
+ "SELECT field,field2 AS alias " .
+ "FROM table " .
+ "WHERE alias = 'text'"
+ ),
+ array(
+ array(
+ 'tables' => 'table',
+ 'fields' => array( 'field', 'alias' => 'field2' ),
+ 'conds' => array( 'alias' => 'text' ),
+ 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ),
+ ),
+ "SELECT field,field2 AS alias " .
+ "FROM table " .
+ "WHERE alias = 'text' " .
+ "ORDER BY field " .
+ "LIMIT 1"
+ ),
+ array(
+ array(
+ 'tables' => array( 'table', 't2' => 'table2' ),
+ 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ),
+ 'conds' => array( 'alias' => 'text' ),
+ 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ),
+ 'join_conds' => array( 't2' => array(
+ 'LEFT JOIN', 'tid = t2.id'
+ ) ),
+ ),
+ "SELECT tid,field,field2 AS alias,t2.id " .
+ "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+ "WHERE alias = 'text' " .
+ "ORDER BY field " .
+ "LIMIT 1"
+ ),
+ array(
+ array(
+ 'tables' => array( 'table', 't2' => 'table2' ),
+ 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ),
+ 'conds' => array( 'alias' => 'text' ),
+ 'options' => array( 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ),
+ 'join_conds' => array( 't2' => array(
+ 'LEFT JOIN', 'tid = t2.id'
+ ) ),
+ ),
+ "SELECT tid,field,field2 AS alias,t2.id " .
+ "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+ "WHERE alias = 'text' " .
+ "GROUP BY field HAVING COUNT(*) > 1 " .
+ "LIMIT 1"
+ ),
+ array(
+ array(
+ 'tables' => array( 'table', 't2' => 'table2' ),
+ 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ),
+ 'conds' => array( 'alias' => 'text' ),
+ 'options' => array(
+ 'LIMIT' => 1,
+ 'GROUP BY' => array( 'field', 'field2' ),
+ 'HAVING' => array( 'COUNT(*) > 1', 'field' => 1 )
+ ),
+ 'join_conds' => array( 't2' => array(
+ 'LEFT JOIN', 'tid = t2.id'
+ ) ),
+ ),
+ "SELECT tid,field,field2 AS alias,t2.id " .
+ "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+ "WHERE alias = 'text' " .
+ "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " .
+ "LIMIT 1"
+ ),
+ array(
+ array(
+ 'tables' => array( 'table' ),
+ 'fields' => array( 'alias' => 'field' ),
+ 'conds' => array( 'alias' => array( 1, 2, 3, 4 ) ),
+ ),
+ "SELECT field AS alias " .
+ "FROM table " .
+ "WHERE alias IN ('1','2','3','4')"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUpdate
+ * @covers DatabaseBase::update
+ */
+ public function testUpdate( $sql, $sqlText ) {
+ $this->database->update(
+ $sql['table'],
+ $sql['values'],
+ $sql['conds'],
+ __METHOD__,
+ isset( $sql['options'] ) ? $sql['options'] : array()
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideUpdate() {
+ return array(
+ array(
+ array(
+ 'table' => 'table',
+ 'values' => array( 'field' => 'text', 'field2' => 'text2' ),
+ 'conds' => array( 'alias' => 'text' ),
+ ),
+ "UPDATE table " .
+ "SET field = 'text'" .
+ ",field2 = 'text2' " .
+ "WHERE alias = 'text'"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'values' => array( 'field = other', 'field2' => 'text2' ),
+ 'conds' => array( 'id' => '1' ),
+ ),
+ "UPDATE table " .
+ "SET field = other" .
+ ",field2 = 'text2' " .
+ "WHERE id = '1'"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'values' => array( 'field = other', 'field2' => 'text2' ),
+ 'conds' => '*',
+ ),
+ "UPDATE table " .
+ "SET field = other" .
+ ",field2 = 'text2'"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDelete
+ * @covers DatabaseBase::delete
+ */
+ public function testDelete( $sql, $sqlText ) {
+ $this->database->delete(
+ $sql['table'],
+ $sql['conds'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideDelete() {
+ return array(
+ array(
+ array(
+ 'table' => 'table',
+ 'conds' => array( 'alias' => 'text' ),
+ ),
+ "DELETE FROM table " .
+ "WHERE alias = 'text'"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'conds' => '*',
+ ),
+ "DELETE FROM table"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUpsert
+ * @covers DatabaseBase::upsert
+ */
+ public function testUpsert( $sql, $sqlText ) {
+ $this->database->upsert(
+ $sql['table'],
+ $sql['rows'],
+ $sql['uniqueIndexes'],
+ $sql['set'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideUpsert() {
+ return array(
+ array(
+ array(
+ 'table' => 'upsert_table',
+ 'rows' => array( 'field' => 'text', 'field2' => 'text2' ),
+ 'uniqueIndexes' => array( 'field' ),
+ 'set' => array( 'field' => 'set' ),
+ ),
+ "BEGIN; " .
+ "UPDATE upsert_table " .
+ "SET field = 'set' " .
+ "WHERE ((field = 'text')); " .
+ "INSERT IGNORE INTO upsert_table " .
+ "(field,field2) " .
+ "VALUES ('text','text2'); " .
+ "COMMIT"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideDeleteJoin
+ * @covers DatabaseBase::deleteJoin
+ */
+ public function testDeleteJoin( $sql, $sqlText ) {
+ $this->database->deleteJoin(
+ $sql['delTable'],
+ $sql['joinTable'],
+ $sql['delVar'],
+ $sql['joinVar'],
+ $sql['conds'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideDeleteJoin() {
+ return array(
+ array(
+ array(
+ 'delTable' => 'table',
+ 'joinTable' => 'table_join',
+ 'delVar' => 'field',
+ 'joinVar' => 'field_join',
+ 'conds' => array( 'alias' => 'text' ),
+ ),
+ "DELETE FROM table " .
+ "WHERE field IN (" .
+ "SELECT field_join FROM table_join WHERE alias = 'text'" .
+ ")"
+ ),
+ array(
+ array(
+ 'delTable' => 'table',
+ 'joinTable' => 'table_join',
+ 'delVar' => 'field',
+ 'joinVar' => 'field_join',
+ 'conds' => '*',
+ ),
+ "DELETE FROM table " .
+ "WHERE field IN (" .
+ "SELECT field_join FROM table_join " .
+ ")"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideInsert
+ * @covers DatabaseBase::insert
+ */
+ public function testInsert( $sql, $sqlText ) {
+ $this->database->insert(
+ $sql['table'],
+ $sql['rows'],
+ __METHOD__,
+ isset( $sql['options'] ) ? $sql['options'] : array()
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideInsert() {
+ return array(
+ array(
+ array(
+ 'table' => 'table',
+ 'rows' => array( 'field' => 'text', 'field2' => 2 ),
+ ),
+ "INSERT INTO table " .
+ "(field,field2) " .
+ "VALUES ('text','2')"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'rows' => array( 'field' => 'text', 'field2' => 2 ),
+ 'options' => 'IGNORE',
+ ),
+ "INSERT IGNORE INTO table " .
+ "(field,field2) " .
+ "VALUES ('text','2')"
+ ),
+ array(
+ array(
+ 'table' => 'table',
+ 'rows' => array(
+ array( 'field' => 'text', 'field2' => 2 ),
+ array( 'field' => 'multi', 'field2' => 3 ),
+ ),
+ 'options' => 'IGNORE',
+ ),
+ "INSERT IGNORE INTO table " .
+ "(field,field2) " .
+ "VALUES " .
+ "('text','2')," .
+ "('multi','3')"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideInsertSelect
+ * @covers DatabaseBase::insertSelect
+ */
+ public function testInsertSelect( $sql, $sqlText ) {
+ $this->database->insertSelect(
+ $sql['destTable'],
+ $sql['srcTable'],
+ $sql['varMap'],
+ $sql['conds'],
+ __METHOD__,
+ isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : array(),
+ isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : array()
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideInsertSelect() {
+ return array(
+ array(
+ array(
+ 'destTable' => 'insert_table',
+ 'srcTable' => 'select_table',
+ 'varMap' => array( 'field_insert' => 'field_select', 'field' => 'field2' ),
+ 'conds' => '*',
+ ),
+ "INSERT INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table"
+ ),
+ array(
+ array(
+ 'destTable' => 'insert_table',
+ 'srcTable' => 'select_table',
+ 'varMap' => array( 'field_insert' => 'field_select', 'field' => 'field2' ),
+ 'conds' => array( 'field' => 2 ),
+ ),
+ "INSERT INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table " .
+ "WHERE field = '2'"
+ ),
+ array(
+ array(
+ 'destTable' => 'insert_table',
+ 'srcTable' => 'select_table',
+ 'varMap' => array( 'field_insert' => 'field_select', 'field' => 'field2' ),
+ 'conds' => array( 'field' => 2 ),
+ 'insertOptions' => 'IGNORE',
+ 'selectOptions' => array( 'ORDER BY' => 'field' ),
+ ),
+ "INSERT IGNORE INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table " .
+ "WHERE field = '2' " .
+ "ORDER BY field"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideReplace
+ * @covers DatabaseBase::replace
+ */
+ public function testReplace( $sql, $sqlText ) {
+ $this->database->replace(
+ $sql['table'],
+ $sql['uniqueIndexes'],
+ $sql['rows'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideReplace() {
+ return array(
+ array(
+ array(
+ 'table' => 'replace_table',
+ 'uniqueIndexes' => array( 'field' ),
+ 'rows' => array( 'field' => 'text', 'field2' => 'text2' ),
+ ),
+ "DELETE FROM replace_table " .
+ "WHERE ( field='text' ); " .
+ "INSERT INTO replace_table " .
+ "(field,field2) " .
+ "VALUES ('text','text2')"
+ ),
+ array(
+ array(
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => array( array( 'md_module', 'md_skin' ) ),
+ 'rows' => array(
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ),
+ ),
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module' AND md_skin='skin' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps')"
+ ),
+ array(
+ array(
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => array( array( 'md_module', 'md_skin' ) ),
+ 'rows' => array(
+ array(
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ), array(
+ 'md_module' => 'module2',
+ 'md_skin' => 'skin2',
+ 'md_deps' => 'deps2',
+ ),
+ ),
+ ),
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module' AND md_skin='skin' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps'); " .
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module2' AND md_skin='skin2' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module2','skin2','deps2')"
+ ),
+ array(
+ array(
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => array( 'md_module', 'md_skin' ),
+ 'rows' => array(
+ array(
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ), array(
+ 'md_module' => 'module2',
+ 'md_skin' => 'skin2',
+ 'md_deps' => 'deps2',
+ ),
+ ),
+ ),
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module' ) OR ( md_skin='skin' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps'); " .
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module2' ) OR ( md_skin='skin2' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module2','skin2','deps2')"
+ ),
+ array(
+ array(
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => array(),
+ 'rows' => array(
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ),
+ ),
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps')"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideNativeReplace
+ * @covers DatabaseBase::nativeReplace
+ */
+ public function testNativeReplace( $sql, $sqlText ) {
+ $this->database->nativeReplace(
+ $sql['table'],
+ $sql['rows'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideNativeReplace() {
+ return array(
+ array(
+ array(
+ 'table' => 'replace_table',
+ 'rows' => array( 'field' => 'text', 'field2' => 'text2' ),
+ ),
+ "REPLACE INTO replace_table " .
+ "(field,field2) " .
+ "VALUES ('text','text2')"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideConditional
+ * @covers DatabaseBase::conditional
+ */
+ public function testConditional( $sql, $sqlText ) {
+ $this->assertEquals( trim( $this->database->conditional(
+ $sql['conds'],
+ $sql['true'],
+ $sql['false']
+ ) ), $sqlText );
+ }
+
+ public static function provideConditional() {
+ return array(
+ array(
+ array(
+ 'conds' => array( 'field' => 'text' ),
+ 'true' => 1,
+ 'false' => 'NULL',
+ ),
+ "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)"
+ ),
+ array(
+ array(
+ 'conds' => array( 'field' => 'text', 'field2' => 'anothertext' ),
+ 'true' => 1,
+ 'false' => 'NULL',
+ ),
+ "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)"
+ ),
+ array(
+ array(
+ 'conds' => 'field=1',
+ 'true' => 1,
+ 'false' => 'NULL',
+ ),
+ "(CASE WHEN field=1 THEN 1 ELSE NULL END)"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideBuildConcat
+ * @covers DatabaseBase::buildConcat
+ */
+ public function testBuildConcat( $stringList, $sqlText ) {
+ $this->assertEquals( trim( $this->database->buildConcat(
+ $stringList
+ ) ), $sqlText );
+ }
+
+ public static function provideBuildConcat() {
+ return array(
+ array(
+ array( 'field', 'field2' ),
+ "CONCAT(field,field2)"
+ ),
+ array(
+ array( "'test'", 'field2' ),
+ "CONCAT('test',field2)"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideBuildLike
+ * @covers DatabaseBase::buildLike
+ */
+ public function testBuildLike( $array, $sqlText ) {
+ $this->assertEquals( trim( $this->database->buildLike(
+ $array
+ ) ), $sqlText );
+ }
+
+ public static function provideBuildLike() {
+ return array(
+ array(
+ 'text',
+ "LIKE 'text'"
+ ),
+ array(
+ array( 'text', new LikeMatch( '%' ) ),
+ "LIKE 'text%'"
+ ),
+ array(
+ array( 'text', new LikeMatch( '%' ), 'text2' ),
+ "LIKE 'text%text2'"
+ ),
+ array(
+ array( 'text', new LikeMatch( '_' ) ),
+ "LIKE 'text_'"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideUnionQueries
+ * @covers DatabaseBase::unionQueries
+ */
+ public function testUnionQueries( $sql, $sqlText ) {
+ $this->assertEquals( trim( $this->database->unionQueries(
+ $sql['sqls'],
+ $sql['all']
+ ) ), $sqlText );
+ }
+
+ public static function provideUnionQueries() {
+ return array(
+ array(
+ array(
+ 'sqls' => array( 'RAW SQL', 'RAW2SQL' ),
+ 'all' => true,
+ ),
+ "(RAW SQL) UNION ALL (RAW2SQL)"
+ ),
+ array(
+ array(
+ 'sqls' => array( 'RAW SQL', 'RAW2SQL' ),
+ 'all' => false,
+ ),
+ "(RAW SQL) UNION (RAW2SQL)"
+ ),
+ array(
+ array(
+ 'sqls' => array( 'RAW SQL', 'RAW2SQL', 'RAW3SQL' ),
+ 'all' => false,
+ ),
+ "(RAW SQL) UNION (RAW2SQL) UNION (RAW3SQL)"
+ ),
+ );
+ }
+
+ /**
+ * @covers DatabaseBase::commit
+ */
+ public function testTransactionCommit() {
+ $this->database->begin( __METHOD__ );
+ $this->database->commit( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; COMMIT' );
+ }
+
+ /**
+ * @covers DatabaseBase::rollback
+ */
+ public function testTransactionRollback() {
+ $this->database->begin( __METHOD__ );
+ $this->database->rollback( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; ROLLBACK' );
+ }
+
+ /**
+ * @covers DatabaseBase::dropTable
+ */
+ public function testDropTable() {
+ $this->database->setExistingTables( array( 'table' ) );
+ $this->database->dropTable( 'table', __METHOD__ );
+ $this->assertLastSql( 'DROP TABLE table' );
+ }
+
+ /**
+ * @covers DatabaseBase::dropTable
+ */
+ public function testDropNonExistingTable() {
+ $this->assertFalse(
+ $this->database->dropTable( 'non_existing', __METHOD__ )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php
new file mode 100644
index 00000000..98b4ca04
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php
@@ -0,0 +1,455 @@
+<?php
+
+class MockDatabaseSqlite extends DatabaseSqliteStandalone {
+ private $lastQuery;
+
+ function __construct() {
+ parent::__construct( ':memory:' );
+ }
+
+ function query( $sql, $fname = '', $tempIgnore = false ) {
+ $this->lastQuery = $sql;
+
+ return true;
+ }
+
+ /**
+ * Override parent visibility to public
+ */
+ public function replaceVars( $s ) {
+ return parent::replaceVars( $s );
+ }
+}
+
+/**
+ * @group sqlite
+ * @group Database
+ * @group medium
+ */
+class DatabaseSqliteTest extends MediaWikiTestCase {
+ /** @var MockDatabaseSqlite */
+ protected $db;
+
+ protected function setUp() {
+ parent::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 static function provideAddQuotes() {
+ return array(
+ array( // #0: empty
+ '', "''"
+ ),
+ array( // #1: simple
+ 'foo bar', "'foo bar'"
+ ),
+ array( // #2: including quote
+ 'foo\'bar', "'foo''bar'"
+ ),
+ // #3: including \0 (must be represented as hex, per https://bugs.php.net/bug.php?id=63419)
+ array(
+ "x\0y",
+ "x'780079'",
+ ),
+ array( // #4: blob object (must be represented as hex)
+ new Blob( "hello" ),
+ "x'68656c6c6f'",
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideAddQuotes()
+ * @covers DatabaseSqlite::addQuotes
+ */
+ public function testAddQuotes( $value, $expected ) {
+ // check quoting
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $this->assertEquals( $expected, $db->addQuotes( $value ), 'string not quoted as expected' );
+
+ // ok, quoting works as expected, now try a round trip.
+ $re = $db->query( 'select ' . $db->addQuotes( $value ) );
+
+ $this->assertTrue( $re !== false, 'query failed' );
+
+ if ( $row = $re->fetchRow() ) {
+ if ( $value instanceof Blob ) {
+ $value = $value->fetch();
+ }
+
+ $this->assertEquals( $value, $row[0], 'string mangled by the database' );
+ } else {
+ $this->fail( 'query returned no result' );
+ }
+ }
+
+ /**
+ * @covers DatabaseSqlite::replaceVars
+ */
+ 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" )
+ );
+
+ $this->assertEquals( "DROP INDEX foo",
+ $this->replaceVars( "DROP INDEX /*i*/foo ON /*_*/bar" )
+ );
+
+ $this->assertEquals( "DROP INDEX foo -- dropping index",
+ $this->replaceVars( "DROP INDEX /*i*/foo ON /*_*/bar -- dropping index" )
+ );
+ $this->assertEquals( "INSERT OR IGNORE INTO foo VALUES ('bar')",
+ $this->replaceVars( "INSERT OR IGNORE INTO foo VALUES ('bar')" )
+ );
+ }
+
+ /**
+ * @covers DatabaseSqlite::tableName
+ */
+ 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' ) );
+ }
+
+ /**
+ * @covers DatabaseSqlite::duplicateTableStructure
+ */
+ 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'
+ );
+ }
+
+ /**
+ * @covers DatabaseSqlite::duplicateTableStructure
+ */
+ 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"
+ );
+ }
+
+ /**
+ * @covers DatabaseSqlite::deleteJoin
+ */
+ 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, $wgProfileToDatabase;
+
+ // 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',
+ '1.18',
+ );
+
+ // 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" );
+ if ( $wgProfileToDatabase ) {
+ $currentDB->sourceFile( "$IP/maintenance/sqlite/archives/patch-profiling.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();
+ }
+ }
+
+ /**
+ * @covers DatabaseSqlite::insertId
+ */
+ public function testInsertIdType() {
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+
+ $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ );
+ $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Database creation" );
+
+ $insertion = $db->insert( 'a', array( 'a_1' => 10 ), __METHOD__ );
+ $this->assertTrue( $insertion, "Insertion worked" );
+
+ $this->assertInternalType( 'integer', $db->insertId(), "Actual typecheck" );
+ $this->assertTrue( $db->close(), "closing database" );
+ }
+
+ private function prepareDB( $version ) {
+ static $maint = null;
+ if ( $maint === null ) {
+ $maint = new FakeMaintenance();
+ $maint->loadParamsAndArgs( null, array( 'quiet' => 1 ) );
+ }
+
+ global $IP;
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $db->sourceFile( "$IP/tests/phpunit/data/db/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(
+ 'external_user', // removed from core in 1.22
+ 'math', // moved out of core in 1.18
+ 'trackbacks', // removed from core in 1.19
+ '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;
+ }
+
+ public function testCaseInsensitiveLike() {
+ // TODO: Test this for all databases
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $res = $db->query( 'SELECT "a" LIKE "A" AS a' );
+ $row = $res->fetchRow();
+ $this->assertFalse( (bool)$row['a'] );
+ }
+
+ /**
+ * @covers DatabaseSqlite::numFields
+ */
+ public function testNumFields() {
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+
+ $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ );
+ $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Failed to create table a" );
+ $res = $db->select( 'a', '*' );
+ $this->assertEquals( 0, $db->numFields( $res ), "expects to get 0 fields for an empty table" );
+ $insertion = $db->insert( 'a', array( 'a_1' => 10 ), __METHOD__ );
+ $this->assertTrue( $insertion, "Insertion failed" );
+ $res = $db->select( 'a', '*' );
+ $this->assertEquals( 1, $db->numFields( $res ), "wrong number of fields" );
+
+ $this->assertTrue( $db->close(), "closing database" );
+ }
+}
diff --git a/tests/phpunit/includes/db/DatabaseTest.php b/tests/phpunit/includes/db/DatabaseTest.php
new file mode 100644
index 00000000..7e704396
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseTest.php
@@ -0,0 +1,237 @@
+<?php
+
+/**
+ * @group Database
+ * @group DatabaseBase
+ */
+class DatabaseTest extends MediaWikiTestCase {
+ /**
+ * @var DatabaseBase
+ */
+ protected $db;
+
+ private $functionTest = false;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->db = wfGetDB( DB_MASTER );
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ if ( $this->functionTest ) {
+ $this->dropFunctions();
+ $this->functionTest = false;
+ }
+ }
+ /**
+ * @covers DatabaseBase::dropTable
+ */
+ public function testAddQuotesNull() {
+ $check = "NULL";
+ if ( $this->db->getType() === 'sqlite' || $this->db->getType() === 'oracle' ) {
+ $check = "''";
+ }
+ $this->assertEquals( $check, $this->db->addQuotes( null ) );
+ }
+
+ public function testAddQuotesInt() {
+ # returning just "1234" should be ok too, though...
+ # maybe
+ $this->assertEquals(
+ "'1234'",
+ $this->db->addQuotes( 1234 ) );
+ }
+
+ public function testAddQuotesFloat() {
+ # returning just "1234.5678" would be ok too, though
+ $this->assertEquals(
+ "'1234.5678'",
+ $this->db->addQuotes( 1234.5678 ) );
+ }
+
+ public function testAddQuotesString() {
+ $this->assertEquals(
+ "'string'",
+ $this->db->addQuotes( 'string' ) );
+ }
+
+ public 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" ) );
+ }
+
+ private function getSharedTableName( $table, $database, $prefix, $format = 'quoted' ) {
+ global $wgSharedDB, $wgSharedTables, $wgSharedPrefix;
+
+ $oldName = $wgSharedDB;
+ $oldTables = $wgSharedTables;
+ $oldPrefix = $wgSharedPrefix;
+
+ $wgSharedDB = $database;
+ $wgSharedTables = array( $table );
+ $wgSharedPrefix = $prefix;
+
+ $ret = $this->db->tableName( $table, $format );
+
+ $wgSharedDB = $oldName;
+ $wgSharedTables = $oldTables;
+ $wgSharedPrefix = $oldPrefix;
+
+ return $ret;
+ }
+
+ private function prefixAndQuote( $table, $database = null, $prefix = null, $format = 'quoted' ) {
+ if ( $this->db->getType() === 'sqlite' || $format !== 'quoted' ) {
+ $quote = '';
+ } elseif ( $this->db->getType() === 'mysql' ) {
+ $quote = '`';
+ } elseif ( $this->db->getType() === 'oracle' ) {
+ $quote = '/*Q*/';
+ } else {
+ $quote = '"';
+ }
+
+ if ( $database !== null ) {
+ if ( $this->db->getType() === 'oracle' ) {
+ $database = $quote . $database . '.';
+ } else {
+ $database = $quote . $database . $quote . '.';
+ }
+ }
+
+ if ( $prefix === null ) {
+ $prefix = $this->dbPrefix();
+ }
+
+ if ( $this->db->getType() === 'oracle' ) {
+ return strtoupper( $database . $quote . $prefix . $table );
+ } else {
+ return $database . $quote . $prefix . $table . $quote;
+ }
+ }
+
+ public function testTableNameLocal() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename' ),
+ $this->db->tableName( 'tablename' )
+ );
+ }
+
+ public function testTableNameRawLocal() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', null, null, 'raw' ),
+ $this->db->tableName( 'tablename', 'raw' )
+ );
+ }
+
+ public function testTableNameShared() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'sharedatabase', 'sh_' ),
+ $this->getSharedTableName( 'tablename', 'sharedatabase', 'sh_' )
+ );
+
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'sharedatabase', null ),
+ $this->getSharedTableName( 'tablename', 'sharedatabase', null )
+ );
+ }
+
+ public function testTableNameRawShared() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'sharedatabase', 'sh_', 'raw' ),
+ $this->getSharedTableName( 'tablename', 'sharedatabase', 'sh_', 'raw' )
+ );
+
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'sharedatabase', null, 'raw' ),
+ $this->getSharedTableName( 'tablename', 'sharedatabase', null, 'raw' )
+ );
+ }
+
+ public function testTableNameForeign() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'databasename', '' ),
+ $this->db->tableName( 'databasename.tablename' )
+ );
+ }
+
+ public function testTableNameRawForeign() {
+ $this->assertEquals(
+ $this->prefixAndQuote( 'tablename', 'databasename', '', 'raw' ),
+ $this->db->tableName( 'databasename.tablename', 'raw' )
+ );
+ }
+
+ public function testFillPreparedEmpty() {
+ $sql = $this->db->fillPrepared(
+ 'SELECT * FROM interwiki', array() );
+ $this->assertEquals(
+ "SELECT * FROM interwiki",
+ $sql );
+ }
+
+ public 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 );
+ }
+
+ public 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 );
+ }
+
+ public 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 );
+ }
+
+ public function testStoredFunctions() {
+ if ( !in_array( wfGetDB( DB_MASTER )->getType(), array( 'mysql', 'postgres' ) ) ) {
+ $this->markTestSkipped( 'MySQL or Postgres required' );
+ }
+ global $IP;
+ $this->dropFunctions();
+ $this->functionTest = true;
+ $this->assertTrue(
+ $this->db->sourceFile( "$IP/tests/phpunit/data/db/{$this->db->getType()}/functions.sql" )
+ );
+ $res = $this->db->query( 'SELECT mw_test_function() AS test', __METHOD__ );
+ $this->assertEquals( 42, $res->fetchObject()->test );
+ }
+
+ private function dropFunctions() {
+ $this->db->query( 'DROP FUNCTION IF EXISTS mw_test_function'
+ . ( $this->db->getType() == 'postgres' ? '()' : '' )
+ );
+ }
+
+ public function testUnknownTableCorruptsResults() {
+ $res = $this->db->select( 'page', '*', array( 'page_id' => 1 ) );
+ $this->assertFalse( $this->db->tableExists( 'foobarbaz' ) );
+ $this->assertInternalType( 'int', $res->numRows() );
+ }
+}
diff --git a/tests/phpunit/includes/db/DatabaseTestHelper.php b/tests/phpunit/includes/db/DatabaseTestHelper.php
new file mode 100644
index 00000000..0c0b3902
--- /dev/null
+++ b/tests/phpunit/includes/db/DatabaseTestHelper.php
@@ -0,0 +1,170 @@
+<?php
+
+/**
+ * Helper for testing the methods from the DatabaseBase class
+ * @since 1.22
+ */
+class DatabaseTestHelper extends DatabaseBase {
+
+ /**
+ * __CLASS__ of the test suite,
+ * used to determine, if the function name is passed every time to query()
+ */
+ protected $testName = array();
+
+ /**
+ * Array of lastSqls passed to query(),
+ * This is an array since some methods in DatabaseBase can do more than one
+ * query. Cleared when calling getLastSqls().
+ */
+ protected $lastSqls = array();
+
+ /**
+ * Array of tables to be considered as existing by tableExist()
+ * Use setExistingTables() to alter.
+ */
+ protected $tablesExists;
+
+ public function __construct( $testName ) {
+ $this->testName = $testName;
+ }
+
+ /**
+ * Returns SQL queries grouped by '; '
+ * Clear the list of queries that have been done so far.
+ */
+ public function getLastSqls() {
+ $lastSqls = implode( '; ', $this->lastSqls );
+ $this->lastSqls = array();
+
+ return $lastSqls;
+ }
+
+ public function setExistingTables( $tablesExists ) {
+ $this->tablesExists = (array)$tablesExists;
+ }
+
+ protected function addSql( $sql ) {
+ // clean up spaces before and after some words and the whole string
+ $this->lastSqls[] = trim( preg_replace(
+ '/\s{2,}(?=FROM|WHERE|GROUP BY|ORDER BY|LIMIT)|(?<=SELECT|INSERT|UPDATE)\s{2,}/',
+ ' ', $sql
+ ) );
+ }
+
+ protected function checkFunctionName( $fname ) {
+ if ( substr( $fname, 0, strlen( $this->testName ) ) !== $this->testName ) {
+ throw new MWException( 'function name does not start with test class. ' .
+ $fname . ' vs. ' . $this->testName . '. ' .
+ 'Please provide __METHOD__ to database methods.' );
+ }
+ }
+
+ function strencode( $s ) {
+ // Choose apos to avoid handling of escaping double quotes in quoted text
+ return str_replace( "'", "\'", $s );
+ }
+
+ public function addIdentifierQuotes( $s ) {
+ // no escaping to avoid handling of double quotes in quoted text
+ return $s;
+ }
+
+ public function query( $sql, $fname = '', $tempIgnore = false ) {
+ $this->checkFunctionName( $fname );
+ $this->addSql( $sql );
+
+ return parent::query( $sql, $fname, $tempIgnore );
+ }
+
+ public function tableExists( $table, $fname = __METHOD__ ) {
+ $this->checkFunctionName( $fname );
+
+ return in_array( $table, (array)$this->tablesExists );
+ }
+
+ // Redeclare parent method to make it public
+ public function nativeReplace( $table, $rows, $fname ) {
+ return parent::nativeReplace( $table, $rows, $fname );
+ }
+
+ function getType() {
+ return 'test';
+ }
+
+ function open( $server, $user, $password, $dbName ) {
+ return false;
+ }
+
+ function fetchObject( $res ) {
+ return false;
+ }
+
+ function fetchRow( $res ) {
+ return false;
+ }
+
+ function numRows( $res ) {
+ return -1;
+ }
+
+ function numFields( $res ) {
+ return -1;
+ }
+
+ function fieldName( $res, $n ) {
+ return 'test';
+ }
+
+ function insertId() {
+ return -1;
+ }
+
+ function dataSeek( $res, $row ) {
+ /* nop */
+ }
+
+ function lastErrno() {
+ return -1;
+ }
+
+ function lastError() {
+ return 'test';
+ }
+
+ function fieldInfo( $table, $field ) {
+ return false;
+ }
+
+ function indexInfo( $table, $index, $fname = 'DatabaseBase::indexInfo' ) {
+ return false;
+ }
+
+ function affectedRows() {
+ return -1;
+ }
+
+ function getSoftwareLink() {
+ return 'test';
+ }
+
+ function getServerVersion() {
+ return 'test';
+ }
+
+ function getServerInfo() {
+ return 'test';
+ }
+
+ function isOpen() {
+ return true;
+ }
+
+ protected function closeConnection() {
+ return false;
+ }
+
+ protected function doQuery( $sql ) {
+ return array();
+ }
+}
diff --git a/tests/phpunit/includes/db/LBFactoryTest.php b/tests/phpunit/includes/db/LBFactoryTest.php
new file mode 100644
index 00000000..4c59f474
--- /dev/null
+++ b/tests/phpunit/includes/db/LBFactoryTest.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Holds tests for LBFactory abstract MediaWiki class.
+ *
+ * @section LICENSE
+ * 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
+ *
+ * @group Database
+ * @file
+ * @author Antoine Musso
+ * @copyright © 2013 Antoine Musso
+ * @copyright © 2013 Wikimedia Foundation Inc.
+ */
+class LBFactoryTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider getLBFactoryClassProvider
+ */
+ public function testGetLBFactoryClass( $expected, $deprecated ) {
+ $mockDB = $this->getMockBuilder( 'DatabaseMysql' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $config = array(
+ 'class' => $deprecated,
+ 'connection' => $mockDB,
+ # Various other parameters required:
+ 'sectionsByDB' => array(),
+ 'sectionLoads' => array(),
+ 'serverTemplate' => array(),
+ );
+
+ $this->hideDeprecated( '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details' );
+ $result = LBFactory::getLBFactoryClass( $config );
+
+ $this->assertEquals( $expected, $result );
+ }
+
+ public function getLBFactoryClassProvider() {
+ return array(
+ # Format: new class, old class
+ array( 'LBFactorySimple', 'LBFactory_Simple' ),
+ array( 'LBFactorySingle', 'LBFactory_Single' ),
+ array( 'LBFactoryMulti', 'LBFactory_Multi' ),
+ array( 'LBFactoryFake', 'LBFactory_Fake' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/db/ORMRowTest.php b/tests/phpunit/includes/db/ORMRowTest.php
new file mode 100644
index 00000000..447bf219
--- /dev/null
+++ b/tests/phpunit/includes/db/ORMRowTest.php
@@ -0,0 +1,226 @@
+<?php
+
+/**
+ * Abstract class to construct tests for ORMRow deriving classes.
+ *
+ * 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
+ * @since 1.20
+ *
+ * @ingroup Test
+ *
+ * @group ORM
+ *
+ * The database group has as a side effect that temporal database tables are created. This makes
+ * it possible to test without poisoning a production database.
+ * @group Database
+ *
+ * Some of the tests takes more time, and needs therefor longer time before they can be aborted
+ * as non-functional. The reason why tests are aborted is assumed to be set up of temporal databases
+ * that hold the first tests in a pending state awaiting access to the database.
+ * @group medium
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+abstract class ORMRowTest extends \MediaWikiTestCase {
+
+ /**
+ * @since 1.20
+ * @return string
+ */
+ abstract protected function getRowClass();
+
+ /**
+ * @since 1.20
+ * @return IORMTable
+ */
+ abstract protected function getTableInstance();
+
+ /**
+ * @since 1.20
+ * @return array
+ */
+ abstract public function constructorTestProvider();
+
+ /**
+ * @since 1.20
+ * @param IORMRow $row
+ * @param array $data
+ */
+ protected function verifyFields( IORMRow $row, array $data ) {
+ foreach ( array_keys( $data ) as $fieldName ) {
+ $this->assertEquals( $data[$fieldName], $row->getField( $fieldName ) );
+ }
+ }
+
+ /**
+ * @since 1.20
+ * @param array $data
+ * @param bool $loadDefaults
+ * @return IORMRow
+ */
+ protected function getRowInstance( array $data, $loadDefaults ) {
+ $class = $this->getRowClass();
+
+ return new $class( $this->getTableInstance(), $data, $loadDefaults );
+ }
+
+ /**
+ * @since 1.20
+ * @return array
+ */
+ protected function getMockValues() {
+ return array(
+ 'id' => 1,
+ 'str' => 'foobar4645645',
+ 'int' => 42,
+ 'float' => 4.2,
+ 'bool' => true,
+ 'array' => array( 42, 'foobar' ),
+ 'blob' => new stdClass()
+ );
+ }
+
+ /**
+ * @since 1.20
+ * @return array
+ */
+ protected function getMockFields() {
+ $mockValues = $this->getMockValues();
+ $mockFields = array();
+
+ foreach ( $this->getTableInstance()->getFields() as $name => $type ) {
+ if ( $name !== 'id' ) {
+ $mockFields[$name] = $mockValues[$type];
+ }
+ }
+
+ return $mockFields;
+ }
+
+ /**
+ * @since 1.20
+ * @return array Array of IORMRow
+ */
+ public function instanceProvider() {
+ $instances = array();
+
+ foreach ( $this->constructorTestProvider() as $arguments ) {
+ $instances[] = array( call_user_func_array( array( $this, 'getRowInstance' ), $arguments ) );
+ }
+
+ return $instances;
+ }
+
+ /**
+ * @dataProvider constructorTestProvider
+ */
+ public function testConstructor( array $data, $loadDefaults ) {
+ $this->verifyFields( $this->getRowInstance( $data, $loadDefaults ), $data );
+ }
+
+ /**
+ * @dataProvider constructorTestProvider
+ */
+ public function testSaveAndRemove( array $data, $loadDefaults ) {
+ $item = $this->getRowInstance( $data, $loadDefaults );
+
+ $this->assertTrue( $item->save() );
+
+ $this->assertTrue( $item->hasIdField() );
+ $this->assertTrue( is_integer( $item->getId() ) );
+
+ $id = $item->getId();
+
+ $this->assertTrue( $item->save() );
+
+ $this->assertEquals( $id, $item->getId() );
+
+ $this->verifyFields( $item, $data );
+
+ $this->assertTrue( $item->remove() );
+
+ $this->assertFalse( $item->hasIdField() );
+
+ $this->assertTrue( $item->save() );
+
+ $this->verifyFields( $item, $data );
+
+ $this->assertTrue( $item->remove() );
+
+ $this->assertFalse( $item->hasIdField() );
+
+ $this->verifyFields( $item, $data );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ */
+ public function testSetField( IORMRow $item ) {
+ foreach ( $this->getMockFields() as $name => $value ) {
+ $item->setField( $name, $value );
+ $this->assertEquals( $value, $item->getField( $name ) );
+ }
+ }
+
+ /**
+ * @since 1.20
+ * @param array $expected
+ * @param IORMRow $item
+ */
+ protected function assertFieldValues( array $expected, IORMRow $item ) {
+ foreach ( $expected as $name => $type ) {
+ if ( $name !== 'id' ) {
+ $this->assertEquals( $expected[$name], $item->getField( $name ) );
+ }
+ }
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ */
+ public function testSetFields( IORMRow $item ) {
+ $originalValues = $item->getFields();
+
+ $item->setFields( array(), false );
+
+ foreach ( $item->getTable()->getFields() as $name => $type ) {
+ $originalHas = array_key_exists( $name, $originalValues );
+ $newHas = $item->hasField( $name );
+
+ $this->assertEquals( $originalHas, $newHas );
+
+ if ( $originalHas && $newHas ) {
+ $this->assertEquals( $originalValues[$name], $item->getField( $name ) );
+ }
+ }
+
+ $mockFields = $this->getMockFields();
+
+ $item->setFields( $mockFields, false );
+
+ $this->assertFieldValues( $originalValues, $item );
+
+ $item->setFields( $mockFields, true );
+
+ $this->assertFieldValues( $mockFields, $item );
+ }
+
+ // TODO: test all of the methods!
+
+}
diff --git a/tests/phpunit/includes/db/ORMTableTest.php b/tests/phpunit/includes/db/ORMTableTest.php
new file mode 100644
index 00000000..7171ee59
--- /dev/null
+++ b/tests/phpunit/includes/db/ORMTableTest.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * Abstract class to construct tests for ORMTable deriving classes.
+ *
+ * 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
+ * @since 1.21
+ *
+ * @ingroup Test
+ *
+ * @group ORM
+ * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author Daniel Kinzler
+ */
+
+/**
+ * @covers PageORMTableForTesting
+ */
+class ORMTableTest extends MediaWikiTestCase {
+
+ /**
+ * @since 1.21
+ * @return string
+ */
+ protected function getTableClass() {
+ return 'PageORMTableForTesting';
+ }
+
+ /**
+ * @since 1.21
+ * @return IORMTable
+ */
+ public function getTable() {
+ $class = $this->getTableClass();
+
+ return $class::singleton();
+ }
+
+ /**
+ * @since 1.21
+ * @return string
+ */
+ public function getRowClass() {
+ return $this->getTable()->getRowClass();
+ }
+
+ /**
+ * @since 1.21
+ */
+ public function testSingleton() {
+ $class = $this->getTableClass();
+
+ $this->assertInstanceOf( $class, $class::singleton() );
+ $this->assertTrue( $class::singleton() === $class::singleton() );
+ }
+
+ /**
+ * @since 1.21
+ */
+ public function testIgnoreErrorsOverride() {
+ $table = $this->getTable();
+
+ $db = $table->getReadDbConnection();
+ $db->ignoreErrors( true );
+
+ try {
+ $table->rawSelect( "this is invalid" );
+ $this->fail( "An invalid query should trigger a DBQueryError even if ignoreErrors is enabled." );
+ } catch ( DBQueryError $ex ) {
+ $this->assertTrue( true, "just making phpunit happy" );
+ }
+
+ $db->ignoreErrors( false );
+ }
+}
+
+/**
+ * Dummy ORM table for testing, reading Title objects from the page table.
+ *
+ * @since 1.21
+ */
+
+class PageORMTableForTesting extends ORMTable {
+
+ /**
+ * @see ORMTable::getName
+ *
+ * @return string
+ */
+ public function getName() {
+ return 'page';
+ }
+
+ /**
+ * @see ORMTable::getRowClass
+ *
+ * @return string
+ */
+ public function getRowClass() {
+ return 'Title';
+ }
+
+ /**
+ * @see ORMTable::newRow
+ *
+ * @return IORMRow
+ */
+ public function newRow( array $data, $loadDefaults = false ) {
+ return Title::makeTitle( $data['namespace'], $data['title'] );
+ }
+
+ /**
+ * @see ORMTable::getFields
+ *
+ * @return array
+ */
+ public function getFields() {
+ return array(
+ 'id' => 'int',
+ 'namespace' => 'int',
+ 'title' => 'str',
+ );
+ }
+
+ /**
+ * @see ORMTable::getFieldPrefix
+ *
+ * @return string
+ */
+ protected function getFieldPrefix() {
+ return 'page_';
+ }
+}
diff --git a/tests/phpunit/includes/db/TestORMRowTest.php b/tests/phpunit/includes/db/TestORMRowTest.php
new file mode 100644
index 00000000..c9459c90
--- /dev/null
+++ b/tests/phpunit/includes/db/TestORMRowTest.php
@@ -0,0 +1,218 @@
+<?php
+
+/**
+ * Tests for the TestORMRow class.
+ * TestORMRow is a dummy class to be able to test the abstract ORMRow class.
+ *
+ * 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
+ * @since 1.20
+ *
+ * @ingroup Test
+ *
+ * @group ORM
+ *
+ * The database group has as a side effect that temporal database tables are created. This makes
+ * it possible to test without poisoning a production database.
+ * @group Database
+ *
+ * Some of the tests takes more time, and needs therefor longer time before they can be aborted
+ * as non-functional. The reason why tests are aborted is assumed to be set up of temporal databases
+ * that hold the first tests in a pending state awaiting access to the database.
+ * @group medium
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+require_once __DIR__ . "/ORMRowTest.php";
+
+/**
+ * @covers TestORMRow
+ */
+class TestORMRowTest extends ORMRowTest {
+
+ /**
+ * @since 1.20
+ * @return string
+ */
+ protected function getRowClass() {
+ return 'TestORMRow';
+ }
+
+ /**
+ * @since 1.20
+ * @return IORMTable
+ */
+ protected function getTableInstance() {
+ return TestORMTable::singleton();
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $isSqlite = $GLOBALS['wgDBtype'] === 'sqlite';
+ $isPostgres = $GLOBALS['wgDBtype'] === 'postgres';
+
+ $idField = $isSqlite ? 'INTEGER' : 'INT unsigned';
+ $primaryKey = $isSqlite ? 'PRIMARY KEY AUTOINCREMENT' : 'auto_increment PRIMARY KEY';
+
+ if ( $isPostgres ) {
+ $dbw->query(
+ 'CREATE TABLE IF NOT EXISTS ' . $dbw->tableName( 'orm_test' ) . "(
+ test_id serial PRIMARY KEY,
+ test_name TEXT NOT NULL DEFAULT '',
+ test_age INTEGER NOT NULL DEFAULT 0,
+ test_height REAL NOT NULL DEFAULT 0,
+ test_awesome INTEGER NOT NULL DEFAULT 0,
+ test_stuff BYTEA,
+ test_moarstuff BYTEA,
+ test_time TIMESTAMPTZ
+ );",
+ __METHOD__
+ );
+ } else {
+ $dbw->query(
+ 'CREATE TABLE IF NOT EXISTS ' . $dbw->tableName( 'orm_test' ) . '(
+ test_id ' . $idField . ' NOT NULL ' . $primaryKey . ',
+ test_name VARCHAR(255) NOT NULL,
+ test_age TINYINT unsigned NOT NULL,
+ test_height FLOAT NOT NULL,
+ test_awesome TINYINT unsigned NOT NULL,
+ test_stuff BLOB NOT NULL,
+ test_moarstuff BLOB NOT NULL,
+ test_time varbinary(14) NOT NULL
+ );',
+ __METHOD__
+ );
+ }
+ }
+
+ protected function tearDown() {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->dropTable( 'orm_test', __METHOD__ );
+
+ parent::tearDown();
+ }
+
+ public function constructorTestProvider() {
+ $dbw = wfGetDB( DB_MASTER );
+ return array(
+ array(
+ array(
+ 'name' => 'Foobar',
+ 'time' => $dbw->timestamp( '20120101020202' ),
+ 'age' => 42,
+ 'height' => 9000.1,
+ 'awesome' => true,
+ 'stuff' => array( 13, 11, 7, 5, 3, 2 ),
+ 'moarstuff' => (object)array( 'foo' => 'bar', 'bar' => array( 4, 2 ), 'baz' => true )
+ ),
+ true
+ ),
+ );
+ }
+
+ /**
+ * @since 1.21
+ * @return array
+ */
+ protected function getMockValues() {
+ return array(
+ 'id' => 1,
+ 'str' => 'foobar4645645',
+ 'int' => 42,
+ 'float' => 4.2,
+ 'bool' => '',
+ 'array' => array( 42, 'foobar' ),
+ 'blob' => new stdClass()
+ );
+ }
+}
+
+class TestORMRow extends ORMRow {
+}
+
+class TestORMTable extends ORMTable {
+
+ /**
+ * Returns the name of the database table objects of this type are stored in.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ public function getName() {
+ return 'orm_test';
+ }
+
+ /**
+ * Returns the name of a IORMRow implementing class that
+ * represents single rows in this table.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ public function getRowClass() {
+ return 'TestORMRow';
+ }
+
+ /**
+ * Returns an array with the fields and their types this object contains.
+ * This corresponds directly to the fields in the database, without prefix.
+ *
+ * field name => type
+ *
+ * Allowed types:
+ * * id
+ * * str
+ * * int
+ * * float
+ * * bool
+ * * array
+ * * blob
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function getFields() {
+ return array(
+ 'id' => 'id',
+ 'name' => 'str',
+ 'age' => 'int',
+ 'height' => 'float',
+ 'awesome' => 'bool',
+ 'stuff' => 'array',
+ 'moarstuff' => 'blob',
+ 'time' => 'str', // TS_MW
+ );
+ }
+
+ /**
+ * Gets the db field prefix.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ protected function getFieldPrefix() {
+ return 'test_';
+ }
+}
diff --git a/tests/phpunit/includes/debug/MWDebugTest.php b/tests/phpunit/includes/debug/MWDebugTest.php
new file mode 100644
index 00000000..6e41de75
--- /dev/null
+++ b/tests/phpunit/includes/debug/MWDebugTest.php
@@ -0,0 +1,141 @@
+<?php
+
+class MWDebugTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ // Make sure MWDebug class is enabled
+ static $MWDebugEnabled = false;
+ if ( !$MWDebugEnabled ) {
+ MWDebug::init();
+ $MWDebugEnabled = true;
+ }
+ /** Clear log before each test */
+ MWDebug::clearLog();
+ wfSuppressWarnings();
+ }
+
+ protected function tearDown() {
+ wfRestoreWarnings();
+ parent::tearDown();
+ }
+
+ /**
+ * @covers MWDebug::log
+ */
+ public function testAddLog() {
+ MWDebug::log( 'logging a string' );
+ $this->assertEquals(
+ array( array(
+ 'msg' => 'logging a string',
+ 'type' => 'log',
+ 'caller' => __METHOD__,
+ ) ),
+ MWDebug::getLog()
+ );
+ }
+
+ /**
+ * @covers MWDebug::warning
+ */
+ public function testAddWarning() {
+ MWDebug::warning( 'Warning message' );
+ $this->assertEquals(
+ array( array(
+ 'msg' => 'Warning message',
+ 'type' => 'warn',
+ 'caller' => 'MWDebugTest::testAddWarning',
+ ) ),
+ MWDebug::getLog()
+ );
+ }
+
+ /**
+ * @covers MWDebug::deprecated
+ */
+ public function testAvoidDuplicateDeprecations() {
+ MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+ MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+
+ // assertCount() not available on WMF integration server
+ $this->assertEquals( 1,
+ count( MWDebug::getLog() ),
+ "Only one deprecated warning per function should be kept"
+ );
+ }
+
+ /**
+ * @covers MWDebug::deprecated
+ */
+ public function testAvoidNonConsecutivesDuplicateDeprecations() {
+ MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+ MWDebug::warning( 'some warning' );
+ MWDebug::log( 'we could have logged something too' );
+ // Another deprecation
+ MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+
+ // assertCount() not available on WMF integration server
+ $this->assertEquals( 3,
+ count( MWDebug::getLog() ),
+ "Only one deprecated warning per function should be kept"
+ );
+ }
+
+ /**
+ * @covers MWDebug::appendDebugInfoToApiResult
+ */
+ public function testAppendDebugInfoToApiResultXmlFormat() {
+ $request = $this->newApiRequest(
+ array( 'action' => 'help', 'format' => 'xml' ),
+ '/api.php?action=help&format=xml'
+ );
+
+ $context = new RequestContext();
+ $context->setRequest( $request );
+
+ $apiMain = new ApiMain( $context );
+
+ $result = new ApiResult( $apiMain );
+ $result->setRawMode( true );
+
+ MWDebug::appendDebugInfoToApiResult( $context, $result );
+
+ $this->assertInstanceOf( 'ApiResult', $result );
+ $data = $result->getData();
+
+ $expectedKeys = array( 'mwVersion', 'phpEngine', 'phpVersion', 'gitRevision', 'gitBranch',
+ 'gitViewUrl', 'time', 'log', 'debugLog', 'queries', 'request', 'memory',
+ 'memoryPeak', 'includes', 'profile', '_element' );
+
+ foreach ( $expectedKeys as $expectedKey ) {
+ $this->assertArrayHasKey( $expectedKey, $data['debuginfo'], "debuginfo has $expectedKey" );
+ }
+
+ $xml = ApiFormatXml::recXmlPrint( 'help', $data );
+
+ // exception not thrown
+ $this->assertInternalType( 'string', $xml );
+ }
+
+ /**
+ * @param string[] $params
+ * @param string $requestUrl
+ *
+ * @return FauxRequest
+ */
+ private function newApiRequest( array $params, $requestUrl ) {
+ $request = $this->getMockBuilder( 'FauxRequest' )
+ ->setMethods( array( 'getRequestURL' ) )
+ ->setConstructorArgs( array(
+ $params
+ ) )
+ ->getMock();
+
+ $request->expects( $this->any() )
+ ->method( 'getRequestURL' )
+ ->will( $this->returnValue( $requestUrl ) );
+
+ return $request;
+ }
+
+}
diff --git a/tests/phpunit/includes/deferred/DeferredUpdatesTest.php b/tests/phpunit/includes/deferred/DeferredUpdatesTest.php
new file mode 100644
index 00000000..5348c854
--- /dev/null
+++ b/tests/phpunit/includes/deferred/DeferredUpdatesTest.php
@@ -0,0 +1,38 @@
+<?php
+
+class DeferredUpdatesTest extends MediaWikiTestCase {
+
+ public function testDoUpdates() {
+ $updates = array(
+ '1' => 'deferred update 1',
+ '2' => 'deferred update 2',
+ '3' => 'deferred update 3',
+ '2-1' => 'deferred update 1 within deferred update 2',
+ );
+ DeferredUpdates::addCallableUpdate(
+ function () use ( $updates ) {
+ echo $updates['1'];
+ }
+ );
+ DeferredUpdates::addCallableUpdate(
+ function () use ( $updates ) {
+ echo $updates['2'];
+ DeferredUpdates::addCallableUpdate(
+ function () use ( $updates ) {
+ echo $updates['2-1'];
+ }
+ );
+ }
+ );
+ DeferredUpdates::addCallableUpdate(
+ function () use ( $updates ) {
+ echo $updates[3];
+ }
+ );
+
+ $this->expectOutputString( implode( '', $updates ) );
+
+ DeferredUpdates::doUpdates();
+ }
+
+}
diff --git a/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php b/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php
new file mode 100644
index 00000000..188ad3fd
--- /dev/null
+++ b/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * @licence GNU GPL v2+
+ * @author Adam Shorland
+ *
+ * @group Diff
+ */
+class ArrayDiffFormatterTest extends MediaWikiTestCase {
+
+ /**
+ * @param Diff $input
+ * @param array $expectedOutput
+ * @dataProvider provideTestFormat
+ * @covers ArrayDiffFormatter::format
+ */
+ public function testFormat( $input, $expectedOutput ) {
+ $instance = new ArrayDiffFormatter();
+ $output = $instance->format( $input );
+ $this->assertEquals( $expectedOutput, $output );
+ }
+
+ private function getMockDiff( $edits ) {
+ $diff = $this->getMockBuilder( 'Diff' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $diff->expects( $this->any() )
+ ->method( 'getEdits' )
+ ->will( $this->returnValue( $edits ) );
+ return $diff;
+ }
+
+ private function getMockDiffOp( $type = null, $orig = array(), $closing = array() ) {
+ $diffOp = $this->getMockBuilder( 'DiffOp' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $diffOp->expects( $this->any() )
+ ->method( 'getType' )
+ ->will( $this->returnValue( $type ) );
+ $diffOp->expects( $this->any() )
+ ->method( 'getOrig' )
+ ->will( $this->returnValue( $orig ) );
+ if ( $type === 'change' ) {
+ $diffOp->expects( $this->any() )
+ ->method( 'getClosing' )
+ ->with( $this->isType( 'integer' ) )
+ ->will( $this->returnCallback( function () {
+ return 'mockLine';
+ } ) );
+ } else {
+ $diffOp->expects( $this->any() )
+ ->method( 'getClosing' )
+ ->will( $this->returnValue( $closing ) );
+ }
+ return $diffOp;
+ }
+
+ public function provideTestFormat() {
+ $emptyArrayTestCases = array(
+ $this->getMockDiff( array() ),
+ $this->getMockDiff( array( $this->getMockDiffOp( 'add' ) ) ),
+ $this->getMockDiff( array( $this->getMockDiffOp( 'delete' ) ) ),
+ $this->getMockDiff( array( $this->getMockDiffOp( 'change' ) ) ),
+ $this->getMockDiff( array( $this->getMockDiffOp( 'copy' ) ) ),
+ $this->getMockDiff( array( $this->getMockDiffOp( 'FOOBARBAZ' ) ) ),
+ $this->getMockDiff( array( $this->getMockDiffOp( 'add', 'line' ) ) ),
+ $this->getMockDiff( array( $this->getMockDiffOp( 'delete', array(), array( 'line' ) ) ) ),
+ $this->getMockDiff( array( $this->getMockDiffOp( 'copy', array(), array( 'line' ) ) ) ),
+ );
+
+ $otherTestCases = array();
+ $otherTestCases[] = array(
+ $this->getMockDiff( array( $this->getMockDiffOp( 'add', array( ), array( 'a1' ) ) ) ),
+ array( array( 'action' => 'add', 'new' => 'a1', 'newline' => 1 ) ),
+ );
+ $otherTestCases[] = array(
+ $this->getMockDiff( array( $this->getMockDiffOp( 'add', array( ), array( 'a1', 'a2' ) ) ) ),
+ array(
+ array( 'action' => 'add', 'new' => 'a1', 'newline' => 1 ),
+ array( 'action' => 'add', 'new' => 'a2', 'newline' => 2 ),
+ ),
+ );
+ $otherTestCases[] = array(
+ $this->getMockDiff( array( $this->getMockDiffOp( 'delete', array( 'd1' ) ) ) ),
+ array( array( 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ) ),
+ );
+ $otherTestCases[] = array(
+ $this->getMockDiff( array( $this->getMockDiffOp( 'delete', array( 'd1', 'd2' ) ) ) ),
+ array(
+ array( 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ),
+ array( 'action' => 'delete', 'old' => 'd2', 'oldline' => 2 ),
+ ),
+ );
+ $otherTestCases[] = array(
+ $this->getMockDiff( array( $this->getMockDiffOp( 'change', array( 'd1' ), array( 'a1' ) ) ) ),
+ array( array(
+ 'action' => 'change',
+ 'old' => 'd1',
+ 'new' => 'mockLine',
+ 'newline' => 1, 'oldline' => 1
+ ) ),
+ );
+ $otherTestCases[] = array(
+ $this->getMockDiff( array( $this->getMockDiffOp(
+ 'change',
+ array( 'd1', 'd2' ),
+ array( 'a1', 'a2' )
+ ) ) ),
+ array(
+ array(
+ 'action' => 'change',
+ 'old' => 'd1',
+ 'new' => 'mockLine',
+ 'newline' => 1, 'oldline' => 1
+ ),
+ array(
+ 'action' => 'change',
+ 'old' => 'd2',
+ 'new' => 'mockLine',
+ 'newline' => 2, 'oldline' => 2
+ ),
+ ),
+ );
+
+ $testCases = array();
+ foreach ( $emptyArrayTestCases as $testCase ) {
+ $testCases[] = array( $testCase, array() );
+ }
+ foreach ( $otherTestCases as $testCase ) {
+ $testCases[] = array( $testCase[0], $testCase[1] );
+ }
+ return $testCases;
+ }
+
+}
diff --git a/tests/phpunit/includes/diff/DiffOpTest.php b/tests/phpunit/includes/diff/DiffOpTest.php
new file mode 100644
index 00000000..d89b89fe
--- /dev/null
+++ b/tests/phpunit/includes/diff/DiffOpTest.php
@@ -0,0 +1,73 @@
+<?php
+
+//Load our FakeDiffOp
+require_once __DIR__ . DIRECTORY_SEPARATOR . 'FakeDiffOp.php';
+
+/**
+ * @licence GNU GPL v2+
+ * @author Adam Shorland
+ *
+ * @group Diff
+ */
+class DiffOpTest extends MediaWikiTestCase {
+
+ /**
+ * @covers DiffOp::getType
+ */
+ public function testGetType() {
+ $obj = new FakeDiffOp();
+ $obj->type = 'foo';
+ $this->assertEquals( 'foo', $obj->getType() );
+ }
+
+ /**
+ * @covers DiffOp::getOrig
+ */
+ public function testGetOrig() {
+ $obj = new FakeDiffOp();
+ $obj->orig = array( 'foo' );
+ $this->assertEquals( array( 'foo' ), $obj->getOrig() );
+ }
+
+ /**
+ * @covers DiffOp::getClosing
+ */
+ public function testGetClosing() {
+ $obj = new FakeDiffOp();
+ $obj->closing = array( 'foo' );
+ $this->assertEquals( array( 'foo' ), $obj->getClosing() );
+ }
+
+ /**
+ * @covers DiffOp::getClosing
+ */
+ public function testGetClosingWithParameter() {
+ $obj = new FakeDiffOp();
+ $obj->closing = array( 'foo', 'bar', 'baz' );
+ $this->assertEquals( 'foo', $obj->getClosing( 0 ) );
+ $this->assertEquals( 'bar', $obj->getClosing( 1 ) );
+ $this->assertEquals( 'baz', $obj->getClosing( 2 ) );
+ $this->assertEquals( null, $obj->getClosing( 3 ) );
+ }
+
+ /**
+ * @covers DiffOp::norig
+ */
+ public function testNorig() {
+ $obj = new FakeDiffOp();
+ $this->assertEquals( 0, $obj->norig() );
+ $obj->orig = array( 'foo' );
+ $this->assertEquals( 1, $obj->norig() );
+ }
+
+ /**
+ * @covers DiffOp::nclosing
+ */
+ public function testNclosing() {
+ $obj = new FakeDiffOp();
+ $this->assertEquals( 0, $obj->nclosing() );
+ $obj->closing = array( 'foo' );
+ $this->assertEquals( 1, $obj->nclosing() );
+ }
+
+}
diff --git a/tests/phpunit/includes/diff/DiffTest.php b/tests/phpunit/includes/diff/DiffTest.php
new file mode 100644
index 00000000..1911c82a
--- /dev/null
+++ b/tests/phpunit/includes/diff/DiffTest.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @licence GNU GPL v2+
+ * @author Adam Shorland
+ *
+ * @group Diff
+ */
+class DiffTest extends MediaWikiTestCase {
+
+ /**
+ * @covers Diff::getEdits
+ */
+ public function testGetEdits() {
+ $obj = new Diff( array(), array() );
+ $obj->edits = 'FooBarBaz';
+ $this->assertEquals( 'FooBarBaz', $obj->getEdits() );
+ }
+
+}
diff --git a/tests/phpunit/includes/diff/DifferenceEngineTest.php b/tests/phpunit/includes/diff/DifferenceEngineTest.php
new file mode 100644
index 00000000..5474b963
--- /dev/null
+++ b/tests/phpunit/includes/diff/DifferenceEngineTest.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @covers DifferenceEngine
+ *
+ * @todo tests for the rest of DifferenceEngine!
+ *
+ * @group Database
+ * @group Diff
+ *
+ * @licence GNU GPL v2+
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class DifferenceEngineTest extends MediaWikiTestCase {
+
+ protected $context;
+
+ private static $revisions;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $title = $this->getTitle();
+
+ $this->context = new RequestContext();
+ $this->context->setTitle( $title );
+
+ if ( !self::$revisions ) {
+ self::$revisions = $this->doEdits();
+ }
+ }
+
+ /**
+ * @return Title
+ */
+ protected function getTitle() {
+ $namespace = $this->getDefaultWikitextNS();
+ return Title::newFromText( 'Kitten', $namespace );
+ }
+
+ /**
+ * @return int[] Revision ids
+ */
+ protected function doEdits() {
+ $title = $this->getTitle();
+ $page = WikiPage::factory( $title );
+
+ $strings = array( "it is a kitten", "two kittens", "three kittens", "four kittens" );
+ $revisions = array();
+
+ foreach ( $strings as $string ) {
+ $content = ContentHandler::makeContent( $string, $title );
+ $page->doEditContent( $content, 'edit page' );
+ $revisions[] = $page->getLatest();
+ }
+
+ return $revisions;
+ }
+
+ public function testMapDiffPrevNext() {
+ $cases = $this->getMapDiffPrevNextCases();
+
+ foreach ( $cases as $case ) {
+ list( $expected, $old, $new, $message ) = $case;
+
+ $diffEngine = new DifferenceEngine( $this->context, $old, $new, 2, true, false );
+ $diffMap = $diffEngine->mapDiffPrevNext( $old, $new );
+ $this->assertEquals( $expected, $diffMap, $message );
+ }
+ }
+
+ private function getMapDiffPrevNextCases() {
+ $revs = self::$revisions;
+
+ return array(
+ array( array( $revs[1], $revs[2] ), $revs[2], 'prev', 'diff=prev' ),
+ array( array( $revs[2], $revs[3] ), $revs[2], 'next', 'diff=next' ),
+ array( array( $revs[1], $revs[3] ), $revs[1], $revs[3], 'diff=' . $revs[3] )
+ );
+ }
+
+ public function testLoadRevisionData() {
+ $cases = $this->getLoadRevisionDataCases();
+
+ foreach ( $cases as $case ) {
+ list( $expectedOld, $expectedNew, $old, $new, $message ) = $case;
+
+ $diffEngine = new DifferenceEngine( $this->context, $old, $new, 2, true, false );
+ $diffEngine->loadRevisionData();
+
+ $this->assertEquals( $diffEngine->getOldid(), $expectedOld, $message );
+ $this->assertEquals( $diffEngine->getNewid(), $expectedNew, $message );
+ }
+ }
+
+ private function getLoadRevisionDataCases() {
+ $revs = self::$revisions;
+
+ return array(
+ array( $revs[2], $revs[3], $revs[3], 'prev', 'diff=prev' ),
+ array( $revs[2], $revs[3], $revs[2], 'next', 'diff=next' ),
+ array( $revs[1], $revs[3], $revs[1], $revs[3], 'diff=' . $revs[3] ),
+ array( $revs[1], $revs[3], $revs[1], 0, 'diff=0' )
+ );
+ }
+
+ public function testGetOldid() {
+ $revs = self::$revisions;
+
+ $diffEngine = new DifferenceEngine( $this->context, $revs[1], $revs[2], 2, true, false );
+ $this->assertEquals( $revs[1], $diffEngine->getOldid(), 'diff get old id' );
+ }
+
+ public function testGetNewid() {
+ $revs = self::$revisions;
+
+ $diffEngine = new DifferenceEngine( $this->context, $revs[1], $revs[2], 2, true, false );
+ $this->assertEquals( $revs[2], $diffEngine->getNewid(), 'diff get new id' );
+ }
+
+}
diff --git a/tests/phpunit/includes/diff/FakeDiffOp.php b/tests/phpunit/includes/diff/FakeDiffOp.php
new file mode 100644
index 00000000..70c8f64a
--- /dev/null
+++ b/tests/phpunit/includes/diff/FakeDiffOp.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * Class FakeDiffOp used to test abstract class DiffOp
+ */
+class FakeDiffOp extends DiffOp {
+
+ public function reverse() {
+ return null;
+ }
+}
diff --git a/tests/phpunit/includes/exception/BadTitleErrorTest.php b/tests/phpunit/includes/exception/BadTitleErrorTest.php
new file mode 100644
index 00000000..003efd27
--- /dev/null
+++ b/tests/phpunit/includes/exception/BadTitleErrorTest.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * @covers BadTitleError
+ * @author Adam Shorland
+ */
+class BadTitleErrorTest extends MediaWikiTestCase {
+
+ protected $wgOut;
+
+ protected function setUp() {
+ parent::setUp();
+ global $wgOut;
+ $this->wgOut = clone $wgOut;
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ global $wgOut;
+ $wgOut = $this->wgOut;
+ }
+
+ public function testExceptionSetsStatusCode() {
+ global $wgOut;
+ $wgOut = $this->getMockWgOut();
+ try {
+ throw new BadTitleError();
+ } catch ( BadTitleError $e ) {
+ $e->report();
+ $this->assertTrue( true );
+ }
+ }
+
+ private function getMockWgOut() {
+ $mock = $this->getMockBuilder( 'OutputPage' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->once() )
+ ->method( 'setStatusCode' )
+ ->with( 400 );
+ return $mock;
+ }
+
+}
diff --git a/tests/phpunit/includes/exception/ErrorPageErrorTest.php b/tests/phpunit/includes/exception/ErrorPageErrorTest.php
new file mode 100644
index 00000000..13dcf33b
--- /dev/null
+++ b/tests/phpunit/includes/exception/ErrorPageErrorTest.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @covers ErrorPageError
+ * @author Adam Shorland
+ */
+class ErrorPageErrorTest extends MediaWikiTestCase {
+
+ private $wgOut;
+
+ protected function setUp() {
+ parent::setUp();
+ global $wgOut;
+ $this->wgOut = clone $wgOut;
+ }
+
+ protected function tearDown() {
+ global $wgOut;
+ $wgOut = $this->wgOut;
+ parent::tearDown();
+ }
+
+ private function getMockMessage() {
+ $mockMessage = $this->getMockBuilder( 'Message' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mockMessage->expects( $this->once() )
+ ->method( 'inLanguage' )
+ ->will( $this->returnValue( $mockMessage ) );
+ $mockMessage->expects( $this->once() )
+ ->method( 'useDatabase' )
+ ->will( $this->returnValue( $mockMessage ) );
+ return $mockMessage;
+ }
+
+ public function testConstruction() {
+ $mockMessage = $this->getMockMessage();
+ $title = 'Foo';
+ $params = array( 'Baz' );
+ $e = new ErrorPageError( $title, $mockMessage, $params );
+ $this->assertEquals( $title, $e->title );
+ $this->assertEquals( $mockMessage, $e->msg );
+ $this->assertEquals( $params, $e->params );
+ }
+
+ public function testReport() {
+ $mockMessage = $this->getMockMessage();
+ $title = 'Foo';
+ $params = array( 'Baz' );
+
+ global $wgOut;
+ $wgOut = $this->getMockBuilder( 'OutputPage' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $wgOut->expects( $this->once() )
+ ->method( 'showErrorPage' )
+ ->with( $title, $mockMessage, $params );
+ $wgOut->expects( $this->once() )
+ ->method( 'output' );
+
+ $e = new ErrorPageError( $title, $mockMessage, $params );
+ $e->report();
+ }
+
+
+
+}
diff --git a/tests/phpunit/includes/exception/MWExceptionHandlerTest.php b/tests/phpunit/includes/exception/MWExceptionHandlerTest.php
new file mode 100644
index 00000000..dc5dc6aa
--- /dev/null
+++ b/tests/phpunit/includes/exception/MWExceptionHandlerTest.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * @author Antoine Musso
+ * @copyright Copyright © 2013, Antoine Musso
+ * @copyright Copyright © 2013, Wikimedia Foundation Inc.
+ * @file
+ */
+
+class MWExceptionHandlerTest extends MediaWikiTestCase {
+
+ /**
+ * @covers MWExceptionHandler::getRedactedTrace
+ */
+ public function testGetRedactedTrace() {
+ $refvar = 'value';
+ try {
+ $array = array( 'a', 'b' );
+ $object = new StdClass();
+ self::helperThrowAnException( $array, $object, $refvar );
+ } catch ( Exception $e ) {
+ }
+
+ # Make sure our stack trace contains an array and an object passed to
+ # some function in the stacktrace. Else, we can not assert the trace
+ # redaction achieved its job.
+ $trace = $e->getTrace();
+ $hasObject = false;
+ $hasArray = false;
+ foreach ( $trace as $frame ) {
+ if ( !isset( $frame['args'] ) ) {
+ continue;
+ }
+ foreach ( $frame['args'] as $arg ) {
+ $hasObject = $hasObject || is_object( $arg );
+ $hasArray = $hasArray || is_array( $arg );
+ }
+
+ if ( $hasObject && $hasArray ) {
+ break;
+ }
+ }
+ $this->assertTrue( $hasObject,
+ "The stacktrace must have a function having an object has parameter" );
+ $this->assertTrue( $hasArray,
+ "The stacktrace must have a function having an array has parameter" );
+
+ # Now we redact the trace.. and make sure no function arguments are
+ # arrays or objects.
+ $redacted = MWExceptionHandler::getRedactedTrace( $e );
+
+ foreach ( $redacted as $frame ) {
+ if ( !isset( $frame['args'] ) ) {
+ continue;
+ }
+ foreach ( $frame['args'] as $arg ) {
+ $this->assertNotInternalType( 'array', $arg );
+ $this->assertNotInternalType( 'object', $arg );
+ }
+ }
+
+ $this->assertEquals( 'value', $refvar, 'Ensuring reference variable wasn\'t changed' );
+ }
+
+ /**
+ * Helper function for testExpandArgumentsInCall
+ *
+ * Pass it an object and an array, and something by reference :-)
+ *
+ * @throws Exception
+ */
+ protected static function helperThrowAnException( $a, $b, &$c ) {
+ throw new Exception();
+ }
+}
diff --git a/tests/phpunit/includes/exception/MWExceptionTest.php b/tests/phpunit/includes/exception/MWExceptionTest.php
new file mode 100644
index 00000000..ef0f2a9e
--- /dev/null
+++ b/tests/phpunit/includes/exception/MWExceptionTest.php
@@ -0,0 +1,241 @@
+<?php
+/**
+ * @author Antoine Musso
+ * @copyright Copyright © 2013, Antoine Musso
+ * @copyright Copyright © 2013, Wikimedia Foundation Inc.
+ * @file
+ */
+
+class MWExceptionTest extends MediaWikiTestCase {
+
+ /**
+ * @expectedException MWException
+ */
+ public function testMwexceptionThrowing() {
+ throw new MWException();
+ }
+
+ /**
+ * @dataProvider provideTextUseOutputPage
+ * @covers MWException::useOutputPage
+ */
+ public function testUseOutputPage( $expected, $wgLang, $wgFullyInitialised, $wgOut ) {
+ $this->setMwGlobals( array(
+ 'wgLang' => $wgLang,
+ 'wgFullyInitialised' => $wgFullyInitialised,
+ 'wgOut' => $wgOut,
+ ) );
+
+ $e = new MWException();
+ $this->assertEquals( $expected, $e->useOutputPage() );
+ }
+
+ public function provideTextUseOutputPage() {
+ return array(
+ // expected, wgLang, wgFullyInitialised, wgOut
+ array( false, null, null, null ),
+ array( false, $this->getMockLanguage(), null, null ),
+ array( false, $this->getMockLanguage(), true, null ),
+ array( false, null, true, null ),
+ array( false, null, null, true ),
+ array( true, $this->getMockLanguage(), true, true ),
+ );
+ }
+
+ private function getMockLanguage() {
+ return $this->getMockBuilder( 'Language' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ /**
+ * @dataProvider provideUseMessageCache
+ * @covers MWException::useMessageCache
+ */
+ public function testUseMessageCache( $expected, $wgLang ) {
+ $this->setMwGlobals( array(
+ 'wgLang' => $wgLang,
+ ) );
+ $e = new MWException();
+ $this->assertEquals( $expected, $e->useMessageCache() );
+ }
+
+ public function provideUseMessageCache() {
+ return array(
+ array( false, null ),
+ array( true, $this->getMockLanguage() ),
+ );
+ }
+
+ /**
+ * @covers MWException::isLoggable
+ */
+ public function testIsLogable() {
+ $e = new MWException();
+ $this->assertTrue( $e->isLoggable() );
+ }
+
+ /**
+ * @dataProvider provideRunHooks
+ * @covers MWException::runHooks
+ */
+ public function testRunHooks( $wgExceptionHooks, $name, $args, $expectedReturn ) {
+ $this->setMwGlobals( array(
+ 'wgExceptionHooks' => $wgExceptionHooks,
+ ) );
+ $e = new MWException();
+ $this->assertEquals( $expectedReturn, $e->runHooks( $name, $args ) );
+ }
+
+ public static function provideRunHooks() {
+ return array(
+ array( null, null, null, null ),
+ array( array(), 'name', array(), null ),
+ array( array( 'name' => false ), 'name', array(), null ),
+ array(
+ array( 'mockHook' => array( 'MWExceptionTest::mockHook' ) ),
+ 'mockHook', array(), 'YAY.[]'
+ ),
+ array(
+ array( 'mockHook' => array( 'MWExceptionTest::mockHook' ) ),
+ 'mockHook', array( 'a' ), 'YAY.{"1":"a"}'
+ ),
+ array(
+ array( 'mockHook' => array( 'MWExceptionTest::mockHook' ) ),
+ 'mockHook', array( null ), null
+ ),
+ );
+ }
+
+ /**
+ * Used in conjunction with provideRunHooks and testRunHooks as a mock callback for a hook
+ */
+ public static function mockHook() {
+ $args = func_get_args();
+ if ( !$args[0] instanceof MWException ) {
+ return '$caller not instance of MWException';
+ }
+ unset( $args[0] );
+ if ( array_key_exists( 1, $args ) && $args[1] === null ) {
+ return null;
+ }
+ return 'YAY.' . json_encode( $args );
+ }
+
+ /**
+ * @dataProvider provideIsCommandLine
+ * @covers MWException::isCommandLine
+ */
+ public function testisCommandLine( $expected, $wgCommandLineMode ) {
+ $this->setMwGlobals( array(
+ 'wgCommandLineMode' => $wgCommandLineMode,
+ ) );
+ $e = new MWException();
+ $this->assertEquals( $expected, $e->isCommandLine() );
+ }
+
+ public static function provideIsCommandLine() {
+ return array(
+ array( false, null ),
+ array( true, true ),
+ );
+ }
+
+ /**
+ * Verify the exception classes are JSON serializabe.
+ *
+ * @covers MWExceptionHandler::jsonSerializeException
+ * @dataProvider provideExceptionClasses
+ */
+ public function testJsonSerializeExceptions( $exception_class ) {
+ $json = MWExceptionHandler::jsonSerializeException(
+ new $exception_class()
+ );
+ $this->assertNotEquals( false, $json,
+ "The $exception_class exception should be JSON serializable, got false." );
+ }
+
+ public static function provideExceptionClasses() {
+ return array(
+ array( 'Exception' ),
+ array( 'MWException' ),
+ );
+ }
+
+ /**
+ * Lame JSON schema validation.
+ *
+ * @covers MWExceptionHandler::jsonSerializeException
+ *
+ * @param string $expectedKeyType Type expected as returned by gettype()
+ * @param string $exClass An exception class (ie: Exception, MWException)
+ * @param string $key Name of the key to validate in the serialized JSON
+ * @dataProvider provideJsonSerializedKeys
+ */
+ public function testJsonserializeexceptionKeys( $expectedKeyType, $exClass, $key ) {
+
+ # Make sure we log a backtrace:
+ $this->setMwGlobals( array( 'wgLogExceptionBacktrace' => true ) );
+
+ $json = json_decode(
+ MWExceptionHandler::jsonSerializeException( new $exClass())
+ );
+ $this->assertObjectHasAttribute( $key, $json,
+ "JSON serialized exception is missing key '$key'"
+ );
+ $this->assertInternalType( $expectedKeyType, $json->$key,
+ "JSON serialized key '$key' has type " . gettype( $json->$key )
+ . " (expected: $expectedKeyType)."
+ );
+ }
+
+ /**
+ * Returns test cases: exception class, key name, gettype()
+ */
+ public static function provideJsonSerializedKeys() {
+ $testCases = array();
+ foreach ( array( 'Exception', 'MWException' ) as $exClass ) {
+ $exTests = array(
+ array( 'string', $exClass, 'id' ),
+ array( 'string', $exClass, 'file' ),
+ array( 'integer', $exClass, 'line' ),
+ array( 'string', $exClass, 'message' ),
+ array( 'null', $exClass, 'url' ),
+ # Backtrace only enabled with wgLogExceptionBacktrace = true
+ array( 'array', $exClass, 'backtrace' ),
+ );
+ $testCases = array_merge( $testCases, $exTests );
+ }
+ return $testCases;
+ }
+
+ /**
+ * Given wgLogExceptionBacktrace is true
+ * then serialized exception SHOULD have a backtrace
+ *
+ * @covers MWExceptionHandler::jsonSerializeException
+ */
+ public function testJsonserializeexceptionBacktracingEnabled() {
+ $this->setMwGlobals( array( 'wgLogExceptionBacktrace' => true ) );
+ $json = json_decode(
+ MWExceptionHandler::jsonSerializeException( new Exception() )
+ );
+ $this->assertObjectHasAttribute( 'backtrace', $json );
+ }
+
+ /**
+ * Given wgLogExceptionBacktrace is false
+ * then serialized exception SHOULD NOT have a backtrace
+ *
+ * @covers MWExceptionHandler::jsonSerializeException
+ */
+ public function testJsonserializeexceptionBacktracingDisabled() {
+ $this->setMwGlobals( array( 'wgLogExceptionBacktrace' => false ) );
+ $json = json_decode(
+ MWExceptionHandler::jsonSerializeException( new Exception() )
+ );
+ $this->assertObjectNotHasAttribute( 'backtrace', $json );
+
+ }
+
+}
diff --git a/tests/phpunit/includes/exception/ReadOnlyErrorTest.php b/tests/phpunit/includes/exception/ReadOnlyErrorTest.php
new file mode 100644
index 00000000..6f6aba47
--- /dev/null
+++ b/tests/phpunit/includes/exception/ReadOnlyErrorTest.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @covers ReadOnlyError
+ * @author Adam Shorland
+ */
+class ReadOnlyErrorTest extends MediaWikiTestCase {
+
+ public function testConstruction() {
+ $e = new ReadOnlyError();
+ $this->assertEquals( 'readonly', $e->title );
+ $this->assertEquals( 'readonlytext', $e->msg );
+ $this->assertEquals( wfReadOnlyReason() ?: array(), $e->params );
+ }
+
+}
diff --git a/tests/phpunit/includes/exception/ThrottledErrorTest.php b/tests/phpunit/includes/exception/ThrottledErrorTest.php
new file mode 100644
index 00000000..bdb143fa
--- /dev/null
+++ b/tests/phpunit/includes/exception/ThrottledErrorTest.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @covers ThrottledError
+ * @author Adam Shorland
+ */
+class ThrottledErrorTest extends MediaWikiTestCase {
+
+ protected $wgOut;
+
+ protected function setUp() {
+ parent::setUp();
+ global $wgOut;
+ $this->wgOut = clone $wgOut;
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ global $wgOut;
+ $wgOut = $this->wgOut;
+ }
+
+ public function testExceptionSetsStatusCode() {
+ global $wgOut;
+ $wgOut = $this->getMockWgOut();
+ try {
+ throw new ThrottledError();
+ } catch ( ThrottledError $e ) {
+ $e->report();
+ $this->assertTrue( true );
+ }
+ }
+
+ private function getMockWgOut() {
+ $mock = $this->getMockBuilder( 'OutputPage' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->once() )
+ ->method( 'setStatusCode' )
+ ->with( 429 );
+ return $mock;
+ }
+
+}
diff --git a/tests/phpunit/includes/exception/UserNotLoggedInTest.php b/tests/phpunit/includes/exception/UserNotLoggedInTest.php
new file mode 100644
index 00000000..591a0fa1
--- /dev/null
+++ b/tests/phpunit/includes/exception/UserNotLoggedInTest.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @covers UserNotLoggedIn
+ * @author Adam Shorland
+ */
+class UserNotLoggedInTest extends MediaWikiTestCase {
+
+ public function testConstruction() {
+ $e = new UserNotLoggedIn();
+ $this->assertEquals( 'exception-nologin', $e->title );
+ $this->assertEquals( 'exception-nologin-text', $e->msg );
+ $this->assertEquals( array(), $e->params );
+ }
+
+}
diff --git a/tests/phpunit/includes/filebackend/FileBackendTest.php b/tests/phpunit/includes/filebackend/FileBackendTest.php
new file mode 100644
index 00000000..9558cc7d
--- /dev/null
+++ b/tests/phpunit/includes/filebackend/FileBackendTest.php
@@ -0,0 +1,2472 @@
+<?php
+
+/**
+ * @group FileRepo
+ * @group FileBackend
+ * @group medium
+ */
+class FileBackendTest extends MediaWikiTestCase {
+
+ /** @var FileBackend */
+ private $backend;
+ /** @var FileBackendMultiWrite */
+ private $multiBackend;
+ /** @var FSFileBackend */
+ public $singleBackend;
+ private $filesToPrune = array();
+ private static $backendToUse;
+
+ protected function setUp() {
+ global $wgFileBackends;
+ parent::setUp();
+ $uniqueId = time() . '-' . mt_rand();
+ $tmpPrefix = wfTempDir() . '/filebackend-unittest-' . $uniqueId;
+ if ( $this->getCliArg( 'use-filebackend' ) ) {
+ if ( self::$backendToUse ) {
+ $this->singleBackend = self::$backendToUse;
+ } else {
+ $name = $this->getCliArg( 'use-filebackend' );
+ $useConfig = array();
+ foreach ( $wgFileBackends as $conf ) {
+ if ( $conf['name'] == $name ) {
+ $useConfig = $conf;
+ break;
+ }
+ }
+ $useConfig['name'] = 'localtesting'; // swap name
+ $useConfig['shardViaHashLevels'] = array( // test sharding
+ 'unittest-cont1' => array( 'levels' => 1, 'base' => 16, 'repeat' => 1 )
+ );
+ if ( isset( $useConfig['fileJournal'] ) ) {
+ $useConfig['fileJournal'] = FileJournal::factory( $useConfig['fileJournal'], $name );
+ }
+ $useConfig['lockManager'] = LockManagerGroup::singleton()->get( $useConfig['lockManager'] );
+ $class = $useConfig['class'];
+ self::$backendToUse = new $class( $useConfig );
+ $this->singleBackend = self::$backendToUse;
+ }
+ } else {
+ $this->singleBackend = new FSFileBackend( array(
+ 'name' => 'localtesting',
+ 'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ),
+ 'wikiId' => wfWikiID(),
+ 'containerPaths' => array(
+ 'unittest-cont1' => "{$tmpPrefix}-localtesting-cont1",
+ 'unittest-cont2' => "{$tmpPrefix}-localtesting-cont2" )
+ ) );
+ }
+ $this->multiBackend = new FileBackendMultiWrite( array(
+ 'name' => 'localtesting',
+ 'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ),
+ 'parallelize' => 'implicit',
+ 'wikiId' => wfWikiId() . $uniqueId,
+ 'backends' => array(
+ array(
+ 'name' => 'localmultitesting1',
+ 'class' => 'FSFileBackend',
+ 'containerPaths' => array(
+ 'unittest-cont1' => "{$tmpPrefix}-localtestingmulti1-cont1",
+ 'unittest-cont2' => "{$tmpPrefix}-localtestingmulti1-cont2" ),
+ 'isMultiMaster' => false
+ ),
+ array(
+ 'name' => 'localmultitesting2',
+ 'class' => 'FSFileBackend',
+ 'containerPaths' => array(
+ 'unittest-cont1' => "{$tmpPrefix}-localtestingmulti2-cont1",
+ 'unittest-cont2' => "{$tmpPrefix}-localtestingmulti2-cont2" ),
+ 'isMultiMaster' => true
+ )
+ )
+ ) );
+ $this->filesToPrune = array();
+ }
+
+ private static function baseStorePath() {
+ return 'mwstore://localtesting';
+ }
+
+ private function backendClass() {
+ return get_class( $this->backend );
+ }
+
+ /**
+ * @dataProvider provider_testIsStoragePath
+ * @covers FileBackend::isStoragePath
+ */
+ public function testIsStoragePath( $path, $isStorePath ) {
+ $this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ),
+ "FileBackend::isStoragePath on path '$path'" );
+ }
+
+ public static function provider_testIsStoragePath() {
+ return array(
+ array( 'mwstore://', true ),
+ array( 'mwstore://backend', true ),
+ array( 'mwstore://backend/container', true ),
+ array( 'mwstore://backend/container/', true ),
+ array( 'mwstore://backend/container/path', true ),
+ array( 'mwstore://backend//container/', true ),
+ array( 'mwstore://backend//container//', true ),
+ array( 'mwstore://backend//container//path', true ),
+ array( 'mwstore:///', true ),
+ array( 'mwstore:/', false ),
+ array( 'mwstore:', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provider_testSplitStoragePath
+ * @covers FileBackend::splitStoragePath
+ */
+ public function testSplitStoragePath( $path, $res ) {
+ $this->assertEquals( $res, FileBackend::splitStoragePath( $path ),
+ "FileBackend::splitStoragePath on path '$path'" );
+ }
+
+ public static function provider_testSplitStoragePath() {
+ return array(
+ array( 'mwstore://backend/container', array( 'backend', 'container', '' ) ),
+ array( 'mwstore://backend/container/', array( 'backend', 'container', '' ) ),
+ array( 'mwstore://backend/container/path', array( 'backend', 'container', 'path' ) ),
+ array( 'mwstore://backend/container//path', array( 'backend', 'container', '/path' ) ),
+ array( 'mwstore://backend//container/path', array( null, null, null ) ),
+ array( 'mwstore://backend//container//path', array( null, null, null ) ),
+ array( 'mwstore://', array( null, null, null ) ),
+ array( 'mwstore://backend', array( null, null, null ) ),
+ array( 'mwstore:///', array( null, null, null ) ),
+ array( 'mwstore:/', array( null, null, null ) ),
+ array( 'mwstore:', array( null, null, null ) )
+ );
+ }
+
+ /**
+ * @dataProvider provider_normalizeStoragePath
+ * @covers FileBackend::normalizeStoragePath
+ */
+ public function testNormalizeStoragePath( $path, $res ) {
+ $this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ),
+ "FileBackend::normalizeStoragePath on path '$path'" );
+ }
+
+ public static function provider_normalizeStoragePath() {
+ return array(
+ array( 'mwstore://backend/container', 'mwstore://backend/container' ),
+ array( 'mwstore://backend/container/', 'mwstore://backend/container' ),
+ array( 'mwstore://backend/container/path', 'mwstore://backend/container/path' ),
+ array( 'mwstore://backend/container//path', 'mwstore://backend/container/path' ),
+ array( 'mwstore://backend/container///path', 'mwstore://backend/container/path' ),
+ array(
+ 'mwstore://backend/container///path//to///obj',
+ 'mwstore://backend/container/path/to/obj'
+ ),
+ array( 'mwstore://', null ),
+ array( 'mwstore://backend', null ),
+ array( 'mwstore://backend//container/path', null ),
+ array( 'mwstore://backend//container//path', null ),
+ array( 'mwstore:///', null ),
+ array( 'mwstore:/', null ),
+ array( 'mwstore:', null ),
+ );
+ }
+
+ /**
+ * @dataProvider provider_testParentStoragePath
+ * @covers FileBackend::parentStoragePath
+ */
+ public function testParentStoragePath( $path, $res ) {
+ $this->assertEquals( $res, FileBackend::parentStoragePath( $path ),
+ "FileBackend::parentStoragePath on path '$path'" );
+ }
+
+ public static function provider_testParentStoragePath() {
+ return array(
+ array( 'mwstore://backend/container/path/to/obj', 'mwstore://backend/container/path/to' ),
+ array( 'mwstore://backend/container/path/to', 'mwstore://backend/container/path' ),
+ array( 'mwstore://backend/container/path', 'mwstore://backend/container' ),
+ array( 'mwstore://backend/container', null ),
+ array( 'mwstore://backend/container/path/to/obj/', 'mwstore://backend/container/path/to' ),
+ array( 'mwstore://backend/container/path/to/', 'mwstore://backend/container/path' ),
+ array( 'mwstore://backend/container/path/', 'mwstore://backend/container' ),
+ array( 'mwstore://backend/container/', null ),
+ );
+ }
+
+ /**
+ * @dataProvider provider_testExtensionFromPath
+ * @covers FileBackend::extensionFromPath
+ */
+ public function testExtensionFromPath( $path, $res ) {
+ $this->assertEquals( $res, FileBackend::extensionFromPath( $path ),
+ "FileBackend::extensionFromPath on path '$path'" );
+ }
+
+ public static function provider_testExtensionFromPath() {
+ return array(
+ array( 'mwstore://backend/container/path.txt', 'txt' ),
+ array( 'mwstore://backend/container/path.svg.png', 'png' ),
+ array( 'mwstore://backend/container/path', '' ),
+ array( 'mwstore://backend/container/path.', '' ),
+ );
+ }
+
+ /**
+ * @dataProvider provider_testStore
+ */
+ public function testStore( $op ) {
+ $this->filesToPrune[] = $op['src'];
+
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestStore( $op );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestStore( $op );
+ $this->filesToPrune[] = $op['src']; # avoid file leaking
+ $this->tearDownFiles();
+ }
+
+ /**
+ * @covers FileBackend::doOperation
+ */
+ private function doTestStore( $op ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $dest = $op['dst'];
+ $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
+ file_put_contents( $source, "Unit test file" );
+
+ if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
+ $this->backend->store( $op );
+ }
+
+ $status = $this->backend->doOperation( $op );
+
+ $this->assertGoodStatus( $status,
+ "Store from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Store from $source to $dest succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Store from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( true, file_exists( $source ),
+ "Source file $source still exists ($backendName)." );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest exists ($backendName)." );
+
+ $this->assertEquals( filesize( $source ),
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has correct size ($backendName)." );
+
+ $props1 = FSFile::getPropsFromPath( $source );
+ $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
+ $this->assertEquals( $props1, $props2,
+ "Source and destination have the same props ($backendName)." );
+
+ $this->assertBackendPathsConsistent( array( $dest ) );
+ }
+
+ public static function provider_testStore() {
+ $cases = array();
+
+ $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ $toPath = self::baseStorePath() . '/unittest-cont1/e/fun/obj1.txt';
+ $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath );
+ $cases[] = array(
+ $op, // operation
+ $tmpName, // source
+ $toPath, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwrite'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $tmpName, // source
+ $toPath, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwriteSame'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $tmpName, // source
+ $toPath, // dest
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testCopy
+ * @covers FileBackend::doOperation
+ */
+ public function testCopy( $op ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestCopy( $op );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestCopy( $op );
+ $this->tearDownFiles();
+ }
+
+ private function doTestCopy( $op ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $dest = $op['dst'];
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+ $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
+ if ( isset( $op['ignoreMissingSource'] ) ) {
+ $status = $this->backend->doOperation( $op );
+ $this->assertGoodStatus( $status,
+ "Move from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not exist ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest does not exist ($backendName)." );
+
+ return; // done
+ }
+
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+
+ if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
+ $this->backend->copy( $op );
+ }
+
+ $status = $this->backend->doOperation( $op );
+
+ $this->assertGoodStatus( $status,
+ "Copy from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Copy from $source to $dest succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Copy from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source still exists ($backendName)." );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest exists after copy ($backendName)." );
+
+ $this->assertEquals(
+ $this->backend->getFileSize( array( 'src' => $source ) ),
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has correct size ($backendName)." );
+
+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
+ $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
+ $this->assertEquals( $props1, $props2,
+ "Source and destination have the same props ($backendName)." );
+
+ $this->assertBackendPathsConsistent( array( $source, $dest ) );
+ }
+
+ public static function provider_testCopy() {
+ $cases = array();
+
+ $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
+ $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
+
+ $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest );
+ $cases[] = array(
+ $op, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwrite'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwriteSame'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
+ $dest, // dest
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testMove
+ * @covers FileBackend::doOperation
+ */
+ public function testMove( $op ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestMove( $op );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestMove( $op );
+ $this->tearDownFiles();
+ }
+
+ private function doTestMove( $op ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $dest = $op['dst'];
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+ $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
+ if ( isset( $op['ignoreMissingSource'] ) ) {
+ $status = $this->backend->doOperation( $op );
+ $this->assertGoodStatus( $status,
+ "Move from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not exist ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest does not exist ($backendName)." );
+
+ return; // done
+ }
+
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+
+ if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
+ $this->backend->copy( $op );
+ }
+
+ $status = $this->backend->doOperation( $op );
+ $this->assertGoodStatus( $status,
+ "Move from $source to $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Move from $source to $dest succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not still exists ($backendName)." );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest exists after move ($backendName)." );
+
+ $this->assertNotEquals(
+ $this->backend->getFileSize( array( 'src' => $source ) ),
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has correct size ($backendName)." );
+
+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
+ $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
+ $this->assertEquals( false, $props1['fileExists'],
+ "Source file does not exist accourding to props ($backendName)." );
+ $this->assertEquals( true, $props2['fileExists'],
+ "Destination file exists accourding to props ($backendName)." );
+
+ $this->assertBackendPathsConsistent( array( $source, $dest ) );
+ }
+
+ public static function provider_testMove() {
+ $cases = array();
+
+ $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
+ $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
+
+ $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest );
+ $cases[] = array(
+ $op, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwrite'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['overwriteSame'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ $source, // source
+ $dest, // dest
+ );
+
+ $op2 = $op;
+ $op2['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op2, // operation
+ self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
+ $dest, // dest
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testDelete
+ * @covers FileBackend::doOperation
+ */
+ public function testDelete( $op, $withSource, $okStatus ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDelete( $op, $withSource, $okStatus );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDelete( $op, $withSource, $okStatus );
+ $this->tearDownFiles();
+ }
+
+ private function doTestDelete( $op, $withSource, $okStatus ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+
+ if ( $withSource ) {
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+ }
+
+ $status = $this->backend->doOperation( $op );
+ if ( $okStatus ) {
+ $this->assertGoodStatus( $status,
+ "Deletion of file at $source succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Deletion of file at $source succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Deletion of file at $source has proper 'success' field in Status ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Deletion of file at $source failed ($backendName)." );
+ }
+
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
+ "Source file $source does not exist after move ($backendName)." );
+
+ $this->assertFalse(
+ $this->backend->getFileSize( array( 'src' => $source ) ),
+ "Source file $source has correct size (false) ($backendName)." );
+
+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
+ $this->assertFalse( $props1['fileExists'],
+ "Source file $source does not exist according to props ($backendName)." );
+
+ $this->assertBackendPathsConsistent( array( $source ) );
+ }
+
+ public static function provider_testDelete() {
+ $cases = array();
+
+ $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt';
+
+ $op = array( 'op' => 'delete', 'src' => $source );
+ $cases[] = array(
+ $op, // operation
+ true, // with source
+ true // succeeds
+ );
+
+ $cases[] = array(
+ $op, // operation
+ false, // without source
+ false // fails
+ );
+
+ $op['ignoreMissingSource'] = true;
+ $cases[] = array(
+ $op, // operation
+ false, // without source
+ true // succeeds
+ );
+
+ $op['ignoreMissingSource'] = true;
+ $op['src'] = self::baseStorePath() . '/unittest-cont-bad/e/file.txt';
+ $cases[] = array(
+ $op, // operation
+ false, // without source
+ true // succeeds
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testDescribe
+ * @covers FileBackend::doOperation
+ */
+ public function testDescribe( $op, $withSource, $okStatus ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDescribe( $op, $withSource, $okStatus );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDescribe( $op, $withSource, $okStatus );
+ $this->tearDownFiles();
+ }
+
+ private function doTestDescribe( $op, $withSource, $okStatus ) {
+ $backendName = $this->backendClass();
+
+ $source = $op['src'];
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+
+ if ( $withSource ) {
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source,
+ 'headers' => array( 'Content-Disposition' => 'xxx' ) ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+ if ( $this->backend->hasFeatures( FileBackend::ATTR_HEADERS ) ) {
+ $attr = $this->backend->getFileXAttributes( array( 'src' => $source ) );
+ $this->assertHasHeaders( array( 'Content-Disposition' => 'xxx' ), $attr );
+ }
+
+ $status = $this->backend->describe( array( 'src' => $source,
+ 'headers' => array( 'Content-Disposition' => '' ) ) ); // remove
+ $this->assertGoodStatus( $status,
+ "Removal of header for $source succeeded ($backendName)." );
+
+ if ( $this->backend->hasFeatures( FileBackend::ATTR_HEADERS ) ) {
+ $attr = $this->backend->getFileXAttributes( array( 'src' => $source ) );
+ $this->assertFalse( isset( $attr['headers']['content-disposition'] ),
+ "File 'Content-Disposition' header removed." );
+ }
+ }
+
+ $status = $this->backend->doOperation( $op );
+ if ( $okStatus ) {
+ $this->assertGoodStatus( $status,
+ "Describe of file at $source succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Describe of file at $source succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Describe of file at $source has proper 'success' field in Status ($backendName)." );
+ if ( $this->backend->hasFeatures( FileBackend::ATTR_HEADERS ) ) {
+ $attr = $this->backend->getFileXAttributes( array( 'src' => $source ) );
+ $this->assertHasHeaders( $op['headers'], $attr );
+ }
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Describe of file at $source failed ($backendName)." );
+ }
+
+ $this->assertBackendPathsConsistent( array( $source ) );
+ }
+
+ private function assertHasHeaders( array $headers, array $attr ) {
+ foreach ( $headers as $n => $v ) {
+ if ( $n !== '' ) {
+ $this->assertTrue( isset( $attr['headers'][strtolower( $n )] ),
+ "File has '$n' header." );
+ $this->assertEquals( $v, $attr['headers'][strtolower( $n )],
+ "File has '$n' header value." );
+ } else {
+ $this->assertFalse( isset( $attr['headers'][strtolower( $n )] ),
+ "File does not have '$n' header." );
+ }
+ }
+ }
+
+ public static function provider_testDescribe() {
+ $cases = array();
+
+ $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt';
+
+ $op = array( 'op' => 'describe', 'src' => $source,
+ 'headers' => array( 'Content-Disposition' => 'inline' ), );
+ $cases[] = array(
+ $op, // operation
+ true, // with source
+ true // succeeds
+ );
+
+ $cases[] = array(
+ $op, // operation
+ false, // without source
+ false // fails
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testCreate
+ * @covers FileBackend::doOperation
+ */
+ public function testCreate( $op, $alreadyExists, $okStatus, $newSize ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize );
+ $this->tearDownFiles();
+ }
+
+ private function doTestCreate( $op, $alreadyExists, $okStatus, $newSize ) {
+ $backendName = $this->backendClass();
+
+ $dest = $op['dst'];
+ $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
+ $oldText = 'blah...blah...waahwaah';
+ if ( $alreadyExists ) {
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $oldText, 'dst' => $dest ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $dest succeeded ($backendName)." );
+ }
+
+ $status = $this->backend->doOperation( $op );
+ if ( $okStatus ) {
+ $this->assertGoodStatus( $status,
+ "Creation of file at $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Creation of file at $dest succeeded ($backendName)." );
+ $this->assertEquals( array( 0 => true ), $status->success,
+ "Creation of file at $dest has proper 'success' field in Status ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Creation of file at $dest failed ($backendName)." );
+ }
+
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
+ "Destination file $dest exists after creation ($backendName)." );
+
+ $props1 = $this->backend->getFileProps( array( 'src' => $dest ) );
+ $this->assertEquals( true, $props1['fileExists'],
+ "Destination file $dest exists according to props ($backendName)." );
+ if ( $okStatus ) { // file content is what we saved
+ $this->assertEquals( $newSize, $props1['size'],
+ "Destination file $dest has expected size according to props ($backendName)." );
+ $this->assertEquals( $newSize,
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has correct size ($backendName)." );
+ } else { // file content is some other previous text
+ $this->assertEquals( strlen( $oldText ), $props1['size'],
+ "Destination file $dest has original size according to props ($backendName)." );
+ $this->assertEquals( strlen( $oldText ),
+ $this->backend->getFileSize( array( 'src' => $dest ) ),
+ "Destination file $dest has original size according to props ($backendName)." );
+ }
+
+ $this->assertBackendPathsConsistent( array( $dest ) );
+ }
+
+ /**
+ * @dataProvider provider_testCreate
+ */
+ public static function provider_testCreate() {
+ $cases = array();
+
+ $dest = self::baseStorePath() . '/unittest-cont2/a/myspacefile.txt';
+
+ $op = array( 'op' => 'create', 'content' => 'test test testing', 'dst' => $dest );
+ $cases[] = array(
+ $op, // operation
+ false, // no dest already exists
+ true, // succeeds
+ strlen( $op['content'] )
+ );
+
+ $op2 = $op;
+ $op2['content'] = "\n";
+ $cases[] = array(
+ $op2, // operation
+ false, // no dest already exists
+ true, // succeeds
+ strlen( $op2['content'] )
+ );
+
+ $op2 = $op;
+ $op2['content'] = "fsf\n waf 3kt";
+ $cases[] = array(
+ $op2, // operation
+ true, // dest already exists
+ false, // fails
+ strlen( $op2['content'] )
+ );
+
+ $op2 = $op;
+ $op2['content'] = "egm'g gkpe gpqg eqwgwqg";
+ $op2['overwrite'] = true;
+ $cases[] = array(
+ $op2, // operation
+ true, // dest already exists
+ true, // succeeds
+ strlen( $op2['content'] )
+ );
+
+ $op2 = $op;
+ $op2['content'] = "39qjmg3-qg";
+ $op2['overwriteSame'] = true;
+ $cases[] = array(
+ $op2, // operation
+ true, // dest already exists
+ false, // succeeds
+ strlen( $op2['content'] )
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @covers FileBackend::doQuickOperations
+ */
+ public function testDoQuickOperations() {
+ $this->backend = $this->singleBackend;
+ $this->doTestDoQuickOperations();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->doTestDoQuickOperations();
+ $this->tearDownFiles();
+ }
+
+ private function doTestDoQuickOperations() {
+ $backendName = $this->backendClass();
+
+ $base = self::baseStorePath();
+ $files = array(
+ "$base/unittest-cont1/e/fileA.a",
+ "$base/unittest-cont1/e/fileB.a",
+ "$base/unittest-cont1/e/fileC.a"
+ );
+ $createOps = array();
+ $purgeOps = array();
+ foreach ( $files as $path ) {
+ $status = $this->prepare( array( 'dir' => dirname( $path ) ) );
+ $this->assertGoodStatus( $status,
+ "Preparing $path succeeded without warnings ($backendName)." );
+ $createOps[] = array( 'op' => 'create', 'dst' => $path, 'content' => mt_rand( 0, 50000 ) );
+ $copyOps[] = array( 'op' => 'copy', 'src' => $path, 'dst' => "$path-2" );
+ $moveOps[] = array( 'op' => 'move', 'src' => "$path-2", 'dst' => "$path-3" );
+ $purgeOps[] = array( 'op' => 'delete', 'src' => $path );
+ $purgeOps[] = array( 'op' => 'delete', 'src' => "$path-3" );
+ }
+ $purgeOps[] = array( 'op' => 'null' );
+
+ $this->assertGoodStatus(
+ $this->backend->doQuickOperations( $createOps ),
+ "Creation of source files succeeded ($backendName)." );
+ foreach ( $files as $file ) {
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => $file ) ),
+ "File $file exists." );
+ }
+
+ $this->assertGoodStatus(
+ $this->backend->doQuickOperations( $copyOps ),
+ "Quick copy of source files succeeded ($backendName)." );
+ foreach ( $files as $file ) {
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => "$file-2" ) ),
+ "File $file-2 exists." );
+ }
+
+ $this->assertGoodStatus(
+ $this->backend->doQuickOperations( $moveOps ),
+ "Quick move of source files succeeded ($backendName)." );
+ foreach ( $files as $file ) {
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => "$file-3" ) ),
+ "File $file-3 move in." );
+ $this->assertFalse( $this->backend->fileExists( array( 'src' => "$file-2" ) ),
+ "File $file-2 moved away." );
+ }
+
+ $this->assertGoodStatus(
+ $this->backend->quickCopy( array( 'src' => $files[0], 'dst' => $files[0] ) ),
+ "Copy of file {$files[0]} over itself succeeded ($backendName)." );
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => $files[0] ) ),
+ "File {$files[0]} still exists." );
+
+ $this->assertGoodStatus(
+ $this->backend->quickMove( array( 'src' => $files[0], 'dst' => $files[0] ) ),
+ "Move of file {$files[0]} over itself succeeded ($backendName)." );
+ $this->assertTrue( $this->backend->fileExists( array( 'src' => $files[0] ) ),
+ "File {$files[0]} still exists." );
+
+ $this->assertGoodStatus(
+ $this->backend->doQuickOperations( $purgeOps ),
+ "Quick deletion of source files succeeded ($backendName)." );
+ foreach ( $files as $file ) {
+ $this->assertFalse( $this->backend->fileExists( array( 'src' => $file ) ),
+ "File $file purged." );
+ $this->assertFalse( $this->backend->fileExists( array( 'src' => "$file-3" ) ),
+ "File $file-3 purged." );
+ }
+ }
+
+ /**
+ * @dataProvider provider_testConcatenate
+ */
+ public function testConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
+ $this->filesToPrune[] = $op['dst'];
+
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
+ $this->filesToPrune[] = $op['dst']; # avoid file leaking
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
+ $this->filesToPrune[] = $op['dst']; # avoid file leaking
+ $this->tearDownFiles();
+ }
+
+ private function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
+ $backendName = $this->backendClass();
+
+ $expContent = '';
+ // Create sources
+ $ops = array();
+ foreach ( $srcs as $i => $source ) {
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+ $ops[] = array(
+ 'op' => 'create', // operation
+ 'dst' => $source, // source
+ 'content' => $srcsContent[$i]
+ );
+ $expContent .= $srcsContent[$i];
+ }
+ $status = $this->backend->doOperations( $ops );
+
+ $this->assertGoodStatus( $status,
+ "Creation of source files succeeded ($backendName)." );
+
+ $dest = $params['dst'];
+ if ( $alreadyExists ) {
+ $ok = file_put_contents( $dest, 'blah...blah...waahwaah' ) !== false;
+ $this->assertEquals( true, $ok,
+ "Creation of file at $dest succeeded ($backendName)." );
+ } else {
+ $ok = file_put_contents( $dest, '' ) !== false;
+ $this->assertEquals( true, $ok,
+ "Creation of 0-byte file at $dest succeeded ($backendName)." );
+ }
+
+ // Combine the files into one
+ $status = $this->backend->concatenate( $params );
+ if ( $okStatus ) {
+ $this->assertGoodStatus( $status,
+ "Creation of concat file at $dest succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Creation of concat file at $dest succeeded ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Creation of concat file at $dest failed ($backendName)." );
+ }
+
+ if ( $okStatus ) {
+ $this->assertEquals( true, is_file( $dest ),
+ "Dest concat file $dest exists after creation ($backendName)." );
+ } else {
+ $this->assertEquals( true, is_file( $dest ),
+ "Dest concat file $dest exists after failed creation ($backendName)." );
+ }
+
+ $contents = file_get_contents( $dest );
+ $this->assertNotEquals( false, $contents, "File at $dest exists ($backendName)." );
+
+ if ( $okStatus ) {
+ $this->assertEquals( $expContent, $contents,
+ "Concat file at $dest has correct contents ($backendName)." );
+ } else {
+ $this->assertNotEquals( $expContent, $contents,
+ "Concat file at $dest has correct contents ($backendName)." );
+ }
+ }
+
+ public static function provider_testConcatenate() {
+ $cases = array();
+
+ $rand = mt_rand( 0, 2000000000 ) . time();
+ $dest = wfTempDir() . "/randomfile!$rand.txt";
+ $srcs = array(
+ self::baseStorePath() . '/unittest-cont1/e/file1.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file2.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file3.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file4.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file5.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file6.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file7.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file8.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file9.txt',
+ self::baseStorePath() . '/unittest-cont1/e/file10.txt'
+ );
+ $content = array(
+ 'egfage',
+ 'ageageag',
+ 'rhokohlr',
+ 'shgmslkg',
+ 'kenga',
+ 'owagmal',
+ 'kgmae',
+ 'g eak;g',
+ 'lkaem;a',
+ 'legma'
+ );
+ $params = array( 'srcs' => $srcs, 'dst' => $dest );
+
+ $cases[] = array(
+ $params, // operation
+ $srcs, // sources
+ $content, // content for each source
+ false, // no dest already exists
+ true, // succeeds
+ );
+
+ $cases[] = array(
+ $params, // operation
+ $srcs, // sources
+ $content, // content for each source
+ true, // dest already exists
+ false, // succeeds
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testGetFileStat
+ * @covers FileBackend::getFileStat
+ */
+ public function testGetFileStat( $path, $content, $alreadyExists ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileStat( $path, $content, $alreadyExists );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileStat( $path, $content, $alreadyExists );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetFileStat( $path, $content, $alreadyExists ) {
+ $backendName = $this->backendClass();
+
+ if ( $alreadyExists ) {
+ $this->prepare( array( 'dir' => dirname( $path ) ) );
+ $status = $this->create( array( 'dst' => $path, 'content' => $content ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $path succeeded ($backendName)." );
+
+ $size = $this->backend->getFileSize( array( 'src' => $path ) );
+ $time = $this->backend->getFileTimestamp( array( 'src' => $path ) );
+ $stat = $this->backend->getFileStat( array( 'src' => $path ) );
+
+ $this->assertEquals( strlen( $content ), $size,
+ "Correct file size of '$path'" );
+ $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10,
+ "Correct file timestamp of '$path'" );
+
+ $size = $stat['size'];
+ $time = $stat['mtime'];
+ $this->assertEquals( strlen( $content ), $size,
+ "Correct file size of '$path'" );
+ $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10,
+ "Correct file timestamp of '$path'" );
+
+ $this->backend->clearCache( array( $path ) );
+
+ $size = $this->backend->getFileSize( array( 'src' => $path ) );
+
+ $this->assertEquals( strlen( $content ), $size,
+ "Correct file size of '$path'" );
+
+ $this->backend->preloadCache( array( $path ) );
+
+ $size = $this->backend->getFileSize( array( 'src' => $path ) );
+
+ $this->assertEquals( strlen( $content ), $size,
+ "Correct file size of '$path'" );
+ } else {
+ $size = $this->backend->getFileSize( array( 'src' => $path ) );
+ $time = $this->backend->getFileTimestamp( array( 'src' => $path ) );
+ $stat = $this->backend->getFileStat( array( 'src' => $path ) );
+
+ $this->assertFalse( $size, "Correct file size of '$path'" );
+ $this->assertFalse( $time, "Correct file timestamp of '$path'" );
+ $this->assertFalse( $stat, "Correct file stat of '$path'" );
+ }
+ }
+
+ public static function provider_testGetFileStat() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents", true );
+ $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "", true );
+ $cases[] = array( "$base/unittest-cont1/e/b/some-diff_file.txt", null, false );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testGetFileStat
+ * @covers FileBackend::streamFile
+ */
+ public function testStreamFile( $path, $content, $alreadyExists ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestStreamFile( $path, $content, $alreadyExists );
+ $this->tearDownFiles();
+ }
+
+ private function doTestStreamFile( $path, $content ) {
+ $backendName = $this->backendClass();
+
+ // Test doStreamFile() directly to avoid header madness
+ $class = new ReflectionClass( $this->backend );
+ $method = $class->getMethod( 'doStreamFile' );
+ $method->setAccessible( true );
+
+ if ( $content !== null ) {
+ $this->prepare( array( 'dir' => dirname( $path ) ) );
+ $status = $this->create( array( 'dst' => $path, 'content' => $content ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $path succeeded ($backendName)." );
+
+ ob_start();
+ $method->invokeArgs( $this->backend, array( array( 'src' => $path ) ) );
+ $data = ob_get_contents();
+ ob_end_clean();
+
+ $this->assertEquals( $content, $data, "Correct content streamed from '$path'" );
+ } else { // 404 case
+ ob_start();
+ $method->invokeArgs( $this->backend, array( array( 'src' => $path ) ) );
+ $data = ob_get_contents();
+ ob_end_clean();
+
+ $this->assertEquals( '', $data, "Correct content streamed from '$path' ($backendName)" );
+ }
+ }
+
+ public static function provider_testStreamFile() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", null );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testGetFileContents
+ * @covers FileBackend::getFileContents
+ * @covers FileBackend::getFileContentsMulti
+ */
+ public function testGetFileContents( $source, $content ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileContents( $source, $content );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileContents( $source, $content );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetFileContents( $source, $content ) {
+ $backendName = $this->backendClass();
+
+ $srcs = (array)$source;
+ $content = (array)$content;
+ foreach ( $srcs as $i => $src ) {
+ $this->prepare( array( 'dir' => dirname( $src ) ) );
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $src succeeded ($backendName)." );
+ }
+
+ if ( is_array( $source ) ) {
+ $contents = $this->backend->getFileContentsMulti( array( 'srcs' => $source ) );
+ foreach ( $contents as $path => $data ) {
+ $this->assertNotEquals( false, $data, "Contents of $path exists ($backendName)." );
+ $this->assertEquals(
+ current( $content ),
+ $data,
+ "Contents of $path is correct ($backendName)."
+ );
+ next( $content );
+ }
+ $this->assertEquals(
+ $source,
+ array_keys( $contents ),
+ "Contents in right order ($backendName)."
+ );
+ $this->assertEquals(
+ count( $source ),
+ count( $contents ),
+ "Contents array size correct ($backendName)."
+ );
+ } else {
+ $data = $this->backend->getFileContents( array( 'src' => $source ) );
+ $this->assertNotEquals( false, $data, "Contents of $source exists ($backendName)." );
+ $this->assertEquals( $content[0], $data, "Contents of $source is correct ($backendName)." );
+ }
+ }
+
+ public static function provider_testGetFileContents() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "more file contents" );
+ $cases[] = array(
+ array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
+ "$base/unittest-cont1/e/a/z.txt" ),
+ array( "contents xx", "contents xy", "contents xz" )
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testGetLocalCopy
+ * @covers FileBackend::getLocalCopy
+ */
+ public function testGetLocalCopy( $source, $content ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalCopy( $source, $content );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalCopy( $source, $content );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetLocalCopy( $source, $content ) {
+ $backendName = $this->backendClass();
+
+ $srcs = (array)$source;
+ $content = (array)$content;
+ foreach ( $srcs as $i => $src ) {
+ $this->prepare( array( 'dir' => dirname( $src ) ) );
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $src succeeded ($backendName)." );
+ }
+
+ if ( is_array( $source ) ) {
+ $tmpFiles = $this->backend->getLocalCopyMulti( array( 'srcs' => $source ) );
+ foreach ( $tmpFiles as $path => $tmpFile ) {
+ $this->assertNotNull( $tmpFile,
+ "Creation of local copy of $path succeeded ($backendName)." );
+ $contents = file_get_contents( $tmpFile->getPath() );
+ $this->assertNotEquals( false, $contents, "Local copy of $path exists ($backendName)." );
+ $this->assertEquals(
+ current( $content ),
+ $contents,
+ "Local copy of $path is correct ($backendName)."
+ );
+ next( $content );
+ }
+ $this->assertEquals(
+ $source,
+ array_keys( $tmpFiles ),
+ "Local copies in right order ($backendName)."
+ );
+ $this->assertEquals(
+ count( $source ),
+ count( $tmpFiles ),
+ "Local copies array size correct ($backendName)."
+ );
+ } else {
+ $tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) );
+ $this->assertNotNull( $tmpFile,
+ "Creation of local copy of $source succeeded ($backendName)." );
+ $contents = file_get_contents( $tmpFile->getPath() );
+ $this->assertNotEquals( false, $contents, "Local copy of $source exists ($backendName)." );
+ $this->assertEquals(
+ $content[0],
+ $contents,
+ "Local copy of $source is correct ($backendName)."
+ );
+ }
+
+ $obj = new stdClass();
+ $tmpFile->bind( $obj );
+ }
+
+ public static function provider_testGetLocalCopy() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" );
+ $cases[] = array(
+ array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
+ "$base/unittest-cont1/e/a/z.txt" ),
+ array( "contents xx $", "contents xy 111", "contents xz" )
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testGetLocalReference
+ * @covers FileBackend::getLocalReference
+ */
+ public function testGetLocalReference( $source, $content ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalReference( $source, $content );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalReference( $source, $content );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetLocalReference( $source, $content ) {
+ $backendName = $this->backendClass();
+
+ $srcs = (array)$source;
+ $content = (array)$content;
+ foreach ( $srcs as $i => $src ) {
+ $this->prepare( array( 'dir' => dirname( $src ) ) );
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $src succeeded ($backendName)." );
+ }
+
+ if ( is_array( $source ) ) {
+ $tmpFiles = $this->backend->getLocalReferenceMulti( array( 'srcs' => $source ) );
+ foreach ( $tmpFiles as $path => $tmpFile ) {
+ $this->assertNotNull( $tmpFile,
+ "Creation of local copy of $path succeeded ($backendName)." );
+ $contents = file_get_contents( $tmpFile->getPath() );
+ $this->assertNotEquals( false, $contents, "Local ref of $path exists ($backendName)." );
+ $this->assertEquals(
+ current( $content ),
+ $contents,
+ "Local ref of $path is correct ($backendName)."
+ );
+ next( $content );
+ }
+ $this->assertEquals(
+ $source,
+ array_keys( $tmpFiles ),
+ "Local refs in right order ($backendName)."
+ );
+ $this->assertEquals(
+ count( $source ),
+ count( $tmpFiles ),
+ "Local refs array size correct ($backendName)."
+ );
+ } else {
+ $tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) );
+ $this->assertNotNull( $tmpFile,
+ "Creation of local copy of $source succeeded ($backendName)." );
+ $contents = file_get_contents( $tmpFile->getPath() );
+ $this->assertNotEquals( false, $contents, "Local ref of $source exists ($backendName)." );
+ $this->assertEquals( $content[0], $contents, "Local ref of $source is correct ($backendName)." );
+ }
+ }
+
+ public static function provider_testGetLocalReference() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" );
+ $cases[] = array(
+ array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
+ "$base/unittest-cont1/e/a/z.txt" ),
+ array( "contents xx 1111", "contents xy %", "contents xz $" )
+ );
+
+ return $cases;
+ }
+
+ /**
+ * @covers FileBackend::getLocalCopy
+ * @covers FileBackend::getLocalReference
+ */
+ public function testGetLocalCopyAndReference404() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalCopyAndReference404();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetLocalCopyAndReference404();
+ $this->tearDownFiles();
+ }
+
+ public function doTestGetLocalCopyAndReference404() {
+ $backendName = $this->backendClass();
+
+ $base = self::baseStorePath();
+
+ $tmpFile = $this->backend->getLocalCopy( array(
+ 'src' => "$base/unittest-cont1/not-there" ) );
+ $this->assertEquals( null, $tmpFile, "Local copy of not existing file is null ($backendName)." );
+
+ $tmpFile = $this->backend->getLocalReference( array(
+ 'src' => "$base/unittest-cont1/not-there" ) );
+ $this->assertEquals( null, $tmpFile, "Local ref of not existing file is null ($backendName)." );
+ }
+
+ /**
+ * @dataProvider provider_testGetFileHttpUrl
+ * @covers FileBackend::getFileHttpUrl
+ */
+ public function testGetFileHttpUrl( $source, $content ) {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileHttpUrl( $source, $content );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileHttpUrl( $source, $content );
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetFileHttpUrl( $source, $content ) {
+ $backendName = $this->backendClass();
+
+ $this->prepare( array( 'dir' => dirname( $source ) ) );
+ $status = $this->backend->doOperation(
+ array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
+ $this->assertGoodStatus( $status,
+ "Creation of file at $source succeeded ($backendName)." );
+
+ $url = $this->backend->getFileHttpUrl( array( 'src' => $source ) );
+
+ if ( $url !== null ) { // supported
+ $data = Http::request( "GET", $url );
+ $this->assertEquals( $content, $data,
+ "HTTP GET of URL has right contents ($backendName)." );
+ }
+ }
+
+ public static function provider_testGetFileHttpUrl() {
+ $cases = array();
+
+ $base = self::baseStorePath();
+ $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
+ $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" );
+
+ return $cases;
+ }
+
+ /**
+ * @dataProvider provider_testPrepareAndClean
+ * @covers FileBackend::prepare
+ * @covers FileBackend::clean
+ */
+ public function testPrepareAndClean( $path, $isOK ) {
+ $this->backend = $this->singleBackend;
+ $this->doTestPrepareAndClean( $path, $isOK );
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->doTestPrepareAndClean( $path, $isOK );
+ $this->tearDownFiles();
+ }
+
+ public static function provider_testPrepareAndClean() {
+ $base = self::baseStorePath();
+
+ return array(
+ array( "$base/unittest-cont1/e/a/z/some_file1.txt", true ),
+ array( "$base/unittest-cont2/a/z/some_file2.txt", true ),
+ # Specific to FS backend with no basePath field set
+ #array( "$base/unittest-cont3/a/z/some_file3.txt", false ),
+ );
+ }
+
+ private function doTestPrepareAndClean( $path, $isOK ) {
+ $backendName = $this->backendClass();
+
+ $status = $this->prepare( array( 'dir' => dirname( $path ) ) );
+ if ( $isOK ) {
+ $this->assertGoodStatus( $status,
+ "Preparing dir $path succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Preparing dir $path succeeded ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Preparing dir $path failed ($backendName)." );
+ }
+
+ $status = $this->backend->secure( array( 'dir' => dirname( $path ) ) );
+ if ( $isOK ) {
+ $this->assertGoodStatus( $status,
+ "Securing dir $path succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Securing dir $path succeeded ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Securing dir $path failed ($backendName)." );
+ }
+
+ $status = $this->backend->publish( array( 'dir' => dirname( $path ) ) );
+ if ( $isOK ) {
+ $this->assertGoodStatus( $status,
+ "Publishing dir $path succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Publishing dir $path succeeded ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Publishing dir $path failed ($backendName)." );
+ }
+
+ $status = $this->backend->clean( array( 'dir' => dirname( $path ) ) );
+ if ( $isOK ) {
+ $this->assertGoodStatus( $status,
+ "Cleaning dir $path succeeded without warnings ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Cleaning dir $path succeeded ($backendName)." );
+ } else {
+ $this->assertEquals( false, $status->isOK(),
+ "Cleaning dir $path failed ($backendName)." );
+ }
+ }
+
+ public function testRecursiveClean() {
+ $this->backend = $this->singleBackend;
+ $this->doTestRecursiveClean();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->doTestRecursiveClean();
+ $this->tearDownFiles();
+ }
+
+ /**
+ * @covers FileBackend::clean
+ */
+ private function doTestRecursiveClean() {
+ $backendName = $this->backendClass();
+
+ $base = self::baseStorePath();
+ $dirs = array(
+ "$base/unittest-cont1",
+ "$base/unittest-cont1/e",
+ "$base/unittest-cont1/e/a",
+ "$base/unittest-cont1/e/a/b",
+ "$base/unittest-cont1/e/a/b/c",
+ "$base/unittest-cont1/e/a/b/c/d0",
+ "$base/unittest-cont1/e/a/b/c/d1",
+ "$base/unittest-cont1/e/a/b/c/d2",
+ "$base/unittest-cont1/e/a/b/c/d0/1",
+ "$base/unittest-cont1/e/a/b/c/d0/2",
+ "$base/unittest-cont1/e/a/b/c/d1/3",
+ "$base/unittest-cont1/e/a/b/c/d1/4",
+ "$base/unittest-cont1/e/a/b/c/d2/5",
+ "$base/unittest-cont1/e/a/b/c/d2/6"
+ );
+ foreach ( $dirs as $dir ) {
+ $status = $this->prepare( array( 'dir' => $dir ) );
+ $this->assertGoodStatus( $status,
+ "Preparing dir $dir succeeded without warnings ($backendName)." );
+ }
+
+ if ( $this->backend instanceof FSFileBackend ) {
+ foreach ( $dirs as $dir ) {
+ $this->assertEquals( true, $this->backend->directoryExists( array( 'dir' => $dir ) ),
+ "Dir $dir exists ($backendName)." );
+ }
+ }
+
+ $status = $this->backend->clean(
+ array( 'dir' => "$base/unittest-cont1", 'recursive' => 1 ) );
+ $this->assertGoodStatus( $status,
+ "Recursive cleaning of dir $dir succeeded without warnings ($backendName)." );
+
+ foreach ( $dirs as $dir ) {
+ $this->assertEquals( false, $this->backend->directoryExists( array( 'dir' => $dir ) ),
+ "Dir $dir no longer exists ($backendName)." );
+ }
+ }
+
+ /**
+ * @covers FileBackend::doOperations
+ */
+ public function testDoOperations() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperations();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperations();
+ $this->tearDownFiles();
+ }
+
+ private function doTestDoOperations() {
+ $base = self::baseStorePath();
+
+ $fileA = "$base/unittest-cont1/e/a/b/fileA.txt";
+ $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+ $fileB = "$base/unittest-cont1/e/a/b/fileB.txt";
+ $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+ $fileC = "$base/unittest-cont1/e/a/b/fileC.txt";
+ $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+ $fileD = "$base/unittest-cont1/e/a/b/fileD.txt";
+
+ $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+ $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+ $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+ $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileD ) ) );
+
+ $status = $this->backend->doOperations( array(
+ array( 'op' => 'describe', 'src' => $fileA,
+ 'headers' => array( 'X-Content-Length' => '91.3' ), 'disposition' => 'inline' ),
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<empty>, D:<A>
+ array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<A>
+ array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ),
+ // Now: A:<B>, B:<empty>, C:<empty>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ),
+ // Now: A:<B>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Now: A:<empty>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'null' ),
+ // Does nothing
+ ) );
+
+ $this->assertGoodStatus( $status, "Operation batch succeeded" );
+ $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+ $this->assertEquals( 14, count( $status->success ),
+ "Operation batch has correct success array" );
+
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ),
+ "File does not exist at $fileA" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+ "File does not exist at $fileB" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+ "File does not exist at $fileD" );
+
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+ "File exists at $fileC" );
+ $this->assertEquals( $fileBContents,
+ $this->backend->getFileContents( array( 'src' => $fileC ) ),
+ "Correct file contents of $fileC" );
+ $this->assertEquals( strlen( $fileBContents ),
+ $this->backend->getFileSize( array( 'src' => $fileC ) ),
+ "Correct file size of $fileC" );
+ $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+ $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ),
+ "Correct file SHA-1 of $fileC" );
+ }
+
+ /**
+ * @covers FileBackend::doOperations
+ */
+ public function testDoOperationsPipeline() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperationsPipeline();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperationsPipeline();
+ $this->tearDownFiles();
+ }
+
+ // concurrency orientated
+ private function doTestDoOperationsPipeline() {
+ $base = self::baseStorePath();
+
+ $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+ $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+ $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+
+ $tmpNameA = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ file_put_contents( $tmpNameA, $fileAContents );
+ $tmpNameB = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ file_put_contents( $tmpNameB, $fileBContents );
+ $tmpNameC = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+ file_put_contents( $tmpNameC, $fileCContents );
+
+ $this->filesToPrune[] = $tmpNameA; # avoid file leaking
+ $this->filesToPrune[] = $tmpNameB; # avoid file leaking
+ $this->filesToPrune[] = $tmpNameC; # avoid file leaking
+
+ $fileA = "$base/unittest-cont1/e/a/b/fileA.txt";
+ $fileB = "$base/unittest-cont1/e/a/b/fileB.txt";
+ $fileC = "$base/unittest-cont1/e/a/b/fileC.txt";
+ $fileD = "$base/unittest-cont1/e/a/b/fileD.txt";
+
+ $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+ $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+ $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+ $this->prepare( array( 'dir' => dirname( $fileD ) ) );
+
+ $status = $this->backend->doOperations( array(
+ array( 'op' => 'store', 'src' => $tmpNameA, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ array( 'op' => 'store', 'src' => $tmpNameB, 'dst' => $fileB, 'overwrite' => 1 ),
+ array( 'op' => 'store', 'src' => $tmpNameC, 'dst' => $fileC, 'overwrite' => 1 ),
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<empty>, D:<A>
+ array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<A>
+ array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ),
+ // Now: A:<B>, B:<empty>, C:<empty>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ),
+ // Now: A:<B>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Now: A:<empty>, B:<empty>, C:<B>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Does nothing
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Does nothing
+ array( 'op' => 'null' ),
+ // Does nothing
+ ) );
+
+ $this->assertGoodStatus( $status, "Operation batch succeeded" );
+ $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+ $this->assertEquals( 16, count( $status->success ),
+ "Operation batch has correct success array" );
+
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ),
+ "File does not exist at $fileA" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+ "File does not exist at $fileB" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+ "File does not exist at $fileD" );
+
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+ "File exists at $fileC" );
+ $this->assertEquals( $fileBContents,
+ $this->backend->getFileContents( array( 'src' => $fileC ) ),
+ "Correct file contents of $fileC" );
+ $this->assertEquals( strlen( $fileBContents ),
+ $this->backend->getFileSize( array( 'src' => $fileC ) ),
+ "Correct file size of $fileC" );
+ $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+ $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ),
+ "Correct file SHA-1 of $fileC" );
+ }
+
+ /**
+ * @covers FileBackend::doOperations
+ */
+ public function testDoOperationsFailing() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperationsFailing();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestDoOperationsFailing();
+ $this->tearDownFiles();
+ }
+
+ private function doTestDoOperationsFailing() {
+ $base = self::baseStorePath();
+
+ $fileA = "$base/unittest-cont2/a/b/fileA.txt";
+ $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+ $fileB = "$base/unittest-cont2/a/b/fileB.txt";
+ $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+ $fileC = "$base/unittest-cont2/a/b/fileC.txt";
+ $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+ $fileD = "$base/unittest-cont2/a/b/fileD.txt";
+
+ $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+ $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+ $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
+ $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+ $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
+
+ $status = $this->backend->doOperations( array(
+ array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+ array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+ array( 'op' => 'copy', 'src' => $fileB, 'dst' => $fileD, 'overwrite' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<B>
+ array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (failed)
+ array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC, 'overwriteSame' => 1 ),
+ // Now: A:<A>, B:<B>, C:<A>, D:<empty> (failed)
+ array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileA, 'overwrite' => 1 ),
+ // Now: A:<B>, B:<empty>, C:<A>, D:<empty>
+ array( 'op' => 'delete', 'src' => $fileD ),
+ // Now: A:<B>, B:<empty>, C:<A>, D:<empty>
+ array( 'op' => 'null' ),
+ // Does nothing
+ ), array( 'force' => 1 ) );
+
+ $this->assertNotEquals( array(), $status->errors, "Operation had warnings" );
+ $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+ $this->assertEquals( 8, count( $status->success ),
+ "Operation batch has correct success array" );
+
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+ "File does not exist at $fileB" );
+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+ "File does not exist at $fileD" );
+
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileA ) ),
+ "File does not exist at $fileA" );
+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+ "File exists at $fileC" );
+ $this->assertEquals( $fileBContents,
+ $this->backend->getFileContents( array( 'src' => $fileA ) ),
+ "Correct file contents of $fileA" );
+ $this->assertEquals( strlen( $fileBContents ),
+ $this->backend->getFileSize( array( 'src' => $fileA ) ),
+ "Correct file size of $fileA" );
+ $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+ $this->backend->getFileSha1Base36( array( 'src' => $fileA ) ),
+ "Correct file SHA-1 of $fileA" );
+ }
+
+ /**
+ * @covers FileBackend::getFileList
+ */
+ public function testGetFileList() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileList();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetFileList();
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetFileList() {
+ $backendName = $this->backendClass();
+ $base = self::baseStorePath();
+
+ // Should have no errors
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont-notexists" ) );
+
+ $files = array(
+ "$base/unittest-cont1/e/test1.txt",
+ "$base/unittest-cont1/e/test2.txt",
+ "$base/unittest-cont1/e/test3.txt",
+ "$base/unittest-cont1/e/subdir1/test1.txt",
+ "$base/unittest-cont1/e/subdir1/test2.txt",
+ "$base/unittest-cont1/e/subdir2/test3.txt",
+ "$base/unittest-cont1/e/subdir2/test4.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test1.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test2.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test3.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test4.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test5.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/sub/test0.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/sub/120-px-file.txt",
+ );
+
+ // Add the files
+ $ops = array();
+ foreach ( $files as $file ) {
+ $this->prepare( array( 'dir' => dirname( $file ) ) );
+ $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
+ }
+ $status = $this->backend->doQuickOperations( $ops );
+ $this->assertGoodStatus( $status,
+ "Creation of files succeeded ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Creation of files succeeded with OK status ($backendName)." );
+
+ // Expected listing at root
+ $expected = array(
+ "e/test1.txt",
+ "e/test2.txt",
+ "e/test3.txt",
+ "e/subdir1/test1.txt",
+ "e/subdir1/test2.txt",
+ "e/subdir2/test3.txt",
+ "e/subdir2/test4.txt",
+ "e/subdir2/subdir/test1.txt",
+ "e/subdir2/subdir/test2.txt",
+ "e/subdir2/subdir/test3.txt",
+ "e/subdir2/subdir/test4.txt",
+ "e/subdir2/subdir/test5.txt",
+ "e/subdir2/subdir/sub/test0.txt",
+ "e/subdir2/subdir/sub/120-px-file.txt",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash) at root
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1" ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (no trailing slash) at root with advise
+ $iter = $this->backend->getFileList( array(
+ 'dir' => "$base/unittest-cont1",
+ 'adviseStat' => 1
+ ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (with trailing slash) at root
+ $list = array();
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Expected listing at subdir
+ $expected = array(
+ "test1.txt",
+ "test2.txt",
+ "test3.txt",
+ "test4.txt",
+ "test5.txt",
+ "sub/test0.txt",
+ "sub/120-px-file.txt",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash) at subdir
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (no trailing slash) at subdir with advise
+ $iter = $this->backend->getFileList( array(
+ 'dir' => "$base/unittest-cont1/e/subdir2/subdir",
+ 'adviseStat' => 1
+ ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (with trailing slash) at subdir
+ $list = array();
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+ // Actual listing (using iterator second time)
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName), second iteration." );
+
+ // Actual listing (top files only) at root
+ $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1" ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( array(), $list, "Correct top file listing ($backendName)." );
+
+ // Expected listing (top files only) at subdir
+ $expected = array(
+ "test1.txt",
+ "test2.txt",
+ "test3.txt",
+ "test4.txt",
+ "test5.txt"
+ );
+ sort( $expected );
+
+ // Actual listing (top files only) at subdir
+ $iter = $this->backend->getTopFileList(
+ array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" )
+ );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." );
+
+ // Actual listing (top files only) at subdir with advise
+ $iter = $this->backend->getTopFileList( array(
+ 'dir' => "$base/unittest-cont1/e/subdir2/subdir",
+ 'adviseStat' => 1
+ ) );
+ $list = $this->listToArray( $iter );
+ sort( $list );
+ $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." );
+
+ foreach ( $files as $file ) { // clean up
+ $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
+ }
+
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/not/exists" ) );
+ foreach ( $iter as $iter ) {
+ // no errors
+ }
+ }
+
+ /**
+ * @covers FileBackend::getTopDirectoryList
+ * @covers FileBackend::getDirectoryList
+ */
+ public function testGetDirectoryList() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetDirectoryList();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetDirectoryList();
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetDirectoryList() {
+ $backendName = $this->backendClass();
+
+ $base = self::baseStorePath();
+ $files = array(
+ "$base/unittest-cont1/e/test1.txt",
+ "$base/unittest-cont1/e/test2.txt",
+ "$base/unittest-cont1/e/test3.txt",
+ "$base/unittest-cont1/e/subdir1/test1.txt",
+ "$base/unittest-cont1/e/subdir1/test2.txt",
+ "$base/unittest-cont1/e/subdir2/test3.txt",
+ "$base/unittest-cont1/e/subdir2/test4.txt",
+ "$base/unittest-cont1/e/subdir2/subdir/test1.txt",
+ "$base/unittest-cont1/e/subdir3/subdir/test2.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/test3.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/test4.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/test5.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/sub/test0.txt",
+ "$base/unittest-cont1/e/subdir4/subdir/sub/120-px-file.txt",
+ );
+
+ // Add the files
+ $ops = array();
+ foreach ( $files as $file ) {
+ $this->prepare( array( 'dir' => dirname( $file ) ) );
+ $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
+ }
+ $status = $this->backend->doQuickOperations( $ops );
+ $this->assertGoodStatus( $status,
+ "Creation of files succeeded ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Creation of files succeeded with OK status ($backendName)." );
+
+ $this->assertEquals( true,
+ $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir1" ) ),
+ "Directory exists in ($backendName)." );
+ $this->assertEquals( true,
+ $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ),
+ "Directory exists in ($backendName)." );
+ $this->assertEquals( false,
+ $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/test1.txt" ) ),
+ "Directory does not exists in ($backendName)." );
+
+ // Expected listing
+ $expected = array(
+ "e",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Expected listing
+ $expected = array(
+ "subdir1",
+ "subdir2",
+ "subdir3",
+ "subdir4",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Actual listing (with trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Expected listing
+ $expected = array(
+ "subdir",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Actual listing (with trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList(
+ array( 'dir' => "$base/unittest-cont1/e/subdir2/" )
+ );
+
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Actual listing (using iterator second time)
+ $list = array();
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals(
+ $expected,
+ $list,
+ "Correct top dir listing ($backendName), second iteration."
+ );
+
+ // Expected listing (recursive)
+ $expected = array(
+ "e",
+ "e/subdir1",
+ "e/subdir2",
+ "e/subdir3",
+ "e/subdir4",
+ "e/subdir2/subdir",
+ "e/subdir3/subdir",
+ "e/subdir4/subdir",
+ "e/subdir4/subdir/sub",
+ );
+ sort( $expected );
+
+ // Actual listing (recursive)
+ $list = array();
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+ // Expected listing (recursive)
+ $expected = array(
+ "subdir",
+ "subdir/sub",
+ );
+ sort( $expected );
+
+ // Actual listing (recursive)
+ $list = array();
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir4" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+ // Actual listing (recursive, second time)
+ $list = array();
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir1" ) );
+ $items = $this->listToArray( $iter );
+ $this->assertEquals( array(), $items, "Directory listing is empty." );
+
+ foreach ( $files as $file ) { // clean up
+ $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
+ }
+
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/not/exists" ) );
+ foreach ( $iter as $file ) {
+ // no errors
+ }
+
+ $items = $this->listToArray( $iter );
+ $this->assertEquals( array(), $items, "Directory listing is empty." );
+
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/not/exists" ) );
+ $items = $this->listToArray( $iter );
+ $this->assertEquals( array(), $items, "Directory listing is empty." );
+ }
+
+ /**
+ * @covers FileBackend::lockFiles
+ * @covers FileBackend::unlockFiles
+ */
+ public function testLockCalls() {
+ $this->backend = $this->singleBackend;
+ $this->doTestLockCalls();
+ }
+
+ private function doTestLockCalls() {
+ $backendName = $this->backendClass();
+
+ $paths = array(
+ "test1.txt",
+ "test2.txt",
+ "test3.txt",
+ "subdir1",
+ "subdir1", // duplicate
+ "subdir1/test1.txt",
+ "subdir1/test2.txt",
+ "subdir2",
+ "subdir2", // duplicate
+ "subdir2/test3.txt",
+ "subdir2/test4.txt",
+ "subdir2/subdir",
+ "subdir2/subdir/test1.txt",
+ "subdir2/subdir/test2.txt",
+ "subdir2/subdir/test3.txt",
+ "subdir2/subdir/test4.txt",
+ "subdir2/subdir/test5.txt",
+ "subdir2/subdir/sub",
+ "subdir2/subdir/sub/test0.txt",
+ "subdir2/subdir/sub/120-px-file.txt",
+ );
+
+ for ( $i = 0; $i < 25; $i++ ) {
+ $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName). ($i)" );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ ## Flip the acquire/release ordering around ##
+
+ $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName). ($i)" );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+
+ $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
+ $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ),
+ "Locking of files succeeded ($backendName) ($i)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Locking of files succeeded with OK status ($backendName) ($i)." );
+ }
+
+ $status = Status::newGood();
+ $sl = $this->backend->getScopedFileLocks( $paths, LockManager::LOCK_EX, $status );
+ $this->assertType( 'ScopedLock', $sl,
+ "Scoped locking of files succeeded ($backendName)." );
+ $this->assertEquals( array(), $status->errors,
+ "Scoped locking of files succeeded ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Scoped locking of files succeeded with OK status ($backendName)." );
+
+ ScopedLock::release( $sl );
+ $this->assertEquals( null, $sl,
+ "Scoped unlocking of files succeeded ($backendName)." );
+ $this->assertEquals( array(), $status->errors,
+ "Scoped unlocking of files succeeded ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Scoped unlocking of files succeeded with OK status ($backendName)." );
+ }
+
+ // helper function
+ private function listToArray( $iter ) {
+ return is_array( $iter ) ? $iter : iterator_to_array( $iter );
+ }
+
+ // test helper wrapper for backend prepare() function
+ private function prepare( array $params ) {
+ return $this->backend->prepare( $params );
+ }
+
+ // test helper wrapper for backend prepare() function
+ private function create( array $params ) {
+ $params['op'] = 'create';
+
+ return $this->backend->doQuickOperations( array( $params ) );
+ }
+
+ function tearDownFiles() {
+ foreach ( $this->filesToPrune as $file ) {
+ if ( is_file( $file ) ) {
+ unlink( $file );
+ }
+ }
+ $containers = array( 'unittest-cont1', 'unittest-cont2', 'unittest-cont-bad' );
+ foreach ( $containers as $container ) {
+ $this->deleteFiles( $container );
+ }
+ $this->filesToPrune = array();
+ }
+
+ private function deleteFiles( $container ) {
+ $base = self::baseStorePath();
+ $iter = $this->backend->getFileList( array( 'dir' => "$base/$container" ) );
+ if ( $iter ) {
+ foreach ( $iter as $file ) {
+ $this->backend->quickDelete( array( 'src' => "$base/$container/$file" ) );
+ }
+ // free the directory, to avoid Permission denied under windows on rmdir
+ unset( $iter );
+ }
+ $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) );
+ }
+
+ function assertBackendPathsConsistent( array $paths ) {
+ if ( $this->backend instanceof FileBackendMultiWrite ) {
+ $status = $this->backend->consistencyCheck( $paths );
+ $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) );
+ }
+ }
+
+ function assertGoodStatus( $status, $msg ) {
+ $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg );
+ }
+}
diff --git a/tests/phpunit/includes/filerepo/FileRepoTest.php b/tests/phpunit/includes/filerepo/FileRepoTest.php
new file mode 100644
index 00000000..a196dca8
--- /dev/null
+++ b/tests/phpunit/includes/filerepo/FileRepoTest.php
@@ -0,0 +1,55 @@
+<?php
+
+class FileRepoTest extends MediaWikiTestCase {
+
+ /**
+ * @expectedException MWException
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionOptionCanNotBeNull() {
+ new FileRepo();
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionOptionCanNotBeAnEmptyArray() {
+ new FileRepo( array() );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionOptionNeedNameKey() {
+ new FileRepo( array(
+ 'backend' => 'foobar'
+ ) );
+ }
+
+ /**
+ * @expectedException MWException
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionOptionNeedBackendKey() {
+ new FileRepo( array(
+ 'name' => 'foobar'
+ ) );
+ }
+
+ /**
+ * @covers FileRepo::__construct
+ */
+ public function testFileRepoConstructionWithRequiredOptions() {
+ $f = new FileRepo( array(
+ 'name' => 'FileRepoTestRepository',
+ 'backend' => new FSFileBackend( array(
+ 'name' => 'local-testing',
+ 'wikiId' => 'test_wiki',
+ 'containerPaths' => array()
+ ) )
+ ) );
+ $this->assertInstanceOf( 'FileRepo', $f );
+ }
+}
diff --git a/tests/phpunit/includes/filerepo/RepoGroupTest.php b/tests/phpunit/includes/filerepo/RepoGroupTest.php
new file mode 100644
index 00000000..5bdb7e7f
--- /dev/null
+++ b/tests/phpunit/includes/filerepo/RepoGroupTest.php
@@ -0,0 +1,59 @@
+<?php
+class RepoGroupTest extends MediaWikiTestCase {
+
+ function testHasForeignRepoNegative() {
+ $this->setMwGlobals( 'wgForeignFileRepos', array() );
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+ $this->assertFalse( RepoGroup::singleton()->hasForeignRepos() );
+ }
+
+ function testHasForeignRepoPositive() {
+ $this->setUpForeignRepo();
+ $this->assertTrue( RepoGroup::singleton()->hasForeignRepos() );
+ }
+
+ function testForEachForeignRepo() {
+ $this->setUpForeignRepo();
+ $fakeCallback = $this->getMock( 'RepoGroupTestHelper' );
+ $fakeCallback->expects( $this->once() )->method( 'callback' );
+ RepoGroup::singleton()->forEachForeignRepo(
+ array( $fakeCallback, 'callback' ), array( array() ) );
+ }
+
+ function testForEachForeignRepoNone() {
+ $this->setMwGlobals( 'wgForeignFileRepos', array() );
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+ $fakeCallback = $this->getMock( 'RepoGroupTestHelper' );
+ $fakeCallback->expects( $this->never() )->method( 'callback' );
+ RepoGroup::singleton()->forEachForeignRepo(
+ array( $fakeCallback, 'callback' ), array( array() ) );
+ }
+
+ private function setUpForeignRepo() {
+ global $wgUploadDirectory;
+ $this->setMwGlobals( 'wgForeignFileRepos', array( array(
+ 'class' => 'ForeignAPIRepo',
+ 'name' => 'wikimediacommons',
+ 'backend' => 'wikimediacommons-backend',
+ 'apibase' => 'https://commons.wikimedia.org/w/api.php',
+ 'hashLevels' => 2,
+ 'fetchDescription' => true,
+ 'descriptionCacheExpiry' => 43200,
+ 'apiThumbCacheExpiry' => 86400,
+ 'directory' => $wgUploadDirectory
+ ) ) );
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+ }
+}
+
+/**
+ * Quick helper class to use as a mock callback for RepoGroup::singleton()->forEachForeignRepo.
+ */
+class RepoGroupTestHelper {
+ function callback( FileRepo $repo, array $foo ) {
+ return true;
+ }
+}
diff --git a/tests/phpunit/includes/filerepo/StoreBatchTest.php b/tests/phpunit/includes/filerepo/StoreBatchTest.php
new file mode 100644
index 00000000..9cc2efbf
--- /dev/null
+++ b/tests/phpunit/includes/filerepo/StoreBatchTest.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * @group FileRepo
+ * @group medium
+ */
+class StoreBatchTest extends MediaWikiTestCase {
+
+ protected $createdFiles;
+ protected $date;
+ /** @var FileRepo */
+ protected $repo;
+
+ protected function setUp() {
+ global $wgFileBackends;
+ parent::setUp();
+
+ # Forge a FSRepo object to not have to rely on local wiki settings
+ $tmpPrefix = wfTempDir() . '/storebatch-test-' . time() . '-' . mt_rand();
+ if ( $this->getCliArg( 'use-filebackend' ) ) {
+ $name = $this->getCliArg( 'use-filebackend' );
+ $useConfig = array();
+ foreach ( $wgFileBackends as $conf ) {
+ if ( $conf['name'] == $name ) {
+ $useConfig = $conf;
+ }
+ }
+ $useConfig['lockManager'] = LockManagerGroup::singleton()->get( $useConfig['lockManager'] );
+ unset( $useConfig['fileJournal'] );
+ $useConfig['name'] = 'local-testing'; // swap name
+ $class = $useConfig['class'];
+ $backend = new $class( $useConfig );
+ } else {
+ $backend = new FSFileBackend( array(
+ 'name' => 'local-testing',
+ 'wikiId' => wfWikiID(),
+ 'containerPaths' => array(
+ 'unittests-public' => "{$tmpPrefix}-public",
+ 'unittests-thumb' => "{$tmpPrefix}-thumb",
+ 'unittests-temp' => "{$tmpPrefix}-temp",
+ 'unittests-deleted' => "{$tmpPrefix}-deleted",
+ )
+ ) );
+ }
+ $this->repo = new FileRepo( array(
+ 'name' => 'unittests',
+ 'backend' => $backend
+ ) );
+
+ $this->date = gmdate( "YmdHis" );
+ $this->createdFiles = array();
+ }
+
+ protected function tearDown() {
+ $this->repo->cleanupBatch( $this->createdFiles ); // delete files
+ foreach ( $this->createdFiles as $tmp ) { // delete dirs
+ $tmp = $this->repo->resolveVirtualUrl( $tmp );
+ while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) {
+ $this->repo->getBackend()->clean( array( 'dir' => $tmp ) );
+ }
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * Store a file or virtual URL source into a media file name.
+ *
+ * @param string $originalName The title of the image
+ * @param string $srcPath The filepath or virtual URL
+ * @param int $flags Flags to pass into repo::store().
+ * @return FileRepoStatus
+ */
+ private function storeit( $originalName, $srcPath, $flags ) {
+ $hashPath = $this->repo->getHashPath( $originalName );
+ $dstRel = "$hashPath{$this->date}!$originalName";
+ $dstUrlRel = $hashPath . $this->date . '!' . rawurlencode( $originalName );
+
+ $result = $this->repo->store( $srcPath, 'temp', $dstRel, $flags );
+ $result->value = $this->repo->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
+ $this->createdFiles[] = $result->value;
+
+ return $result;
+ }
+
+ /**
+ * Test storing a file using different flags.
+ *
+ * @param string $fn The title of the image
+ * @param string $infn The name of the file (in the filesystem)
+ * @param string $otherfn The name of the different file (in the filesystem)
+ * @param bool $fromrepo 'true' if we want to copy from a virtual URL out of the Repo.
+ */
+ private function storecohort( $fn, $infn, $otherfn, $fromrepo ) {
+ $f = $this->storeit( $fn, $infn, 0 );
+ $this->assertTrue( $f->isOK(), 'failed to store a new file' );
+ $this->assertEquals( $f->failCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ if ( $fromrepo ) {
+ $f = $this->storeit( "Other-$fn", $infn, FileRepo::OVERWRITE );
+ $infn = $f->value;
+ }
+ // This should work because we're allowed to overwrite
+ $f = $this->storeit( $fn, $infn, FileRepo::OVERWRITE );
+ $this->assertTrue( $f->isOK(), 'We should be allowed to overwrite' );
+ $this->assertEquals( $f->failCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ // This should fail because we're overwriting.
+ $f = $this->storeit( $fn, $infn, 0 );
+ $this->assertFalse( $f->isOK(), 'We should not be allowed to overwrite' );
+ $this->assertEquals( $f->failCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ // This should succeed because we're overwriting the same content.
+ $f = $this->storeit( $fn, $infn, FileRepo::OVERWRITE_SAME );
+ $this->assertTrue( $f->isOK(), 'We should be able to overwrite the same content' );
+ $this->assertEquals( $f->failCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ // This should fail because we're overwriting different content.
+ if ( $fromrepo ) {
+ $f = $this->storeit( "Other-$fn", $otherfn, FileRepo::OVERWRITE );
+ $otherfn = $f->value;
+ }
+ $f = $this->storeit( $fn, $otherfn, FileRepo::OVERWRITE_SAME );
+ $this->assertFalse( $f->isOK(), 'We should not be allowed to overwrite different content' );
+ $this->assertEquals( $f->failCount, 1, "counts wrong {$f->successCount} {$f->failCount}" );
+ $this->assertEquals( $f->successCount, 0, "counts wrong {$f->successCount} {$f->failCount}" );
+ }
+
+ /**
+ * @covers FileRepo::store
+ */
+ public function teststore() {
+ global $IP;
+ $this->storecohort(
+ "Test1.png",
+ "$IP/tests/phpunit/data/filerepo/wiki.png",
+ "$IP/tests/phpunit/data/filerepo/video.png",
+ false
+ );
+ $this->storecohort(
+ "Test2.png",
+ "$IP/tests/phpunit/data/filerepo/wiki.png",
+ "$IP/tests/phpunit/data/filerepo/video.png",
+ true
+ );
+ }
+}
diff --git a/tests/phpunit/includes/filerepo/file/FileTest.php b/tests/phpunit/includes/filerepo/file/FileTest.php
new file mode 100644
index 00000000..8e8b8a9e
--- /dev/null
+++ b/tests/phpunit/includes/filerepo/file/FileTest.php
@@ -0,0 +1,386 @@
+<?php
+
+class FileTest extends MediaWikiMediaTestCase {
+
+ /**
+ * @param string $filename
+ * @param bool $expected
+ * @dataProvider providerCanAnimate
+ */
+ function testCanAnimateThumbIfAppropriate( $filename, $expected ) {
+ $this->setMwGlobals( 'wgMaxAnimatedGifArea', 9000 );
+ $file = $this->dataFile( $filename );
+ $this->assertEquals( $file->canAnimateThumbIfAppropriate(), $expected );
+ }
+
+ function providerCanAnimate() {
+ return array(
+ array( 'nonanimated.gif', true ),
+ array( 'jpeg-comment-utf.jpg', true ),
+ array( 'test.tiff', true ),
+ array( 'Animated_PNG_example_bouncing_beach_ball.png', false ),
+ array( 'greyscale-png.png', true ),
+ array( 'Toll_Texas_1.svg', true ),
+ array( 'LoremIpsum.djvu', true ),
+ array( '80x60-2layers.xcf', true ),
+ array( 'Soccer_ball_animated.svg', false ),
+ array( 'Bishzilla_blink.gif', false ),
+ array( 'animated.gif', true ),
+ );
+ }
+
+ /**
+ * @dataProvider getThumbnailBucketProvider
+ * @covers File::getThumbnailBucket
+ */
+ public function testGetThumbnailBucket( $data ) {
+ $this->setMwGlobals( 'wgThumbnailBuckets', $data['buckets'] );
+ $this->setMwGlobals( 'wgThumbnailMinimumBucketDistance', $data['minimumBucketDistance'] );
+
+ $fileMock = $this->getMockBuilder( 'File' )
+ ->setConstructorArgs( array( 'fileMock', false ) )
+ ->setMethods( array( 'getWidth' ) )
+ ->getMockForAbstractClass();
+
+ $fileMock->expects( $this->any() )
+ ->method( 'getWidth' )
+ ->will( $this->returnValue( $data['width'] ) );
+
+ $this->assertEquals(
+ $data['expectedBucket'],
+ $fileMock->getThumbnailBucket( $data['requestedWidth'] ),
+ $data['message'] );
+ }
+
+ public function getThumbnailBucketProvider() {
+ $defaultBuckets = array( 256, 512, 1024, 2048, 4096 );
+
+ return array(
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'minimumBucketDistance' => 0,
+ 'width' => 3000,
+ 'requestedWidth' => 120,
+ 'expectedBucket' => 256,
+ 'message' => 'Picking bucket bigger than requested size'
+ ) ),
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'minimumBucketDistance' => 0,
+ 'width' => 3000,
+ 'requestedWidth' => 300,
+ 'expectedBucket' => 512,
+ 'message' => 'Picking bucket bigger than requested size'
+ ) ),
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'minimumBucketDistance' => 0,
+ 'width' => 3000,
+ 'requestedWidth' => 1024,
+ 'expectedBucket' => 2048,
+ 'message' => 'Picking bucket bigger than requested size'
+ ) ),
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'minimumBucketDistance' => 0,
+ 'width' => 3000,
+ 'requestedWidth' => 2048,
+ 'expectedBucket' => false,
+ 'message' => 'Picking no bucket because none is bigger than the requested size'
+ ) ),
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'minimumBucketDistance' => 0,
+ 'width' => 3000,
+ 'requestedWidth' => 3500,
+ 'expectedBucket' => false,
+ 'message' => 'Picking no bucket because requested size is bigger than original'
+ ) ),
+ array( array(
+ 'buckets' => array( 1024 ),
+ 'minimumBucketDistance' => 0,
+ 'width' => 3000,
+ 'requestedWidth' => 1024,
+ 'expectedBucket' => false,
+ 'message' => 'Picking no bucket because requested size equals biggest bucket'
+ ) ),
+ array( array(
+ 'buckets' => null,
+ 'minimumBucketDistance' => 0,
+ 'width' => 3000,
+ 'requestedWidth' => 1024,
+ 'expectedBucket' => false,
+ 'message' => 'Picking no bucket because no buckets have been specified'
+ ) ),
+ array( array(
+ 'buckets' => array( 256, 512 ),
+ 'minimumBucketDistance' => 10,
+ 'width' => 3000,
+ 'requestedWidth' => 245,
+ 'expectedBucket' => 256,
+ 'message' => 'Requested width is distant enough from next bucket for it to be picked'
+ ) ),
+ array( array(
+ 'buckets' => array( 256, 512 ),
+ 'minimumBucketDistance' => 10,
+ 'width' => 3000,
+ 'requestedWidth' => 246,
+ 'expectedBucket' => 512,
+ 'message' => 'Requested width is too close to next bucket, picking next one'
+ ) ),
+ );
+ }
+
+ /**
+ * @dataProvider getThumbnailSourceProvider
+ * @covers File::getThumbnailSource
+ */
+ public function testGetThumbnailSource( $data ) {
+ $backendMock = $this->getMockBuilder( 'FSFileBackend' )
+ ->setConstructorArgs( array( array( 'name' => 'backendMock', 'wikiId' => wfWikiId() ) ) )
+ ->getMock();
+
+ $repoMock = $this->getMockBuilder( 'FileRepo' )
+ ->setConstructorArgs( array( array( 'name' => 'repoMock', 'backend' => $backendMock ) ) )
+ ->setMethods( array( 'fileExists', 'getLocalReference' ) )
+ ->getMock();
+
+ $fsFile = new FSFile( 'fsFilePath' );
+
+ $repoMock->expects( $this->any() )
+ ->method( 'fileExists' )
+ ->will( $this->returnValue( true ) );
+
+ $repoMock->expects( $this->any() )
+ ->method( 'getLocalReference' )
+ ->will( $this->returnValue( $fsFile ) );
+
+ $handlerMock = $this->getMock( 'BitmapHandler', array( 'supportsBucketing' ) );
+ $handlerMock->expects( $this->any() )
+ ->method( 'supportsBucketing' )
+ ->will( $this->returnValue( $data['supportsBucketing'] ) );
+
+ $fileMock = $this->getMockBuilder( 'File' )
+ ->setConstructorArgs( array( 'fileMock', $repoMock ) )
+ ->setMethods( array( 'getThumbnailBucket', 'getLocalRefPath', 'getHandler' ) )
+ ->getMockForAbstractClass();
+
+ $fileMock->expects( $this->any() )
+ ->method( 'getThumbnailBucket' )
+ ->will( $this->returnValue( $data['thumbnailBucket'] ) );
+
+ $fileMock->expects( $this->any() )
+ ->method( 'getLocalRefPath' )
+ ->will( $this->returnValue( 'localRefPath' ) );
+
+ $fileMock->expects( $this->any() )
+ ->method( 'getHandler' )
+ ->will( $this->returnValue( $handlerMock ) );
+
+ $reflection = new ReflectionClass( $fileMock );
+ $reflection_property = $reflection->getProperty( 'handler' );
+ $reflection_property->setAccessible( true );
+ $reflection_property->setValue( $fileMock, $handlerMock );
+
+ if ( !is_null( $data['tmpBucketedThumbCache'] ) ) {
+ $reflection_property = $reflection->getProperty( 'tmpBucketedThumbCache' );
+ $reflection_property->setAccessible( true );
+ $reflection_property->setValue( $fileMock, $data['tmpBucketedThumbCache'] );
+ }
+
+ $result = $fileMock->getThumbnailSource(
+ array( 'physicalWidth' => $data['physicalWidth'] ) );
+
+ $this->assertEquals( $data['expectedPath'], $result['path'], $data['message'] );
+ }
+
+ public function getThumbnailSourceProvider() {
+ return array(
+ array( array(
+ 'supportsBucketing' => true,
+ 'tmpBucketedThumbCache' => null,
+ 'thumbnailBucket' => 1024,
+ 'physicalWidth' => 2048,
+ 'expectedPath' => 'fsFilePath',
+ 'message' => 'Path downloaded from storage'
+ ) ),
+ array( array(
+ 'supportsBucketing' => true,
+ 'tmpBucketedThumbCache' => array( 1024 => '/tmp/shouldnotexist' + rand() ),
+ 'thumbnailBucket' => 1024,
+ 'physicalWidth' => 2048,
+ 'expectedPath' => 'fsFilePath',
+ 'message' => 'Path downloaded from storage because temp file is missing'
+ ) ),
+ array( array(
+ 'supportsBucketing' => true,
+ 'tmpBucketedThumbCache' => array( 1024 => '/tmp' ),
+ 'thumbnailBucket' => 1024,
+ 'physicalWidth' => 2048,
+ 'expectedPath' => '/tmp',
+ 'message' => 'Temporary path because temp file was found'
+ ) ),
+ array( array(
+ 'supportsBucketing' => false,
+ 'tmpBucketedThumbCache' => null,
+ 'thumbnailBucket' => 1024,
+ 'physicalWidth' => 2048,
+ 'expectedPath' => 'localRefPath',
+ 'message' => 'Original file path because bucketing is unsupported by handler'
+ ) ),
+ array( array(
+ 'supportsBucketing' => true,
+ 'tmpBucketedThumbCache' => null,
+ 'thumbnailBucket' => false,
+ 'physicalWidth' => 2048,
+ 'expectedPath' => 'localRefPath',
+ 'message' => 'Original file path because no width provided'
+ ) ),
+ );
+ }
+
+ /**
+ * @dataProvider generateBucketsIfNeededProvider
+ * @covers File::generateBucketsIfNeeded
+ */
+ public function testGenerateBucketsIfNeeded( $data ) {
+ $this->setMwGlobals( 'wgThumbnailBuckets', $data['buckets'] );
+
+ $backendMock = $this->getMockBuilder( 'FSFileBackend' )
+ ->setConstructorArgs( array( array( 'name' => 'backendMock', 'wikiId' => wfWikiId() ) ) )
+ ->getMock();
+
+ $repoMock = $this->getMockBuilder( 'FileRepo' )
+ ->setConstructorArgs( array( array( 'name' => 'repoMock', 'backend' => $backendMock ) ) )
+ ->setMethods( array( 'fileExists', 'getLocalReference' ) )
+ ->getMock();
+
+ $fileMock = $this->getMockBuilder( 'File' )
+ ->setConstructorArgs( array( 'fileMock', $repoMock ) )
+ ->setMethods( array( 'getWidth', 'getBucketThumbPath', 'makeTransformTmpFile',
+ 'generateAndSaveThumb', 'getHandler' ) )
+ ->getMockForAbstractClass();
+
+ $handlerMock = $this->getMock( 'JpegHandler', array( 'supportsBucketing' ) );
+ $handlerMock->expects( $this->any() )
+ ->method( 'supportsBucketing' )
+ ->will( $this->returnValue( true ) );
+
+ $fileMock->expects( $this->any() )
+ ->method( 'getHandler' )
+ ->will( $this->returnValue( $handlerMock ) );
+
+ $reflectionMethod = new ReflectionMethod( 'File', 'generateBucketsIfNeeded' );
+ $reflectionMethod->setAccessible( true );
+
+ $fileMock->expects( $this->any() )
+ ->method( 'getWidth' )
+ ->will( $this->returnValue( $data['width'] ) );
+
+ $fileMock->expects( $data['expectedGetBucketThumbPathCalls'] )
+ ->method( 'getBucketThumbPath' );
+
+ $repoMock->expects( $data['expectedFileExistsCalls'] )
+ ->method( 'fileExists' )
+ ->will( $this->returnValue( $data['fileExistsReturn'] ) );
+
+ $fileMock->expects( $data['expectedMakeTransformTmpFile'] )
+ ->method( 'makeTransformTmpFile' )
+ ->will( $this->returnValue( $data['makeTransformTmpFileReturn'] ) );
+
+ $fileMock->expects( $data['expectedGenerateAndSaveThumb'] )
+ ->method( 'generateAndSaveThumb' )
+ ->will( $this->returnValue( $data['generateAndSaveThumbReturn'] ) );
+
+ $this->assertEquals( $data['expectedResult'],
+ $reflectionMethod->invoke(
+ $fileMock,
+ array(
+ 'physicalWidth' => $data['physicalWidth'],
+ 'physicalHeight' => $data['physicalHeight'] )
+ ),
+ $data['message'] );
+ }
+
+ public function generateBucketsIfNeededProvider() {
+ $defaultBuckets = array( 256, 512, 1024, 2048, 4096 );
+
+ return array(
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'width' => 256,
+ 'physicalWidth' => 256,
+ 'physicalHeight' => 100,
+ 'expectedGetBucketThumbPathCalls' => $this->never(),
+ 'expectedFileExistsCalls' => $this->never(),
+ 'fileExistsReturn' => null,
+ 'expectedMakeTransformTmpFile' => $this->never(),
+ 'makeTransformTmpFileReturn' => false,
+ 'expectedGenerateAndSaveThumb' => $this->never(),
+ 'generateAndSaveThumbReturn' => false,
+ 'expectedResult' => false,
+ 'message' => 'No bucket found, nothing to generate'
+ ) ),
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'width' => 5000,
+ 'physicalWidth' => 300,
+ 'physicalHeight' => 200,
+ 'expectedGetBucketThumbPathCalls' => $this->once(),
+ 'expectedFileExistsCalls' => $this->once(),
+ 'fileExistsReturn' => true,
+ 'expectedMakeTransformTmpFile' => $this->never(),
+ 'makeTransformTmpFileReturn' => false,
+ 'expectedGenerateAndSaveThumb' => $this->never(),
+ 'generateAndSaveThumbReturn' => false,
+ 'expectedResult' => false,
+ 'message' => 'File already exists, no reason to generate buckets'
+ ) ),
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'width' => 5000,
+ 'physicalWidth' => 300,
+ 'physicalHeight' => 200,
+ 'expectedGetBucketThumbPathCalls' => $this->once(),
+ 'expectedFileExistsCalls' => $this->once(),
+ 'fileExistsReturn' => false,
+ 'expectedMakeTransformTmpFile' => $this->once(),
+ 'makeTransformTmpFileReturn' => false,
+ 'expectedGenerateAndSaveThumb' => $this->never(),
+ 'generateAndSaveThumbReturn' => false,
+ 'expectedResult' => false,
+ 'message' => 'Cannot generate temp file for bucket'
+ ) ),
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'width' => 5000,
+ 'physicalWidth' => 300,
+ 'physicalHeight' => 200,
+ 'expectedGetBucketThumbPathCalls' => $this->once(),
+ 'expectedFileExistsCalls' => $this->once(),
+ 'fileExistsReturn' => false,
+ 'expectedMakeTransformTmpFile' => $this->once(),
+ 'makeTransformTmpFileReturn' => new TempFSFile( '/tmp/foo' ),
+ 'expectedGenerateAndSaveThumb' => $this->once(),
+ 'generateAndSaveThumbReturn' => false,
+ 'expectedResult' => false,
+ 'message' => 'Bucket image could not be generated'
+ ) ),
+ array( array(
+ 'buckets' => $defaultBuckets,
+ 'width' => 5000,
+ 'physicalWidth' => 300,
+ 'physicalHeight' => 200,
+ 'expectedGetBucketThumbPathCalls' => $this->once(),
+ 'expectedFileExistsCalls' => $this->once(),
+ 'fileExistsReturn' => false,
+ 'expectedMakeTransformTmpFile' => $this->once(),
+ 'makeTransformTmpFileReturn' => new TempFSFile( '/tmp/foo' ),
+ 'expectedGenerateAndSaveThumb' => $this->once(),
+ 'generateAndSaveThumbReturn' => new ThumbnailImage( false, 'bar', false, false ),
+ 'expectedResult' => true,
+ 'message' => 'Bucket image could not be generated'
+ ) ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php b/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php
new file mode 100644
index 00000000..2c7f50c9
--- /dev/null
+++ b/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Unit tests for HTMLAutoCompleteSelectField
+ *
+ * @covers HTMLAutoCompleteSelectField
+ */
+class HtmlAutoCompleteSelectFieldTest extends MediaWikiTestCase {
+
+ var $options = array(
+ 'Bulgaria' => 'BGR',
+ 'Burkina Faso' => 'BFA',
+ 'Burundi' => 'BDI',
+ );
+
+ /**
+ * Verify that attempting to instantiate an HTMLAutoCompleteSelectField
+ * without providing any autocomplete options causes an exception to be
+ * thrown.
+ *
+ * @expectedException MWException
+ * @expectedExceptionMessage called without any autocompletions
+ */
+ function testMissingAutocompletions() {
+ new HTMLAutoCompleteSelectField( array( 'fieldname' => 'Test' ) );
+ }
+
+ /**
+ * Verify that the autocomplete options are correctly encoded as
+ * the 'data-autocomplete' attribute of the field.
+ *
+ * @covers HTMLAutoCompleteSelectField::getAttributes
+ */
+ function testGetAttributes() {
+ $field = new HTMLAutoCompleteSelectField( array(
+ 'fieldname' => 'Test',
+ 'autocomplete' => $this->options,
+ ) );
+
+ $attributes = $field->getAttributes( array() );
+ $this->assertEquals( array_keys( $this->options ),
+ FormatJson::decode( $attributes['data-autocomplete'] ),
+ "The 'data-autocomplete' attribute encodes autocomplete option keys as a JSON array."
+ );
+ }
+
+ /**
+ * Test that the optional select dropdown is included or excluded based on
+ * the presence or absence of the 'options' parameter.
+ */
+ function testOptionalSelectElement() {
+ $params = array(
+ 'fieldname' => 'Test',
+ 'autocomplete' => $this->options,
+ 'options' => $this->options,
+ );
+
+ $field = new HTMLAutoCompleteSelectField( $params );
+ $html = $field->getInputHTML( false );
+ $this->assertRegExp( '/select/', $html,
+ "When the 'options' parameter is set, the HTML includes a <select>" );
+
+ unset( $params['options'] );
+ $field = new HTMLAutoCompleteSelectField( $params );
+ $html = $field->getInputHTML( false );
+ $this->assertNotRegExp( '/select/', $html,
+ "When the 'options' parameter is not set, the HTML does not include a <select>" );
+ }
+}
diff --git a/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php b/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php
new file mode 100644
index 00000000..5a822f53
--- /dev/null
+++ b/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * Unit tests for the HTMLCheckMatrix
+ * @covers HTMLCheckMatrix
+ */
+class HtmlCheckMatrixTest extends MediaWikiTestCase {
+ static private $defaultOptions = array(
+ 'rows' => array( 'r1', 'r2' ),
+ 'columns' => array( 'c1', 'c2' ),
+ 'fieldname' => 'test',
+ );
+
+ public function testPlainInstantiation() {
+ try {
+ new HTMLCheckMatrix( array() );
+ } catch ( MWException $e ) {
+ $this->assertInstanceOf( 'HTMLFormFieldRequiredOptionsException', $e );
+ return;
+ }
+
+ $this->fail( 'Expected MWException indicating missing parameters but none was thrown.' );
+ }
+
+ public function testInstantiationWithMinimumRequiredParameters() {
+ new HTMLCheckMatrix( self::$defaultOptions );
+ $this->assertTrue( true ); // form instantiation must throw exception on failure
+ }
+
+ public function testValidateCallsUserDefinedValidationCallback() {
+ $called = false;
+ $field = new HTMLCheckMatrix( self::$defaultOptions + array(
+ 'validation-callback' => function () use ( &$called ) {
+ $called = true;
+
+ return false;
+ },
+ ) );
+ $this->assertEquals( false, $this->validate( $field, array() ) );
+ $this->assertTrue( $called );
+ }
+
+ public function testValidateRequiresArrayInput() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions );
+ $this->assertEquals( false, $this->validate( $field, null ) );
+ $this->assertEquals( false, $this->validate( $field, true ) );
+ $this->assertEquals( false, $this->validate( $field, 'abc' ) );
+ $this->assertEquals( false, $this->validate( $field, new stdClass ) );
+ $this->assertEquals( true, $this->validate( $field, array() ) );
+ }
+
+ public function testValidateAllowsOnlyKnownTags() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions );
+ $this->assertInternalType( 'string', $this->validate( $field, array( 'foo' ) ) );
+ }
+
+ public function testValidateAcceptsPartialTagList() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions );
+ $this->assertTrue( $this->validate( $field, array() ) );
+ $this->assertTrue( $this->validate( $field, array( 'c1-r1' ) ) );
+ $this->assertTrue( $this->validate( $field, array( 'c1-r1', 'c1-r2', 'c2-r1', 'c2-r2' ) ) );
+ }
+
+ /**
+ * This form object actually has no visibility into what happens later on, but essentially
+ * if the data submitted by the user passes validate the following is run:
+ * foreach ( $field->filterDataForSubmit( $data ) as $k => $v ) {
+ * $user->setOption( $k, $v );
+ * }
+ */
+ public function testValuesForcedOnRemainOn() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions + array(
+ 'force-options-on' => array( 'c2-r1' ),
+ ) );
+ $expected = array(
+ 'c1-r1' => false,
+ 'c1-r2' => false,
+ 'c2-r1' => true,
+ 'c2-r2' => false,
+ );
+ $this->assertEquals( $expected, $field->filterDataForSubmit( array() ) );
+ }
+
+ public function testValuesForcedOffRemainOff() {
+ $field = new HTMLCheckMatrix( self::$defaultOptions + array(
+ 'force-options-off' => array( 'c1-r2', 'c2-r2' ),
+ ) );
+ $expected = array(
+ 'c1-r1' => true,
+ 'c1-r2' => false,
+ 'c2-r1' => true,
+ 'c2-r2' => false,
+ );
+ // array_keys on the result simulates submitting all fields checked
+ $this->assertEquals( $expected, $field->filterDataForSubmit( array_keys( $expected ) ) );
+ }
+
+ protected function validate( HTMLFormField $field, $submitted ) {
+ return $field->validate(
+ $submitted,
+ array( self::$defaultOptions['fieldname'] => $submitted )
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/installer/InstallDocFormatterTest.php b/tests/phpunit/includes/installer/InstallDocFormatterTest.php
new file mode 100644
index 00000000..064d5185
--- /dev/null
+++ b/tests/phpunit/includes/installer/InstallDocFormatterTest.php
@@ -0,0 +1,72 @@
+<?php
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+class InstallDocFormatterTest extends MediaWikiTestCase {
+ /**
+ * @covers InstallDocFormatter::format
+ * @dataProvider provideDocFormattingTests
+ */
+ public function testFormat( $expected, $unformattedText, $message = '' ) {
+ $this->assertEquals(
+ $expected,
+ InstallDocFormatter::format( $unformattedText ),
+ $message
+ );
+ }
+
+ /**
+ * Provider for testFormat()
+ */
+ public static 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 &#95;&#95;TOC&#95;&#95;', 'Install __TOC__', '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">'
+ . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar $wgFooBar]</span>',
+ '$wgFooBar', 'Testing basic $wgFooBar' ),
+ array(
+ '<span class="config-plainlink">'
+ . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar45 $wgFooBar45]</span>',
+ '$wgFooBar45', 'Testing $wgFooBar45 (with numbers)' ),
+ array(
+ '<span class="config-plainlink">'
+ . '[https://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/installer/OracleInstallerTest.php b/tests/phpunit/includes/installer/OracleInstallerTest.php
new file mode 100644
index 00000000..fdcecf9e
--- /dev/null
+++ b/tests/phpunit/includes/installer/OracleInstallerTest.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * Tests for OracleInstaller
+ *
+ * @group Database
+ * @group Installer
+ */
+
+class OracleInstallerTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider provideOracleConnectStrings
+ * @covers OracleInstaller::checkConnectStringFormat
+ */
+ public function testCheckConnectStringFormat( $expected, $connectString, $msg = '' ) {
+ $validity = $expected ? 'should be valid' : 'should NOT be valid';
+ $msg = "'$connectString' ($msg) $validity.";
+ $this->assertEquals( $expected,
+ OracleInstaller::checkConnectStringFormat( $connectString ),
+ $msg
+ );
+ }
+
+ /**
+ * Provider to test OracleInstaller::checkConnectStringFormat()
+ */
+ function provideOracleConnectStrings() {
+ // expected result, connectString[, message]
+ return array(
+ array( true, 'simple_01', 'Simple TNS name' ),
+ array( true, 'simple_01.world', 'TNS name with domain' ),
+ array( true, 'simple_01.domain.net', 'TNS name with domain' ),
+ array( true, 'host123', 'Host only' ),
+ array( true, 'host123.domain.net', 'FQDN only' ),
+ array( true, '//host123.domain.net', 'FQDN URL only' ),
+ array( true, '123.223.213.132', 'Host IP only' ),
+ array( true, 'host:1521', 'Host and port' ),
+ array( true, 'host:1521/service', 'Host, port and service' ),
+ array( true, 'host:1521/service:shared', 'Host, port, service and shared server type' ),
+ array( true, 'host:1521/service:dedicated', 'Host, port, service and dedicated server type' ),
+ array( true, 'host:1521/service:pooled', 'Host, port, service and pooled server type' ),
+ array(
+ true,
+ 'host:1521/service:shared/instance1',
+ 'Host, port, service, server type and instance'
+ ),
+ array( true, 'host:1521//instance1', 'Host, port and instance' ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/jobqueue/JobQueueTest.php b/tests/phpunit/includes/jobqueue/JobQueueTest.php
new file mode 100644
index 00000000..69e40068
--- /dev/null
+++ b/tests/phpunit/includes/jobqueue/JobQueueTest.php
@@ -0,0 +1,344 @@
+<?php
+
+/**
+ * @group JobQueue
+ * @group medium
+ * @group Database
+ */
+class JobQueueTest extends MediaWikiTestCase {
+ protected $key;
+ protected $queueRand, $queueRandTTL, $queueFifo, $queueFifoTTL;
+
+ function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed[] = 'job';
+ }
+
+ protected function setUp() {
+ global $wgJobTypeConf;
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgMemc', new HashBagOStuff() );
+
+ if ( $this->getCliArg( 'use-jobqueue' ) ) {
+ $name = $this->getCliArg( 'use-jobqueue' );
+ if ( !isset( $wgJobTypeConf[$name] ) ) {
+ throw new MWException( "No \$wgJobTypeConf entry for '$name'." );
+ }
+ $baseConfig = $wgJobTypeConf[$name];
+ } else {
+ $baseConfig = array( 'class' => 'JobQueueDB' );
+ }
+ $baseConfig['type'] = 'null';
+ $baseConfig['wiki'] = wfWikiID();
+ $variants = array(
+ 'queueRand' => array( 'order' => 'random', 'claimTTL' => 0 ),
+ 'queueRandTTL' => array( 'order' => 'random', 'claimTTL' => 10 ),
+ 'queueTimestamp' => array( 'order' => 'timestamp', 'claimTTL' => 0 ),
+ 'queueTimestampTTL' => array( 'order' => 'timestamp', 'claimTTL' => 10 ),
+ 'queueFifo' => array( 'order' => 'fifo', 'claimTTL' => 0 ),
+ 'queueFifoTTL' => array( 'order' => 'fifo', 'claimTTL' => 10 ),
+ );
+ foreach ( $variants as $q => $settings ) {
+ try {
+ $this->$q = JobQueue::factory( $settings + $baseConfig );
+ if ( !( $this->$q instanceof JobQueueDB ) ) {
+ $this->$q->setTestingPrefix( 'unittests-' . wfRandomString( 32 ) );
+ }
+ } catch ( MWException $e ) {
+ // unsupported?
+ // @todo What if it was another error?
+ };
+ }
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ foreach (
+ array(
+ 'queueRand', 'queueRandTTL', 'queueTimestamp', 'queueTimestampTTL',
+ 'queueFifo', 'queueFifoTTL'
+ ) as $q
+ ) {
+ if ( $this->$q ) {
+ $this->$q->delete();
+ }
+ $this->$q = null;
+ }
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue::getWiki
+ */
+ public function testGetWiki( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+ $this->assertEquals( wfWikiID(), $queue->getWiki(), "Proper wiki ID ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue::getType
+ */
+ public function testGetType( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+ $this->assertEquals( 'null', $queue->getType(), "Proper job type ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue
+ */
+ public function testBasicOperations( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ $this->assertNull( $queue->push( $this->newJob() ), "Push worked ($desc)" );
+ $this->assertNull( $queue->batchPush( array( $this->newJob() ) ), "Push worked ($desc)" );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 2, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+ $jobs = iterator_to_array( $queue->getAllQueuedJobs() );
+ $this->assertEquals( 2, count( $jobs ), "Queue iterator size is correct ($desc)" );
+
+ $job1 = $queue->pop();
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" );
+
+ $queue->flushCaches();
+ if ( $recycles ) {
+ $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ } else {
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $job2 = $queue->pop();
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ if ( $recycles ) {
+ $this->assertEquals( 2, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ } else {
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $queue->ack( $job1 );
+
+ $queue->flushCaches();
+ if ( $recycles ) {
+ $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ } else {
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $queue->ack( $job2 );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+
+ $this->assertNull( $queue->batchPush( array( $this->newJob(), $this->newJob() ) ),
+ "Push worked ($desc)" );
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->delete();
+ $queue->flushCaches();
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue
+ */
+ public function testBasicDeduplication( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ $this->assertNull(
+ $queue->batchPush(
+ array( $this->newDedupedJob(), $this->newDedupedJob(), $this->newDedupedJob() )
+ ),
+ "Push worked ($desc)" );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+
+ $this->assertNull(
+ $queue->batchPush(
+ array( $this->newDedupedJob(), $this->newDedupedJob(), $this->newDedupedJob() )
+ ),
+ "Push worked ($desc)"
+ );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+
+ $job1 = $queue->pop();
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ if ( $recycles ) {
+ $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ } else {
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ $queue->ack( $job1 );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_queueLists
+ * @covers JobQueue
+ */
+ public function testRootDeduplication( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ $id = wfRandomString( 32 );
+ $root1 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp
+ for ( $i = 0; $i < 5; ++$i ) {
+ $this->assertNull( $queue->push( $this->newJob( 0, $root1 ) ), "Push worked ($desc)" );
+ }
+ $queue->deduplicateRootJob( $this->newJob( 0, $root1 ) );
+ sleep( 1 ); // roo job timestamp will increase
+ $root2 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp
+ $this->assertNotEquals( $root1['rootJobTimestamp'], $root2['rootJobTimestamp'],
+ "Root job signatures have different timestamps." );
+ for ( $i = 0; $i < 5; ++$i ) {
+ $this->assertNull( $queue->push( $this->newJob( 0, $root2 ) ), "Push worked ($desc)" );
+ }
+ $queue->deduplicateRootJob( $this->newJob( 0, $root2 ) );
+
+ $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 10, $queue->getSize(), "Queue size is correct ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+
+ $dupcount = 0;
+ $jobs = array();
+ do {
+ $job = $queue->pop();
+ if ( $job ) {
+ $jobs[] = $job;
+ $queue->ack( $job );
+ }
+ if ( $job instanceof DuplicateJob ) {
+ ++$dupcount;
+ }
+ } while ( $job );
+
+ $this->assertEquals( 10, count( $jobs ), "Correct number of jobs popped ($desc)" );
+ $this->assertEquals( 5, $dupcount, "Correct number of duplicate jobs popped ($desc)" );
+ }
+
+ /**
+ * @dataProvider provider_fifoQueueLists
+ * @covers JobQueue
+ */
+ public function testJobOrder( $queue, $recycles, $desc ) {
+ $queue = $this->$queue;
+ if ( !$queue ) {
+ $this->markTestSkipped( $desc );
+ }
+
+ $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
+
+ for ( $i = 0; $i < 10; ++$i ) {
+ $this->assertNull( $queue->push( $this->newJob( $i ) ), "Push worked ($desc)" );
+ }
+
+ for ( $i = 0; $i < 10; ++$i ) {
+ $job = $queue->pop();
+ $this->assertTrue( $job instanceof Job, "Jobs popped from queue ($desc)" );
+ $params = $job->getParams();
+ $this->assertEquals( $i, $params['i'], "Job popped from queue is FIFO ($desc)" );
+ $queue->ack( $job );
+ }
+
+ $this->assertFalse( $queue->pop(), "Queue is not empty ($desc)" );
+
+ $queue->flushCaches();
+ $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
+ $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" );
+ }
+
+ public static function provider_queueLists() {
+ return array(
+ array( 'queueRand', false, 'Random queue without ack()' ),
+ array( 'queueRandTTL', true, 'Random queue with ack()' ),
+ array( 'queueTimestamp', false, 'Time ordered queue without ack()' ),
+ array( 'queueTimestampTTL', true, 'Time ordered queue with ack()' ),
+ array( 'queueFifo', false, 'FIFO ordered queue without ack()' ),
+ array( 'queueFifoTTL', true, 'FIFO ordered queue with ack()' )
+ );
+ }
+
+ public static function provider_fifoQueueLists() {
+ return array(
+ array( 'queueFifo', false, 'Ordered queue without ack()' ),
+ array( 'queueFifoTTL', true, 'Ordered queue with ack()' )
+ );
+ }
+
+ function newJob( $i = 0, $rootJob = array() ) {
+ return new NullJob( Title::newMainPage(),
+ array( 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 0, 'i' => $i ) + $rootJob );
+ }
+
+ function newDedupedJob( $i = 0, $rootJob = array() ) {
+ return new NullJob( Title::newMainPage(),
+ array( 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 1, 'i' => $i ) + $rootJob );
+ }
+}
diff --git a/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php b/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php
new file mode 100644
index 00000000..3e232a93
--- /dev/null
+++ b/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * @group JobQueue
+ * @group medium
+ * @group Database
+ */
+class RefreshLinksPartitionTest extends MediaWikiTestCase {
+ public function __construct( $name = null, array $data = array(), $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed[] = 'page';
+ $this->tablesUsed[] = 'revision';
+ $this->tablesUsed[] = 'pagelinks';
+ }
+
+ /**
+ * @dataProvider provider_backlinks
+ */
+ public function testRefreshLinks( $ns, $dbKey, $pages ) {
+ $title = Title::makeTitle( $ns, $dbKey );
+
+ foreach ( $pages as $page ) {
+ list( $bns, $bdbkey ) = $page;
+ $bpage = WikiPage::factory( Title::makeTitle( $bns, $bdbkey ) );
+ $content = ContentHandler::makeContent( "[[{$title->getPrefixedText()}]]", $bpage->getTitle() );
+ $bpage->doEditContent( $content, "test" );
+ }
+
+ $title->getBacklinkCache()->clear();
+ $this->assertEquals(
+ 20,
+ $title->getBacklinkCache()->getNumLinks( 'pagelinks' ),
+ 'Correct number of backlinks'
+ );
+
+ $job = new RefreshLinksJob( $title, array( 'recursive' => true, 'table' => 'pagelinks' )
+ + Job::newRootJobParams( "refreshlinks:pagelinks:{$title->getPrefixedText()}" ) );
+ $extraParams = $job->getRootJobParams();
+ $jobs = BacklinkJobUtils::partitionBacklinkJob( $job, 9, 1, array( 'params' => $extraParams ) );
+
+ $this->assertEquals( 10, count( $jobs ), 'Correct number of sub-jobs' );
+ $this->assertEquals( $pages[0], current( $jobs[0]->params['pages'] ),
+ 'First job is leaf job with proper title' );
+ $this->assertEquals( $pages[8], current( $jobs[8]->params['pages'] ),
+ 'Last leaf job is leaf job with proper title' );
+ $this->assertEquals( true, isset( $jobs[9]->params['recursive'] ),
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( true, $jobs[9]->params['recursive'],
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( true, is_array( $jobs[9]->params['range'] ),
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( $title->getPrefixedText(), $jobs[0]->getTitle()->getPrefixedText(),
+ 'Base job title retainend in leaf job' );
+ $this->assertEquals( $title->getPrefixedText(), $jobs[9]->getTitle()->getPrefixedText(),
+ 'Base job title retainend recursive sub-job' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs[0]->params['rootJobSignature'],
+ 'Leaf job has root params' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs[9]->params['rootJobSignature'],
+ 'Recursive sub-job has root params' );
+
+ $jobs2 = BacklinkJobUtils::partitionBacklinkJob(
+ $jobs[9],
+ 9,
+ 1,
+ array( 'params' => $extraParams )
+ );
+
+ $this->assertEquals( 10, count( $jobs2 ), 'Correct number of sub-jobs' );
+ $this->assertEquals( $pages[9], current( $jobs2[0]->params['pages'] ),
+ 'First job is leaf job with proper title' );
+ $this->assertEquals( $pages[17], current( $jobs2[8]->params['pages'] ),
+ 'Last leaf job is leaf job with proper title' );
+ $this->assertEquals( true, isset( $jobs2[9]->params['recursive'] ),
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( true, $jobs2[9]->params['recursive'],
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( true, is_array( $jobs2[9]->params['range'] ),
+ 'Last job is recursive sub-job' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs2[0]->params['rootJobSignature'],
+ 'Leaf job has root params' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs2[9]->params['rootJobSignature'],
+ 'Recursive sub-job has root params' );
+
+ $jobs3 = BacklinkJobUtils::partitionBacklinkJob(
+ $jobs2[9],
+ 9,
+ 1,
+ array( 'params' => $extraParams )
+ );
+
+ $this->assertEquals( 2, count( $jobs3 ), 'Correct number of sub-jobs' );
+ $this->assertEquals( $pages[18], current( $jobs3[0]->params['pages'] ),
+ 'First job is leaf job with proper title' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs3[0]->params['rootJobSignature'],
+ 'Leaf job has root params' );
+ $this->assertEquals( $pages[19], current( $jobs3[1]->params['pages'] ),
+ 'Last job is leaf job with proper title' );
+ $this->assertEquals( $extraParams['rootJobSignature'], $jobs3[1]->params['rootJobSignature'],
+ 'Last leaf job has root params' );
+ }
+
+ public static function provider_backlinks() {
+ $pages = array();
+ for ( $i = 0; $i < 20; ++$i ) {
+ $pages[] = array( 0, "Page-$i" );
+ }
+ return array(
+ array( 10, 'Bang', $pages )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/json/FormatJsonTest.php b/tests/phpunit/includes/json/FormatJsonTest.php
new file mode 100644
index 00000000..af68ab03
--- /dev/null
+++ b/tests/phpunit/includes/json/FormatJsonTest.php
@@ -0,0 +1,279 @@
+<?php
+
+/**
+ * @covers FormatJson
+ */
+class FormatJsonTest extends MediaWikiTestCase {
+
+ public static function provideEncoderPrettyPrinting() {
+ return array(
+ // Four spaces
+ array( true, ' ' ),
+ array( ' ', ' ' ),
+ // Two spaces
+ array( ' ', ' ' ),
+ // One tab
+ array( "\t", "\t" ),
+ );
+ }
+
+ /**
+ * @dataProvider provideEncoderPrettyPrinting
+ */
+ public function testEncoderPrettyPrinting( $pretty, $expectedIndent ) {
+ $obj = array(
+ 'emptyObject' => new stdClass,
+ 'emptyArray' => array(),
+ 'string' => 'foobar\\',
+ 'filledArray' => array(
+ array(
+ 123,
+ 456,
+ ),
+ // Nested json works without problems
+ '"7":["8",{"9":"10"}]',
+ // Whitespace clean up doesn't touch strings that look alike
+ "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}",
+ ),
+ );
+
+ // No trailing whitespace, no trailing linefeed
+ $json = '{
+ "emptyObject": {},
+ "emptyArray": [],
+ "string": "foobar\\\\",
+ "filledArray": [
+ [
+ 123,
+ 456
+ ],
+ "\"7\":[\"8\",{\"9\":\"10\"}]",
+ "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}"
+ ]
+}';
+
+ $json = str_replace( "\r", '', $json ); // Windows compat
+ $json = str_replace( "\t", $expectedIndent, $json );
+ $this->assertSame( $json, FormatJson::encode( $obj, $pretty ) );
+ }
+
+ public static function provideEncodeDefault() {
+ return self::getEncodeTestCases( array() );
+ }
+
+ /**
+ * @dataProvider provideEncodeDefault
+ */
+ public function testEncodeDefault( $from, $to ) {
+ $this->assertSame( $to, FormatJson::encode( $from ) );
+ }
+
+ public static function provideEncodeUtf8() {
+ return self::getEncodeTestCases( array( 'unicode' ) );
+ }
+
+ /**
+ * @dataProvider provideEncodeUtf8
+ */
+ public function testEncodeUtf8( $from, $to ) {
+ $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) );
+ }
+
+ public static function provideEncodeXmlMeta() {
+ return self::getEncodeTestCases( array( 'xmlmeta' ) );
+ }
+
+ /**
+ * @dataProvider provideEncodeXmlMeta
+ */
+ public function testEncodeXmlMeta( $from, $to ) {
+ $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) );
+ }
+
+ public static function provideEncodeAllOk() {
+ return self::getEncodeTestCases( array( 'unicode', 'xmlmeta' ) );
+ }
+
+ /**
+ * @dataProvider provideEncodeAllOk
+ */
+ public function testEncodeAllOk( $from, $to ) {
+ $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) );
+ }
+
+ public function testEncodePhpBug46944() {
+ $this->assertNotEquals(
+ '\ud840\udc00',
+ strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ),
+ 'Test encoding an broken json_encode character (U+20000)'
+ );
+ }
+
+ public function testDecodeReturnType() {
+ $this->assertInternalType(
+ 'object',
+ FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ),
+ 'Default to object'
+ );
+
+ $this->assertInternalType(
+ 'array',
+ FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ),
+ 'Optional array'
+ );
+ }
+
+ public static function provideParse() {
+ return array(
+ array( null ),
+ array( true ),
+ array( false ),
+ array( 0 ),
+ array( 1 ),
+ array( 1.2 ),
+ array( '' ),
+ array( 'str' ),
+ array( array( 0, 1, 2 ) ),
+ array( array( 'a' => 'b' ) ),
+ array( array( 'a' => 'b' ) ),
+ array( array( 'a' => 'b', 'x' => array( 'c' => 'd' ) ) ),
+ );
+ }
+
+ /**
+ * Recursively convert arrays into stdClass
+ * @param array|string|bool|int|float|null $value
+ * @return stdClass|string|bool|int|float|null
+ */
+ public static function toObject( $value ) {
+ return !is_array( $value ) ? $value : (object) array_map( __METHOD__, $value );
+ }
+
+ /**
+ * @dataProvider provideParse
+ * @param mixed $value
+ */
+ public function testParse( $value ) {
+ $expected = self::toObject( $value );
+ $json = FormatJson::encode( $expected, false, FormatJson::ALL_OK );
+ $this->assertJson( $json );
+
+ $st = FormatJson::parse( $json );
+ $this->assertType( 'Status', $st );
+ $this->assertTrue( $st->isGood() );
+ $this->assertEquals( $expected, $st->getValue() );
+
+ $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC );
+ $this->assertType( 'Status', $st );
+ $this->assertTrue( $st->isGood() );
+ $this->assertEquals( $value, $st->getValue() );
+ }
+
+ public static function provideParseTryFixing() {
+ return array(
+ array( "[,]", '[]' ),
+ array( "[ , ]", '[]' ),
+ array( "[ , }", false ),
+ array( '[1],', false ),
+ array( "[1,]", '[1]' ),
+ array( "[1\n,]", '[1]' ),
+ array( "[1,\n]", '[1]' ),
+ array( "[1,]\n", '[1]' ),
+ array( "[1\n,\n]\n", '[1]' ),
+ array( '["a,",]', '["a,"]' ),
+ array( "[[1,]\n,[2,\n],[3\n,]]", '[[1],[2],[3]]' ),
+ array( '[[1,],[2,],[3,]]', false ), // I wish we could parse this, but would need quote parsing
+ array( '[1,,]', false ),
+ );
+ }
+
+ /**
+ * @dataProvider provideParseTryFixing
+ * @param string $value
+ * @param string|bool $expected
+ */
+ public function testParseTryFixing( $value, $expected ) {
+ $st = FormatJson::parse( $value, FormatJson::TRY_FIXING );
+ $this->assertType( 'Status', $st );
+ if ( $expected === false ) {
+ $this->assertFalse( $st->isOK() );
+ } else {
+ $this->assertFalse( $st->isGood() );
+ $this->assertTrue( $st->isOK() );
+ $val = FormatJson::encode( $st->getValue(), false, FormatJson::ALL_OK );
+ $this->assertEquals( $expected, $val );
+ }
+ }
+
+ public static function provideParseErrors() {
+ return array(
+ array( 'aaa' ),
+ array( '{"j": 1 ] }' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideParseErrors
+ * @param mixed $value
+ */
+ public function testParseErrors( $value ) {
+ $st = FormatJson::parse( $value );
+ $this->assertType( 'Status', $st );
+ $this->assertFalse( $st->isOK() );
+ }
+
+ /**
+ * Generate a set of test cases for a particular combination of encoder options.
+ *
+ * @param array $unescapedGroups List of character groups to leave unescaped
+ * @return array Arrays of unencoded strings and corresponding encoded strings
+ */
+ private static function getEncodeTestCases( array $unescapedGroups ) {
+ $groups = array(
+ 'always' => array(
+ // Forward slash (always unescaped)
+ '/' => '/',
+
+ // Control characters
+ "\0" => '\u0000',
+ "\x08" => '\b',
+ "\t" => '\t',
+ "\n" => '\n',
+ "\r" => '\r',
+ "\f" => '\f',
+ "\x1f" => '\u001f', // representative example
+
+ // Double quotes
+ '"' => '\"',
+
+ // Backslashes
+ '\\' => '\\\\',
+ '\\\\' => '\\\\\\\\',
+ '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping
+
+ // Line terminators
+ "\xe2\x80\xa8" => '\u2028',
+ "\xe2\x80\xa9" => '\u2029',
+ ),
+ 'unicode' => array(
+ "\xc3\xa9" => '\u00e9',
+ "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP
+ ),
+ 'xmlmeta' => array(
+ '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits
+ '>' => '\u003E',
+ '&' => '\u0026',
+ ),
+ );
+
+ $cases = array();
+ foreach ( $groups as $name => $rules ) {
+ $leaveUnescaped = in_array( $name, $unescapedGroups );
+ foreach ( $rules as $from => $to ) {
+ $cases[] = array( $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' );
+ }
+ }
+
+ return $cases;
+ }
+}
diff --git a/tests/phpunit/includes/libs/CSSMinTest.php b/tests/phpunit/includes/libs/CSSMinTest.php
new file mode 100644
index 00000000..43c50869
--- /dev/null
+++ b/tests/phpunit/includes/libs/CSSMinTest.php
@@ -0,0 +1,401 @@
+<?php
+/**
+ * This file test the CSSMin library shipped with Mediawiki.
+ *
+ * @author Timo Tijhof
+ */
+
+class CSSMinTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $server = 'http://doc.example.org';
+
+ $this->setMwGlobals( array(
+ 'wgServer' => $server,
+ 'wgCanonicalServer' => $server,
+ ) );
+ }
+
+ /**
+ * @dataProvider provideMinifyCases
+ * @covers CSSMin::minify
+ */
+ public function testMinify( $code, $expectedOutput ) {
+ $minified = CSSMin::minify( $code );
+
+ $this->assertEquals(
+ $expectedOutput,
+ $minified,
+ 'Minified output should be in the form expected.'
+ );
+ }
+
+ public static function provideMinifyCases() {
+ return array(
+ // Whitespace
+ array( "\r\t\f \v\n\r", "" ),
+ array( "foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+
+ // Loose comments
+ array( "/* foo */", "" ),
+ array( "/*******\n foo\n *******/", "" ),
+ array( "/*!\n foo\n */", "" ),
+
+ // Inline comments in various different places
+ array( "/* comment */foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo/* comment */, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo,/* comment */ bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo, bar/* comment */ {\n\tprop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo, bar {\n\t/* comment */prop: value;\n}", "foo,bar{prop:value}" ),
+ array( "foo, bar {\n\tprop: /* comment */value;\n}", "foo,bar{prop:value}" ),
+ array( "foo, bar {\n\tprop: value /* comment */;\n}", "foo,bar{prop:value }" ),
+ array( "foo, bar {\n\tprop: value; /* comment */\n}", "foo,bar{prop:value; }" ),
+
+ // Keep track of things that aren't as minified as much as they
+ // could be (bug 35493)
+ array( 'foo { prop: value ;}', 'foo{prop:value }' ),
+ array( 'foo { prop : value; }', 'foo{prop :value}' ),
+ array( 'foo { prop: value ; }', 'foo{prop:value }' ),
+ array( 'foo { font-family: "foo" , "bar"; }', 'foo{font-family:"foo" ,"bar"}' ),
+ array( "foo { src:\n\turl('foo') ,\n\turl('bar') ; }", "foo{src:url('foo') ,url('bar') }" ),
+
+ // Interesting cases with string values
+ // - Double quotes, single quotes
+ array( 'foo { content: ""; }', 'foo{content:""}' ),
+ array( "foo { content: ''; }", "foo{content:''}" ),
+ array( 'foo { content: "\'"; }', 'foo{content:"\'"}' ),
+ array( "foo { content: '\"'; }", "foo{content:'\"'}" ),
+ // - Whitespace in string values
+ array( 'foo { content: " "; }', 'foo{content:" "}' ),
+ );
+ }
+
+ /**
+ * This tests funky parameters to CSSMin::remap. testRemapRemapping tests
+ * the basic functionality.
+ *
+ * @dataProvider provideRemapCases
+ * @covers CSSMin::remap
+ */
+ public function testRemap( $message, $params, $expectedOutput ) {
+ $remapped = call_user_func_array( 'CSSMin::remap', $params );
+
+ $messageAdd = " Case: $message";
+ $this->assertEquals(
+ $expectedOutput,
+ $remapped,
+ 'CSSMin::remap should return the expected url form.' . $messageAdd
+ );
+ }
+
+ public static function provideRemapCases() {
+ // Parameter signature:
+ // CSSMin::remap( $code, $local, $remote, $embedData = true )
+ return array(
+ array(
+ 'Simple case',
+ array( 'foo { prop: url(bar.png); }', false, 'http://example.org', false ),
+ 'foo { prop: url(http://example.org/bar.png); }',
+ ),
+ array(
+ 'Without trailing slash',
+ array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux', false ),
+ 'foo { prop: url(http://example.org/quux/../bar.png); }',
+ ),
+ array(
+ 'With trailing slash on remote (bug 27052)',
+ array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux/', false ),
+ 'foo { prop: url(http://example.org/quux/../bar.png); }',
+ ),
+ array(
+ 'Guard against stripping double slashes from query',
+ array( 'foo { prop: url(bar.png?corge=//grault); }', false, 'http://example.org/quux/', false ),
+ 'foo { prop: url(http://example.org/quux/bar.png?corge=//grault); }',
+ ),
+ array(
+ 'Expand absolute paths',
+ array( 'foo { prop: url(/w/skin/images/bar.png); }', false, 'http://example.org/quux', false ),
+ 'foo { prop: url(http://doc.example.org/w/skin/images/bar.png); }',
+ ),
+ );
+ }
+
+ /**
+ * This tests basic functionality of CSSMin::remap. testRemapRemapping tests funky parameters.
+ *
+ * @dataProvider provideRemapRemappingCases
+ * @covers CSSMin::remap
+ */
+ public function testRemapRemapping( $message, $input, $expectedOutput ) {
+ $localPath = __DIR__ . '/../../data/cssmin/';
+ $remotePath = 'http://localhost/w/';
+
+ $realOutput = CSSMin::remap( $input, $localPath, $remotePath );
+
+ $this->assertEquals(
+ $expectedOutput,
+ preg_replace( '/\d+-\d+-\d+T\d+:\d+:\d+Z/', 'timestamp', $realOutput ),
+ "CSSMin::remap: $message"
+ );
+ }
+
+ public static function provideRemapRemappingCases() {
+ // red.gif and green.gif are one-pixel 35-byte GIFs.
+ // large.png is a 35K PNG that should be non-embeddable.
+ // Full paths start with http://localhost/w/.
+ // Timestamps in output are replaced with 'timestamp'.
+
+ // data: URIs for red.gif and green.gif
+ $red = 'data:image/gif;base64,R0lGODlhAQABAIAAAP8AADAAACwAAAAAAQABAAACAkQBADs=';
+ $green = 'data:image/gif;base64,R0lGODlhAQABAIAAAACAADAAACwAAAAAAQABAAACAkQBADs=';
+
+ return array(
+ array(
+ 'Regular file',
+ 'foo { background: url(red.gif); }',
+ 'foo { background: url(http://localhost/w/red.gif?timestamp); }',
+ ),
+ array(
+ 'Regular file (missing)',
+ 'foo { background: url(theColorOfHerHair.gif); }',
+ 'foo { background: url(http://localhost/w/theColorOfHerHair.gif); }',
+ ),
+ array(
+ 'Remote URL',
+ 'foo { background: url(http://example.org/w/foo.png); }',
+ 'foo { background: url(http://example.org/w/foo.png); }',
+ ),
+ array(
+ 'Protocol-relative remote URL',
+ 'foo { background: url(//example.org/w/foo.png); }',
+ 'foo { background: url(//example.org/w/foo.png); }',
+ ),
+ array(
+ 'Remote URL with query',
+ 'foo { background: url(http://example.org/w/foo.png?query=yes); }',
+ 'foo { background: url(http://example.org/w/foo.png?query=yes); }',
+ ),
+ array(
+ 'Protocol-relative remote URL with query',
+ 'foo { background: url(//example.org/w/foo.png?query=yes); }',
+ 'foo { background: url(//example.org/w/foo.png?query=yes); }',
+ ),
+ array(
+ 'Domain-relative URL',
+ 'foo { background: url(/static/foo.png); }',
+ 'foo { background: url(http://doc.example.org/static/foo.png); }',
+ ),
+ array(
+ 'Domain-relative URL with query',
+ 'foo { background: url(/static/foo.png?query=yes); }',
+ 'foo { background: url(http://doc.example.org/static/foo.png?query=yes); }',
+ ),
+ array(
+ 'Remote URL (unnecessary quotes not preserved)',
+ 'foo { background: url("http://example.org/w/foo.png"); }',
+ 'foo { background: url(http://example.org/w/foo.png); }',
+ ),
+ array(
+ 'Embedded file',
+ 'foo { /* @embed */ background: url(red.gif); }',
+ "foo { background: url($red); background: url(http://localhost/w/red.gif?timestamp)!ie; }",
+ ),
+ array(
+ 'Embedded file, other comments before the rule',
+ "foo { /* Bar. */ /* @embed */ background: url(red.gif); }",
+ "foo { /* Bar. */ background: url($red); /* Bar. */ background: url(http://localhost/w/red.gif?timestamp)!ie; }",
+ ),
+ array(
+ 'Can not re-embed data: URIs',
+ "foo { /* @embed */ background: url($red); }",
+ "foo { background: url($red); }",
+ ),
+ array(
+ 'Can not remap data: URIs',
+ "foo { background: url($red); }",
+ "foo { background: url($red); }",
+ ),
+ array(
+ 'Can not embed remote URLs',
+ 'foo { /* @embed */ background: url(http://example.org/w/foo.png); }',
+ 'foo { background: url(http://example.org/w/foo.png); }',
+ ),
+ array(
+ 'Embedded file (inline @embed)',
+ 'foo { background: /* @embed */ url(red.gif); }',
+ "foo { background: url($red); "
+ . "background: url(http://localhost/w/red.gif?timestamp)!ie; }",
+ ),
+ array(
+ 'Can not embed large files',
+ 'foo { /* @embed */ background: url(large.png); }',
+ "foo { background: url(http://localhost/w/large.png?timestamp); }",
+ ),
+ array(
+ 'Two regular files in one rule',
+ 'foo { background: url(red.gif), url(green.gif); }',
+ 'foo { background: url(http://localhost/w/red.gif?timestamp), '
+ . 'url(http://localhost/w/green.gif?timestamp); }',
+ ),
+ array(
+ 'Two embedded files in one rule',
+ 'foo { /* @embed */ background: url(red.gif), url(green.gif); }',
+ "foo { background: url($red), url($green); "
+ . "background: url(http://localhost/w/red.gif?timestamp), "
+ . "url(http://localhost/w/green.gif?timestamp)!ie; }",
+ ),
+ array(
+ 'Two embedded files in one rule (inline @embed)',
+ 'foo { background: /* @embed */ url(red.gif), /* @embed */ url(green.gif); }',
+ "foo { background: url($red), url($green); "
+ . "background: url(http://localhost/w/red.gif?timestamp), "
+ . "url(http://localhost/w/green.gif?timestamp)!ie; }",
+ ),
+ array(
+ 'Two embedded files in one rule (inline @embed), one too large',
+ 'foo { background: /* @embed */ url(red.gif), /* @embed */ url(large.png); }',
+ "foo { background: url($red), url(http://localhost/w/large.png?timestamp); "
+ . "background: url(http://localhost/w/red.gif?timestamp), "
+ . "url(http://localhost/w/large.png?timestamp)!ie; }",
+ ),
+ array(
+ 'Practical example with some noise',
+ 'foo { /* @embed */ background: #f9f9f9 url(red.gif) 0 0 no-repeat; }',
+ "foo { background: #f9f9f9 url($red) 0 0 no-repeat; "
+ . "background: #f9f9f9 url(http://localhost/w/red.gif?timestamp) 0 0 no-repeat!ie; }",
+ ),
+ array(
+ 'Does not mess with other properties',
+ 'foo { color: red; background: url(red.gif); font-size: small; }',
+ 'foo { color: red; background: url(http://localhost/w/red.gif?timestamp); font-size: small; }',
+ ),
+ array(
+ 'Spacing and miscellanea not changed (1)',
+ 'foo { background: url(red.gif); }',
+ 'foo { background: url(http://localhost/w/red.gif?timestamp); }',
+ ),
+ array(
+ 'Spacing and miscellanea not changed (2)',
+ 'foo {background:url(red.gif)}',
+ 'foo {background:url(http://localhost/w/red.gif?timestamp)}',
+ ),
+ array(
+ 'Spaces within url() parentheses are ignored',
+ 'foo { background: url( red.gif ); }',
+ 'foo { background: url(http://localhost/w/red.gif?timestamp); }',
+ ),
+ array(
+ '@import rule to local file (should we remap this?)',
+ '@import url(/styles.css)',
+ '@import url(http://doc.example.org/styles.css)',
+ ),
+ array(
+ '@import rule to URL (should we remap this?)',
+ '@import url(//localhost/styles.css?query=yes)',
+ '@import url(//localhost/styles.css?query=yes)',
+ ),
+ array(
+ 'Simple case with comments before url',
+ 'foo { prop: /* some {funny;} comment */ url(bar.png); }',
+ 'foo { prop: /* some {funny;} comment */ url(http://localhost/w/bar.png); }',
+ ),
+ array(
+ 'Simple case with comments after url',
+ 'foo { prop: url(red.gif)/* some {funny;} comment */ ; }',
+ 'foo { prop: url(http://localhost/w/red.gif?timestamp)/* some {funny;} comment */ ; }',
+ ),
+ array(
+ 'Embedded file with comment before url',
+ 'foo { /* @embed */ background: /* some {funny;} comment */ url(red.gif); }',
+ "foo { background: /* some {funny;} comment */ url($red); background: /* some {funny;} comment */ url(http://localhost/w/red.gif?timestamp)!ie; }",
+ ),
+ array(
+ 'Embedded file with comments inside and outside the rule',
+ 'foo { /* @embed */ background: url(red.gif) /* some {foo;} comment */; /* some {bar;} comment */ }',
+ "foo { background: url($red) /* some {foo;} comment */; background: url(http://localhost/w/red.gif?timestamp) /* some {foo;} comment */!ie; /* some {bar;} comment */ }",
+ ),
+ array(
+ 'Embedded file with comment outside the rule',
+ 'foo { /* @embed */ background: url(red.gif); /* some {funny;} comment */ }',
+ "foo { background: url($red); background: url(http://localhost/w/red.gif?timestamp)!ie; /* some {funny;} comment */ }",
+ ),
+ array(
+ 'Rule with two urls, each with comments',
+ '{ background: /*asd*/ url(something.png); background: /*jkl*/ url(something.png); }',
+ '{ background: /*asd*/ url(http://localhost/w/something.png); background: /*jkl*/ url(http://localhost/w/something.png); }',
+ ),
+ array(
+ 'Sanity check for offending line from jquery.ui.theme.css (bug 60077)',
+ '.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; }',
+ '.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(http://localhost/w/images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; }',
+ ),
+ );
+ }
+
+ /**
+ * This tests basic functionality of CSSMin::buildUrlValue.
+ *
+ * @dataProvider provideBuildUrlValueCases
+ * @covers CSSMin::buildUrlValue
+ */
+ public function testBuildUrlValue( $message, $input, $expectedOutput ) {
+ $this->assertEquals(
+ $expectedOutput,
+ CSSMin::buildUrlValue( $input ),
+ "CSSMin::buildUrlValue: $message"
+ );
+ }
+
+ public static function provideBuildUrlValueCases() {
+ return array(
+ array(
+ 'Full URL',
+ 'scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s',
+ 'url(scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s)',
+ ),
+ array(
+ 'data: URI',
+ 'data:image/png;base64,R0lGODlh/+==',
+ 'url(data:image/png;base64,R0lGODlh/+==)',
+ ),
+ array(
+ 'URL with quotes',
+ "https://en.wikipedia.org/wiki/Wendy's",
+ "url(\"https://en.wikipedia.org/wiki/Wendy's\")",
+ ),
+ array(
+ 'URL with parentheses',
+ 'https://en.wikipedia.org/wiki/Boston_(band)',
+ 'url("https://en.wikipedia.org/wiki/Boston_(band)")',
+ ),
+ );
+ }
+
+ /**
+ * Seperated because they are currently broken (bug 35492)
+ *
+ * @group Broken
+ * @dataProvider provideStringCases
+ * @covers CSSMin::remap
+ */
+ public function testMinifyWithCSSStringValues( $code, $expectedOutput ) {
+ $this->testMinifyOutput( $code, $expectedOutput );
+ }
+
+ public static function provideStringCases() {
+ return array(
+ // String values should be respected
+ // - More than one space in a string value
+ array( 'foo { content: " "; }', 'foo{content:" "}' ),
+ // - Using a tab in a string value (turns into a space)
+ array( "foo { content: '\t'; }", "foo{content:'\t'}" ),
+ // - Using css-like syntax in string values
+ array(
+ 'foo::after { content: "{;}"; position: absolute; }',
+ 'foo::after{content:"{;}";position:absolute}'
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/libs/GenericArrayObjectTest.php b/tests/phpunit/includes/libs/GenericArrayObjectTest.php
new file mode 100644
index 00000000..4911f73a
--- /dev/null
+++ b/tests/phpunit/includes/libs/GenericArrayObjectTest.php
@@ -0,0 +1,280 @@
+<?php
+
+/**
+ * Tests for the GenericArrayObject and deriving classes.
+ *
+ * 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
+ * @since 1.20
+ *
+ * @ingroup Test
+ * @group GenericArrayObject
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+abstract class GenericArrayObjectTest extends MediaWikiTestCase {
+
+ /**
+ * Returns objects that can serve as elements in the concrete
+ * GenericArrayObject deriving class being tested.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ abstract public function elementInstancesProvider();
+
+ /**
+ * Returns the name of the concrete class being tested.
+ *
+ * @since 1.20
+ *
+ * @return string
+ */
+ abstract public function getInstanceClass();
+
+ /**
+ * Provides instances of the concrete class being tested.
+ *
+ * @since 1.20
+ *
+ * @return array
+ */
+ public function instanceProvider() {
+ $instances = array();
+
+ foreach ( $this->elementInstancesProvider() as $elementInstances ) {
+ $instances[] = $this->getNew( $elementInstances[0] );
+ }
+
+ return $this->arrayWrap( $instances );
+ }
+
+ /**
+ * @since 1.20
+ *
+ * @param array $elements
+ *
+ * @return GenericArrayObject
+ */
+ protected function getNew( array $elements = array() ) {
+ $class = $this->getInstanceClass();
+
+ return new $class( $elements );
+ }
+
+ /**
+ * @dataProvider elementInstancesProvider
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ *
+ * @covers GenericArrayObject::__construct
+ */
+ public function testConstructor( array $elements ) {
+ $arrayObject = $this->getNew( $elements );
+
+ $this->assertEquals( count( $elements ), $arrayObject->count() );
+ }
+
+ /**
+ * @dataProvider elementInstancesProvider
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ *
+ * @covers GenericArrayObject::isEmpty
+ */
+ public function testIsEmpty( array $elements ) {
+ $arrayObject = $this->getNew( $elements );
+
+ $this->assertEquals( $elements === array(), $arrayObject->isEmpty() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ *
+ * @since 1.20
+ *
+ * @param GenericArrayObject $list
+ *
+ * @covers GenericArrayObject::offsetUnset
+ */
+ public function testUnset( GenericArrayObject $list ) {
+ if ( $list->isEmpty() ) {
+ $this->assertTrue( true ); // We cannot test unset if there are no elements
+ } else {
+ $offset = $list->getIterator()->key();
+ $count = $list->count();
+ $list->offsetUnset( $offset );
+ $this->assertEquals( $count - 1, $list->count() );
+ }
+
+ if ( !$list->isEmpty() ) {
+ $offset = $list->getIterator()->key();
+ $count = $list->count();
+ unset( $list[$offset] );
+ $this->assertEquals( $count - 1, $list->count() );
+ }
+ }
+
+ /**
+ * @dataProvider elementInstancesProvider
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ *
+ * @covers GenericArrayObject::append
+ */
+ public function testAppend( array $elements ) {
+ $list = $this->getNew();
+
+ $listSize = count( $elements );
+
+ foreach ( $elements as $element ) {
+ $list->append( $element );
+ }
+
+ $this->assertEquals( $listSize, $list->count() );
+
+ $list = $this->getNew();
+
+ foreach ( $elements as $element ) {
+ $list[] = $element;
+ }
+
+ $this->assertEquals( $listSize, $list->count() );
+
+ $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) {
+ $list->append( $element );
+ } );
+ }
+
+ /**
+ * @since 1.20
+ *
+ * @param callable $function
+ *
+ * @covers GenericArrayObject::getObjectType
+ */
+ protected function checkTypeChecks( $function ) {
+ $excption = null;
+ $list = $this->getNew();
+
+ $elementClass = $list->getObjectType();
+
+ foreach ( array( 42, 'foo', array(), new stdClass(), 4.2 ) as $element ) {
+ $validValid = $element instanceof $elementClass;
+
+ try {
+ call_user_func( $function, $list, $element );
+ $valid = true;
+ } catch ( InvalidArgumentException $exception ) {
+ $valid = false;
+ }
+
+ $this->assertEquals(
+ $validValid,
+ $valid,
+ 'Object of invalid type got successfully added to a GenericArrayObject'
+ );
+ }
+ }
+
+ /**
+ * @dataProvider elementInstancesProvider
+ *
+ * @since 1.20
+ *
+ * @param array $elements
+ *
+ * @covers GenericArrayObject::offsetSet
+ */
+ public function testOffsetSet( array $elements ) {
+ if ( $elements === array() ) {
+ $this->assertTrue( true );
+
+ return;
+ }
+
+ $list = $this->getNew();
+
+ $element = reset( $elements );
+ $list->offsetSet( 42, $element );
+ $this->assertEquals( $element, $list->offsetGet( 42 ) );
+
+ $list = $this->getNew();
+
+ $element = reset( $elements );
+ $list['oHai'] = $element;
+ $this->assertEquals( $element, $list['oHai'] );
+
+ $list = $this->getNew();
+
+ $element = reset( $elements );
+ $list->offsetSet( 9001, $element );
+ $this->assertEquals( $element, $list[9001] );
+
+ $list = $this->getNew();
+
+ $element = reset( $elements );
+ $list->offsetSet( null, $element );
+ $this->assertEquals( $element, $list[0] );
+
+ $list = $this->getNew();
+ $offset = 0;
+
+ foreach ( $elements as $element ) {
+ $list->offsetSet( null, $element );
+ $this->assertEquals( $element, $list[$offset++] );
+ }
+
+ $this->assertEquals( count( $elements ), $list->count() );
+
+ $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) {
+ $list->offsetSet( mt_rand(), $element );
+ } );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ *
+ * @since 1.21
+ *
+ * @param GenericArrayObject $list
+ *
+ * @covers GenericArrayObject::getSerializationData
+ * @covers GenericArrayObject::serialize
+ * @covers GenericArrayObject::unserialize
+ */
+ public function testSerialization( GenericArrayObject $list ) {
+ $serialization = serialize( $list );
+ $copy = unserialize( $serialization );
+
+ $this->assertEquals( $serialization, serialize( $copy ) );
+ $this->assertEquals( count( $list ), count( $copy ) );
+
+ $list = $list->getArrayCopy();
+ $copy = $copy->getArrayCopy();
+
+ $this->assertArrayEquals( $list, $copy, true, true );
+ }
+}
diff --git a/tests/phpunit/includes/libs/HashRingTest.php b/tests/phpunit/includes/libs/HashRingTest.php
new file mode 100644
index 00000000..68dfea1f
--- /dev/null
+++ b/tests/phpunit/includes/libs/HashRingTest.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @group HashRing
+ */
+class HashRingTest extends MediaWikiTestCase {
+ /**
+ * @covers HashRing
+ */
+ public function testHashRing() {
+ $ring = new HashRing( array( 's1' => 1, 's2' => 1, 's3' => 2, 's4' => 2, 's5' => 2, 's6' => 3 ) );
+
+ $locations = array();
+ for ( $i = 0; $i < 20; $i++ ) {
+ $locations[ "hello$i"] = $ring->getLocation( "hello$i" );
+ }
+ $expectedLocations = array(
+ "hello0" => "s5",
+ "hello1" => "s6",
+ "hello2" => "s2",
+ "hello3" => "s5",
+ "hello4" => "s6",
+ "hello5" => "s4",
+ "hello6" => "s5",
+ "hello7" => "s4",
+ "hello8" => "s5",
+ "hello9" => "s5",
+ "hello10" => "s3",
+ "hello11" => "s6",
+ "hello12" => "s1",
+ "hello13" => "s3",
+ "hello14" => "s3",
+ "hello15" => "s5",
+ "hello16" => "s4",
+ "hello17" => "s6",
+ "hello18" => "s6",
+ "hello19" => "s3"
+ );
+
+ $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' );
+
+ $locations = array();
+ for ( $i = 0; $i < 5; $i++ ) {
+ $locations[ "hello$i"] = $ring->getLocations( "hello$i", 2 );
+ }
+
+ $expectedLocations = array(
+ "hello0" => array( "s5", "s6" ),
+ "hello1" => array( "s6", "s4" ),
+ "hello2" => array( "s2", "s1" ),
+ "hello3" => array( "s5", "s6" ),
+ "hello4" => array( "s6", "s4" ),
+ );
+ $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' );
+ }
+}
diff --git a/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/includes/libs/IEUrlExtensionTest.php
new file mode 100644
index 00000000..b7071230
--- /dev/null
+++ b/tests/phpunit/includes/libs/IEUrlExtensionTest.php
@@ -0,0 +1,173 @@
+<?php
+
+/**
+ * Tests for IEUrlExtension::findIE6Extension
+ * @todo tests below for findIE6Extension should be split into...
+ * ...a dataprovider and test method.
+ */
+class IEUrlExtensionTest extends MediaWikiTestCase {
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testSimple() {
+ $this->assertEquals(
+ 'y',
+ IEUrlExtension::findIE6Extension( 'x.y' ),
+ 'Simple extension'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testSimpleNoExt() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( 'x' ),
+ 'No extension'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testEmpty() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '' ),
+ 'Empty string'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testQuestionMark() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '?' ),
+ 'Question mark only'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testExtQuestionMark() {
+ $this->assertEquals(
+ 'x',
+ IEUrlExtension::findIE6Extension( '.x?' ),
+ 'Extension then question mark'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testQuestionMarkExt() {
+ $this->assertEquals(
+ 'x',
+ IEUrlExtension::findIE6Extension( '?.x' ),
+ 'Question mark then extension'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testInvalidChar() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '.x*' ),
+ 'Extension with invalid character'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testInvalidCharThenExtension() {
+ $this->assertEquals(
+ 'x',
+ IEUrlExtension::findIE6Extension( '*.x' ),
+ 'Invalid character followed by an extension'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testMultipleQuestionMarks() {
+ $this->assertEquals(
+ 'c',
+ IEUrlExtension::findIE6Extension( 'a?b?.c?.d?e?f' ),
+ 'Multiple question marks'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testExeException() {
+ $this->assertEquals(
+ 'd',
+ IEUrlExtension::findIE6Extension( 'a?b?.exe?.d?.e' ),
+ '.exe exception'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testExeException2() {
+ $this->assertEquals(
+ 'exe',
+ IEUrlExtension::findIE6Extension( 'a?b?.exe' ),
+ '.exe exception 2'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testHash() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( 'a#b.c' ),
+ 'Hash character preceding extension'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testHash2() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( 'a?#b.c' ),
+ 'Hash character preceding extension 2'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testDotAtEnd() {
+ $this->assertEquals(
+ '',
+ IEUrlExtension::findIE6Extension( '.' ),
+ 'Dot at end of string'
+ );
+ }
+
+ /**
+ * @covers IEUrlExtension::findIE6Extension
+ */
+ public function testTwoDots() {
+ $this->assertEquals(
+ 'z',
+ IEUrlExtension::findIE6Extension( 'x.y.z' ),
+ 'Two dots'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/libs/IPSetTest.php b/tests/phpunit/includes/libs/IPSetTest.php
new file mode 100644
index 00000000..d4e5214a
--- /dev/null
+++ b/tests/phpunit/includes/libs/IPSetTest.php
@@ -0,0 +1,252 @@
+<?php
+
+/**
+ * @group IPSet
+ */
+class IPSetTest extends MediaWikiTestCase {
+ /**
+ * Provides test cases for IPSetTest::testIPSet
+ *
+ * Returns an array of test cases. Each case is an array of (description,
+ * config, tests). Description is just text output for failure messages,
+ * config is an array constructor argument for IPSet, and the tests are
+ * an array of IP => expected (boolean) result against the config dataset.
+ */
+ public static function provideIPSets() {
+ return array(
+ array(
+ 'old_list_subset',
+ array(
+ '208.80.152.162',
+ '10.64.0.123',
+ '10.64.0.124',
+ '10.64.0.125',
+ '10.64.0.126',
+ '10.64.0.127',
+ '10.64.0.128',
+ '10.64.0.129',
+ '10.64.32.104',
+ '10.64.32.105',
+ '10.64.32.106',
+ '10.64.32.107',
+ '91.198.174.45',
+ '91.198.174.46',
+ '91.198.174.47',
+ '91.198.174.57',
+ '2620:0:862:1:A6BA:DBFF:FE30:CFB3',
+ '91.198.174.58',
+ '2620:0:862:1:A6BA:DBFF:FE38:FFDA',
+ '208.80.152.16',
+ '208.80.152.17',
+ '208.80.152.18',
+ '208.80.152.19',
+ '91.198.174.102',
+ '91.198.174.103',
+ '91.198.174.104',
+ '91.198.174.105',
+ '91.198.174.106',
+ '91.198.174.107',
+ '91.198.174.81',
+ '2620:0:862:1:26B6:FDFF:FEF5:B2D4',
+ '91.198.174.82',
+ '2620:0:862:1:26B6:FDFF:FEF5:ABB4',
+ '10.20.0.113',
+ '2620:0:862:102:26B6:FDFF:FEF5:AD9C',
+ '10.20.0.114',
+ '2620:0:862:102:26B6:FDFF:FEF5:7C38',
+ ),
+ array(
+ '0.0.0.0' => false,
+ '255.255.255.255' => false,
+ '10.64.0.122' => false,
+ '10.64.0.123' => true,
+ '10.64.0.124' => true,
+ '10.64.0.129' => true,
+ '10.64.0.130' => false,
+ '91.198.174.81' => true,
+ '91.198.174.80' => false,
+ '0::0' => false,
+ 'ffff:ffff:ffff:ffff:FFFF:FFFF:FFFF:FFFF' => false,
+ '2001:db8::1234' => false,
+ '2620:0:862:1:26b6:fdff:fef5:abb3' => false,
+ '2620:0:862:1:26b6:fdff:fef5:abb4' => true,
+ '2620:0:862:1:26b6:fdff:fef5:abb5' => false,
+ ),
+ ),
+ array(
+ 'new_cidr_set',
+ array(
+ '208.80.154.0/26',
+ '2620:0:861:1::/64',
+ '208.80.154.128/26',
+ '2620:0:861:2::/64',
+ '208.80.154.64/26',
+ '2620:0:861:3::/64',
+ '208.80.155.96/27',
+ '2620:0:861:4::/64',
+ '10.64.0.0/22',
+ '2620:0:861:101::/64',
+ '10.64.16.0/22',
+ '2620:0:861:102::/64',
+ '10.64.32.0/22',
+ '2620:0:861:103::/64',
+ '10.64.48.0/22',
+ '2620:0:861:107::/64',
+ '91.198.174.0/25',
+ '2620:0:862:1::/64',
+ '10.20.0.0/24',
+ '2620:0:862:102::/64',
+ '10.128.0.0/24',
+ '2620:0:863:101::/64',
+ '10.2.4.26',
+ ),
+ array(
+ '0.0.0.0' => false,
+ '255.255.255.255' => false,
+ '10.2.4.25' => false,
+ '10.2.4.26' => true,
+ '10.2.4.27' => false,
+ '10.20.0.255' => true,
+ '10.128.0.0' => true,
+ '10.64.17.55' => true,
+ '10.64.20.0' => false,
+ '10.64.27.207' => false,
+ '10.64.31.255' => false,
+ '0::0' => false,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => false,
+ '2001:DB8::1' => false,
+ '2620:0:861:106::45' => false,
+ '2620:0:862:103::' => false,
+ '2620:0:862:102:10:20:0:113' => true,
+ ),
+ ),
+ array(
+ 'empty_set',
+ array(),
+ array(
+ '0.0.0.0' => false,
+ '255.255.255.255' => false,
+ '10.2.4.25' => false,
+ '10.2.4.26' => false,
+ '10.2.4.27' => false,
+ '10.20.0.255' => false,
+ '10.128.0.0' => false,
+ '10.64.17.55' => false,
+ '10.64.20.0' => false,
+ '10.64.27.207' => false,
+ '10.64.31.255' => false,
+ '0::0' => false,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => false,
+ '2001:DB8::1' => false,
+ '2620:0:861:106::45' => false,
+ '2620:0:862:103::' => false,
+ '2620:0:862:102:10:20:0:113' => false,
+ ),
+ ),
+ array(
+ 'edge_cases',
+ array(
+ '0.0.0.0',
+ '255.255.255.255',
+ '::',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
+ '10.10.10.10/25', // host bits intentional
+ ),
+ array(
+ '0.0.0.0' => true,
+ '255.255.255.255' => true,
+ '10.2.4.25' => false,
+ '10.2.4.26' => false,
+ '10.2.4.27' => false,
+ '10.20.0.255' => false,
+ '10.128.0.0' => false,
+ '10.64.17.55' => false,
+ '10.64.20.0' => false,
+ '10.64.27.207' => false,
+ '10.64.31.255' => false,
+ '0::0' => true,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => true,
+ '2001:DB8::1' => false,
+ '2620:0:861:106::45' => false,
+ '2620:0:862:103::' => false,
+ '2620:0:862:102:10:20:0:113' => false,
+ '10.10.9.255' => false,
+ '10.10.10.0' => true,
+ '10.10.10.1' => true,
+ '10.10.10.10' => true,
+ '10.10.10.126' => true,
+ '10.10.10.127' => true,
+ '10.10.10.128' => false,
+ '10.10.10.177' => false,
+ '10.10.10.255' => false,
+ '10.10.11.0' => false,
+ ),
+ ),
+ array(
+ 'exercise_optimizer',
+ array(
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fffe:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fffd:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fffc:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fffb:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fffa:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff9:8000/113',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff9:0/113',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff8:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff7:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff6:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff5:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff4:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff3:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff2:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff1:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff0:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffef:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffee:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffec:0/111',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffeb:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffea:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe9:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe8:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe7:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe6:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe5:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe4:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe3:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe2:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe1:0/112',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffe0:0/110',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffc0:0/107',
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffa0:0/107',
+ ),
+ array(
+ '0.0.0.0' => false,
+ '255.255.255.255' => false,
+ '::' => false,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ff9f:ffff' => false,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffa0:0' => true,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffc0:1234' => true,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffed:ffff' => true,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff4:4444' => true,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:fff9:8080' => true,
+ 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => true,
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Validates IPSet loading and matching code
+ *
+ * @covers IPSet
+ * @dataProvider provideIPSets
+ */
+ public function testIPSet( $desc, array $cfg, array $tests ) {
+ $ipset = new IPSet( $cfg );
+ foreach ( $tests as $ip => $expected ) {
+ $result = $ipset->match( $ip );
+ $this->assertEquals( $expected, $result, "Incorrect match() result for $ip in dataset $desc" );
+ }
+ }
+}
diff --git a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php
new file mode 100644
index 00000000..c8795b2e
--- /dev/null
+++ b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php
@@ -0,0 +1,204 @@
+<?php
+
+class JavaScriptMinifierTest extends MediaWikiTestCase {
+
+ public static function provideCases() {
+ return array(
+
+ // Basic whitespace and comments that should be stripped entirely
+ array( "\r\t\f \v\n\r", "" ),
+ array( "/* Foo *\n*bar\n*/", "" ),
+
+ /**
+ * Slashes used inside block comments (bug 26931).
+ * At some point there was a bug that caused this comment to be ended at '* /',
+ * causing /M... to be left as the beginning of a regex.
+ */
+ array(
+ "/**\n * Foo\n * {\n * 'bar' : {\n * "
+ . "//Multiple rules with configurable operators\n * 'baz' : false\n * }\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;" ),
+
+ // Semicolon insertion between an expression having an inline
+ // comment after it, and a statement on the next line (bug 27046).
+ array(
+ "var a = this //foo bar \n for ( b = 0; c < d; b++ ) {}",
+ "var a=this\nfor(b=0;c<d;b++){}"
+ ),
+
+ // 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", "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" ),
+
+ // 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={}' ),
+
+ // 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={}' ),
+
+ // Some structures that might look invalid at first sight
+ array( "var a = 5.;", "var a=5.;" ),
+ array( "5.0.toString();", "5.0.toString();" ),
+ array( "5..toString();", "5..toString();" ),
+ array( "5...toString();", false ),
+ array( "5.\n.toString();", '5..toString();' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCases
+ * @covers JavaScriptMinifier::minify
+ */
+ public 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."
+ );
+ }
+
+ public static function provideBug32548() {
+ return array(
+ array(
+ // This one gets interpreted all together by the prior code;
+ // no break at the 'E' happens.
+ '1.23456789E55',
+ ),
+ array(
+ // This one breaks under the bad code; splits between 'E' and '+'
+ '1.23456789E+5',
+ ),
+ array(
+ // This one breaks under the bad code; splits between 'E' and '-'
+ '1.23456789E-5',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideBug32548
+ * @covers JavaScriptMinifier::minify
+ * @todo give this test a real name explaining what is being tested here
+ */
+ public function testBug32548Exponent( $num ) {
+ // Long line breaking was being incorrectly done between the base and
+ // exponent part of a number, causing a syntax error. The line should
+ // instead break at the start of the number.
+ $prefix = 'var longVarName' . str_repeat( '_', 973 ) . '=';
+ $suffix = ',shortVarName=0;';
+
+ $input = $prefix . $num . $suffix;
+ $expected = $prefix . "\n" . $num . $suffix;
+
+ $minified = JavaScriptMinifier::minify( $input );
+
+ $this->assertEquals( $expected, $minified, "Line breaks must not occur in middle of exponent" );
+ }
+}
diff --git a/tests/phpunit/includes/libs/MWMessagePackTest.php b/tests/phpunit/includes/libs/MWMessagePackTest.php
new file mode 100644
index 00000000..f80f78df
--- /dev/null
+++ b/tests/phpunit/includes/libs/MWMessagePackTest.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * PHP Unit tests for MWMessagePack
+ * @covers MWMessagePack
+ */
+class MWMessagePackTest extends MediaWikiTestCase {
+
+ /**
+ * Provides test cases for MWMessagePackTest::testMessagePack
+ *
+ * Returns an array of test cases. Each case is an array of (type, value,
+ * expected encoding as hex string). The expected values were generated
+ * using <https://github.com/msgpack/msgpack-php>, which includes a
+ * serialization function.
+ */
+ public static function providePacks() {
+ $tests = array(
+ array( 'nil', null, 'c0' ),
+ array( 'bool', true, 'c3' ),
+ array( 'bool', false, 'c2' ),
+ array( 'positive fixnum', 0, '00' ),
+ array( 'positive fixnum', 1, '01' ),
+ array( 'positive fixnum', 5, '05' ),
+ array( 'positive fixnum', 35, '23' ),
+ array( 'uint 8', 128, 'cc80' ),
+ array( 'uint 16', 1000, 'cd03e8' ),
+ array( 'uint 32', 100000, 'ce000186a0' ),
+ array( 'negative fixnum', -1, 'ff' ),
+ array( 'negative fixnum', -2, 'fe' ),
+ array( 'int 8', -128, 'd080' ),
+ array( 'int 8', -35, 'd0dd' ),
+ array( 'int 16', -1000, 'd1fc18' ),
+ array( 'int 32', -100000, 'd2fffe7960' ),
+ array( 'double', 0.1, 'cb3fb999999999999a' ),
+ array( 'double', 1.1, 'cb3ff199999999999a' ),
+ array( 'double', 123.456, 'cb405edd2f1a9fbe77' ),
+ array( 'fix raw', '', 'a0' ),
+ array( 'fix raw', 'foobar', 'a6666f6f626172' ),
+ array(
+ 'raw 16',
+ 'Lorem ipsum dolor sit amet amet.',
+ 'da00204c6f72656d20697073756d20646f6c6f722073697420616d657420616d65742e'
+ ),
+ array(
+ 'fix array',
+ array( 'abc', 'def', 'ghi' ),
+ '93a3616263a3646566a3676869'
+ ),
+ array(
+ 'fix map',
+ array( 'one' => 1, 'two' => 2 ),
+ '82a36f6e6501a374776f02'
+ ),
+ );
+
+ if ( PHP_INT_SIZE > 4 ) {
+ $tests[] = array( 'uint 64', 10000000000, 'cf00000002540be400' );
+ $tests[] = array( 'int 64', -10000000000, 'd3fffffffdabf41c00' );
+ $tests[] = array( 'int 64', -223372036854775807, 'd3fce66c50e2840001' );
+ $tests[] = array( 'int 64', -9223372036854775807, 'd38000000000000001' );
+ }
+
+ return $tests;
+ }
+
+ /**
+ * Verify that values are serialized correctly.
+ * @covers MWMessagePack::pack
+ * @dataProvider providePacks
+ */
+ public function testPack( $type, $value, $expected ) {
+ $actual = bin2hex( MWMessagePack::pack( $value ) );
+ $this->assertEquals( $expected, $actual, $type );
+ }
+}
diff --git a/tests/phpunit/includes/libs/ProcessCacheLRUTest.php b/tests/phpunit/includes/libs/ProcessCacheLRUTest.php
new file mode 100644
index 00000000..1a8a1e56
--- /dev/null
+++ b/tests/phpunit/includes/libs/ProcessCacheLRUTest.php
@@ -0,0 +1,237 @@
+<?php
+
+/**
+ * Test for ProcessCacheLRU class.
+ *
+ * Note that it uses the ProcessCacheLRUTestable class which extends some
+ * properties and methods visibility. That class is defined at the end of the
+ * file containing this class.
+ *
+ * @group Cache
+ */
+class ProcessCacheLRUTest extends MediaWikiTestCase {
+
+ /**
+ * Helper to verify emptiness of a cache object.
+ * Compare against an array so we get the cache content difference.
+ */
+ function assertCacheEmpty( $cache, $msg = 'Cache should be empty' ) {
+ $this->assertAttributeEquals( array(), 'cache', $cache, $msg );
+ }
+
+ /**
+ * Helper to fill a cache object passed by reference
+ */
+ function fillCache( &$cache, $numEntries ) {
+ // Fill cache with three values
+ for ( $i = 1; $i <= $numEntries; $i++ ) {
+ $cache->set( "cache-key-$i", "prop-$i", "value-$i" );
+ }
+ }
+
+ /**
+ * Generates an array of what would be expected in cache for a given cache
+ * size and a number of entries filled in sequentially
+ */
+ function getExpectedCache( $cacheMaxEntries, $entryToFill ) {
+ $expected = array();
+
+ if ( $entryToFill === 0 ) {
+ # The cache is empty!
+ return array();
+ } elseif ( $entryToFill <= $cacheMaxEntries ) {
+ # Cache is not fully filled
+ $firstKey = 1;
+ } else {
+ # Cache overflowed
+ $firstKey = 1 + $entryToFill - $cacheMaxEntries;
+ }
+
+ $lastKey = $entryToFill;
+
+ for ( $i = $firstKey; $i <= $lastKey; $i++ ) {
+ $expected["cache-key-$i"] = array( "prop-$i" => "value-$i" );
+ }
+
+ return $expected;
+ }
+
+ /**
+ * Highlight diff between assertEquals and assertNotSame
+ */
+ public function testPhpUnitArrayEquality() {
+ $one = array( 'A' => 1, 'B' => 2 );
+ $two = array( 'B' => 2, 'A' => 1 );
+ $this->assertEquals( $one, $two ); // ==
+ $this->assertNotSame( $one, $two ); // ===
+ }
+
+ /**
+ * @dataProvider provideInvalidConstructorArg
+ * @expectedException UnexpectedValueException
+ */
+ public function testConstructorGivenInvalidValue( $maxSize ) {
+ new ProcessCacheLRUTestable( $maxSize );
+ }
+
+ /**
+ * Value which are forbidden by the constructor
+ */
+ public static function provideInvalidConstructorArg() {
+ return array(
+ array( null ),
+ array( array() ),
+ array( new stdClass() ),
+ array( 0 ),
+ array( '5' ),
+ array( -1 ),
+ );
+ }
+
+ public function testAddAndGetAKey() {
+ $oneCache = new ProcessCacheLRUTestable( 1 );
+ $this->assertCacheEmpty( $oneCache );
+
+ // First set just one value
+ $oneCache->set( 'cache-key', 'prop1', 'value1' );
+ $this->assertEquals( 1, $oneCache->getEntriesCount() );
+ $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) );
+ $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) );
+ }
+
+ public function testDeleteOldKey() {
+ $oneCache = new ProcessCacheLRUTestable( 1 );
+ $this->assertCacheEmpty( $oneCache );
+
+ $oneCache->set( 'cache-key', 'prop1', 'value1' );
+ $oneCache->set( 'cache-key', 'prop1', 'value2' );
+ $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) );
+ }
+
+ /**
+ * This test that we properly overflow when filling a cache with
+ * a sequence of always different cache-keys. Meant to verify we correclty
+ * delete the older key.
+ *
+ * @dataProvider provideCacheFilling
+ * @param int $cacheMaxEntries Maximum entry the created cache will hold
+ * @param int $entryToFill Number of entries to insert in the created cache.
+ */
+ public function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) {
+ $cache = new ProcessCacheLRUTestable( $cacheMaxEntries );
+ $this->fillCache( $cache, $entryToFill );
+
+ $this->assertSame(
+ $this->getExpectedCache( $cacheMaxEntries, $entryToFill ),
+ $cache->getCache(),
+ "Filling a $cacheMaxEntries entries cache with $entryToFill entries"
+ );
+ }
+
+ /**
+ * Provider for testFillingCache
+ */
+ public static function provideCacheFilling() {
+ // ($cacheMaxEntries, $entryToFill, $msg='')
+ return array(
+ array( 1, 0 ),
+ array( 1, 1 ),
+ array( 1, 2 ), # overflow
+ array( 5, 33 ), # overflow
+ );
+ }
+
+ /**
+ * Create a cache with only one remaining entry then update
+ * the first inserted entry. Should bump it to the top.
+ */
+ public function testReplaceExistingKeyShouldBumpEntryToTop() {
+ $maxEntries = 3;
+
+ $cache = new ProcessCacheLRUTestable( $maxEntries );
+ // Fill cache leaving just one remaining slot
+ $this->fillCache( $cache, $maxEntries - 1 );
+
+ // Set an existing cache key
+ $cache->set( "cache-key-1", "prop-1", "new-value-for-1" );
+
+ $this->assertSame(
+ array(
+ 'cache-key-2' => array( 'prop-2' => 'value-2' ),
+ 'cache-key-1' => array( 'prop-1' => 'new-value-for-1' ),
+ ),
+ $cache->getCache()
+ );
+ }
+
+ public function testRecentlyAccessedKeyStickIn() {
+ $cache = new ProcessCacheLRUTestable( 2 );
+ $cache->set( 'first', 'prop1', 'value1' );
+ $cache->set( 'second', 'prop2', 'value2' );
+
+ // Get first
+ $cache->get( 'first', 'prop1' );
+ // Cache a third value, should invalidate the least used one
+ $cache->set( 'third', 'prop3', 'value3' );
+
+ $this->assertFalse( $cache->has( 'second', 'prop2' ) );
+ }
+
+ /**
+ * This first create a full cache then update the value for the 2nd
+ * filled entry.
+ * Given a cache having 1,2,3 as key, updating 2 should bump 2 to
+ * the top of the queue with the new value: 1,3,2* (* = updated).
+ */
+ public function testReplaceExistingKeyInAFullCacheShouldBumpToTop() {
+ $maxEntries = 3;
+
+ $cache = new ProcessCacheLRUTestable( $maxEntries );
+ $this->fillCache( $cache, $maxEntries );
+
+ // Set an existing cache key
+ $cache->set( "cache-key-2", "prop-2", "new-value-for-2" );
+ $this->assertSame(
+ array(
+ 'cache-key-1' => array( 'prop-1' => 'value-1' ),
+ 'cache-key-3' => array( 'prop-3' => 'value-3' ),
+ 'cache-key-2' => array( 'prop-2' => 'new-value-for-2' ),
+ ),
+ $cache->getCache()
+ );
+ $this->assertEquals( 'new-value-for-2',
+ $cache->get( 'cache-key-2', 'prop-2' )
+ );
+ }
+
+ public function testBumpExistingKeyToTop() {
+ $cache = new ProcessCacheLRUTestable( 3 );
+ $this->fillCache( $cache, 3 );
+
+ // Set the very first cache key to a new value
+ $cache->set( "cache-key-1", "prop-1", "new value for 1" );
+ $this->assertEquals(
+ array(
+ 'cache-key-2' => array( 'prop-2' => 'value-2' ),
+ 'cache-key-3' => array( 'prop-3' => 'value-3' ),
+ 'cache-key-1' => array( 'prop-1' => 'new value for 1' ),
+ ),
+ $cache->getCache()
+ );
+ }
+}
+
+/**
+ * Overrides some ProcessCacheLRU methods and properties accessibility.
+ */
+class ProcessCacheLRUTestable extends ProcessCacheLRU {
+ public $cache = array();
+
+ public function getCache() {
+ return $this->cache;
+ }
+
+ public function getEntriesCount() {
+ return count( $this->cache );
+ }
+}
diff --git a/tests/phpunit/includes/libs/RunningStatTest.php b/tests/phpunit/includes/libs/RunningStatTest.php
new file mode 100644
index 00000000..dc5db82c
--- /dev/null
+++ b/tests/phpunit/includes/libs/RunningStatTest.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * PHP Unit tests for RunningStat class.
+ * @covers RunningStat
+ */
+class RunningStatTest extends MediaWikiTestCase {
+
+ public $points = array(
+ 49.7168, 74.3804, 7.0115, 96.5769, 34.9458,
+ 36.9947, 33.8926, 89.0774, 23.7745, 73.5154,
+ 86.1322, 53.2124, 16.2046, 73.5130, 10.4209,
+ 42.7299, 49.3330, 47.0215, 34.9950, 18.2914,
+ );
+
+ /**
+ * Verify that the statistical moments and extrema computed by RunningStat
+ * match expected values.
+ * @covers RunningStat::push
+ * @covers RunningStat::count
+ * @covers RunningStat::getMean
+ * @covers RunningStat::getVariance
+ * @covers RunningStat::getStdDev
+ */
+ public function testRunningStatAccuracy() {
+ $rstat = new RunningStat();
+ foreach( $this->points as $point ) {
+ $rstat->push( $point );
+ }
+
+ $mean = array_sum( $this->points ) / count( $this->points );
+ $variance = array_sum( array_map( function ( $x ) use ( $mean ) {
+ return pow( $mean - $x, 2 );
+ }, $this->points ) ) / ( count( $rstat ) - 1 );
+ $stddev = sqrt( $variance );
+
+ $this->assertEquals( count( $rstat ), count( $this->points ) );
+ $this->assertEquals( $rstat->min, min( $this->points ) );
+ $this->assertEquals( $rstat->max, max( $this->points ) );
+ $this->assertEquals( $rstat->getMean(), $mean );
+ $this->assertEquals( $rstat->getVariance(), $variance );
+ $this->assertEquals( $rstat->getStdDev(), $stddev );
+ }
+
+ /**
+ * When one RunningStat instance is merged into another, the state of the
+ * target RunningInstance should have the state that it would have had if
+ * all the data had been accumulated by it alone.
+ * @covers RunningStat::merge
+ * @covers RunningStat::count
+ */
+ public function testRunningStatMerge() {
+ $expected = new RunningStat();
+
+ foreach( $this->points as $point ) {
+ $expected->push( $point );
+ }
+
+ // Split the data into two sets
+ $sets = array_chunk( $this->points, floor( count( $this->points ) / 2 ) );
+
+ // Accumulate the first half into one RunningStat object
+ $first = new RunningStat();
+ foreach( $sets[0] as $point ) {
+ $first->push( $point );
+ }
+
+ // Accumulate the second half into another RunningStat object
+ $second = new RunningStat();
+ foreach( $sets[1] as $point ) {
+ $second->push( $point );
+ }
+
+ // Merge the second RunningStat object into the first
+ $first->merge( $second );
+
+ $this->assertEquals( count( $first ), count( $this->points ) );
+ $this->assertEquals( $first, $expected );
+ }
+}
diff --git a/tests/phpunit/includes/logging/LogFormatterTest.php b/tests/phpunit/includes/logging/LogFormatterTest.php
new file mode 100644
index 00000000..6210d098
--- /dev/null
+++ b/tests/phpunit/includes/logging/LogFormatterTest.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * @group Database
+ */
+class LogFormatterTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * @var Title
+ */
+ protected $title;
+
+ /**
+ * @var RequestContext
+ */
+ protected $context;
+
+ protected function setUp() {
+ parent::setUp();
+
+ global $wgLang;
+
+ $this->setMwGlobals( array(
+ 'wgLogTypes' => array( 'phpunit' ),
+ 'wgLogActionsHandlers' => array( 'phpunit/test' => 'LogFormatter',
+ 'phpunit/param' => 'LogFormatter' ),
+ 'wgUser' => User::newFromName( 'Testuser' ),
+ 'wgExtensionMessagesFiles' => array( 'LogTests' => __DIR__ . '/LogTests.i18n.php' ),
+ ) );
+
+ Language::getLocalisationCache()->recache( $wgLang->getCode() );
+
+ $this->user = User::newFromName( 'Testuser' );
+ $this->title = Title::newMainPage();
+
+ $this->context = new RequestContext();
+ $this->context->setUser( $this->user );
+ $this->context->setTitle( $this->title );
+ $this->context->setLanguage( $wgLang );
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+
+ global $wgLang;
+ Language::getLocalisationCache()->recache( $wgLang->getCode() );
+ }
+
+ public function newLogEntry( $action, $params ) {
+ $logEntry = new ManualLogEntry( 'phpunit', $action );
+ $logEntry->setPerformer( $this->user );
+ $logEntry->setTarget( $this->title );
+ $logEntry->setComment( 'A very good reason' );
+
+ $logEntry->setParameters( $params );
+
+ return $logEntry;
+ }
+
+ /**
+ * @covers LogFormatter::newFromEntry
+ */
+ public function testNormalLogParams() {
+ $entry = $this->newLogEntry( 'test', array() );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $formatter->setShowUserToolLinks( false );
+ $paramsWithoutTools = $formatter->getMessageParametersForTesting();
+ unset( $formatter->parsedParameters );
+
+ $formatter->setShowUserToolLinks( true );
+ $paramsWithTools = $formatter->getMessageParametersForTesting();
+
+ $userLink = Linker::userLink(
+ $this->user->getId(),
+ $this->user->getName()
+ );
+
+ $userTools = Linker::userToolLinksRedContribs(
+ $this->user->getId(),
+ $this->user->getName(),
+ $this->user->getEditCount()
+ );
+
+ $titleLink = Linker::link( $this->title, null, array(), array() );
+
+ // $paramsWithoutTools and $paramsWithTools should be only different
+ // in index 0
+ $this->assertEquals( $paramsWithoutTools[1], $paramsWithTools[1] );
+ $this->assertEquals( $paramsWithoutTools[2], $paramsWithTools[2] );
+
+ $this->assertEquals( $userLink, $paramsWithoutTools[0]['raw'] );
+ $this->assertEquals( $userLink . $userTools, $paramsWithTools[0]['raw'] );
+
+ $this->assertEquals( $this->user->getName(), $paramsWithoutTools[1] );
+
+ $this->assertEquals( $titleLink, $paramsWithoutTools[2]['raw'] );
+ }
+
+ /**
+ * @covers LogFormatter::newFromEntry
+ * @covers LogFormatter::getActionText
+ */
+ public function testLogParamsTypeRaw() {
+ $params = array( '4:raw:raw' => Linker::link( $this->title, null, array(), array() ) );
+ $expected = Linker::link( $this->title, null, array(), array() );
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ /**
+ * @covers LogFormatter::newFromEntry
+ * @covers LogFormatter::getActionText
+ */
+ public function testLogParamsTypeMsg() {
+ $params = array( '4:msg:msg' => 'log-description-phpunit' );
+ $expected = wfMessage( 'log-description-phpunit' )->text();
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ /**
+ * @covers LogFormatter::newFromEntry
+ * @covers LogFormatter::getActionText
+ */
+ public function testLogParamsTypeMsgContent() {
+ $params = array( '4:msg-content:msgContent' => 'log-description-phpunit' );
+ $expected = wfMessage( 'log-description-phpunit' )->inContentLanguage()->text();
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ /**
+ * @covers LogFormatter::newFromEntry
+ * @covers LogFormatter::getActionText
+ */
+ public function testLogParamsTypeNumber() {
+ global $wgLang;
+
+ $params = array( '4:number:number' => 123456789 );
+ $expected = $wgLang->formatNum( 123456789 );
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ /**
+ * @covers LogFormatter::newFromEntry
+ * @covers LogFormatter::getActionText
+ */
+ public function testLogParamsTypeUserLink() {
+ $params = array( '4:user-link:userLink' => $this->user->getName() );
+ $expected = Linker::userLink(
+ $this->user->getId(),
+ $this->user->getName()
+ );
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ /**
+ * @covers LogFormatter::newFromEntry
+ * @covers LogFormatter::getActionText
+ */
+ public function testLogParamsTypeTitleLink() {
+ $params = array( '4:title-link:titleLink' => $this->title->getText() );
+ $expected = Linker::link( $this->title, null, array(), array() );
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ /**
+ * @covers LogFormatter::newFromEntry
+ * @covers LogFormatter::getActionText
+ */
+ public function testLogParamsTypePlain() {
+ $params = array( '4:plain:plain' => 'Some plain text' );
+ $expected = 'Some plain text';
+
+ $entry = $this->newLogEntry( 'param', $params );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $logParam = $formatter->getActionText();
+
+ $this->assertEquals( $expected, $logParam );
+ }
+
+ /**
+ * @covers LogFormatter::newFromEntry
+ * @covers LogFormatter::getComment
+ */
+ public function testLogComment() {
+ $entry = $this->newLogEntry( 'test', array() );
+ $formatter = LogFormatter::newFromEntry( $entry );
+ $formatter->setContext( $this->context );
+
+ $comment = ltrim( Linker::commentBlock( $entry->getComment() ) );
+
+ $this->assertEquals( $comment, $formatter->getComment() );
+ }
+}
diff --git a/tests/phpunit/includes/logging/LogTests.i18n.php b/tests/phpunit/includes/logging/LogTests.i18n.php
new file mode 100644
index 00000000..78787ba1
--- /dev/null
+++ b/tests/phpunit/includes/logging/LogTests.i18n.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Internationalisation file for log tests.
+ *
+ * @file
+ */
+
+$messages = array();
+
+$messages['en'] = array(
+ 'log-name-phpunit' => 'PHPUnit-log',
+ 'log-description-phpunit' => 'Log for PHPUnit-tests',
+ 'logentry-phpunit-test' => '$1 {{GENDER:$2|tests}} with page $3',
+ 'logentry-phpunit-param' => '$4',
+);
diff --git a/tests/phpunit/includes/mail/MailAddressTest.php b/tests/phpunit/includes/mail/MailAddressTest.php
new file mode 100644
index 00000000..2d078120
--- /dev/null
+++ b/tests/phpunit/includes/mail/MailAddressTest.php
@@ -0,0 +1,63 @@
+<?php
+
+class MailAddressTest extends MediaWikiTestCase {
+
+ /**
+ * @covers MailAddress::__construct
+ */
+ public function testConstructor() {
+ $ma = new MailAddress( 'foo@bar.baz', 'UserName', 'Real name' );
+ $this->assertInstanceOf( 'MailAddress', $ma );
+ }
+
+ /**
+ * @covers MailAddress::newFromUser
+ */
+ public function testNewFromUser() {
+ $user = $this->getMock( 'User' );
+ $user->expects( $this->any() )->method( 'getName' )->will( $this->returnValue( 'UserName' ) );
+ $user->expects( $this->any() )->method( 'getEmail' )->will( $this->returnValue( 'foo@bar.baz' ) );
+ $user->expects( $this->any() )->method( 'getRealName' )->will( $this->returnValue( 'Real name' ) );
+
+ $ma = MailAddress::newFromUser( $user );
+ $this->assertInstanceOf( 'MailAddress', $ma );
+ $this->setMwGlobals( 'wgEnotifUseRealName', true );
+ $this->assertEquals( 'Real name <foo@bar.baz>', $ma->toString() );
+ $this->setMwGlobals( 'wgEnotifUseRealName', false );
+ $this->assertEquals( 'UserName <foo@bar.baz>', $ma->toString() );
+ }
+
+ /**
+ * @covers MailAddress::toString
+ * @dataProvider provideToString
+ */
+ public function testToString( $useRealName, $address, $name, $realName, $expected ) {
+ if ( wfIsWindows() ) {
+ $this->markTestSkipped( 'This test only works on non-Windows platforms' );
+ }
+ $this->setMwGlobals( 'wgEnotifUseRealName', $useRealName );
+ $ma = new MailAddress( $address, $name, $realName );
+ $this->assertEquals( $expected, $ma->toString() );
+ }
+
+ public static function provideToString() {
+ return array(
+ array( true, 'foo@bar.baz', 'FooBar', 'Foo Bar', 'Foo Bar <foo@bar.baz>' ),
+ array( true, 'foo@bar.baz', 'UserName', null, 'UserName <foo@bar.baz>' ),
+ array( true, 'foo@bar.baz', 'AUser', 'My real name', 'My real name <foo@bar.baz>' ),
+ array( true, 'foo@bar.baz', 'A.user.name', 'my@real.name', '"my@real.name" <foo@bar.baz>' ),
+ array( false, 'foo@bar.baz', 'AUserName', 'Some real name', 'AUserName <foo@bar.baz>' ),
+ array( false, 'foo@bar.baz', '', '', 'foo@bar.baz' ),
+ array( true, 'foo@bar.baz', '', '', 'foo@bar.baz' ),
+ );
+ }
+
+ /**
+ * @covers MailAddress::__toString
+ */
+ public function test__ToString() {
+ $ma = new MailAddress( 'some@email.com', 'UserName', 'A real name' );
+ $this->assertEquals( $ma->toString(), (string)$ma );
+ }
+
+} \ No newline at end of file
diff --git a/tests/phpunit/includes/mail/UserMailerTest.php b/tests/phpunit/includes/mail/UserMailerTest.php
new file mode 100644
index 00000000..dca8aeb9
--- /dev/null
+++ b/tests/phpunit/includes/mail/UserMailerTest.php
@@ -0,0 +1,14 @@
+<?php
+
+class UserMailerTest extends MediaWikiLangTestCase {
+
+ /**
+ * @covers UserMailer::quotedPrintable
+ */
+ public function testQuotedPrintable() {
+ $this->assertEquals(
+ "=?UTF-8?Q?=C4=88u=20legebla=3F?=",
+ UserMailer::quotedPrintable( "\xc4\x88u legebla?", "UTF-8" ) );
+ }
+
+}
diff --git a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php
new file mode 100644
index 00000000..c720d7b7
--- /dev/null
+++ b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * @group Media
+ */
+class BitmapMetadataHandlerTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgShowEXIF', false );
+
+ $this->filePath = __DIR__ . '/../../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.
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ public function testMultilingualCascade() {
+ $this->checkPHPExtension( 'exif' );
+ $this->checkPHPExtension( 'xml' );
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+
+ $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
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ public function testJpegComment() {
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ 'jpeg-comment-utf.jpg' );
+
+ $this->assertEquals( 'UTF-8 JPEG Comment — ¼',
+ $meta['JPEGFileComment'][0] );
+ }
+
+ /**
+ * Make sure a bad iptc block doesn't stop the other metadata
+ * from being extracted.
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ public function testBadIPTC() {
+ $meta = BitmapMetadataHandler::Jpeg( $this->filePath .
+ 'iptc-invalid-psir.jpg' );
+ $this->assertEquals( 'Created with GIMP', $meta['JPEGFileComment'][0] );
+ }
+
+ /**
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ 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
+ * @covers BitmapMetadataHandler::Jpeg
+ */
+ 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.
+ * @covers BitmapMetadataHandler::addMetadata
+ * @covers BitmapMetadataHandler::getMetadataArray
+ */
+ 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 );
+ }
+
+ /**
+ * @covers BitmapMetadataHandler::png
+ */
+ public function testPNGXMP() {
+ if ( !extension_loaded( 'xml' ) ) {
+ $this->markTestSkipped( "This test needs the xml extension." );
+ }
+ $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 );
+ }
+
+ /**
+ * @covers BitmapMetadataHandler::png
+ */
+ 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'] );
+ }
+
+ /**
+ * @covers BitmapMetadataHandler::getTiffByteOrder
+ */
+ 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..1972c969
--- /dev/null
+++ b/tests/phpunit/includes/media/BitmapScalingTest.php
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * @group Media
+ */
+class BitmapScalingTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgMaxImageArea' => 1.25e7, // 3500x3500
+ 'wgCustomConvertCommand' => 'dummy', // Set so that we don't get client side rendering
+ ) );
+ }
+
+ /**
+ * @dataProvider provideNormaliseParams
+ * @covers BitmapHandler::normaliseParams
+ */
+ public 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 );
+ }
+
+ public static 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',
+ ),
+ );
+ }
+
+ /**
+ * @covers BitmapHandler::doTransform
+ */
+ public function testTooBigImage() {
+ $file = new FakeDimensionFile( array( 4000, 4000 ) );
+ $handler = new BitmapHandler;
+ $params = array( 'width' => '3700' ); // Still bigger than max size.
+ $this->assertEquals( 'TransformParameterError',
+ get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) );
+ }
+
+ /**
+ * @covers BitmapHandler::doTransform
+ */
+ public function testTooBigMustRenderImage() {
+ $file = new FakeDimensionFile( array( 4000, 4000 ) );
+ $file->mustRender = true;
+ $handler = new BitmapHandler;
+ $params = array( 'width' => '5000' ); // Still bigger than max size.
+ $this->assertEquals( 'TransformParameterError',
+ get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) );
+ }
+
+ /**
+ * @covers BitmapHandler::getImageArea
+ */
+ public function testImageArea() {
+ $file = new FakeDimensionFile( array( 7, 9 ) );
+ $handler = new BitmapHandler;
+ $this->assertEquals( 63, $handler->getImageArea( $file ) );
+ }
+}
diff --git a/tests/phpunit/includes/media/DjVuTest.php b/tests/phpunit/includes/media/DjVuTest.php
new file mode 100644
index 00000000..c0871f19
--- /dev/null
+++ b/tests/phpunit/includes/media/DjVuTest.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * @group Media
+ * @covers DjVuHandler
+ */
+class DjVuTest extends MediaWikiMediaTestCase {
+
+ /**
+ * @var DjVuHandler
+ */
+ protected $handler;
+
+ protected function setUp() {
+ parent::setUp();
+
+ //cli tool setup
+ $djvuSupport = new DjVuSupport();
+
+ if ( !$djvuSupport->isEnabled() ) {
+ $this->markTestSkipped(
+ 'This test needs the installation of the ddjvu, djvutoxml and djvudump tools' );
+ }
+
+ $this->handler = new DjVuHandler();
+ }
+
+ public function testGetImageSize() {
+ $this->assertArrayEquals(
+ array( 2480, 3508, 'DjVu', 'width="2480" height="3508"' ),
+ $this->handler->getImageSize( null, $this->filePath . '/LoremIpsum.djvu' ),
+ 'Test file LoremIpsum.djvu should have a size of 2480 * 3508'
+ );
+ }
+
+ public function testInvalidFile() {
+ $this->assertEquals(
+ 'a:1:{s:5:"error";s:25:"Error extracting metadata";}',
+ $this->handler->getMetadata( null, $this->filePath . '/some-nonexistent-file' ),
+ 'Getting metadata for an inexistent file should return false'
+ );
+ }
+
+ public function testPageCount() {
+ $file = $this->dataFile( 'LoremIpsum.djvu', 'image/x.djvu' );
+ $this->assertEquals(
+ 5,
+ $this->handler->pageCount( $file ),
+ 'Test file LoremIpsum.djvu should be detected as containing 5 pages'
+ );
+ }
+
+ public function testGetPageDimensions() {
+ $file = $this->dataFile( 'LoremIpsum.djvu', 'image/x.djvu' );
+ $this->assertArrayEquals(
+ array( 2480, 3508 ),
+ $this->handler->getPageDimensions( $file, 1 ),
+ 'Page 1 of test file LoremIpsum.djvu should have a size of 2480 * 3508'
+ );
+ }
+
+ public function testGetPageText() {
+ $file = $this->dataFile( 'LoremIpsum.djvu', 'image/x.djvu' );
+ $this->assertEquals(
+ "Lorem ipsum \n1 \n",
+ (string)$this->handler->getPageText( $file, 1 ),
+ "Text layer of page 1 of file LoremIpsum.djvu should be 'Lorem ipsum \n1 \n'"
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/ExifBitmapTest.php b/tests/phpunit/includes/media/ExifBitmapTest.php
new file mode 100644
index 00000000..41330f41
--- /dev/null
+++ b/tests/phpunit/includes/media/ExifBitmapTest.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * @group Media
+ */
+class ExifBitmapTest extends MediaWikiTestCase {
+
+ /**
+ * @var ExifBitmapHandler
+ */
+ protected $handler;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->checkPHPExtension( 'exif' );
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+
+ $this->handler = new ExifBitmapHandler;
+
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testIsOldBroken() {
+ $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::OLD_BROKEN_FILE );
+ $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testIsBrokenFile() {
+ $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::BROKEN_FILE );
+ $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testIsInvalid() {
+ $res = $this->handler->isMetadataValid( null, 'Something Invalid Here.' );
+ $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testGoodMetadata() {
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ $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;}';
+ // @codingStandardsIgnoreEnd
+ $res = $this->handler->isMetadataValid( null, $meta );
+ $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testIsOldGood() {
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ $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;}';
+ // @codingStandardsIgnoreEnd
+ $res = $this->handler->isMetadataValid( null, $meta );
+ $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res );
+ }
+
+ /**
+ * Handle metadata from paged tiff handler (gotten via instant commons) gracefully.
+ * @covers ExifBitmapHandler::isMetadataValid
+ */
+ public function testPagedTiffHandledGracefully() {
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ $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";}';
+ // @codingStandardsIgnoreEnd
+ $res = $this->handler->isMetadataValid( null, $meta );
+ $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::convertMetadataVersion
+ */
+ public function testConvertMetadataLatest() {
+ $metadata = array(
+ 'foo' => array( 'First', 'Second', '_type' => 'ol' ),
+ 'MEDIAWIKI_EXIF_VERSION' => 2
+ );
+ $res = $this->handler->convertMetadataVersion( $metadata, 2 );
+ $this->assertEquals( $metadata, $res );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::convertMetadataVersion
+ */
+ public 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 );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::convertMetadataVersion
+ */
+ public 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 );
+ }
+
+ /**
+ * @covers ExifBitmapHandler::convertMetadataVersion
+ */
+ public 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..f0bd42a0
--- /dev/null
+++ b/tests/phpunit/includes/media/ExifRotationTest.php
@@ -0,0 +1,280 @@
+<?php
+/**
+ * Tests related to auto rotation.
+ *
+ * @group Media
+ * @group medium
+ *
+ * @todo covers tags
+ */
+class ExifRotationTest extends MediaWikiMediaTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->checkPHPExtension( 'exif' );
+
+ $this->handler = new BitmapHandler();
+
+ $this->setMwGlobals( array(
+ 'wgShowEXIF' => true,
+ 'wgEnableAutoRotation' => true,
+ ) );
+ }
+
+ /**
+ * Mark this test as creating thumbnail files.
+ */
+ protected function createsThumbnails() {
+ return true;
+ }
+
+ /**
+ * @dataProvider provideFiles
+ */
+ public function testMetadata( $name, $type, $info ) {
+ if ( !$this->handler->canRotate() ) {
+ $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." );
+ }
+ $file = $this->dataFile( $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+ }
+
+ /**
+ * Same as before, but with auto-rotation set to auto.
+ *
+ * This sets scaler to image magick, which we should detect as
+ * supporting rotation.
+ * @dataProvider provideFiles
+ */
+ public function testMetadataAutoRotate( $name, $type, $info ) {
+ $this->setMwGlobals( 'wgEnableAutoRotation', null );
+ $this->setMwGlobals( 'wgUseImageMagick', true );
+ $this->setMwGlobals( 'wgUseImageResize', true );
+
+ $file = $this->dataFile( $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+ }
+
+ /**
+ *
+ * @dataProvider provideFiles
+ */
+ public function testRotationRendering( $name, $type, $info, $thumbs ) {
+ if ( !$this->handler->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->dataFile( $name, $type );
+ $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE );
+
+ $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->getLocalCopyPath() );
+ 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" );
+ }
+ }
+ }
+
+ public static function provideFiles() {
+ 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 provideFilesNoAutoRotate
+ */
+ public function testMetadataNoAutoRotate( $name, $type, $info ) {
+ $this->setMwGlobals( 'wgEnableAutoRotation', false );
+
+ $file = $this->dataFile( $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+ }
+
+ /**
+ * Same as before, but with auto-rotation set to auto and an image scaler that doesn't support it.
+ * @dataProvider provideFilesNoAutoRotate
+ */
+ public function testMetadataAutoRotateUnsupported( $name, $type, $info ) {
+ $this->setMwGlobals( 'wgEnableAutoRotation', null );
+ $this->setMwGlobals( 'wgUseImageResize', false );
+
+ $file = $this->dataFile( $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+ }
+
+ /**
+ *
+ * @dataProvider provideFilesNoAutoRotate
+ */
+ public function testRotationRenderingNoAutoRotate( $name, $type, $info, $thumbs ) {
+ $this->setMwGlobals( '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->dataFile( $name, $type );
+ $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE );
+
+ $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->getLocalCopyPath() );
+ 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" );
+ }
+ }
+ }
+
+ public static function provideFilesNoAutoRotate() {
+ 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
+ */
+ public function testBitmapExtractPreRotationDimensions( $rotation, $expected ) {
+ $result = $this->handler->extractPreRotationDimensions( array(
+ 'physicalWidth' => self::TEST_WIDTH,
+ 'physicalHeight' => self::TEST_HEIGHT,
+ ), $rotation );
+ $this->assertEquals( $expected, $result );
+ }
+
+ public static 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..f3c05fb1
--- /dev/null
+++ b/tests/phpunit/includes/media/ExifTest.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @group Media
+ * @covers Exif
+ */
+class ExifTest extends MediaWikiTestCase {
+
+ /** @var string */
+ protected $mediaPath;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->checkPHPExtension( 'exif' );
+
+ $this->mediaPath = __DIR__ . '/../../data/media/';
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+ }
+
+ public function testGPSExtraction() {
+ $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' => -3.141592653,
+ 'GPSDOP' => '5/1',
+ 'GPSVersionID' => '2.2.0.0',
+ );
+ $this->assertEquals( $expected, $data, '', 0.0000000001 );
+ }
+
+ public function testUnicodeUserComment() {
+ $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/FakeDimensionFile.php b/tests/phpunit/includes/media/FakeDimensionFile.php
new file mode 100644
index 00000000..4b8f213e
--- /dev/null
+++ b/tests/phpunit/includes/media/FakeDimensionFile.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @group Media
+ */
+class FakeDimensionFile extends File {
+ public $mustRender = false;
+
+ public function __construct( $dimensions ) {
+ parent::__construct( Title::makeTitle( NS_FILE, 'Test' ),
+ new NullRepo( 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;
+ }
+
+ public function getPath() {
+ return '';
+ }
+}
diff --git a/tests/phpunit/includes/media/FormatMetadataTest.php b/tests/phpunit/includes/media/FormatMetadataTest.php
new file mode 100644
index 00000000..002e2cb9
--- /dev/null
+++ b/tests/phpunit/includes/media/FormatMetadataTest.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @group Media
+ */
+class FormatMetadataTest extends MediaWikiMediaTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->checkPHPExtension( 'exif' );
+ $this->setMwGlobals( 'wgShowEXIF', true );
+ }
+
+ /**
+ * @covers File::formatMetadata
+ */
+ public function testInvalidDate() {
+ $file = $this->dataFile( '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)' );
+ }
+
+ /**
+ * @param string $filename
+ * @param int $expected Total image area
+ * @dataProvider provideFlattenArray
+ * @covers FormatMetadata::flattenArray
+ */
+ public function testFlattenArray( $vals, $type, $noHtml, $ctx, $expected ) {
+ $actual = FormatMetadata::flattenArray( $vals, $type, $noHtml, $ctx );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideFlattenArray() {
+ return array(
+ array(
+ array( 1, 2, 3 ), 'ul', false, false,
+ "<ul><li>1</li>\n<li>2</li>\n<li>3</li></ul>",
+ ),
+ array(
+ array( 1, 2, 3 ), 'ol', false, false,
+ "<ol><li>1</li>\n<li>2</li>\n<li>3</li></ol>",
+ ),
+ array(
+ array( 1, 2, 3 ), 'ul', true, false,
+ "\n*1\n*2\n*3",
+ ),
+ array(
+ array( 1, 2, 3 ), 'ol', true, false,
+ "\n#1\n#2\n#3",
+ ),
+ // TODO: more test cases
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php
new file mode 100644
index 00000000..6aecd8b1
--- /dev/null
+++ b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * @group Media
+ */
+class GIFMetadataExtractorTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->mediaPath = __DIR__ . '/../../data/media/';
+ }
+
+ /**
+ * Put in a file, and see if the metadata coming out is as expected.
+ * @param string $filename
+ * @param array $expected The extracted metadata.
+ * @dataProvider provideGetMetadata
+ * @covers GIFMetadataExtractor::getMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $actual = GIFMetadataExtractor::getMetadata( $this->mediaPath . $filename );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetMetadata() {
+
+ $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;
+ $xmpNugget = str_replace( "\r", '', $xmpNugget ); // Windows compat
+
+ 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..87ffd995
--- /dev/null
+++ b/tests/phpunit/includes/media/GIFTest.php
@@ -0,0 +1,142 @@
+<?php
+
+/**
+ * @group Media
+ */
+class GIFHandlerTest extends MediaWikiMediaTestCase {
+
+ /** @var GIFHandler */
+ protected $handler;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->handler = new GIFHandler();
+ }
+
+ /**
+ * @covers GIFHandler::getMetadata
+ */
+ public function testInvalidFile() {
+ $res = $this->handler->getMetadata( null, $this->filePath . '/README' );
+ $this->assertEquals( GIFHandler::BROKEN_FILE, $res );
+ }
+
+ /**
+ * @param string $filename Basename of the file to check
+ * @param bool $expected Expected result.
+ * @dataProvider provideIsAnimated
+ * @covers GIFHandler::isAnimatedImage
+ */
+ public function testIsAnimanted( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/gif' );
+ $actual = $this->handler->isAnimatedImage( $file );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideIsAnimated() {
+ return array(
+ array( 'animated.gif', true ),
+ array( 'nonanimated.gif', false ),
+ );
+ }
+
+ /**
+ * @param string $filename
+ * @param int $expected Total image area
+ * @dataProvider provideGetImageArea
+ * @covers GIFHandler::getImageArea
+ */
+ public function testGetImageArea( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/gif' );
+ $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetImageArea() {
+ return array(
+ array( 'animated.gif', 5400 ),
+ array( 'nonanimated.gif', 1350 ),
+ );
+ }
+
+ /**
+ * @param string $metadata Serialized metadata
+ * @param int $expected One of the class constants of GIFHandler
+ * @dataProvider provideIsMetadataValid
+ * @covers GIFHandler::isMetadataValid
+ */
+ public function testIsMetadataValid( $metadata, $expected ) {
+ $actual = $this->handler->isMetadataValid( null, $metadata );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideIsMetadataValid() {
+ return array(
+ array( GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ),
+ array( '', GIFHandler::METADATA_BAD ),
+ array( null, GIFHandler::METADATA_BAD ),
+ array( 'Something invalid!', GIFHandler::METADATA_BAD ),
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ 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 ),
+ // @codingStandardsIgnoreEnd
+ );
+ }
+
+ /**
+ * @param string $filename
+ * @param string $expected Serialized array
+ * @dataProvider provideGetMetadata
+ * @covers GIFHandler::getMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/gif' );
+ $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" );
+ $this->assertEquals( unserialize( $expected ), unserialize( $actual ) );
+ }
+
+ public static function provideGetMetadata() {
+ return array(
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ 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;}}' ),
+ // @codingStandardsIgnoreEnd
+ );
+ }
+
+ /**
+ * @param string $filename
+ * @param string $expected Serialized array
+ * @dataProvider provideGetIndependentMetaArray
+ * @covers GIFHandler::getCommonMetaArray
+ */
+ public function testGetIndependentMetaArray( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/gif' );
+ $actual = $this->handler->getCommonMetaArray( $file );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetIndependentMetaArray() {
+ return array(
+ array( 'nonanimated.gif', array(
+ 'GIFFileComment' => array(
+ 'GIF test file ⁕ Created with GIMP',
+ ),
+ ) ),
+ array( 'animated-xmp.gif',
+ array(
+ 'Artist' => 'Bawolff',
+ 'ImageDescription' => array(
+ 'x-default' => 'A file to test GIF',
+ '_type' => 'lang',
+ ),
+ 'SublocationDest' => 'The interwebs',
+ 'GIFFileComment' =>
+ array(
+ 'GIƒ·test·file',
+ ),
+ )
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/IPTCTest.php b/tests/phpunit/includes/media/IPTCTest.php
new file mode 100644
index 00000000..06542cfe
--- /dev/null
+++ b/tests/phpunit/includes/media/IPTCTest.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @group Media
+ */
+class IPTCTest extends MediaWikiTestCase {
+
+ /**
+ * @covers IPTC::getCharset
+ */
+ public function testRecognizeUtf8() {
+ // utf-8 is the only one used in practise.
+ $res = IPTC::getCharset( "\x1b%G" );
+ $this->assertEquals( 'UTF-8', $res );
+ }
+
+ /**
+ * @covers IPTC::Parse
+ */
+ 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'] );
+ }
+
+ /**
+ * @covers IPTC::Parse
+ */
+ public function testIPTCParseNoCharset88591b() {
+ /* This one contains a sequence that's valid iso 8859-1 but not valid utf8 */
+ /* \xC3 = Ã, \xB8 = ¸ */
+ $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 ø
+ * @covers IPTC::Parse
+ */
+ 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'] );
+ }
+
+ /**
+ * @covers IPTC::Parse
+ */
+ 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
+ * @covers IPTC::Parse
+ */
+ 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'] );
+ }
+
+ /**
+ * @covers IPTC::Parse
+ */
+ 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..7c977d5a
--- /dev/null
+++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php
@@ -0,0 +1,111 @@
+<?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.
+ *
+ * @group Media
+ * @covers JpegMetadataExtractor
+ */
+class JpegMetadataExtractorTest extends MediaWikiTestCase {
+
+ protected $filePath;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->filePath = __DIR__ . '/../../data/media/';
+ }
+
+ /**
+ * We also use this test to test padding bytes don't
+ * screw stuff up
+ *
+ * @param string $file Filename
+ *
+ * @dataProvider provideUtf8Comment
+ */
+ public function testUtf8Comment( $file ) {
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
+ $this->assertEquals( array( 'UTF-8 JPEG Comment — ¼' ), $res['COM'] );
+ }
+
+ public static function provideUtf8Comment() {
+ 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 = '50686f746f73686f7020332e30003842494d04040000000'
+ . '000181c02190004746573741c02190003666f6f1c020000020004';
+ $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
+ }
+
+ 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'][0] );
+
+ $this->assertEquals( 'iptc-no-hash', $res );
+ }
+
+ public function testIPTCHashComparisionBadHash() {
+ $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
+ $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+ $this->assertEquals( 'iptc-bad-hash', $res );
+ }
+
+ public function testIPTCHashComparisionGoodHash() {
+ $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
+ $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+ $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..2436e7d9
--- /dev/null
+++ b/tests/phpunit/includes/media/JpegTest.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @group Media
+ * @covers JpegHandler
+ */
+class JpegTest extends MediaWikiMediaTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->checkPHPExtension( 'exif' );
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+
+ $this->handler = new JpegHandler;
+ }
+
+ public function testInvalidFile() {
+ $file = $this->dataFile( 'README', 'image/jpeg' );
+ $res = $this->handler->getMetadata( $file, $this->filePath . 'README' );
+ $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res );
+ }
+
+ public function testJpegMetadataExtraction() {
+ $file = $this->dataFile( 'test.jpg', 'image/jpeg' );
+ $res = $this->handler->getMetadata( $file, $this->filePath . 'test.jpg' );
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ $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;}';
+ // @codingStandardsIgnoreEnd
+
+ // Unserialize in case serialization format ever changes.
+ $this->assertEquals( unserialize( $expected ), unserialize( $res ) );
+ }
+
+ /**
+ * @covers JpegHandler::getCommonMetaArray
+ */
+ public function testGetIndependentMetaArray() {
+ $file = $this->dataFile( 'test.jpg', 'image/jpeg' );
+ $res = $this->handler->getCommonMetaArray( $file );
+ $expected = array(
+ 'ImageDescription' => 'Test file',
+ 'XResolution' => '72/1',
+ 'YResolution' => '72/1',
+ 'ResolutionUnit' => 2,
+ 'YCbCrPositioning' => 1,
+ 'JPEGFileComment' => array(
+ 'Created with GIMP',
+ ),
+ );
+
+ $this->assertEquals( $res, $expected );
+ }
+}
diff --git a/tests/phpunit/includes/media/MediaHandlerTest.php b/tests/phpunit/includes/media/MediaHandlerTest.php
new file mode 100644
index 00000000..d8cfcc45
--- /dev/null
+++ b/tests/phpunit/includes/media/MediaHandlerTest.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @group Media
+ */
+class MediaHandlerTest extends MediaWikiTestCase {
+
+ /**
+ * @covers MediaHandler::fitBoxWidth
+ * @todo split into a dataprovider and test method
+ */
+ public 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 ) {
+ $tests = $row['tests'];
+ $height = $row['height'];
+ $width = $row['width'];
+ foreach ( $tests as $max => $expected ) {
+ $y = round( $expected * $height / $width );
+ $result = MediaHandler::fitBoxWidth( $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/media/MediaWikiMediaTestCase.php b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php
new file mode 100644
index 00000000..8f28158d
--- /dev/null
+++ b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Specificly for testing Media handlers. Sets up a FSFile backend
+ */
+abstract class MediaWikiMediaTestCase extends MediaWikiTestCase {
+
+ /** @var FSRepo */
+ protected $repo;
+ /** @var FSFileBackend */
+ protected $backend;
+ /** @var string */
+ protected $filePath;
+
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->filePath = $this->getFilePath();
+ $containers = array( 'data' => $this->filePath );
+ if ( $this->createsThumbnails() ) {
+ // We need a temp directory for the thumbnails
+ // the container is named 'temp-thumb' because it is the
+ // thumb directory for a FSRepo named "temp".
+ $containers['temp-thumb'] = $this->getNewTempDirectory();
+ }
+
+ $this->backend = new FSFileBackend( array(
+ 'name' => 'localtesting',
+ 'wikiId' => wfWikiId(),
+ 'containerPaths' => $containers
+ ) );
+ $this->repo = new FSRepo( $this->getRepoOptions() );
+ }
+
+ /**
+ * @return array Argument for FSRepo constructor
+ */
+ protected function getRepoOptions() {
+ return array(
+ 'name' => 'temp',
+ 'url' => 'http://localhost/thumbtest',
+ 'backend' => $this->backend
+ );
+ }
+
+ /**
+ * The result of this method will set the file path to use,
+ * as well as the protected member $filePath
+ *
+ * @return string Path where files are
+ */
+ protected function getFilePath() {
+ return __DIR__ . '/../../data/media/';
+ }
+
+ /**
+ * Will the test create thumbnails (and thus do we need to set aside
+ * a temporary directory for them?)
+ *
+ * Override this method if your test case creates thumbnails
+ *
+ * @return bool
+ */
+ protected function createsThumbnails() {
+ return false;
+ }
+
+ /**
+ * Utility function: Get a new file object for a file on disk but not actually in db.
+ *
+ * File must be in the path returned by getFilePath()
+ * @param string $name File name
+ * @param string $type MIME type [optional]
+ * @return UnregisteredLocalFile
+ */
+ protected function dataFile( $name, $type = null ) {
+ if ( !$type ) {
+ // Autodetect by file extension for the lazy.
+ $magic = MimeMagic::singleton();
+ $parts = explode( $name, '.' );
+ $type = $magic->guessTypesForExtension( $parts[count( $parts ) - 1] );
+ }
+ return new UnregisteredLocalFile( false, $this->repo,
+ "mwstore://localtesting/data/$name", $type );
+ }
+}
diff --git a/tests/phpunit/includes/media/PNGMetadataExtractorTest.php b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php
new file mode 100644
index 00000000..a9eaa9e7
--- /dev/null
+++ b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @group Media
+ * @covers PNGMetadataExtractor
+ */
+class PNGMetadataExtractorTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->filePath = __DIR__ . '/../../data/media/';
+ }
+
+ /**
+ * Tests zTXt tag (compressed textual metadata)
+ */
+ public function testPngNativetZtxt() {
+ $this->checkPHPExtension( 'zlib' );
+
+ $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)
+ */
+ public 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
+ */
+ public 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).
+ */
+ /*
+ public 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.
+ */
+ public 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.
+ */
+ public 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 );
+ }
+
+ public function testPngBitDepth8() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertEquals( 8, $meta['bitDepth'] );
+ }
+
+ public function testPngBitDepth1() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ '1bit-png.png' );
+ $this->assertEquals( 1, $meta['bitDepth'] );
+ }
+
+ public function testPngIndexColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'Png-native-test.png' );
+
+ $this->assertEquals( 'index-coloured', $meta['colorType'] );
+ }
+
+ public function testPngRgbColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'rgb-png.png' );
+ $this->assertEquals( 'truecolour-alpha', $meta['colorType'] );
+ }
+
+ public function testPngRgbNoAlphaColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'rgb-na-png.png' );
+ $this->assertEquals( 'truecolour', $meta['colorType'] );
+ }
+
+ public function testPngGreyscaleColour() {
+ $meta = PNGMetadataExtractor::getMetadata( $this->filePath .
+ 'greyscale-png.png' );
+ $this->assertEquals( 'greyscale-alpha', $meta['colorType'] );
+ }
+
+ public 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..36872a75
--- /dev/null
+++ b/tests/phpunit/includes/media/PNGTest.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * @group Media
+ */
+class PNGHandlerTest extends MediaWikiMediaTestCase {
+
+ /** @var PNGHandler */
+ protected $handler;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->handler = new PNGHandler();
+ }
+
+ /**
+ * @covers PNGHandler::getMetadata
+ */
+ public function testInvalidFile() {
+ $res = $this->handler->getMetadata( null, $this->filePath . '/README' );
+ $this->assertEquals( PNGHandler::BROKEN_FILE, $res );
+ }
+
+ /**
+ * @param string $filename Basename of the file to check
+ * @param bool $expected Expected result.
+ * @dataProvider provideIsAnimated
+ * @covers PNGHandler::isAnimatedImage
+ */
+ public function testIsAnimanted( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/png' );
+ $actual = $this->handler->isAnimatedImage( $file );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideIsAnimated() {
+ return array(
+ array( 'Animated_PNG_example_bouncing_beach_ball.png', true ),
+ array( '1bit-png.png', false ),
+ );
+ }
+
+ /**
+ * @param string $filename
+ * @param int $expected Total image area
+ * @dataProvider provideGetImageArea
+ * @covers PNGHandler::getImageArea
+ */
+ public function testGetImageArea( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/png' );
+ $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetImageArea() {
+ 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 string $metadata Serialized metadata
+ * @param int $expected One of the class constants of PNGHandler
+ * @dataProvider provideIsMetadataValid
+ * @covers PNGHandler::isMetadataValid
+ */
+ public function testIsMetadataValid( $metadata, $expected ) {
+ $actual = $this->handler->isMetadataValid( null, $metadata );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideIsMetadataValid() {
+ return array(
+ array( PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ),
+ array( '', PNGHandler::METADATA_BAD ),
+ array( null, PNGHandler::METADATA_BAD ),
+ array( 'Something invalid!', PNGHandler::METADATA_BAD ),
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ 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 ),
+ // @codingStandardsIgnoreEnd
+ );
+ }
+
+ /**
+ * @param string $filename
+ * @param string $expected Serialized array
+ * @dataProvider provideGetMetadata
+ * @covers PNGHandler::getMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/png' );
+ $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" );
+// $this->assertEquals( unserialize( $expected ), unserialize( $actual ) );
+ $this->assertEquals( ( $expected ), ( $actual ) );
+ }
+
+ public static function provideGetMetadata() {
+ return array(
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ 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;}}' ),
+ // @codingStandardsIgnoreEnd
+ );
+ }
+
+ /**
+ * @param string $filename
+ * @param array $expected Expected standard metadata
+ * @dataProvider provideGetIndependentMetaArray
+ * @covers PNGHandler::getCommonMetaArray
+ */
+ public function testGetIndependentMetaArray( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/png' );
+ $actual = $this->handler->getCommonMetaArray( $file );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetIndependentMetaArray() {
+ return array(
+ array( 'rgb-na-png.png', array() ),
+ array( 'xmp.png',
+ array(
+ 'SerialNumber' => '123456789',
+ )
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php
new file mode 100644
index 00000000..ab33d1c2
--- /dev/null
+++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * @group Media
+ * @covers SVGMetadataExtractor
+ */
+class SVGMetadataExtractorTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ AutoLoader::loadClass( 'SVGMetadataExtractorTest' );
+ }
+
+ /**
+ * @dataProvider provideSvgFiles
+ */
+ public function testGetMetadata( $infile, $expected ) {
+ $this->assertMetadata( $infile, $expected );
+ }
+
+ /**
+ * @dataProvider provideSvgFilesWithXMLMetadata
+ */
+ public 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;
+ }
+ }
+ }
+
+ public static function provideSvgFiles() {
+ $base = __DIR__ . '/../../data/media';
+
+ return array(
+ array(
+ "$base/Wikimedia-logo.svg",
+ array(
+ 'width' => 1024,
+ 'height' => 1024,
+ 'originalWidth' => '1024',
+ 'originalHeight' => '1024',
+ 'translations' => array(),
+ )
+ ),
+ array(
+ "$base/QA_icon.svg",
+ array(
+ 'width' => 60,
+ 'height' => 60,
+ 'originalWidth' => '60',
+ 'originalHeight' => '60',
+ 'translations' => array(),
+ )
+ ),
+ array(
+ "$base/Gtk-media-play-ltr.svg",
+ array(
+ 'width' => 60,
+ 'height' => 60,
+ 'originalWidth' => '60.0000000',
+ 'originalHeight' => '60.0000000',
+ 'translations' => array(),
+ )
+ ),
+ array(
+ "$base/Toll_Texas_1.svg",
+ // This file triggered bug 31719, needs entity expansion in the xmlns checks
+ array(
+ 'width' => 385,
+ 'height' => 385,
+ 'originalWidth' => '385',
+ 'originalHeight' => '385.0004883',
+ 'translations' => array(),
+ )
+ ),
+ array(
+ "$base/Tux.svg",
+ array(
+ 'width' => 512,
+ 'height' => 594,
+ 'originalWidth' => '100%',
+ 'originalHeight' => '100%',
+ 'title' => 'Tux',
+ 'translations' => array(),
+ 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
+ )
+ ),
+ array(
+ "$base/Speech_bubbles.svg",
+ array(
+ 'width' => 627,
+ 'height' => 461,
+ 'originalWidth' => '17.7cm',
+ 'originalHeight' => '13cm',
+ 'translations' => array(
+ 'de' => SVGReader::LANG_FULL_MATCH,
+ 'fr' => SVGReader::LANG_FULL_MATCH,
+ 'nl' => SVGReader::LANG_FULL_MATCH,
+ 'tlh-ca' => SVGReader::LANG_FULL_MATCH,
+ 'tlh' => SVGReader::LANG_PREFIX_MATCH
+ ),
+ )
+ ),
+ array(
+ "$base/Soccer_ball_animated.svg",
+ array(
+ 'width' => 150,
+ 'height' => 150,
+ 'originalWidth' => '150',
+ 'originalHeight' => '150',
+ 'animated' => true,
+ 'translations' => array()
+ ),
+ ),
+ );
+ }
+
+ public static function provideSvgFilesWithXMLMetadata() {
+ $base = __DIR__ . '/../../data/media';
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ $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>';
+ // @codingStandardsIgnoreEnd
+
+ $metadata = str_replace( "\r", '', $metadata ); // Windows compat
+ return array(
+ array(
+ "$base/US_states_by_total_state_tax_revenue.svg",
+ array(
+ 'height' => 593,
+ 'metadata' => $metadata,
+ 'width' => 959,
+ 'originalWidth' => '958.69',
+ 'originalHeight' => '592.78998',
+ 'translations' => array(),
+ )
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/SVGTest.php b/tests/phpunit/includes/media/SVGTest.php
new file mode 100644
index 00000000..8f7a0d69
--- /dev/null
+++ b/tests/phpunit/includes/media/SVGTest.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @group Media
+ */
+class SvgTest extends MediaWikiMediaTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->filePath = __DIR__ . '/../../data/media/';
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+
+ $this->handler = new SvgHandler;
+ }
+
+ /**
+ * @param string $filename
+ * @param array $expected The expected independent metadata
+ * @dataProvider providerGetIndependentMetaArray
+ * @covers SvgHandler::getCommonMetaArray
+ */
+ public function testGetIndependentMetaArray( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/svg+xml' );
+ $res = $this->handler->getCommonMetaArray( $file );
+
+ $this->assertEquals( $res, $expected );
+ }
+
+ public static function providerGetIndependentMetaArray() {
+ return array(
+ array( 'Tux.svg', array(
+ 'ObjectName' => 'Tux',
+ 'ImageDescription' =>
+ 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
+ ) ),
+ array( 'Wikimedia-logo.svg', array() )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/TiffTest.php b/tests/phpunit/includes/media/TiffTest.php
new file mode 100644
index 00000000..d1148202
--- /dev/null
+++ b/tests/phpunit/includes/media/TiffTest.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @group Media
+ */
+class TiffTest extends MediaWikiTestCase {
+
+ /** @var TiffHandler */
+ protected $handler;
+ /** @var string */
+ protected $filePath;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->checkPHPExtension( 'exif' );
+
+ $this->setMwGlobals( 'wgShowEXIF', true );
+
+ $this->filePath = __DIR__ . '/../../data/media/';
+ $this->handler = new TiffHandler;
+ }
+
+ /**
+ * @covers TiffHandler::getMetadata
+ */
+ public function testInvalidFile() {
+ $res = $this->handler->getMetadata( null, $this->filePath . 'README' );
+ $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res );
+ }
+
+ /**
+ * @covers TiffHandler::getMetadata
+ */
+ public function testTiffMetadataExtraction() {
+ $res = $this->handler->getMetadata( null, $this->filePath . 'test.tiff' );
+
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ $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;}';
+ // @codingStandardsIgnoreEnd
+
+ // 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/XCFTest.php b/tests/phpunit/includes/media/XCFTest.php
new file mode 100644
index 00000000..5b2de151
--- /dev/null
+++ b/tests/phpunit/includes/media/XCFTest.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @group Media
+ */
+class XCFHandlerTest extends MediaWikiMediaTestCase {
+
+ /** @var XCFHandler */
+ protected $handler;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->handler = new XCFHandler();
+ }
+
+
+ /**
+ * @param string $filename
+ * @param int $expectedWidth Width
+ * @param int $expectedHeight Height
+ * @dataProvider provideGetImageSize
+ * @covers XCFHandler::getImageSize
+ */
+ public function testGetImageSize( $filename, $expectedWidth, $expectedHeight ) {
+ $file = $this->dataFile( $filename, 'image/x-xcf' );
+ $actual = $this->handler->getImageSize( $file, $file->getLocalRefPath() );
+ $this->assertEquals( $expectedWidth, $actual[0] );
+ $this->assertEquals( $expectedHeight, $actual[1] );
+ }
+
+ public static function provideGetImageSize() {
+ return array(
+ array( '80x60-2layers.xcf', 80, 60 ),
+ array( '80x60-RGB.xcf', 80, 60 ),
+ array( '80x60-Greyscale.xcf', 80, 60 ),
+ );
+ }
+
+ /**
+ * @param string $metadata Serialized metadata
+ * @param int $expected One of the class constants of XCFHandler
+ * @dataProvider provideIsMetadataValid
+ * @covers XCFHandler::isMetadataValid
+ */
+ public function testIsMetadataValid( $metadata, $expected ) {
+ $actual = $this->handler->isMetadataValid( null, $metadata );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideIsMetadataValid() {
+ return array(
+ array( '', XCFHandler::METADATA_BAD ),
+ array( serialize( array( 'error' => true ) ), XCFHandler::METADATA_GOOD ),
+ array( false, XCFHandler::METADATA_BAD ),
+ array( serialize( array( 'colorType' => 'greyscale-alpha' ) ), XCFHandler::METADATA_GOOD ),
+ );
+ }
+
+ /**
+ * @param string $filename
+ * @param string $expected Serialized array
+ * @dataProvider provideGetMetadata
+ * @covers XCFHandler::getMetadata
+ */
+ public function testGetMetadata( $filename, $expected ) {
+ $file = $this->dataFile( $filename, 'image/png' );
+ $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetMetadata() {
+ return array(
+ array( '80x60-2layers.xcf', 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' ),
+ array( '80x60-RGB.xcf', 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' ),
+ array( '80x60-Greyscale.xcf', 'a:1:{s:9:"colorType";s:15:"greyscale-alpha";}' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/XMPTest.php b/tests/phpunit/includes/media/XMPTest.php
new file mode 100644
index 00000000..6758e94c
--- /dev/null
+++ b/tests/phpunit/includes/media/XMPTest.php
@@ -0,0 +1,223 @@
+<?php
+
+/**
+ * @group Media
+ * @covers XMPReader
+ */
+class XMPTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->checkPHPExtension( 'exif' ); # Requires libxml to do XMP parsing
+ }
+
+ /**
+ * Put XMP in, compare what comes out...
+ *
+ * @param string $xmp The actual xml data.
+ * @param array $expected Expected result of parsing the xmp.
+ * @param string $info Short sentence on what's being tested.
+ *
+ * @throws Exception
+ * @dataProvider provideXMPParse
+ *
+ * @covers XMPReader::parse
+ */
+ public function testXMPParse( $xmp, $expected, $info ) {
+ 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, 0.0000000001 );
+ }
+
+ public static function provideXMPParse() {
+ $xmpPath = __DIR__ . '/../../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' ),
+ array( 'gps', 'Handling of exif GPS parameters in XMP' ),
+ );
+
+ $xmpFiles[] = array( 'doctype-included', 'XMP includes doctype' );
+
+ 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.
+ *
+ * @covers XMPReader::parseExtended
+ */
+ public function testExtendedXMP() {
+ $xmpPath = __DIR__ . '/../../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.
+ *
+ * @covers XMPReader::parseExtended
+ */
+ public function testExtendedXMPWithWrongGUID() {
+ $xmpPath = __DIR__ . '/../../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.
+ *
+ * @covers XMPReader::parseExtended
+ */
+ public function testExtendedXMPMissingPacket() {
+ $xmpPath = __DIR__ . '/../../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 );
+ }
+
+ /**
+ * Test for multi-section, hostile XML
+ * @covers checkParseSafety
+ */
+ public function testCheckParseSafety() {
+
+ // Test for detection
+ $xmpPath = __DIR__ . '/../../data/xmp/';
+ $file = fopen( $xmpPath . 'doctype-included.xmp', 'rb' );
+ $valid = false;
+ $reader = new XMPReader();
+ do {
+ $chunk = fread( $file, 10 );
+ $valid = $reader->parse( $chunk, feof( $file ) );
+ } while ( !feof( $file ) );
+ $this->assertFalse( $valid, 'Check that doctype is detected in fragmented XML' );
+ $this->assertEquals(
+ array(),
+ $reader->getResults(),
+ 'Check that doctype is detected in fragmented XML'
+ );
+ fclose( $file );
+ unset( $reader );
+
+ // Test for false positives
+ $file = fopen( $xmpPath . 'doctype-not-included.xmp', 'rb' );
+ $valid = false;
+ $reader = new XMPReader();
+ do {
+ $chunk = fread( $file, 10 );
+ $valid = $reader->parse( $chunk, feof( $file ) );
+ } while ( !feof( $file ) );
+ $this->assertTrue(
+ $valid,
+ 'Check for false-positive detecting doctype in fragmented XML'
+ );
+ $this->assertEquals(
+ array(
+ 'xmp-exif' => array(
+ 'DigitalZoomRatio' => '0/10',
+ 'Flash' => '9'
+ )
+ ),
+ $reader->getResults(),
+ 'Check that doctype is detected in fragmented XML'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/media/XMPValidateTest.php b/tests/phpunit/includes/media/XMPValidateTest.php
new file mode 100644
index 00000000..ebec8f6c
--- /dev/null
+++ b/tests/phpunit/includes/media/XMPValidateTest.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @group Media
+ */
+class XMPValidateTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider provideDates
+ * @covers XMPValidate::validateDate
+ */
+ public function testValidateDate( $value, $expected ) {
+ // The method should modify $value.
+ XMPValidate::validateDate( array(), $value, true );
+ $this->assertEquals( $expected, $value );
+ }
+
+ public static function provideDates() {
+ /* For reference valid date formats are:
+ * YYYY
+ * YYYY-MM
+ * YYYY-MM-DD
+ * YYYY-MM-DDThh:mmTZD
+ * YYYY-MM-DDThh:mm:ssTZD
+ * YYYY-MM-DDThh:mm:ss.sTZD
+ * (Time zone is optional)
+ */
+ return array(
+ array( '1992', '1992' ),
+ array( '1992-04', '1992:04' ),
+ array( '1992-02-01', '1992:02:01' ),
+ array( '2011-09-29', '2011:09:29' ),
+ array( '1982-12-15T20:12', '1982:12:15 20:12' ),
+ array( '1982-12-15T20:12Z', '1982:12:15 20:12' ),
+ array( '1982-12-15T20:12+02:30', '1982:12:15 22:42' ),
+ array( '1982-12-15T01:12-02:30', '1982:12:14 22:42' ),
+ array( '1982-12-15T20:12:11', '1982:12:15 20:12:11' ),
+ array( '1982-12-15T20:12:11Z', '1982:12:15 20:12:11' ),
+ array( '1982-12-15T20:12:11+01:10', '1982:12:15 21:22:11' ),
+ array( '2045-12-15T20:12:11', '2045:12:15 20:12:11' ),
+ array( '1867-06-01T15:00:00', '1867:06:01 15:00:00' ),
+ /* some invalid ones */
+ array( '2001--12', null ),
+ array( '2001-5-12', null ),
+ array( '2001-5-12TZ', null ),
+ array( '2001-05-12T15', null ),
+ array( '2001-12T15:13', null ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/normal/CleanUpTest.php b/tests/phpunit/includes/normal/CleanUpTest.php
new file mode 100644
index 00000000..f4b469b8
--- /dev/null
+++ b/tests/phpunit/includes/normal/CleanUpTest.php
@@ -0,0 +1,409 @@
+<?php
+/**
+ * Tests for UtfNormal::cleanUp() function.
+ *
+ * Copyright © 2004 Brion Vibber <brion@pobox.com>
+ * https://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
+ * @group Large
+ *
+ * @todo covers tags, will be UtfNormal::cleanUp once the below is resolved
+ * @todo split me into test methods and providers per the below comment
+ *
+ * We ignore code coverage for this test suite until they are rewritten
+ * to use data providers (bug 46561).
+ * @codeCoverageIgnore
+ */
+class CleanUpTest extends MediaWikiTestCase {
+ /** @todo document */
+ public function testAscii() {
+ $text = 'This is plain ASCII text.';
+ $this->assertEquals( $text, UtfNormal::cleanUp( $text ) );
+ }
+
+ /** @todo document */
+ public function testNull() {
+ $text = "a \x00 null";
+ $expect = "a \xef\xbf\xbd null";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ public function testLatin() {
+ $text = "L'\xc3\xa9cole";
+ $this->assertEquals( $text, UtfNormal::cleanUp( $text ) );
+ }
+
+ /** @todo document */
+ public 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 */
+ public static function provideAllBytes() {
+ return array(
+ array( '', '' ),
+ array( 'x', '' ),
+ array( '', 'x' ),
+ array( 'x', 'x' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideAllBytes
+ * @todo document
+ */
+ function testBytes( $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;
+ }
+ }
+ }
+ }
+
+ /**
+ * @dataProvider provideAllBytes
+ * @todo document
+ */
+ function testDoubleBytes( $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;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @dataProvider provideAllBytes
+ * @todo document
+ */
+ function testTripleBytes( $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 */
+ public 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 */
+ public 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 */
+ public 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 */
+ public 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 */
+ public 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 */
+ public function testForbiddenRegression() {
+ $text = "\xef\xbf\xbf"; # U+FFFF, illegal char
+ $expect = "\xef\xbf\xbd";
+ $this->assertEquals(
+ bin2hex( $expect ),
+ bin2hex( UtfNormal::cleanUp( $text ) ) );
+ }
+
+ /** @todo document */
+ public 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/objectcache/BagOStuffTest.php b/tests/phpunit/includes/objectcache/BagOStuffTest.php
new file mode 100644
index 00000000..987b6e64
--- /dev/null
+++ b/tests/phpunit/includes/objectcache/BagOStuffTest.php
@@ -0,0 +1,147 @@
+<?php
+/**
+ * This class will test BagOStuff.
+ *
+ * @author Matthias Mullie <mmullie@wikimedia.org>
+ */
+class BagOStuffTest extends MediaWikiTestCase {
+ private $cache;
+
+ protected function setUp() {
+ parent::setUp();
+
+ // type defined through parameter
+ if ( $this->getCliArg( 'use-bagostuff' ) ) {
+ $name = $this->getCliArg( 'use-bagostuff' );
+
+ $this->cache = ObjectCache::newFromId( $name );
+ } else {
+ // no type defined - use simple hash
+ $this->cache = new HashBagOStuff;
+ }
+
+ $this->cache->delete( wfMemcKey( 'test' ) );
+ }
+
+ public function testMerge() {
+ $key = wfMemcKey( 'test' );
+
+ $usleep = 0;
+
+ /**
+ * Callback method: append "merged" to whatever is in cache.
+ *
+ * @param BagOStuff $cache
+ * @param string $key
+ * @param int $existingValue
+ * @use int $usleep
+ * @return int
+ */
+ $callback = function ( BagOStuff $cache, $key, $existingValue ) use ( &$usleep ) {
+ // let's pretend this is an expensive callback to test concurrent merge attempts
+ usleep( $usleep );
+
+ if ( $existingValue === false ) {
+ return 'merged';
+ }
+
+ return $existingValue . 'merged';
+ };
+
+ // merge on non-existing value
+ $merged = $this->cache->merge( $key, $callback, 0 );
+ $this->assertTrue( $merged );
+ $this->assertEquals( $this->cache->get( $key ), 'merged' );
+
+ // merge on existing value
+ $merged = $this->cache->merge( $key, $callback, 0 );
+ $this->assertTrue( $merged );
+ $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' );
+
+ /*
+ * Test concurrent merges by forking this process, if:
+ * - not manually called with --use-bagostuff
+ * - pcntl_fork is supported by the system
+ * - cache type will correctly support calls over forks
+ */
+ $fork = (bool)$this->getCliArg( 'use-bagostuff' );
+ $fork &= function_exists( 'pcntl_fork' );
+ $fork &= !$this->cache instanceof HashBagOStuff;
+ $fork &= !$this->cache instanceof EmptyBagOStuff;
+ $fork &= !$this->cache instanceof MultiWriteBagOStuff;
+ if ( $fork ) {
+ // callback should take awhile now so that we can test concurrent merge attempts
+ $pid = pcntl_fork();
+ if ( $pid == -1 ) {
+ // can't fork, ignore this test...
+ } elseif ( $pid ) {
+ // wait a little, making sure that the child process is calling merge
+ usleep( 3000 );
+
+ // attempt a merge - this should fail
+ $merged = $this->cache->merge( $key, $callback, 0, 1 );
+
+ // merge has failed because child process was merging (and we only attempted once)
+ $this->assertFalse( $merged );
+
+ // make sure the child's merge is completed and verify
+ usleep( 3000 );
+ $this->assertEquals( $this->cache->get( $key ), 'mergedmergedmerged' );
+ } else {
+ $this->cache->merge( $key, $callback, 0, 1 );
+
+ // Note: I'm not even going to check if the merge worked, I'll
+ // compare values in the parent process to test if this merge worked.
+ // I'm just going to exit this child process, since I don't want the
+ // child to output any test results (would be rather confusing to
+ // have test output twice)
+ exit;
+ }
+ }
+ }
+
+ public function testAdd() {
+ $key = wfMemcKey( 'test' );
+ $this->assertTrue( $this->cache->add( $key, 'test' ) );
+ }
+
+ public function testGet() {
+ $value = array( 'this' => 'is', 'a' => 'test' );
+
+ $key = wfMemcKey( 'test' );
+ $this->cache->add( $key, $value );
+ $this->assertEquals( $this->cache->get( $key ), $value );
+ }
+
+ /**
+ * @covers BagOStuff::incr
+ */
+ public function testIncr() {
+ $key = wfMemcKey( 'test' );
+ $this->cache->add( $key, 0 );
+ $this->cache->incr( $key );
+ $expectedValue = 1;
+ $actualValue = $this->cache->get( $key );
+ $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' );
+ }
+
+ public function testGetMulti() {
+ $value1 = array( 'this' => 'is', 'a' => 'test' );
+ $value2 = array( 'this' => 'is', 'another' => 'test' );
+
+ $key1 = wfMemcKey( 'test1' );
+ $key2 = wfMemcKey( 'test2' );
+
+ $this->cache->add( $key1, $value1 );
+ $this->cache->add( $key2, $value2 );
+
+ $this->assertEquals(
+ $this->cache->getMulti( array( $key1, $key2 ) ),
+ array( $key1 => $value1, $key2 => $value2 )
+ );
+
+ // cleanup
+ $this->cache->delete( $key1 );
+ $this->cache->delete( $key2 );
+ }
+}
diff --git a/tests/phpunit/includes/parser/MagicVariableTest.php b/tests/phpunit/includes/parser/MagicVariableTest.php
new file mode 100644
index 00000000..17226113
--- /dev/null
+++ b/tests/phpunit/includes/parser/MagicVariableTest.php
@@ -0,0 +1,229 @@
+<?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 Antoine Musso
+ * @copyright Copyright © 2011, Antoine Musso
+ * @file
+ * @todo covers tags
+ */
+
+class MagicVariableTest extends MediaWikiTestCase {
+ /**
+ * @var Parser
+ */
+ 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 */
+ protected function setUp() {
+ parent::setUp();
+
+ $contLang = Language::factory( 'en' );
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => $contLang,
+ ) );
+
+ $this->testParser = new Parser();
+ $this->testParser->Options( ParserOptions::newFromUserAndLang( new User, $contLang ) );
+
+ # initialize parser output
+ $this->testParser->clearState();
+
+ # Needs a title to do magic word stuff
+ $title = Title::newFromText( 'Tests' );
+ # Else it needs a db connection just to check if it's a redirect
+ # (when deciding the page language).
+ $title->mRedirect = false;
+
+ $this->testParser->setTitle( $title );
+ }
+
+ /**
+ * @param int $num Upper limit for numbers
+ * @return array Array of numbers from 1 up to $num
+ */
+ private static function createProviderUpTo( $num ) {
+ $ret = array();
+ for ( $i = 1; $i <= $num; $i++ ) {
+ $ret[] = array( $i );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @return array Array of months numbers (as an integer)
+ */
+ public static function provideMonths() {
+ return self::createProviderUpTo( 12 );
+ }
+
+ /**
+ * @return array Array of days numbers (as an integer)
+ */
+ public static function provideDays() {
+ return self::createProviderUpTo( 31 );
+ }
+
+ ############### TESTS #############################################
+ # @todo FIXME:
+ # - those got copy pasted, we can probably make them cleaner
+ # - tests are lacking useful messages
+
+ # day
+
+ /** @dataProvider provideDays */
+ public function testCurrentdayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'currentday', $day );
+ }
+
+ /** @dataProvider provideDays */
+ public function testCurrentdaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'currentday2', $day );
+ }
+
+ /** @dataProvider provideDays */
+ public function testLocaldayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'localday', $day );
+ }
+
+ /** @dataProvider provideDays */
+ public function testLocaldaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'localday2', $day );
+ }
+
+ # month
+
+ /** @dataProvider provideMonths */
+ public function testCurrentmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'currentmonth', $month );
+ }
+
+ /** @dataProvider provideMonths */
+ public function testCurrentmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'currentmonth1', $month );
+ }
+
+ /** @dataProvider provideMonths */
+ public function testLocalmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'localmonth', $month );
+ }
+
+ /** @dataProvider provideMonths */
+ public function testLocalmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'localmonth1', $month );
+ }
+
+ # revision day
+
+ /** @dataProvider provideDays */
+ public function testRevisiondayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'revisionday', $day );
+ }
+
+ /** @dataProvider provideDays */
+ public function testRevisiondaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'revisionday2', $day );
+ }
+
+ # revision month
+
+ /** @dataProvider provideMonths */
+ public function testRevisionmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'revisionmonth', $month );
+ }
+
+ /** @dataProvider provideMonths */
+ public function testRevisionmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'revisionmonth1', $month );
+ }
+
+ ############### 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 string $magic Magic variable name
+ * @param mixed $value Month or day
+ * @param string $format 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
+ * @param string $ts
+ */
+ private function setParserTS( $ts ) {
+ $this->testParser->Options()->setTimestamp( $ts );
+ $this->testParser->mRevisionTimestamp = $ts;
+ }
+
+ /**
+ * Assertion helper to test a magic variable output
+ * @param string|int $expected
+ * @param string $magic
+ */
+ 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..df891f5a
--- /dev/null
+++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php
@@ -0,0 +1,134 @@
+<?php
+require_once __DIR__ . '/NewParserTest.php';
+
+/**
+ * The UnitTest must be either a class that inherits from MediaWikiTestCase
+ * or a class that provides a public static suite() method which returns
+ * an PHPUnit_Framework_Test object
+ *
+ * @group Parser
+ * @group Database
+ */
+class MediaWikiParserTest {
+
+ /**
+ * @defgroup filtering_constants Filtering constants
+ *
+ * Limit inclusion of parser tests files coming from MediaWiki core
+ * @{
+ */
+
+ /** Include files shipped with MediaWiki core */
+ const CORE_ONLY = 1;
+ /** Include non core files as set in $wgParserTestFiles */
+ const NO_CORE = 2;
+ /** Include anything set via $wgParserTestFiles */
+ const WITH_ALL = 3; # CORE_ONLY | NO_CORE
+
+ /** @} */
+
+ /**
+ * Get a PHPUnit test suite of parser tests. Optionally filtered with
+ * $flags.
+ *
+ * @par Examples:
+ * Get a suite of parser tests shipped by MediaWiki core:
+ * @code
+ * MediaWikiParserTest::suite( MediaWikiParserTest::CORE_ONLY );
+ * @endcode
+ * Get a suite of various parser tests, like extensions:
+ * @code
+ * MediaWikiParserTest::suite( MediaWikiParserTest::NO_CORE );
+ * @endcode
+ * Get any test defined via $wgParserTestFiles:
+ * @code
+ * MediaWikiParserTest::suite( MediaWikiParserTest::WITH_ALL );
+ * @endcode
+ *
+ * @param int $flags Bitwise flag to filter out the $wgParserTestFiles that
+ * will be included. Default: MediaWikiParserTest::CORE_ONLY
+ *
+ * @return PHPUnit_Framework_TestSuite
+ */
+ public static function suite( $flags = self::CORE_ONLY ) {
+ if ( is_string( $flags ) ) {
+ $flags = self::CORE_ONLY;
+ }
+ global $wgParserTestFiles, $IP;
+
+ $mwTestDir = $IP . '/tests/';
+
+ # Human friendly helpers
+ $wantsCore = ( $flags & self::CORE_ONLY );
+ $wantsRest = ( $flags & self::NO_CORE );
+
+ # Will hold the .txt parser test files we will include
+ $filesToTest = array();
+
+ # Filter out .txt files
+ foreach ( $wgParserTestFiles as $parserTestFile ) {
+ $isCore = ( 0 === strpos( $parserTestFile, $mwTestDir ) );
+
+ if ( $isCore && $wantsCore ) {
+ self::debug( "included core parser tests: $parserTestFile" );
+ $filesToTest[] = $parserTestFile;
+ } elseif ( !$isCore && $wantsRest ) {
+ self::debug( "included non core parser tests: $parserTestFile" );
+ $filesToTest[] = $parserTestFile;
+ } else {
+ self::debug( "skipped parser tests: $parserTestFile" );
+ }
+ }
+ self::debug( 'parser tests files: '
+ . implode( ' ', $filesToTest ) );
+
+ $suite = new PHPUnit_Framework_TestSuite;
+ $testList = array();
+ $counter = 0;
+ foreach ( $filesToTest as $fileName ) {
+ // Call the highest level directory the extension name.
+ // It may or may not actually be, but it should be close
+ // enough to cause there to be separate names for different
+ // things, which is good enough for our purposes.
+ $extensionName = basename( dirname( $fileName ) );
+ $testsName = $extensionName . '⁄' . basename( $fileName, '.txt' );
+ $escapedFileName = strtr( $fileName, array( "'" => "\\'", '\\' => '\\\\' ) );
+ $parserTestClassName = ucfirst( $testsName );
+ // Official spec for class names: http://php.net/manual/en/language.oop5.basic.php
+ // Prepend 'ParserTest_' to be paranoid about it not starting with a number
+ $parserTestClassName = 'ParserTest_' . preg_replace( '/[^a-zA-Z0-9_\x7f-\xff]/', '_', $parserTestClassName );
+ if ( isset( $testList[$parserTestClassName] ) ) {
+ // If a conflict happens, gives a very unclear fatal.
+ // So as a last ditch effort to prevent that eventuality, if there
+ // is a conflict, append a number.
+ $counter++;
+ $parserTestClassName .= $counter;
+ }
+ $testList[$parserTestClassName] = true;
+ $parserTestClassDefinition = <<<EOT
+/**
+ * @group Database
+ * @group Parser
+ * @group ParserTests
+ * @group ParserTests_$parserTestClassName
+ */
+class $parserTestClassName extends NewParserTest {
+ protected \$file = '$escapedFileName';
+}
+EOT;
+
+ eval( $parserTestClassDefinition );
+ self::debug( "Adding test class $parserTestClassName" );
+ $suite->addTestSuite( $parserTestClassName );
+ }
+ return $suite;
+ }
+
+ /**
+ * Write $msg under log group 'tests-parser'
+ * @param string $msg Message to log
+ */
+ protected static function debug( $msg ) {
+ return wfDebugLog( 'tests-parser', wfGetCaller() . ' ' . $msg );
+ }
+}
diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php
new file mode 100644
index 00000000..0df52f5e
--- /dev/null
+++ b/tests/phpunit/includes/parser/NewParserTest.php
@@ -0,0 +1,1091 @@
+<?php
+
+/**
+ * Although marked as a stub, can work independently.
+ *
+ * @group Database
+ * @group Parser
+ * @group Stub
+ *
+ * @todo covers tags
+ */
+class NewParserTest extends MediaWikiTestCase {
+ static protected $articles = array(); // Array of test articles defined by the tests
+ /* The data provider 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.
+ */
+ static protected $backendToUse;
+
+ public $keepUploads = false;
+ public $runDisabled = false;
+ public $runParsoid = false;
+ public $regex = '';
+ public $showProgress = true;
+ public $savedWeirdGlobals = array();
+ public $savedGlobals = array();
+ public $hooks = array();
+ public $functionHooks = array();
+ public $transparentHooks = array();
+
+ //Fuzz test
+ public $maxFuzzTestLength = 300;
+ public $fuzzSeed = 0;
+ public $memoryLimit = 50;
+
+ /**
+ * @var DjVuSupport
+ */
+ private $djVuSupport;
+ /**
+ * @var TidySupport
+ */
+ private $tidySupport;
+
+ protected $file = false;
+
+ public static function setUpBeforeClass() {
+ // Inject ParserTest well-known interwikis
+ ParserTest::setupInterwikis();
+ }
+
+ protected function setUp() {
+ global $wgNamespaceAliases, $wgContLang;
+ global $wgHooks, $IP;
+
+ parent::setUp();
+
+ //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['wgLanguageCode'] = 'en';
+ $tmpGlobals['wgContLang'] = Language::factory( 'en' );
+ $tmpGlobals['wgSitename'] = 'MediaWiki';
+ $tmpGlobals['wgServer'] = 'http://example.org';
+ $tmpGlobals['wgServerName'] = 'example.org';
+ $tmpGlobals['wgScript'] = '/index.php';
+ $tmpGlobals['wgScriptPath'] = '/';
+ $tmpGlobals['wgArticlePath'] = '/wiki/$1';
+ $tmpGlobals['wgActionPaths'] = array();
+ $tmpGlobals['wgVariantArticlePath'] = false;
+ $tmpGlobals['wgExtensionAssetsPath'] = '/extensions';
+ $tmpGlobals['wgStylePath'] = '/skins';
+ $tmpGlobals['wgEnableUploads'] = true;
+ $tmpGlobals['wgUploadNavigationUrl'] = false;
+ $tmpGlobals['wgThumbnailScriptPath'] = false;
+ $tmpGlobals['wgLocalFileRepo'] = array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => 'local-backend'
+ );
+ $tmpGlobals['wgForeignFileRepos'] = array();
+ $tmpGlobals['wgDefaultExternalStore'] = array();
+ $tmpGlobals['wgEnableParserCache'] = false;
+ $tmpGlobals['wgCapitalLinks'] = true;
+ $tmpGlobals['wgNoFollowLinks'] = true;
+ $tmpGlobals['wgNoFollowDomainExceptions'] = array();
+ $tmpGlobals['wgExternalLinkTarget'] = false;
+ $tmpGlobals['wgThumbnailScriptPath'] = false;
+ $tmpGlobals['wgUseImageResize'] = true;
+ $tmpGlobals['wgAllowExternalImages'] = true;
+ $tmpGlobals['wgRawHtml'] = false;
+ $tmpGlobals['wgWellFormedXml'] = true;
+ $tmpGlobals['wgAllowMicrodataAttributes'] = true;
+ $tmpGlobals['wgExperimentalHtmlIds'] = false;
+ $tmpGlobals['wgAdaptiveMessageCache'] = true;
+ $tmpGlobals['wgUseDatabaseMessages'] = true;
+ $tmpGlobals['wgLocaltimezone'] = 'UTC';
+ $tmpGlobals['wgDeferredUpdateList'] = array();
+ $tmpGlobals['wgGroupPermissions'] = array(
+ '*' => array(
+ 'createaccount' => true,
+ 'read' => true,
+ 'edit' => true,
+ 'createpage' => true,
+ 'createtalk' => true,
+ ) );
+ $tmpGlobals['wgNamespaceProtection'] = array( NS_MEDIAWIKI => 'editinterface' );
+
+ $tmpGlobals['wgParser'] = new StubObject(
+ 'wgParser', $GLOBALS['wgParserConf']['class'],
+ array( $GLOBALS['wgParserConf'] ) );
+
+ $tmpGlobals['wgFileExtensions'][] = 'svg';
+ $tmpGlobals['wgSVGConverter'] = 'rsvg';
+ $tmpGlobals['wgSVGConverters']['rsvg'] =
+ '$path/rsvg-convert -w $width -h $height $input -o $output';
+
+ if ( $GLOBALS['wgStyleDirectory'] === false ) {
+ $tmpGlobals['wgStyleDirectory'] = "$IP/skins";
+ }
+
+ # Replace all media handlers with a mock. We do not need to generate
+ # actual thumbnails to do parser testing, we only care about receiving
+ # a ThumbnailImage properly initialized.
+ global $wgMediaHandlers;
+ foreach ( $wgMediaHandlers as $type => $handler ) {
+ $tmpGlobals['wgMediaHandlers'][$type] = 'MockBitmapHandler';
+ }
+ // Vector images have to be handled slightly differently
+ $tmpGlobals['wgMediaHandlers']['image/svg+xml'] = 'MockSvgHandler';
+
+ // DjVu images have to be handled slightly differently
+ $tmpGlobals['wgMediaHandlers']['image/vnd.djvu'] = 'MockDjVuHandler';
+
+ $tmpHooks = $wgHooks;
+ $tmpHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
+ $tmpHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
+ $tmpGlobals['wgHooks'] = $tmpHooks;
+ # add a namespace shadowing a interwiki link, to test
+ # proper precedence when resolving links. (bug 51680)
+ $tmpGlobals['wgExtraNamespaces'] = array( 100 => 'MemoryAlpha' );
+
+ $tmpGlobals['wgLocalInterwikis'] = array( 'local', 'mi' );
+ # "extra language links"
+ # see https://gerrit.wikimedia.org/r/111390
+ $tmpGlobals['wgExtraInterlanguageLinkPrefixes'] = array( 'mul' );
+
+ // DjVu support
+ $this->djVuSupport = new DjVuSupport();
+ // Tidy support
+ $this->tidySupport = new TidySupport();
+ // We always set 'wgUseTidy' to false when parsing, but certain
+ // test-running modes still use tidy if available, so ensure
+ // that the tidy-related options are all set to their defaults.
+ $tmpGlobals['wgUseTidy'] = false;
+ $tmpGlobals['wgAlwaysUseTidy'] = false;
+ $tmpGlobals['wgDebugTidy'] = false;
+ $tmpGlobals['wgTidyConf'] = $IP . '/includes/tidy.conf';
+ $tmpGlobals['wgTidyOpts'] = '';
+ $tmpGlobals['wgTidyInternal'] = $this->tidySupport->isInternal();
+
+ $this->setMwGlobals( $tmpGlobals );
+
+ $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image'];
+ $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk'];
+
+ $wgNamespaceAliases['Image'] = NS_FILE;
+ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ protected function tearDown() {
+ global $wgNamespaceAliases, $wgContLang;
+
+ $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias'];
+ $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias'];
+
+ // Restore backends
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+
+ // Remove temporary pages from the link cache
+ LinkCache::singleton()->clear();
+
+ // Restore message cache (temporary pages and $wgUseDatabaseMessages)
+ MessageCache::destroyInstance();
+
+ parent::tearDown();
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ public static function tearDownAfterClass() {
+ ParserTest::tearDownInterwikis();
+ parent::tearDownAfterClass();
+ }
+
+ function addDBData() {
+ $this->tablesUsed[] = 'site_stats';
+ # disabled for performance
+ #$this->tablesUsed[] = 'image';
+
+ # Update certain things in site_stats
+ $this->db->insert( 'site_stats',
+ array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ),
+ __METHOD__
+ );
+
+ $user = User::newFromId( 0 );
+ LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision
+
+ # Upload DB table entries for files.
+ # We will upload the actual files later. Note that if anything causes LocalFile::load()
+ # to be triggered before then, it will break via maybeUpgrade() setting the fileExists
+ # member to false and storing it in cache.
+ # note that the size/width/height/bits/etc of the file
+ # are actually set by inspecting the file itself; the arguments
+ # to recordUpload2 have no effect. That said, we try to make things
+ # match up so it is less confusing to readers of the code & tests.
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
+ if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
+ $image->recordUpload2(
+ '', // archive name
+ 'Upload of some lame file',
+ 'Some lame file',
+ array(
+ 'size' => 7881,
+ 'width' => 1941,
+ 'height' => 220,
+ 'bits' => 8,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '1', 16, 36, 31 ),
+ 'fileExists' => true ),
+ $this->db->timestamp( '20010115123500' ), $user
+ );
+ }
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) );
+ if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
+ $image->recordUpload2(
+ '', // archive name
+ 'Upload of some lame thumbnail',
+ 'Some lame thumbnail',
+ array(
+ 'size' => 22589,
+ 'width' => 135,
+ 'height' => 135,
+ 'bits' => 8,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/png',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '2', 16, 36, 31 ),
+ 'fileExists' => true ),
+ $this->db->timestamp( '20130225203040' ), $user
+ );
+ }
+
+ # This image will be blacklisted in [[MediaWiki:Bad image list]]
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
+ if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
+ $image->recordUpload2(
+ '', // archive name
+ 'zomgnotcensored',
+ 'Borderline image',
+ array(
+ 'size' => 12345,
+ 'width' => 320,
+ 'height' => 240,
+ 'bits' => 24,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '3', 16, 36, 31 ),
+ 'fileExists' => true ),
+ $this->db->timestamp( '20010115123500' ), $user
+ );
+ }
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) );
+ if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
+ $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', array(
+ 'size' => 12345,
+ 'width' => 240,
+ 'height' => 180,
+ 'bits' => 0,
+ 'media_type' => MEDIATYPE_DRAWING,
+ 'mime' => 'image/svg+xml',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20010115123500' ), $user );
+ }
+
+ # A DjVu file
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) );
+ if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
+ $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', array(
+ 'size' => 3249,
+ 'width' => 2480,
+ 'height' => 3508,
+ 'bits' => 0,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/vnd.djvu',
+ 'metadata' => '<?xml version="1.0" ?>
+<!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd">
+<DjVuXML>
+<HEAD></HEAD>
+<BODY><OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+</BODY>
+</DjVuXML>',
+ 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20140115123600' ), $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.
+ * @param array $opts
+ * @param string $config
+ * @return RequestContext
+ */
+ protected function setupGlobals( $opts = array(), $config = '' ) {
+ global $wgFileBackends;
+ # 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 );
+
+ $uploadDir = $this->getUploadDir();
+ if ( $this->getCliArg( 'use-filebackend' ) ) {
+ if ( self::$backendToUse ) {
+ $backend = self::$backendToUse;
+ } else {
+ $name = $this->getCliArg( 'use-filebackend' );
+ $useConfig = array();
+ foreach ( $wgFileBackends as $conf ) {
+ if ( $conf['name'] == $name ) {
+ $useConfig = $conf;
+ }
+ }
+ $useConfig['name'] = 'local-backend'; // swap name
+ unset( $useConfig['lockManager'] );
+ unset( $useConfig['fileJournal'] );
+ $class = $useConfig['class'];
+ self::$backendToUse = new $class( $useConfig );
+ $backend = self::$backendToUse;
+ }
+ } else {
+ # Replace with a mock. We do not care about generating real
+ # files on the filesystem, just need to expose the file
+ # informations.
+ $backend = new MockFileBackend( array(
+ 'name' => 'local-backend',
+ 'wikiId' => wfWikiId()
+ ) );
+ }
+
+ $settings = array(
+ 'wgLocalFileRepo' => array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => $backend
+ ),
+ 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
+ 'wgLanguageCode' => $lang,
+ 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_',
+ 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ),
+ 'wgNamespacesWithSubpages' => array( NS_MAIN => isset( $opts['subpage'] ) ),
+ 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ),
+ 'wgThumbLimits' => array( self::getOptionValue( 'thumbsize', $opts, 180 ) ),
+ 'wgMaxTocLevel' => $maxtoclevel,
+ 'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ),
+ 'wgMathDirectory' => $uploadDir . '/math',
+ 'wgDefaultLanguageVariant' => $variant,
+ 'wgLinkHolderBatchSize' => $linkHolderBatchSize,
+ );
+
+ if ( $config ) {
+ $configLines = explode( "\n", $config );
+
+ foreach ( $configLines as $line ) {
+ list( $var, $value ) = explode( '=', $line, 2 );
+
+ $settings[$var] = eval( "return $value;" ); //???
+ }
+ }
+
+ $this->savedGlobals = array();
+
+ /** @since 1.20 */
+ wfRunHooks( 'ParserTestGlobals', array( &$settings ) );
+
+ $langObj = Language::factory( $lang );
+ $settings['wgContLang'] = $langObj;
+ $settings['wgLang'] = $langObj;
+
+ $context = new RequestContext();
+ $settings['wgOut'] = $context->getOutput();
+ $settings['wgUser'] = $context->getUser();
+ $settings['wgRequest'] = $context->getRequest();
+
+ // We (re)set $wgThumbLimits to a single-element array above.
+ $context->getUser()->setOption( 'thumbsize', 0 );
+
+ foreach ( $settings as $var => $val ) {
+ if ( array_key_exists( $var, $GLOBALS ) ) {
+ $this->savedGlobals[$var] = $GLOBALS[$var];
+ }
+
+ $GLOBALS[$var] = $val;
+ }
+
+ MagicWord::clearCache();
+
+ # The entries saved into RepoGroup cache with previous globals will be wrong.
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+
+ # Create dummy files in storage
+ $this->setupUploads();
+
+ # Publish the articles after we have the final language set
+ $this->publishTestArticles();
+
+ MessageCache::destroyInstance();
+
+ return $context;
+ }
+
+ /**
+ * Get an FS upload directory (only applies to FSFileBackend)
+ *
+ * @return string The directory
+ */
+ protected function getUploadDir() {
+ 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;
+ }
+
+ return $dir;
+ }
+
+ /**
+ * Create a dummy uploads directory which will contain a couple
+ * of files in order to pass existence tests.
+ *
+ * @return string The directory
+ */
+ protected function setupUploads() {
+ global $IP;
+
+ $base = $this->getBaseDir();
+ $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
+ $backend->prepare( array( 'dir' => "$base/local-public/3/3a" ) );
+ $backend->store( array(
+ 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg",
+ 'dst' => "$base/local-public/3/3a/Foobar.jpg"
+ ) );
+ $backend->prepare( array( 'dir' => "$base/local-public/e/ea" ) );
+ $backend->store( array(
+ 'src' => "$IP/tests/phpunit/data/parser/wiki.png",
+ 'dst' => "$base/local-public/e/ea/Thumb.png"
+ ) );
+ $backend->prepare( array( 'dir' => "$base/local-public/0/09" ) );
+ $backend->store( array(
+ 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg",
+ 'dst' => "$base/local-public/0/09/Bad.jpg"
+ ) );
+ $backend->prepare( array( 'dir' => "$base/local-public/5/5f" ) );
+ $backend->store( array(
+ 'src' => "$IP/tests/phpunit/data/parser/LoremIpsum.djvu",
+ 'dst' => "$base/local-public/5/5f/LoremIpsum.djvu"
+ ) );
+
+ // No helpful SVG file to copy, so make one ourselves
+ $data = '<?xml version="1.0" encoding="utf-8"?>' .
+ '<svg xmlns="http://www.w3.org/2000/svg"' .
+ ' version="1.1" width="240" height="180"/>';
+
+ $backend->prepare( array( 'dir' => "$base/local-public/f/ff" ) );
+ $backend->quickCreate( array(
+ 'content' => $data, 'dst' => "$base/local-public/f/ff/Foobar.svg"
+ ) );
+ }
+
+ /**
+ * Restore default values and perform any necessary clean-up
+ * after each test runs.
+ */
+ protected function teardownGlobals() {
+ $this->teardownUploads();
+
+ foreach ( $this->savedGlobals as $var => $val ) {
+ $GLOBALS[$var] = $val;
+ }
+ }
+
+ /**
+ * Remove the dummy uploads directory
+ */
+ private function teardownUploads() {
+ if ( $this->keepUploads ) {
+ return;
+ }
+
+ $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
+ if ( $backend instanceof MockFileBackend ) {
+ # In memory backend, so dont bother cleaning them up.
+ return;
+ }
+
+ $base = $this->getBaseDir();
+ // delete the files first, then the dirs.
+ self::deleteFiles(
+ array(
+ "$base/local-public/3/3a/Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/100px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/137px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/177px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/206px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/20px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/220px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/265px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/270px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/274px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/300px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/30px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/330px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/353px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/360px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/400px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/40px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/440px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/442px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/450px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/50px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/600px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/70px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/75px-Foobar.jpg",
+ "$base/local-thumb/3/3a/Foobar.jpg/960px-Foobar.jpg",
+
+ "$base/local-public/e/ea/Thumb.png",
+
+ "$base/local-public/0/09/Bad.jpg",
+
+ "$base/local-public/5/5f/LoremIpsum.djvu",
+ "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-2480px-LoremIpsum.djvu.jpg",
+ "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-3720px-LoremIpsum.djvu.jpg",
+ "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-4960px-LoremIpsum.djvu.jpg",
+
+ "$base/local-public/f/ff/Foobar.svg",
+ "$base/local-thumb/f/ff/Foobar.svg/180px-Foobar.svg.png",
+ "$base/local-thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png",
+ "$base/local-thumb/f/ff/Foobar.svg/270px-Foobar.svg.png",
+ "$base/local-thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png",
+ "$base/local-thumb/f/ff/Foobar.svg/360px-Foobar.svg.png",
+ "$base/local-thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png",
+ "$base/local-thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.png",
+ "$base/local-thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.png",
+ "$base/local-thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.png",
+
+ "$base/local-public/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
+ )
+ );
+ }
+
+ /**
+ * Delete the specified files, if they exist.
+ * @param array $files Full paths to files to delete.
+ */
+ private static function deleteFiles( $files ) {
+ $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
+ foreach ( $files as $file ) {
+ $backend->delete( array( 'src' => $file ), array( 'force' => 1 ) );
+ }
+ foreach ( $files as $file ) {
+ $tmp = $file;
+ while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) {
+ if ( !$backend->clean( array( 'dir' => $tmp ) )->isOK() ) {
+ break;
+ }
+ }
+ }
+ }
+
+ protected function getBaseDir() {
+ return 'mwstore://local-backend';
+ }
+
+ 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
+ * @param string $filename
+ */
+ public function setParserTestFile( $filename ) {
+ $this->file = $filename;
+ }
+
+ /**
+ * @group medium
+ * @dataProvider parserTestProvider
+ * @param string $desc
+ * @param string $input
+ * @param string $result
+ * @param array $opts
+ * @param array $config
+ */
+ public function testParserTest( $desc, $input, $result, $opts, $config ) {
+ if ( $this->regex != '' && !preg_match( '/' . $this->regex . '/', $desc ) ) {
+ $this->assertTrue( true ); // XXX: don't flood output with "test made no assertions"
+ //$this->markTestSkipped( 'Filtered out by the user' );
+ return;
+ }
+
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ // parser tests frequently assume that the main namespace contains wikitext.
+ // @todo When setting up pages, force the content model. Only skip if
+ // $wgtContentModelUseDB is false.
+ $this->markTestSkipped( "Main namespace does not support wikitext,"
+ . "skipping parser test: $desc" );
+ }
+
+ wfDebug( "Running parser test: $desc\n" );
+
+ $opts = $this->parseOptions( $opts );
+ $context = $this->setupGlobals( $opts, $config );
+
+ $user = $context->getUser();
+ $options = ParserOptions::newFromContext( $context );
+
+ 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 );
+
+ # Parser test requiring math. Make sure texvc is executable
+ # or just skip such tests.
+ if ( isset( $opts['math'] ) || isset( $opts['texvc'] ) ) {
+ global $wgTexvc;
+
+ if ( !isset( $wgTexvc ) ) {
+ $this->markTestSkipped( "SKIPPED: \$wgTexvc is not set" );
+ } elseif ( !is_executable( $wgTexvc ) ) {
+ $this->markTestSkipped( "SKIPPED: texvc binary does not exist"
+ . " or is not executable.\n"
+ . "Current configuration is:\n\$wgTexvc = '$wgTexvc'" );
+ }
+ }
+ if ( isset( $opts['djvu'] ) ) {
+ if ( !$this->djVuSupport->isEnabled() ) {
+ $this->markTestSkipped( "SKIPPED: djvu binaries do not exist or are not executable.\n" );
+ }
+ }
+
+ 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'] ) ) {
+ $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 );
+ $output->setTOCEnabled( !isset( $opts['notoc'] ) );
+ $out = $output->getText();
+ if ( isset( $opts['tidy'] ) ) {
+ if ( !$this->tidySupport->isEnabled() ) {
+ $this->markTestSkipped( "SKIPPED: tidy extension is not installed.\n" );
+ } else {
+ $out = MWTidy::tidy( $out );
+ $out = preg_replace( '/\s+$/', '', $out );
+ }
+ }
+
+ if ( isset( $opts['showtitle'] ) ) {
+ if ( $output->getTitleText() ) {
+ $title = $output->getTitleText();
+ }
+
+ $out = "$title\n$out";
+ }
+
+ if ( isset( $opts['ill'] ) ) {
+ $out = implode( ' ', $output->getLanguageLinks() );
+ } elseif ( isset( $opts['cat'] ) ) {
+ $outputPage = $context->getOutput();
+ $outputPage->addCategoryLinks( $output->getCategories() );
+ $cats = $outputPage->getCategoryLinks();
+
+ if ( isset( $cats['normal'] ) ) {
+ $out = implode( ' ', $cats['normal'] );
+ } else {
+ $out = '';
+ }
+ }
+ $parser->mPreprocessor = null;
+ }
+
+ $this->teardownGlobals();
+
+ $this->assertEquals( $result, $out, $desc );
+ }
+
+ /**
+ * Run a fuzz test series
+ * Draw input from a set of test files
+ *
+ * @todo fixme Needs some work to not eat memory until the world explodes
+ *
+ * @group ParserFuzz
+ */
+ public function testFuzzTests() {
+ global $wgParserTestFiles;
+
+ $files = $wgParserTestFiles;
+
+ if ( $this->getCliArg( 'file' ) ) {
+ $files = array( $this->getCliArg( 'file' ) );
+ }
+
+ $dict = $this->getFuzzInput( $files );
+ $dictSize = strlen( $dict );
+ $logMaxLength = log( $this->maxFuzzTestLength );
+
+ ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
+
+ $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\n" .
+ "Input: $input_dump\n\nError: {$exception->getMessage()}\n\n" .
+ "Backtrace: {$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
+ * @param array $filenames
+ * @return string
+ */
+ 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
+ * @return array
+ */
+ 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
+ * @param Preprocessor $preprocessor
+ * @return Parser
+ */
+ 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] = array( $text, $line );
+ }
+
+ public function publishTestArticles() {
+ if ( empty( self::$articles ) ) {
+ return;
+ }
+
+ foreach ( self::$articles as $name => $info ) {
+ list( $text, $line ) = $info;
+ ParserTest::addArticle( $name, $text, $line, 'ignoreduplicate' );
+ }
+ }
+
+ /**
+ * 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 string $name
+ * @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] );
+ }
+
+ public function requireTransparentHook( $name ) {
+ global $wgParser;
+ $wgParser->firstCallInit(); // make sure hooks are loaded.
+ return isset( $wgParser->mTransparentTagHooks[$name] );
+ }
+
+ //Various "cleanup" functions
+
+ /**
+ * Remove last character if it is a newline
+ * @param string $s
+ * @return string
+ */
+ public function removeEndingNewline( $s ) {
+ if ( substr( $s, -1 ) === "\n" ) {
+ return substr( $s, 0, -1 );
+ } else {
+ return $s;
+ }
+ }
+
+ //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 string $key Name of option val to retrieve
+ * @param array $opts Options array to look in
+ * @param mixed $default Default value returned if not found
+ * @return mixed
+ */
+ 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/ParserMethodsTest.php b/tests/phpunit/includes/parser/ParserMethodsTest.php
new file mode 100644
index 00000000..1790086a
--- /dev/null
+++ b/tests/phpunit/includes/parser/ParserMethodsTest.php
@@ -0,0 +1,187 @@
+<?php
+
+class ParserMethodsTest extends MediaWikiLangTestCase {
+
+ public static function providePreSaveTransform() {
+ return array(
+ array( 'hello this is ~~~',
+ "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+ ),
+ array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider providePreSaveTransform
+ * @covers Parser::preSaveTransform
+ */
+ public function testPreSaveTransform( $text, $expected ) {
+ global $wgParser;
+
+ $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
+ $user = new User();
+ $user->setName( "127.0.0.1" );
+ $popts = ParserOptions::newFromUser( $user );
+ $text = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+ $this->assertEquals( $expected, $text );
+ }
+
+ public static function provideStripOuterParagraph() {
+ // This mimics the most common use case (stripping paragraphs generated by the parser).
+ $message = new RawMessage( "Message text." );
+
+ return array(
+ array(
+ "<p>Text.</p>",
+ "Text.",
+ ),
+ array(
+ "<p class='foo'>Text.</p>",
+ "<p class='foo'>Text.</p>",
+ ),
+ array(
+ "<p>Text.\n</p>\n",
+ "Text.",
+ ),
+ array(
+ "<p>Text.</p><p>More text.</p>",
+ "<p>Text.</p><p>More text.</p>",
+ ),
+ array(
+ $message->parse(),
+ "Message text.",
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideStripOuterParagraph
+ * @covers Parser::stripOuterParagraph
+ */
+ public function testStripOuterParagraph( $text, $expected ) {
+ $this->assertEquals( $expected, Parser::stripOuterParagraph( $text ) );
+ }
+
+ /**
+ * @expectedException MWException
+ * @expectedExceptionMessage Parser state cleared while parsing. Did you call Parser::parse recursively?
+ * @covers Parser::lock
+ */
+ public function testRecursiveParse() {
+ global $wgParser;
+ $title = Title::newFromText( 'foo' );
+ $po = new ParserOptions;
+ $wgParser->setHook( 'recursivecallparser', array( $this, 'helperParserFunc' ) );
+ $wgParser->parse( '<recursivecallparser>baz</recursivecallparser>', $title, $po );
+ }
+
+ public function helperParserFunc( $input, $args, $parser ) {
+ $title = Title::newFromText( 'foo' );
+ $po = new ParserOptions;
+ $parser->parse( $input, $title, $po );
+ return 'bar';
+ }
+
+ /**
+ * @covers Parser::callParserFunction
+ */
+ public function testCallParserFunction() {
+ global $wgParser;
+
+ // Normal parses test passing PPNodes. Test passing an array.
+ $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
+ $wgParser->startExternalParse( $title, new ParserOptions(), Parser::OT_HTML );
+ $frame = $wgParser->getPreprocessor()->newFrame();
+ $ret = $wgParser->callParserFunction( $frame, '#tag',
+ array( 'pre', 'foo', 'style' => 'margin-left: 1.6em' )
+ );
+ $ret['text'] = $wgParser->mStripState->unstripBoth( $ret['text'] );
+ $this->assertSame( array(
+ 'found' => true,
+ 'text' => '<pre style="margin-left: 1.6em">foo</pre>',
+ ), $ret, 'callParserFunction works for {{#tag:pre|foo|style=margin-left: 1.6em}}' );
+ }
+
+ /**
+ * @covers Parser::parse
+ * @covers ParserOutput::getSections
+ */
+ public function testGetSections() {
+ global $wgParser;
+
+ $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
+ $out = $wgParser->parse( "==foo==\n<h2>bar</h2>\n==baz==\n", $title, new ParserOptions() );
+ $this->assertSame( array(
+ array(
+ 'toclevel' => 1,
+ 'level' => '2',
+ 'line' => 'foo',
+ 'number' => '1',
+ 'index' => '1',
+ 'fromtitle' => $title->getPrefixedDBkey(),
+ 'byteoffset' => 0,
+ 'anchor' => 'foo',
+ ),
+ array(
+ 'toclevel' => 1,
+ 'level' => '2',
+ 'line' => 'bar',
+ 'number' => '2',
+ 'index' => '',
+ 'fromtitle' => false,
+ 'byteoffset' => null,
+ 'anchor' => 'bar',
+ ),
+ array(
+ 'toclevel' => 1,
+ 'level' => '2',
+ 'line' => 'baz',
+ 'number' => '3',
+ 'index' => '2',
+ 'fromtitle' => $title->getPrefixedDBkey(),
+ 'byteoffset' => 21,
+ 'anchor' => 'baz',
+ ),
+ ), $out->getSections(), 'getSections() with proper value when <h2> is used' );
+ }
+
+ /**
+ * @dataProvider provideNormalizeLinkUrl
+ * @covers Parser::normalizeLinkUrl
+ * @covers Parser::normalizeUrlComponent
+ */
+ public function testNormalizeLinkUrl( $explanation, $url, $expected ) {
+ $this->assertEquals( $expected, Parser::normalizeLinkUrl( $url ), $explanation );
+ }
+
+ public static function provideNormalizeLinkUrl() {
+ return array(
+ array(
+ 'Escaping of unsafe characters',
+ 'http://example.org/foo bar?param[]="value"&param[]=valüe',
+ 'http://example.org/foo%20bar?param%5B%5D=%22value%22&param%5B%5D=val%C3%BCe',
+ ),
+ array(
+ 'Case normalization of percent-encoded characters',
+ 'http://example.org/%ab%cD%Ef%FF',
+ 'http://example.org/%AB%CD%EF%FF',
+ ),
+ array(
+ 'Unescaping of safe characters',
+ 'http://example.org/%3C%66%6f%6F%3E?%3C%66%6f%6F%3E#%3C%66%6f%6F%3E',
+ 'http://example.org/%3Cfoo%3E?%3Cfoo%3E#%3Cfoo%3E',
+ ),
+ array(
+ 'Context-sensitive replacement of sometimes-safe characters',
+ 'http://example.org/%23%2F%3F%26%3D%2B%3B?%23%2F%3F%26%3D%2B%3B#%23%2F%3F%26%3D%2B%3B',
+ 'http://example.org/%23%2F%3F&=+;?%23/?%26%3D%2B%3B#%23/?&=+;',
+ ),
+ );
+ }
+
+ // @todo Add tests for cleanSig() / cleanSigInSig(), getSection(),
+ // replaceSection(), getPreloadText()
+}
diff --git a/tests/phpunit/includes/parser/ParserOutputTest.php b/tests/phpunit/includes/parser/ParserOutputTest.php
new file mode 100644
index 00000000..c024cee5
--- /dev/null
+++ b/tests/phpunit/includes/parser/ParserOutputTest.php
@@ -0,0 +1,87 @@
+<?php
+
+class ParserOutputTest extends MediaWikiTestCase {
+
+ public static function provideIsLinkInternal() {
+ return array(
+ // Different domains
+ array( false, 'http://example.org', 'http://mediawiki.org' ),
+ // Same domains
+ array( true, 'http://example.org', 'http://example.org' ),
+ array( true, 'https://example.org', 'https://example.org' ),
+ array( true, '//example.org', '//example.org' ),
+ // Same domain different cases
+ array( true, 'http://example.org', 'http://EXAMPLE.ORG' ),
+ // Paths, queries, and fragments are not relevant
+ array( true, 'http://example.org', 'http://example.org/wiki/Main_Page' ),
+ array( true, 'http://example.org', 'http://example.org?my=query' ),
+ array( true, 'http://example.org', 'http://example.org#its-a-fragment' ),
+ // Different protocols
+ array( false, 'http://example.org', 'https://example.org' ),
+ array( false, 'https://example.org', 'http://example.org' ),
+ // Protocol relative servers always match http and https links
+ array( true, '//example.org', 'http://example.org' ),
+ array( true, '//example.org', 'https://example.org' ),
+ // But they don't match strange things like this
+ array( false, '//example.org', 'irc://example.org' ),
+ );
+ }
+
+ /**
+ * Test to make sure ParserOutput::isLinkInternal behaves properly
+ * @dataProvider provideIsLinkInternal
+ * @covers ParserOutput::isLinkInternal
+ */
+ public function testIsLinkInternal( $shouldMatch, $server, $url ) {
+ $this->assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) );
+ }
+
+ /**
+ * @covers ParserOutput::setExtensionData
+ * @covers ParserOutput::getExtensionData
+ */
+ public function testExtensionData() {
+ $po = new ParserOutput();
+
+ $po->setExtensionData( "one", "Foo" );
+
+ $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
+ $this->assertNull( $po->getExtensionData( "spam" ) );
+
+ $po->setExtensionData( "two", "Bar" );
+ $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
+ $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
+
+ $po->setExtensionData( "one", null );
+ $this->assertNull( $po->getExtensionData( "one" ) );
+ $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
+ }
+
+ /**
+ * @covers ParserOutput::setProperty
+ * @covers ParserOutput::getProperty
+ * @covers ParserOutput::unsetProperty
+ * @covers ParserOutput::getProperties
+ */
+ public function testProperties() {
+ $po = new ParserOutput();
+
+ $po->setProperty( 'foo', 'val' );
+
+ $properties = $po->getProperties();
+ $this->assertEquals( $po->getProperty( 'foo' ), 'val' );
+ $this->assertEquals( $properties['foo'], 'val' );
+
+ $po->setProperty( 'foo', 'second val' );
+
+ $properties = $po->getProperties();
+ $this->assertEquals( $po->getProperty( 'foo' ), 'second val' );
+ $this->assertEquals( $properties['foo'], 'second val' );
+
+ $po->unsetProperty( 'foo' );
+
+ $properties = $po->getProperties();
+ $this->assertEquals( $po->getProperty( 'foo' ), false );
+ $this->assertArrayNotHasKey( 'foo', $properties );
+ }
+}
diff --git a/tests/phpunit/includes/parser/ParserPreloadTest.php b/tests/phpunit/includes/parser/ParserPreloadTest.php
new file mode 100644
index 00000000..d12fee36
--- /dev/null
+++ b/tests/phpunit/includes/parser/ParserPreloadTest.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Basic tests for Parser::getPreloadText
+ * @author Antoine Musso
+ */
+class ParserPreloadTest extends MediaWikiTestCase {
+ /**
+ * @var Parser
+ */
+ private $testParser;
+ /**
+ * @var ParserOptions
+ */
+ private $testParserOptions;
+ /**
+ * @var Title
+ */
+ private $title;
+
+ protected function setUp() {
+ global $wgContLang;
+
+ parent::setUp();
+ $this->testParserOptions = ParserOptions::newFromUserAndLang( new User, $wgContLang );
+
+ $this->testParser = new Parser();
+ $this->testParser->Options( $this->testParserOptions );
+ $this->testParser->clearState();
+
+ $this->title = Title::newFromText( 'Preload Test' );
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+
+ unset( $this->testParser );
+ unset( $this->title );
+ }
+
+ /**
+ * @covers Parser::getPreloadText
+ */
+ public function testPreloadSimpleText() {
+ $this->assertPreloaded( 'simple', 'simple' );
+ }
+
+ /**
+ * @covers Parser::getPreloadText
+ */
+ public function testPreloadedPreIsUnstripped() {
+ $this->assertPreloaded(
+ '<pre>monospaced</pre>',
+ '<pre>monospaced</pre>',
+ '<pre> in preloaded text must be unstripped (bug 27467)'
+ );
+ }
+
+ /**
+ * @covers Parser::getPreloadText
+ */
+ public function testPreloadedNowikiIsUnstripped() {
+ $this->assertPreloaded(
+ '<nowiki>[[Dummy title]]</nowiki>',
+ '<nowiki>[[Dummy title]]</nowiki>',
+ '<nowiki> in preloaded text must be unstripped (bug 27467)'
+ );
+ }
+
+ protected function assertPreloaded( $expected, $text, $msg = '' ) {
+ $this->assertEquals(
+ $expected,
+ $this->testParser->getPreloadText(
+ $text,
+ $this->title,
+ $this->testParserOptions
+ ),
+ $msg
+ );
+ }
+}
diff --git a/tests/phpunit/includes/parser/PreprocessorTest.php b/tests/phpunit/includes/parser/PreprocessorTest.php
new file mode 100644
index 00000000..345fd0a5
--- /dev/null
+++ b/tests/phpunit/includes/parser/PreprocessorTest.php
@@ -0,0 +1,247 @@
+<?php
+
+class PreprocessorTest extends MediaWikiTestCase {
+ protected $mTitle = 'Page title';
+ protected $mPPNodeCount = 0;
+ /**
+ * @var ParserOptions
+ */
+ protected $mOptions;
+ /**
+ * @var Preprocessor
+ */
+ protected $mPreprocessor;
+
+ protected function setUp() {
+ global $wgParserConf, $wgContLang;
+ parent::setUp();
+ $this->mOptions = ParserOptions::newFromUserAndLang( new User, $wgContLang );
+ $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' );
+ }
+
+ public static function provideCases() {
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ 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><title>Foo</title></template></root>" ),
+ array( "\n{{Foo}}", "<root>\n<template lineStart=\"1\"><title>Foo</title></template></root>" ),
+ array( "{{Foo|bar}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template></root>" ),
+ array( "{{Foo|bar}}a", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template>a</root>" ),
+ array( "{{Foo|bar|baz}}", "<root><template><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><title>Foo</title><part><name>1</name>=<value>bar</value></part></template></root>" ),
+ array( "{{Foo|=bar}}", "<root><template><title>Foo</title><part><name></name>=<value>bar</value></part></template></root>" ),
+ array( "{{Foo|bar=baz}}", "<root><template><title>Foo</title><part><name>bar</name>=<value>baz</value></part></template></root>" ),
+ array( "{{Foo|{{bar}}=baz}}", "<root><template><title>Foo</title><part><name><template><title>bar</title></template></name>=<value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar|baz}}", "<root><template><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><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><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><title>1</title></tplarg></root>" ),
+ array( "{{{1|}}}", "<root><tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ),
+ array( "{{{Foo}}}", "<root><tplarg><title>Foo</title></tplarg></root>" ),
+ array( "{{{Foo|}}}", "<root><tplarg><title>Foo</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ),
+ array( "{{{Foo|bar|baz}}}", "<root><tplarg><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><title> <template><title>Foo</title></template> </title></tplarg></root>" ),
+ array( "{{ {{{Foo}}} }}", "<root><template><title> <tplarg><title>Foo</title></tplarg> </title></template></root>" ),
+ array( "{{{{{Foo}}}}}", "<root><template><title><tplarg><title>Foo</title></tplarg></title></template></root>" ),
+ array( "{{{{{Foo}} }}}", "<root><tplarg><title><template><title>Foo</title></template> </title></tplarg></root>" ),
+ array( "{{{{{{Foo}}}}}}", "<root><tplarg><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><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 <gallery bar=\"1\" baz=2 />", "<root>Foo <ext><name>gallery</name><attr> bar=&quot;1&quot; baz=2 </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><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><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><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><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><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><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><title>Foo</title><part><name>} Bar</name>=<value></value></part></template></root>" ),
+ /* array( file_get_contents( __DIR__ . '/QuoteQuran.txt' ), file_get_contents( __DIR__ . '/QuoteQuranExpanded.txt' ) ), */
+ );
+ // @codingStandardsIgnoreEnd
+ }
+
+ /**
+ * Get XML preprocessor tree from the preprocessor (which may not be the
+ * native XML-based one).
+ *
+ * @param string $wikiText
+ * @return string
+ */
+ protected function preprocessToXml( $wikiText ) {
+ if ( method_exists( $this->mPreprocessor, 'preprocessToXml' ) ) {
+ return $this->normalizeXml( $this->mPreprocessor->preprocessToXml( $wikiText ) );
+ }
+
+ $dom = $this->mPreprocessor->preprocessToObj( $wikiText );
+ if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+ return $dom->saveXML();
+ } else {
+ return $this->normalizeXml( $dom->__toString() );
+ }
+ }
+
+ /**
+ * Normalize XML string to the form that a DOMDocument saves out.
+ *
+ * @param string $xml
+ * @return string
+ */
+ protected function normalizeXml( $xml ) {
+ return preg_replace( '!<([a-z]+)/>!', '<$1></$1>', str_replace( ' />', '/>', $xml ) );
+ }
+
+ /**
+ * @dataProvider provideCases
+ * @covers Preprocessor_DOM::preprocessToXml
+ */
+ public function testPreprocessorOutput( $wikiText, $expectedXml ) {
+ $this->assertEquals( $this->normalizeXml( $expectedXml ), $this->preprocessToXml( $wikiText ) );
+ }
+
+ /**
+ * These are more complex test cases taken out of wiki articles.
+ */
+ public static function provideFiles() {
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ 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.
+ array( "NestedTemplates" ), # bug 27936
+ );
+ // @codingStandardsIgnoreEnd
+ }
+
+ /**
+ * @dataProvider provideFiles
+ * @covers Preprocessor_DOM::preprocessToXml
+ */
+ public function testPreprocessorOutputFiles( $filename ) {
+ $folder = __DIR__ . "/../../../parser/preprocess";
+ $wikiText = file_get_contents( "$folder/$filename.txt" );
+ $output = $this->preprocessToXml( $wikiText );
+
+ $expectedFilename = "$folder/$filename.expected";
+ if ( file_exists( $expectedFilename ) ) {
+ $expectedXml = $this->normalizeXml( file_get_contents( $expectedFilename ) );
+ $this->assertEquals( $expectedXml, $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
+ */
+ public static function provideHeadings() {
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ 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>" ),
+ 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>" ),
+
+ /* These are not working: */
+ 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>" ),
+ );
+ // @codingStandardsIgnoreEnd
+ }
+
+ /**
+ * @dataProvider provideHeadings
+ * @covers Preprocessor_DOM::preprocessToXml
+ */
+ public function testHeadings( $wikiText, $expectedXml ) {
+ $this->assertEquals( $this->normalizeXml( $expectedXml ), $this->preprocessToXml( $wikiText ) );
+ }
+}
diff --git a/tests/phpunit/includes/parser/TagHooksTest.php b/tests/phpunit/includes/parser/TagHooksTest.php
new file mode 100644
index 00000000..e3c4cc84
--- /dev/null
+++ b/tests/phpunit/includes/parser/TagHooksTest.php
@@ -0,0 +1,108 @@
+<?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" ) );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( 'wgAlwaysUseTidy', false );
+ }
+
+ /**
+ * @dataProvider provideValidNames
+ * @covers Parser::setHook
+ */
+ public function testTagHooks( $tag ) {
+ global $wgParserConf, $wgContLang;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setHook( $tag, array( $this, 'tagCallback' ) );
+ $parserOutput = $parser->parse(
+ "Foo<$tag>Bar</$tag>Baz",
+ Title::newFromText( 'Test' ),
+ ParserOptions::newFromUserAndLang( new User, $wgContLang )
+ );
+ $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() );
+
+ $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle
+ }
+
+ /**
+ * @dataProvider provideBadNames
+ * @expectedException MWException
+ * @covers Parser::setHook
+ */
+ public function testBadTagHooks( $tag ) {
+ global $wgParserConf, $wgContLang;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setHook( $tag, array( $this, 'tagCallback' ) );
+ $parser->parse(
+ "Foo<$tag>Bar</$tag>Baz",
+ Title::newFromText( 'Test' ),
+ ParserOptions::newFromUserAndLang( new User, $wgContLang )
+ );
+ $this->fail( 'Exception not thrown.' );
+ }
+
+ /**
+ * @dataProvider provideValidNames
+ * @covers Parser::setFunctionTagHook
+ */
+ public function testFunctionTagHooks( $tag ) {
+ global $wgParserConf, $wgContLang;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), 0 );
+ $parserOutput = $parser->parse(
+ "Foo<$tag>Bar</$tag>Baz",
+ Title::newFromText( 'Test' ),
+ ParserOptions::newFromUserAndLang( new User, $wgContLang )
+ );
+ $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() );
+
+ $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle
+ }
+
+ /**
+ * @dataProvider provideBadNames
+ * @expectedException MWException
+ * @covers Parser::setFunctionTagHook
+ */
+ public function testBadFunctionTagHooks( $tag ) {
+ global $wgParserConf, $wgContLang;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), SFH_OBJECT_ARGS );
+ $parser->parse(
+ "Foo<$tag>Bar</$tag>Baz",
+ Title::newFromText( 'Test' ),
+ ParserOptions::newFromUserAndLang( new User, $wgContLang )
+ );
+ $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/parser/TidyTest.php b/tests/phpunit/includes/parser/TidyTest.php
new file mode 100644
index 00000000..f656a74d
--- /dev/null
+++ b/tests/phpunit/includes/parser/TidyTest.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @group Parser
+ */
+class TidyTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $check = MWTidy::tidy( '' );
+ if ( strpos( $check, '<!--' ) !== false ) {
+ $this->markTestSkipped( 'Tidy not found' );
+ }
+ }
+
+ /**
+ * @dataProvider provideTestWrapping
+ */
+ public function testTidyWrapping( $expected, $text, $msg = '' ) {
+ $text = MWTidy::tidy( $text );
+ // We don't care about where Tidy wants to stick is <p>s
+ $text = trim( preg_replace( '#</?p>#', '', $text ) );
+ // Windows, we love you!
+ $text = str_replace( "\r", '', $text );
+ $this->assertEquals( $expected, $text, $msg );
+ }
+
+ public static function provideTestWrapping() {
+ $testMathML = <<<'MathML'
+<math xmlns="http://www.w3.org/1998/Math/MathML">
+ <mrow>
+ <mi>a</mi>
+ <mo>&InvisibleTimes;</mo>
+ <msup>
+ <mi>x</mi>
+ <mn>2</mn>
+ </msup>
+ <mo>+</mo>
+ <mi>b</mi>
+ <mo>&InvisibleTimes; </mo>
+ <mi>x</mi>
+ <mo>+</mo>
+ <mi>c</mi>
+ </mrow>
+ </math>
+MathML;
+ return array(
+ array(
+ '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
+ '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
+ '<mw:editsection> should survive tidy'
+ ),
+ array(
+ '<editsection page="foo" section="bar">foo</editsection>',
+ '<editsection page="foo" section="bar">foo</editsection>',
+ '<editsection> should survive tidy'
+ ),
+ array( '<mw:toc>foo</mw:toc>', '<mw:toc>foo</mw:toc>', '<mw:toc> should survive tidy' ),
+ array( "<link foo=\"bar\" />\nfoo", '<link foo="bar"/>foo', '<link> should survive tidy' ),
+ array( "<meta foo=\"bar\" />\nfoo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ),
+ array( $testMathML, $testMathML, '<math> should survive tidy' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/password/BcryptPasswordTest.php b/tests/phpunit/includes/password/BcryptPasswordTest.php
new file mode 100644
index 00000000..8ac419ff
--- /dev/null
+++ b/tests/phpunit/includes/password/BcryptPasswordTest.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @group large
+ */
+class BcryptPasswordTestCase extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return array( 'bcrypt' => array(
+ 'class' => 'BcryptPassword',
+ 'cost' => 9,
+ ) );
+ }
+
+ public static function providePasswordTests() {
+ /** @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong */
+ return array(
+ // Tests from glibc bcrypt implementation
+ array( true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "U*U" ),
+ array( true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$VGOzA784oUp/Z0DY336zx7pLYAy0lwK', "U*U*" ),
+ array( true, ':bcrypt:5$XXXXXXXXXXXXXXXXXXXXXO$AcXxm9kjPGEMsLznoKqmqw7tc8WCx4a', "U*U*U" ),
+ array( true, ':bcrypt:5$abcdefghijklmnopqrstuu$5s2v8.iXieOjg/.AySBTTZIIVFJeBui', "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789chars after 72 are ignored" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$CE5elHaaO4EbggVDjb8P19RukzXSM3e', "\xff\xff\xa3" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq', "\xa3" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq', "\xa3" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$o./n25XVfn6oAPaUvHe.Csk4zRfsYPi', "\xff\xa334\xff\xff\xff\xa3345" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$nRht2l/HRhr6zmCp9vYUvvsqynflf9e', "\xff\xa3345" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$nRht2l/HRhr6zmCp9vYUvvsqynflf9e', "\xff\xa3345" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$6IflQkJytoRVc1yuaNtHfiuq.FRlSIS', "\xa3ab" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$6IflQkJytoRVc1yuaNtHfiuq.FRlSIS', "\xa3ab" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6', "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaachars after 72 are ignored as usual" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy', "\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55" ),
+ array( true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe', "\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff" ),
+ array( true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy', "" ),
+ // One or two false sanity tests
+ array( false, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "UXU" ),
+ array( false, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "" ),
+ );
+ /** @codingStandardsIgnoreEnd */
+ }
+}
diff --git a/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php b/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php
new file mode 100644
index 00000000..86e8270a
--- /dev/null
+++ b/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php
@@ -0,0 +1,51 @@
+<?php
+
+class LayeredParameterizedPasswordTest extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return array(
+ 'testLargeLayeredTop' => array(
+ 'class' => 'LayeredParameterizedPassword',
+ 'types' => array(
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredFinal',
+ ),
+ ),
+ 'testLargeLayeredBottom' => array(
+ 'class' => 'Pbkdf2Password',
+ 'algo' => 'sha512',
+ 'cost' => 1024,
+ 'length' => 512,
+ ),
+ 'testLargeLayeredFinal' => array(
+ 'class' => 'BcryptPassword',
+ 'cost' => 5,
+ )
+ );
+ }
+
+ public static function providePasswordTests() {
+ /** @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong */
+ return array(
+ array( true, ':testLargeLayeredTop:sha512:1024:512!sha512:1024:512!sha512:1024:512!sha512:1024:512!5!vnRy+2SrSA0fHt3dwhTP5g==!AVnwfZsAQjn+gULv7FSGjA==!xvHUX3WcpkeSn1lvjWcvBg==!It+OC/N9tu+d3ByHhuB0BQ==!Tb.gqUOiD.aWktVwHM.Q/O!7CcyMfXUPky5ptyATJsR2nq3vUqtnBC', 'testPassword123' ),
+ );
+ /** @codingStandardsIgnoreEnd */
+ }
+
+ /**
+ * @covers LayeredParameterizedPassword::partialCrypt
+ */
+ public function testLargeLayeredPartialUpdate() {
+ /** @var ParameterizedPassword $partialPassword */
+ $partialPassword = $this->passwordFactory->newFromType( 'testLargeLayeredBottom' );
+ $partialPassword->crypt( 'testPassword123' );
+
+ /** @var LayeredParameterizedPassword $totalPassword */
+ $totalPassword = $this->passwordFactory->newFromType( 'testLargeLayeredTop' );
+ $totalPassword->partialCrypt( $partialPassword );
+
+ $this->assertTrue( $totalPassword->equals( 'testPassword123' ) );
+ }
+}
diff --git a/tests/phpunit/includes/password/PasswordTestCase.php b/tests/phpunit/includes/password/PasswordTestCase.php
new file mode 100644
index 00000000..ef16f1c4
--- /dev/null
+++ b/tests/phpunit/includes/password/PasswordTestCase.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ * Testing framework for the password hashes
+ *
+ * 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
+ */
+
+/**
+ * @since 1.24
+ */
+abstract class PasswordTestCase extends MediaWikiTestCase {
+ /**
+ * @var PasswordFactory
+ */
+ protected $passwordFactory;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->passwordFactory = new PasswordFactory();
+ foreach ( $this->getTypeConfigs() as $type => $config ) {
+ $this->passwordFactory->register( $type, $config );
+ }
+ }
+
+ /**
+ * Return an array of configs to be used for this class's password type.
+ *
+ * @return array[]
+ */
+ abstract protected function getTypeConfigs();
+
+ /**
+ * An array of tests in the form of (bool, string, string), where the first
+ * element is whether the second parameter (a password hash) and the third
+ * parameter (a password) should match.
+ *
+ * @return array
+ */
+ abstract public static function providePasswordTests();
+
+ /**
+ * @dataProvider providePasswordTests
+ */
+ public function testHashing( $shouldMatch, $hash, $password ) {
+ $hash = $this->passwordFactory->newFromCiphertext( $hash );
+ $password = $this->passwordFactory->newFromPlaintext( $password, $hash );
+ $this->assertSame( $shouldMatch, $hash->equals( $password ) );
+ }
+
+ /**
+ * @dataProvider providePasswordTests
+ */
+ public function testStringSerialization( $shouldMatch, $hash, $password ) {
+ $hashObj = $this->passwordFactory->newFromCiphertext( $hash );
+ $serialized = $hashObj->toString();
+ $unserialized = $this->passwordFactory->newFromCiphertext( $serialized );
+ $this->assertTrue( $hashObj->equals( $unserialized ) );
+ }
+
+ /**
+ * @dataProvider providePasswordTests
+ * @covers InvalidPassword::equals
+ * @covers InvalidPassword::toString
+ */
+ public function testInvalidUnequalNormal( $shouldMatch, $hash, $password ) {
+ $invalid = $this->passwordFactory->newFromCiphertext( null );
+ $normal = $this->passwordFactory->newFromCiphertext( $hash );
+
+ $this->assertFalse( $invalid->equals( $normal ) );
+ $this->assertFalse( $normal->equals( $invalid ) );
+ }
+}
diff --git a/tests/phpunit/includes/password/Pbkdf2PasswordTest.php b/tests/phpunit/includes/password/Pbkdf2PasswordTest.php
new file mode 100644
index 00000000..091853e1
--- /dev/null
+++ b/tests/phpunit/includes/password/Pbkdf2PasswordTest.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @group large
+ */
+class Pbkdf2PasswordTest extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return array( 'pbkdf2' => array(
+ 'class' => 'Pbkdf2Password',
+ 'algo' => 'sha256',
+ 'cost' => '10000',
+ 'length' => '128',
+ ) );
+ }
+
+ public static function providePasswordTests() {
+ return array(
+ array( true, ":pbkdf2:sha1:1:20:c2FsdA==:DGDID5YfDnHzqbUkr2ASBi/gN6Y=", 'password' ),
+ array( true, ":pbkdf2:sha1:2:20:c2FsdA==:6mwBTcctb4zNHtkqzh1B8NjeiVc=", 'password' ),
+ array( true, ":pbkdf2:sha1:4096:20:c2FsdA==:SwB5AbdlSJq+rUnZJvch0GWkKcE=", 'password' ),
+ array( true, ":pbkdf2:sha1:4096:16:c2EAbHQ=:Vvpqp1VICZ3MN9fwNCXgww==", "pass\x00word" ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/poolcounter/PoolCounterTest.php b/tests/phpunit/includes/poolcounter/PoolCounterTest.php
new file mode 100644
index 00000000..019e532c
--- /dev/null
+++ b/tests/phpunit/includes/poolcounter/PoolCounterTest.php
@@ -0,0 +1,72 @@
+<?php
+
+// We will use this class with getMockForAbstractClass to create a concrete mock class.
+// That call will die if the contructor is not public, unless we use disableOriginalConstructor(),
+// in which case we could not test the constructor.
+abstract class PoolCounterAbstractMock extends PoolCounter {
+ public function __construct() {
+ call_user_func_array( 'parent::__construct', func_get_args() );
+ }
+}
+
+class PoolCounterTest extends MediaWikiTestCase {
+ public function testConstruct() {
+ $poolCounterConfig = array(
+ 'class' => 'PoolCounterMock',
+ 'timeout' => 10,
+ 'workers' => 10,
+ 'maxqueue' => 100,
+ );
+
+ $poolCounter = $this->getMockBuilder( 'PoolCounterAbstractMock' )
+ ->setConstructorArgs( array( $poolCounterConfig, 'testCounter', 'someKey' ) )
+ // don't mock anything - the proper syntax would be setMethods(null), but due
+ // to a PHPUnit bug that does not work with getMockForAbstractClass()
+ ->setMethods( array( 'idontexist' ) )
+ ->getMockForAbstractClass();
+ $this->assertInstanceOf( 'PoolCounter', $poolCounter );
+ }
+
+ public function testConstructWithSlots() {
+ $poolCounterConfig = array(
+ 'class' => 'PoolCounterMock',
+ 'timeout' => 10,
+ 'workers' => 10,
+ 'slots' => 2,
+ 'maxqueue' => 100,
+ );
+
+ $poolCounter = $this->getMockBuilder( 'PoolCounterAbstractMock' )
+ ->setConstructorArgs( array( $poolCounterConfig, 'testCounter', 'key' ) )
+ ->setMethods( array( 'idontexist' ) ) // don't mock anything
+ ->getMockForAbstractClass();
+ $this->assertInstanceOf( 'PoolCounter', $poolCounter );
+ }
+
+ public function testHashKeyIntoSlots() {
+ $poolCounter = $this->getMockBuilder( 'PoolCounterAbstractMock' )
+ // don't mock anything - the proper syntax would be setMethods(null), but due
+ // to a PHPUnit bug that does not work with getMockForAbstractClass()
+ ->setMethods( array( 'idontexist' ) )
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $hashKeyIntoSlots = new ReflectionMethod( $poolCounter, 'hashKeyIntoSlots' );
+ $hashKeyIntoSlots->setAccessible( true );
+
+ $keysWithTwoSlots = $keysWithFiveSlots = array();
+ foreach ( range( 1, 100 ) as $i ) {
+ $keysWithTwoSlots[] = $hashKeyIntoSlots->invoke( $poolCounter, 'key ' . $i, 2 );
+ $keysWithFiveSlots[] = $hashKeyIntoSlots->invoke( $poolCounter, 'key ' . $i, 5 );
+ }
+
+ $this->assertArrayEquals( range( 0, 1 ), array_unique( $keysWithTwoSlots ) );
+ $this->assertArrayEquals( range( 0, 4 ), array_unique( $keysWithFiveSlots ) );
+
+ // make sure it is deterministic
+ $this->assertEquals(
+ $hashKeyIntoSlots->invoke( $poolCounter, 'asdfgh', 1000 ),
+ $hashKeyIntoSlots->invoke( $poolCounter, 'asdfgh', 1000 )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php
new file mode 100644
index 00000000..b0edaaf7
--- /dev/null
+++ b/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php
@@ -0,0 +1,132 @@
+<?php
+
+class ResourceLoaderModuleTest extends ResourceLoaderTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ // The return value of the closure shouldn't matter since this test should
+ // never call it
+ SkinFactory::getDefaultInstance()->register(
+ 'fakeskin',
+ 'FakeSkin',
+ function () {
+ }
+ );
+ }
+
+ /**
+ * @covers ResourceLoaderFileModule::getAllSkinStyleFiles
+ */
+ public function testGetAllSkinStyleFiles() {
+ $context = self::getResourceLoaderContext();
+
+ $baseParams = array(
+ 'scripts' => array(
+ 'foo.js',
+ 'bar.js',
+ ),
+ 'styles' => array(
+ 'foo.css',
+ 'bar.css' => array( 'media' => 'print' ),
+ 'screen.less' => array( 'media' => 'screen' ),
+ 'screen-query.css' => array( 'media' => 'screen and (min-width: 400px)' ),
+ ),
+ 'skinStyles' => array(
+ 'default' => 'quux-fallback.less',
+ 'fakeskin' => array(
+ 'baz-vector.css',
+ 'quux-vector.less',
+ ),
+ ),
+ 'messages' => array(
+ 'hello',
+ 'world',
+ ),
+ );
+
+ $module = new ResourceLoaderFileModule( $baseParams );
+
+ $this->assertEquals(
+ array(
+ 'foo.css',
+ 'baz-vector.css',
+ 'quux-vector.less',
+ 'quux-fallback.less',
+ 'bar.css',
+ 'screen.less',
+ 'screen-query.css',
+ ),
+ array_map( 'basename', $module->getAllStyleFiles() )
+ );
+ }
+
+ /**
+ * @covers ResourceLoaderModule::getDefinitionSummary
+ * @covers ResourceLoaderFileModule::getDefinitionSummary
+ */
+ public function testDefinitionSummary() {
+ $context = self::getResourceLoaderContext();
+
+ $baseParams = array(
+ 'scripts' => array( 'foo.js', 'bar.js' ),
+ 'dependencies' => array( 'jquery', 'mediawiki' ),
+ 'messages' => array( 'hello', 'world' ),
+ );
+
+ $module = new ResourceLoaderFileModule( $baseParams );
+
+ $jsonSummary = json_encode( $module->getDefinitionSummary( $context ) );
+
+ // Exactly the same
+ $module = new ResourceLoaderFileModule( $baseParams );
+
+ $this->assertEquals(
+ $jsonSummary,
+ json_encode( $module->getDefinitionSummary( $context ) ),
+ 'Instance is insignificant'
+ );
+
+ // Re-order dependencies
+ $module = new ResourceLoaderFileModule( array(
+ 'dependencies' => array( 'mediawiki', 'jquery' ),
+ ) + $baseParams );
+
+ $this->assertEquals(
+ $jsonSummary,
+ json_encode( $module->getDefinitionSummary( $context ) ),
+ 'Order of dependencies is insignificant'
+ );
+
+ // Re-order messages
+ $module = new ResourceLoaderFileModule( array(
+ 'messages' => array( 'world', 'hello' ),
+ ) + $baseParams );
+
+ $this->assertEquals(
+ $jsonSummary,
+ json_encode( $module->getDefinitionSummary( $context ) ),
+ 'Order of messages is insignificant'
+ );
+
+ // Re-order scripts
+ $module = new ResourceLoaderFileModule( array(
+ 'scripts' => array( 'bar.js', 'foo.js' ),
+ ) + $baseParams );
+
+ $this->assertNotEquals(
+ $jsonSummary,
+ json_encode( $module->getDefinitionSummary( $context ) ),
+ 'Order of scripts is significant'
+ );
+
+ // Subclass
+ $module = new ResourceLoaderFileModuleTestModule( $baseParams );
+
+ $this->assertNotEquals(
+ $jsonSummary,
+ json_encode( $module->getDefinitionSummary( $context ) ),
+ 'Class is significant'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php
new file mode 100644
index 00000000..a1893873
--- /dev/null
+++ b/tests/phpunit/includes/resourceloader/ResourceLoaderStartupModuleTest.php
@@ -0,0 +1,388 @@
+<?php
+
+class ResourceLoaderStartupModuleTest extends ResourceLoaderTestCase {
+
+ public static function provideGetModuleRegistrations() {
+ return array(
+ array( array(
+ 'msg' => 'Empty registry',
+ 'modules' => array(),
+ 'out' => '
+mw.loader.addSource( {
+ "local": "/w/load.php"
+} );mw.loader.register( [] );'
+ ) ),
+ array( array(
+ 'msg' => 'Basic registry',
+ 'modules' => array(
+ 'test.blank' => new ResourceLoaderTestModule(),
+ ),
+ 'out' => '
+mw.loader.addSource( {
+ "local": "/w/load.php"
+} );mw.loader.register( [
+ [
+ "test.blank",
+ "1388534400"
+ ]
+] );',
+ ) ),
+ array( array(
+ 'msg' => 'Group signature',
+ 'modules' => array(
+ 'test.blank' => new ResourceLoaderTestModule(),
+ 'test.group.foo' => new ResourceLoaderTestModule( array( 'group' => 'x-foo' ) ),
+ 'test.group.bar' => new ResourceLoaderTestModule( array( 'group' => 'x-bar' ) ),
+ ),
+ 'out' => '
+mw.loader.addSource( {
+ "local": "/w/load.php"
+} );mw.loader.register( [
+ [
+ "test.blank",
+ "1388534400"
+ ],
+ [
+ "test.group.foo",
+ "1388534400",
+ [],
+ "x-foo"
+ ],
+ [
+ "test.group.bar",
+ "1388534400",
+ [],
+ "x-bar"
+ ]
+] );'
+ ) ),
+ array( array(
+ 'msg' => 'Different target (non-test should not be registered)',
+ 'modules' => array(
+ 'test.blank' => new ResourceLoaderTestModule(),
+ 'test.target.foo' => new ResourceLoaderTestModule( array( 'targets' => array( 'x-foo' ) ) ),
+ ),
+ 'out' => '
+mw.loader.addSource( {
+ "local": "/w/load.php"
+} );mw.loader.register( [
+ [
+ "test.blank",
+ "1388534400"
+ ]
+] );'
+ ) ),
+ array( array(
+ 'msg' => 'Foreign source',
+ 'sources' => array(
+ 'example' => array(
+ 'loadScript' => 'http://example.org/w/load.php',
+ 'apiScript' => 'http://example.org/w/api.php',
+ ),
+ ),
+ 'modules' => array(
+ 'test.blank' => new ResourceLoaderTestModule( array( 'source' => 'example' ) ),
+ ),
+ 'out' => '
+mw.loader.addSource( {
+ "local": "/w/load.php",
+ "example": "http://example.org/w/load.php"
+} );mw.loader.register( [
+ [
+ "test.blank",
+ "1388534400",
+ [],
+ null,
+ "example"
+ ]
+] );'
+ ) ),
+ array( array(
+ 'msg' => 'Conditional dependency function',
+ 'modules' => array(
+ 'test.x.core' => new ResourceLoaderTestModule(),
+ 'test.x.polyfill' => new ResourceLoaderTestModule( array(
+ 'skipFunction' => 'return true;'
+ ) ),
+ 'test.y.polyfill' => new ResourceLoaderTestModule( array(
+ 'skipFunction' =>
+ 'return !!(' .
+ ' window.JSON &&' .
+ ' JSON.parse &&' .
+ ' JSON.stringify' .
+ ');'
+ ) ),
+ 'test.z.foo' => new ResourceLoaderTestModule( array(
+ 'dependencies' => array(
+ 'test.x.core',
+ 'test.x.polyfil',
+ 'test.y.polyfil',
+ ),
+ ) ),
+ ),
+ 'out' => '
+mw.loader.addSource( {
+ "local": "/w/load.php"
+} );mw.loader.register( [
+ [
+ "test.x.core",
+ "1388534400"
+ ],
+ [
+ "test.x.polyfill",
+ "1388534400",
+ [],
+ null,
+ "local",
+ "return true;"
+ ],
+ [
+ "test.y.polyfill",
+ "1388534400",
+ [],
+ null,
+ "local",
+ "return !!( window.JSON \u0026\u0026 JSON.parse \u0026\u0026 JSON.stringify);"
+ ],
+ [
+ "test.z.foo",
+ "1388534400",
+ [
+ "test.x.core",
+ "test.x.polyfil",
+ "test.y.polyfil"
+ ]
+ ]
+] );',
+ ) ),
+ array( array(
+ // This may seem like an edge case, but a plain MediaWiki core install
+ // with a few extensions installed is likely far more complex than this
+ // even, not to mention an install like Wikipedia.
+ // TODO: Make this even more realistic.
+ 'msg' => 'Advanced (everything combined)',
+ 'sources' => array(
+ 'example' => array(
+ 'loadScript' => 'http://example.org/w/load.php',
+ 'apiScript' => 'http://example.org/w/api.php',
+ ),
+ ),
+ 'modules' => array(
+ 'test.blank' => new ResourceLoaderTestModule(),
+ 'test.x.core' => new ResourceLoaderTestModule(),
+ 'test.x.util' => new ResourceLoaderTestModule( array(
+ 'dependencies' => array(
+ 'test.x.core',
+ ),
+ ) ),
+ 'test.x.foo' => new ResourceLoaderTestModule( array(
+ 'dependencies' => array(
+ 'test.x.core',
+ ),
+ ) ),
+ 'test.x.bar' => new ResourceLoaderTestModule( array(
+ 'dependencies' => array(
+ 'test.x.core',
+ 'test.x.util',
+ ),
+ ) ),
+ 'test.x.quux' => new ResourceLoaderTestModule( array(
+ 'dependencies' => array(
+ 'test.x.foo',
+ 'test.x.bar',
+ 'test.x.util',
+ 'test.x.unknown',
+ ),
+ ) ),
+ 'test.group.foo.1' => new ResourceLoaderTestModule( array(
+ 'group' => 'x-foo',
+ ) ),
+ 'test.group.foo.2' => new ResourceLoaderTestModule( array(
+ 'group' => 'x-foo',
+ ) ),
+ 'test.group.bar.1' => new ResourceLoaderTestModule( array(
+ 'group' => 'x-bar',
+ ) ),
+ 'test.group.bar.2' => new ResourceLoaderTestModule( array(
+ 'group' => 'x-bar',
+ 'source' => 'example',
+ ) ),
+ 'test.target.foo' => new ResourceLoaderTestModule( array(
+ 'targets' => array( 'x-foo' ),
+ ) ),
+ 'test.target.bar' => new ResourceLoaderTestModule( array(
+ 'source' => 'example',
+ 'targets' => array( 'x-foo' ),
+ ) ),
+ ),
+ 'out' => '
+mw.loader.addSource( {
+ "local": "/w/load.php",
+ "example": "http://example.org/w/load.php"
+} );mw.loader.register( [
+ [
+ "test.blank",
+ "1388534400"
+ ],
+ [
+ "test.x.core",
+ "1388534400"
+ ],
+ [
+ "test.x.util",
+ "1388534400",
+ [
+ "test.x.core"
+ ]
+ ],
+ [
+ "test.x.foo",
+ "1388534400",
+ [
+ "test.x.core"
+ ]
+ ],
+ [
+ "test.x.bar",
+ "1388534400",
+ [
+ "test.x.util"
+ ]
+ ],
+ [
+ "test.x.quux",
+ "1388534400",
+ [
+ "test.x.foo",
+ "test.x.bar",
+ "test.x.unknown"
+ ]
+ ],
+ [
+ "test.group.foo.1",
+ "1388534400",
+ [],
+ "x-foo"
+ ],
+ [
+ "test.group.foo.2",
+ "1388534400",
+ [],
+ "x-foo"
+ ],
+ [
+ "test.group.bar.1",
+ "1388534400",
+ [],
+ "x-bar"
+ ],
+ [
+ "test.group.bar.2",
+ "1388534400",
+ [],
+ "x-bar",
+ "example"
+ ]
+] );'
+ ) ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetModuleRegistrations
+ * @covers ResourceLoaderStartupModule::optimizeDependencies
+ * @covers ResourceLoaderStartUpModule::getModuleRegistrations
+ * @covers ResourceLoader::makeLoaderSourcesScript
+ * @covers ResourceLoader::makeLoaderRegisterScript
+ */
+ public function testGetModuleRegistrations( $case ) {
+ if ( isset( $case['sources'] ) ) {
+ $this->setMwGlobals( 'wgResourceLoaderSources', $case['sources'] );
+ }
+
+ $context = self::getResourceLoaderContext();
+ $rl = $context->getResourceLoader();
+
+ $rl->register( $case['modules'] );
+
+ $module = new ResourceLoaderStartUpModule();
+ $this->assertEquals(
+ ltrim( $case['out'], "\n" ),
+ $module->getModuleRegistrations( $context ),
+ $case['msg']
+ );
+ }
+
+ public static function provideRegistrations() {
+ return array(
+ array( array(
+ 'test.blank' => new ResourceLoaderTestModule(),
+ 'test.min' => new ResourceLoaderTestModule( array(
+ 'skipFunction' =>
+ 'return !!(' .
+ ' window.JSON &&' .
+ ' JSON.parse &&' .
+ ' JSON.stringify' .
+ ');',
+ 'dependencies' => array(
+ 'test.blank',
+ ),
+ ) ),
+ ) )
+ );
+ }
+ /**
+ * @dataProvider provideRegistrations
+ */
+ public function testRegistrationsMinified( $modules ) {
+ $this->setMwGlobals( 'wgResourceLoaderDebug', false );
+
+ $context = self::getResourceLoaderContext();
+ $rl = $context->getResourceLoader();
+ $rl->register( $modules );
+ $module = new ResourceLoaderStartUpModule();
+ $this->assertEquals(
+'mw.loader.addSource({"local":"/w/load.php"});'
+. 'mw.loader.register(['
+. '["test.blank","1388534400"],'
+. '["test.min","1388534400",["test.blank"],null,"local",'
+. '"return!!(window.JSON\u0026\u0026JSON.parse\u0026\u0026JSON.stringify);"'
+. ']]);',
+ $module->getModuleRegistrations( $context ),
+ 'Minified output'
+ );
+ }
+
+ /**
+ * @dataProvider provideRegistrations
+ */
+ public function testRegistrationsUnminified( $modules ) {
+ $context = self::getResourceLoaderContext();
+ $rl = $context->getResourceLoader();
+ $rl->register( $modules );
+ $module = new ResourceLoaderStartUpModule();
+ $this->assertEquals(
+'mw.loader.addSource( {
+ "local": "/w/load.php"
+} );mw.loader.register( [
+ [
+ "test.blank",
+ "1388534400"
+ ],
+ [
+ "test.min",
+ "1388534400",
+ [
+ "test.blank"
+ ],
+ null,
+ "local",
+ "return !!( window.JSON \u0026\u0026 JSON.parse \u0026\u0026 JSON.stringify);"
+ ]
+] );',
+ $module->getModuleRegistrations( $context ),
+ 'Unminified output'
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
new file mode 100644
index 00000000..f19f6886
--- /dev/null
+++ b/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
@@ -0,0 +1,249 @@
+<?php
+
+class ResourceLoaderTest extends ResourceLoaderTestCase {
+
+ protected static $resourceLoaderRegisterModulesHook;
+
+ protected function setUp() {
+ parent::setUp();
+
+ // $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths; $wgResourceLoaderLESSVars;
+
+ $this->setMwGlobals( array(
+ 'wgResourceLoaderLESSFunctions' => array(
+ 'test-sum' => function ( $frame, $less ) {
+ $sum = 0;
+ foreach ( $frame[2] as $arg ) {
+ $sum += (int)$arg[1];
+ }
+ return $sum;
+ },
+ ),
+ 'wgResourceLoaderLESSImportPaths' => array(
+ dirname( dirname( __DIR__ ) ) . '/data/less/common',
+ ),
+ 'wgResourceLoaderLESSVars' => array(
+ 'foo' => '2px',
+ 'Foo' => '#eeeeee',
+ 'bar' => 5,
+ ),
+ ) );
+ }
+
+ /* Hook Methods */
+
+ /**
+ * ResourceLoaderRegisterModules hook
+ */
+ public static function resourceLoaderRegisterModules( &$resourceLoader ) {
+ self::$resourceLoaderRegisterModulesHook = true;
+
+ return true;
+ }
+
+ /* Provider Methods */
+ public static 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 ) );
+ }
+
+ /**
+ * @covers ResourceLoaderFileModule::compileLessFile
+ */
+ public function testLessFileCompilation() {
+ $context = self::getResourceLoaderContext();
+ $basePath = __DIR__ . '/../../data/less/module';
+ $module = new ResourceLoaderFileModule( array(
+ 'localBasePath' => $basePath,
+ 'styles' => array( 'styles.less' ),
+ ) );
+ $module->setName( 'test.less' );
+ $styles = $module->getStyles( $context );
+ $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
+ }
+
+ /**
+ * Strip @noflip annotations from CSS code.
+ * @param string $css
+ * @return string
+ */
+ private function stripNoflip( $css ) {
+ return str_replace( '/*@noflip*/ ', '', $css );
+ }
+
+ /**
+ * What happens when you mix @embed and @noflip?
+ * This really is an integration test, but oh well.
+ */
+ public function testMixedCssAnnotations( ) {
+ $basePath = __DIR__ . '/../../data/css';
+ $testModule = new ResourceLoaderFileModule( array(
+ 'localBasePath' => $basePath,
+ 'styles' => array( 'test.css' ),
+ ) );
+ $expectedModule = new ResourceLoaderFileModule( array(
+ 'localBasePath' => $basePath,
+ 'styles' => array( 'expected.css' ),
+ ) );
+
+ $contextLtr = self::getResourceLoaderContext( 'en' );
+ $contextRtl = self::getResourceLoaderContext( 'he' );
+
+ // Since we want to compare the effect of @noflip+@embed against the effect of just @embed, and
+ // the @noflip annotations are always preserved, we need to strip them first.
+ $this->assertEquals(
+ $expectedModule->getStyles( $contextLtr ),
+ $this->stripNoflip( $testModule->getStyles( $contextLtr ) ),
+ "/*@noflip*/ with /*@embed*/ gives correct results in LTR mode"
+ );
+ $this->assertEquals(
+ $expectedModule->getStyles( $contextLtr ),
+ $this->stripNoflip( $testModule->getStyles( $contextRtl ) ),
+ "/*@noflip*/ with /*@embed*/ gives correct results in RTL mode"
+ );
+ }
+
+ /**
+ * @dataProvider providePackedModules
+ * @covers ResourceLoader::makePackedModulesString
+ */
+ public function testMakePackedModulesString( $desc, $modules, $packed ) {
+ $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc );
+ }
+
+ /**
+ * @dataProvider providePackedModules
+ * @covers ResourceLoaderContext::expandModuleNames
+ */
+ public function testexpandModuleNames( $desc, $modules, $packed ) {
+ $this->assertEquals( $modules, ResourceLoaderContext::expandModuleNames( $packed ), $desc );
+ }
+
+ public static 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',
+ ),
+ array(
+ 'Prefixless modules after a prefixed module',
+ array( 'single.module', 'foobar', 'foobaz' ),
+ 'single.module|foobar,foobaz',
+ ),
+ );
+ }
+
+ public static function provideAddSource() {
+ return array(
+ array( 'examplewiki', '//example.org/w/load.php', 'examplewiki' ),
+ array( 'example2wiki', array( 'loadScript' => '//example.com/w/load.php' ), 'example2wiki' ),
+ array(
+ array( 'foowiki' => '//foo.org/w/load.php', 'bazwiki' => '//baz.org/w/load.php' ),
+ null,
+ array( 'foowiki', 'bazwiki' )
+ ),
+ array(
+ array( 'foowiki' => '//foo.org/w/load.php' ),
+ null,
+ false,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideAddSource
+ * @covers ResourceLoader::addSource
+ */
+ public function testAddSource( $name, $info, $expected ) {
+ $rl = new ResourceLoader;
+ if ( $expected === false ) {
+ $this->setExpectedException( 'MWException', 'ResourceLoader duplicate source addition error' );
+ $rl->addSource( $name, $info );
+ }
+ $rl->addSource( $name, $info );
+ if ( is_array( $expected ) ) {
+ foreach ( $expected as $source ) {
+ $this->assertArrayHasKey( $source, $rl->getSources() );
+ }
+ } else {
+ $this->assertArrayHasKey( $expected, $rl->getSources() );
+ }
+ }
+
+ public static function fakeSources() {
+ return array(
+ 'examplewiki' => array(
+ 'loadScript' => '//example.org/w/load.php',
+ 'apiScript' => '//example.org/w/api.php',
+ ),
+ 'example2wiki' => array(
+ 'loadScript' => '//example.com/w/load.php',
+ 'apiScript' => '//example.com/w/api.php',
+ ),
+ );
+ }
+
+ /**
+ * @covers ResourceLoader::getLoadScript
+ */
+ public function testGetLoadScript() {
+ $this->setMwGlobals( 'wgResourceLoaderSources', array() );
+ $rl = new ResourceLoader();
+ $sources = self::fakeSources();
+ $rl->addSource( $sources );
+ foreach ( array( 'examplewiki', 'example2wiki' ) as $name ) {
+ $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
+ }
+
+ try {
+ $rl->getLoadScript( 'thiswasneverreigstered' );
+ $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
+ } catch ( MWException $e ) {
+ $this->assertTrue( true );
+ }
+ }
+}
+
+/* Hooks */
+global $wgHooks;
+$wgHooks['ResourceLoaderRegisterModules'][] = 'ResourceLoaderTest::resourceLoaderRegisterModules';
diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php
new file mode 100644
index 00000000..9dc18050
--- /dev/null
+++ b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php
@@ -0,0 +1,67 @@
+<?php
+
+class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase {
+
+ /**
+ * @covers ResourceLoaderWikiModule::isKnownEmpty
+ * @dataProvider provideIsKnownEmpty
+ */
+ public function testIsKnownEmpty( $titleInfo, $group, $expected ) {
+ $module = $this->getMockBuilder( 'ResourceLoaderWikiModuleTestModule' )
+ ->setMethods( array( 'getTitleInfo', 'getGroup' ) )
+ ->getMock();
+ $module->expects( $this->any() )
+ ->method( 'getTitleInfo' )
+ ->will( $this->returnValue( $titleInfo ) );
+ $module->expects( $this->any() )
+ ->method( 'getGroup' )
+ ->will( $this->returnValue( $group ) );
+ $context = $this->getMockBuilder( 'ResourceLoaderContext' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->assertEquals( $expected, $module->isKnownEmpty( $context ) );
+ }
+
+ public static function provideIsKnownEmpty() {
+ return array(
+ // No valid pages
+ array( array(), 'test1', true ),
+ // 'site' module with a non-empty page
+ array(
+ array(
+ 'MediaWiki:Common.js' => array(
+ 'timestamp' => 123456789,
+ 'length' => 1234
+ )
+ ), 'site', false,
+ ),
+ // 'site' module with an empty page
+ array(
+ array(
+ 'MediaWiki:Monobook.js' => array(
+ 'timestamp' => 987654321,
+ 'length' => 0,
+ ),
+ ), 'site', false,
+ ),
+ // 'user' module with a non-empty page
+ array(
+ array(
+ 'User:FooBar/common.js' => array(
+ 'timestamp' => 246813579,
+ 'length' => 25,
+ ),
+ ), 'user', false,
+ ),
+ // 'user' module with an empty page
+ array(
+ array(
+ 'User:FooBar/monobook.js' => array(
+ 'timestamp' => 1357924680,
+ 'length' => 0,
+ ),
+ ), 'user', true,
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/search/SearchEngineTest.php b/tests/phpunit/includes/search/SearchEngineTest.php
new file mode 100644
index 00000000..3da13615
--- /dev/null
+++ b/tests/phpunit/includes/search/SearchEngineTest.php
@@ -0,0 +1,187 @@
+<?php
+
+/**
+ * @group Search
+ * @group Database
+ *
+ * @covers SearchEngine<extended>
+ * @note Coverage will only ever show one of on of the Search* classes
+ */
+class SearchEngineTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var SearchEngine
+ */
+ protected $search;
+
+ protected $pageList;
+
+ /**
+ * Checks for database type & version.
+ * Will skip current test if DB does not support search.
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ // Search tests require MySQL or SQLite with FTS
+ $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->setMwGlobals( array(
+ 'wgSearchType' => $searchType
+ ) );
+
+ if ( !isset( self::$pageList ) ) {
+ $this->addPages();
+ }
+
+ $this->search = new $searchType( $this->db );
+ }
+
+ protected function tearDown() {
+ unset( $this->search );
+
+ parent::tearDown();
+ }
+
+ protected function addPages() {
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ // @todo cover the case of non-wikitext content in the main namespace
+ 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 );
+ }
+
+ protected function fetchIds( $results ) {
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ $this->markTestIncomplete( __CLASS__ . " does no yet support non-wikitext content "
+ . "in the main namespace" );
+ }
+ $this->assertTrue( is_object( $results ) );
+
+ $matches = array();
+ $row = $results->next();
+ while ( $row ) {
+ $matches[] = $row->getTitle()->getPrefixedText();
+ $row = $results->next();
+ }
+ $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 string $pageName Page name
+ * @param string $text Page's content
+ * @param int $ns Unused
+ */
+ protected function insertPage( $pageName, $text, $ns ) {
+ $title = Title::newFromText( $pageName, $ns );
+
+ $user = User::newFromName( 'WikiSysop' );
+ $comment = 'Search Test';
+
+ // avoid memory leak...?
+ LinkCache::singleton()->clear();
+
+ $page = WikiPage::factory( $title );
+ $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
+
+ $this->pageList[] = array( $title, $page->getId() );
+
+ return true;
+ }
+
+ public 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" );
+ }
+
+ public function testTextSearch() {
+ $this->assertEquals(
+ array( 'Smithee' ),
+ $this->fetchIds( $this->search->searchText( 'smithee' ) ),
+ "Plain search failed" );
+ }
+
+ public 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" );
+ }
+
+ public function testTitleSearch() {
+ $this->assertEquals(
+ array(
+ 'Alan Smithee',
+ 'Smithee',
+ ),
+ $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
+ "Title search failed" );
+ }
+
+ public 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..c6275371
--- /dev/null
+++ b/tests/phpunit/includes/search/SearchUpdateTest.php
@@ -0,0 +1,81 @@
+<?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
+ * @group Database
+ */
+class SearchUpdateTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( 'wgSearchType', 'MockSearch' );
+ }
+
+ public function updateText( $text ) {
+ return trim( SearchUpdate::updateText( $text ) );
+ }
+
+ /**
+ * @covers SearchUpdate::updateText
+ */
+ public 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'
+ );
+ }
+
+ /**
+ * @covers SearchUpdate::updateText
+ * @todo give this test a real name explaining what is being tested here
+ */
+ public function testBug32712() {
+ $text = "text „http://example.com“ text";
+ $result = $this->updateText( $text );
+ $processed = preg_replace( '/Q/u', 'Q', $result );
+ $this->assertTrue(
+ $processed != '',
+ 'Link surrounded by unicode quotes should not fail UTF-8 validation'
+ );
+ }
+}
diff --git a/tests/phpunit/includes/site/MediaWikiSiteTest.php b/tests/phpunit/includes/site/MediaWikiSiteTest.php
new file mode 100644
index 00000000..c3fd1557
--- /dev/null
+++ b/tests/phpunit/includes/site/MediaWikiSiteTest.php
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * Tests for the MediaWikiSite class.
+ *
+ * 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
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class MediaWikiSiteTest extends SiteTest {
+
+ public function testNormalizePageTitle() {
+ $this->setMwGlobals( array(
+ 'wgCapitalLinks' => true,
+ ) );
+
+ $site = new MediaWikiSite();
+ $site->setGlobalId( 'enwiki' );
+
+ //NOTE: this does not actually call out to the enwiki site to perform the normalization,
+ // but uses a local Title object to do so. This is hardcoded on SiteLink::normalizePageTitle
+ // for the case that MW_PHPUNIT_TEST is set.
+ $this->assertEquals( 'Foo', $site->normalizePageName( ' foo ' ) );
+ }
+
+ public function fileUrlProvider() {
+ return array(
+ // url, filepath, path arg, expected
+ array( 'https://en.wikipedia.org', '/w/$1', 'api.php', 'https://en.wikipedia.org/w/api.php' ),
+ array( 'https://en.wikipedia.org', '/w/', 'api.php', 'https://en.wikipedia.org/w/' ),
+ array(
+ 'https://en.wikipedia.org',
+ '/foo/page.php?name=$1',
+ 'api.php',
+ 'https://en.wikipedia.org/foo/page.php?name=api.php'
+ ),
+ array(
+ 'https://en.wikipedia.org',
+ '/w/$1',
+ '',
+ 'https://en.wikipedia.org/w/'
+ ),
+ array(
+ 'https://en.wikipedia.org',
+ '/w/$1',
+ 'foo/bar/api.php',
+ 'https://en.wikipedia.org/w/foo/bar/api.php'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider fileUrlProvider
+ * @covers MediaWikiSite::getFileUrl
+ */
+ public function testGetFileUrl( $url, $filePath, $pathArgument, $expected ) {
+ $site = new MediaWikiSite();
+ $site->setFilePath( $url . $filePath );
+
+ $this->assertEquals( $expected, $site->getFileUrl( $pathArgument ) );
+ }
+
+ public static function provideGetPageUrl() {
+ return array(
+ // path, page, expected substring
+ array( 'http://acme.test/wiki/$1', 'Berlin', '/wiki/Berlin' ),
+ array( 'http://acme.test/wiki/', 'Berlin', '/wiki/' ),
+ array( 'http://acme.test/w/index.php?title=$1', 'Berlin', '/w/index.php?title=Berlin' ),
+ array( 'http://acme.test/wiki/$1', '', '/wiki/' ),
+ array( 'http://acme.test/wiki/$1', 'Berlin/sub page', '/wiki/Berlin/sub_page' ),
+ array( 'http://acme.test/wiki/$1', 'Cork (city) ', '/Cork_(city)' ),
+ array( 'http://acme.test/wiki/$1', 'M&M', '/wiki/M%26M' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetPageUrl
+ * @covers MediaWikiSite::getPageUrl
+ */
+ public function testGetPageUrl( $path, $page, $expected ) {
+ $site = new MediaWikiSite();
+ $site->setLinkPath( $path );
+
+ $this->assertContains( $path, $site->getPageUrl() );
+ $this->assertContains( $expected, $site->getPageUrl( $page ) );
+ }
+}
diff --git a/tests/phpunit/includes/site/SiteListTest.php b/tests/phpunit/includes/site/SiteListTest.php
new file mode 100644
index 00000000..534ed9c9
--- /dev/null
+++ b/tests/phpunit/includes/site/SiteListTest.php
@@ -0,0 +1,240 @@
+<?php
+
+/**
+ * Tests for the SiteList class.
+ *
+ * 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
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class SiteListTest extends MediaWikiTestCase {
+
+ /**
+ * Returns instances of SiteList implementing objects.
+ * @return array
+ */
+ public function siteListProvider() {
+ $sitesArrays = $this->siteArrayProvider();
+
+ $listInstances = array();
+
+ foreach ( $sitesArrays as $sitesArray ) {
+ $listInstances[] = new SiteList( $sitesArray[0] );
+ }
+
+ return $this->arrayWrap( $listInstances );
+ }
+
+ /**
+ * Returns arrays with instances of Site implementing objects.
+ * @return array
+ */
+ public function siteArrayProvider() {
+ $sites = TestSites::getSites();
+
+ $siteArrays = array();
+
+ $siteArrays[] = $sites;
+
+ $siteArrays[] = array( array_shift( $sites ) );
+
+ $siteArrays[] = array( array_shift( $sites ), array_shift( $sites ) );
+
+ return $this->arrayWrap( $siteArrays );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::isEmpty
+ */
+ public function testIsEmpty( SiteList $sites ) {
+ $this->assertEquals( count( $sites ) === 0, $sites->isEmpty() );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::getSite
+ */
+ public function testGetSiteByGlobalId( SiteList $sites ) {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $this->assertEquals( $site, $sites->getSite( $site->getGlobalId() ) );
+ }
+
+ $this->assertTrue( true );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::getSiteByInternalId
+ */
+ public function testGetSiteByInternalId( $sites ) {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ if ( is_integer( $site->getInternalId() ) ) {
+ $this->assertEquals( $site, $sites->getSiteByInternalId( $site->getInternalId() ) );
+ }
+ }
+
+ $this->assertTrue( true );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::getSiteByNavigationId
+ */
+ public function testGetSiteByNavigationId( $sites ) {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $ids = $site->getNavigationIds();
+ foreach ( $ids as $navId ) {
+ $this->assertEquals( $site, $sites->getSiteByNavigationId( $navId ) );
+ }
+ }
+
+ $this->assertTrue( true );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::hasSite
+ */
+ public function testHasGlobalId( $sites ) {
+ $this->assertFalse( $sites->hasSite( 'non-existing-global-id' ) );
+ $this->assertFalse( $sites->hasInternalId( 720101010 ) );
+
+ if ( !$sites->isEmpty() ) {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) );
+ }
+ }
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::hasInternalId
+ */
+ public function testHasInternallId( $sites ) {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ if ( is_integer( $site->getInternalId() ) ) {
+ $this->assertTrue( $site, $sites->hasInternalId( $site->getInternalId() ) );
+ }
+ }
+
+ $this->assertFalse( $sites->hasInternalId( -1 ) );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::hasNavigationId
+ */
+ public function testHasNavigationId( $sites ) {
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $ids = $site->getNavigationIds();
+ foreach ( $ids as $navId ) {
+ $this->assertTrue( $sites->hasNavigationId( $navId ) );
+ }
+ }
+
+ $this->assertFalse( $sites->hasNavigationId( 'non-existing-navigation-id' ) );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ * @param SiteList $sites
+ * @covers SiteList::getGlobalIdentifiers
+ */
+ public function testGetGlobalIdentifiers( SiteList $sites ) {
+ $identifiers = $sites->getGlobalIdentifiers();
+
+ $this->assertTrue( is_array( $identifiers ) );
+
+ $expected = array();
+
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $expected[] = $site->getGlobalId();
+ }
+
+ $this->assertArrayEquals( $expected, $identifiers );
+ }
+
+ /**
+ * @dataProvider siteListProvider
+ *
+ * @since 1.21
+ *
+ * @param SiteList $list
+ * @covers SiteList::getSerializationData
+ * @covers SiteList::unserialize
+ */
+ public function testSerialization( SiteList $list ) {
+ $serialization = serialize( $list );
+ /**
+ * @var SiteArray $copy
+ */
+ $copy = unserialize( $serialization );
+
+ $this->assertArrayEquals( $list->getGlobalIdentifiers(), $copy->getGlobalIdentifiers() );
+
+ /**
+ * @var Site $site
+ */
+ foreach ( $list as $site ) {
+ $this->assertTrue( $copy->hasInternalId( $site->getInternalId() ) );
+
+ foreach ( $site->getNavigationIds() as $navId ) {
+ $this->assertTrue(
+ $copy->hasNavigationId( $navId ),
+ 'unserialized data expects nav id ' . $navId . ' for site ' . $site->getGlobalId()
+ );
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/site/SiteSQLStoreTest.php b/tests/phpunit/includes/site/SiteSQLStoreTest.php
new file mode 100644
index 00000000..6002c1a1
--- /dev/null
+++ b/tests/phpunit/includes/site/SiteSQLStoreTest.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * Tests for the SiteSQLStore class.
+ *
+ * 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
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class SiteSQLStoreTest extends MediaWikiTestCase {
+
+ /**
+ * @covers SiteSQLStore::getSites
+ */
+ public function testGetSites() {
+ $expectedSites = TestSites::getSites();
+ TestSites::insertIntoDb();
+
+ $store = SiteSQLStore::newInstance();
+
+ $sites = $store->getSites();
+
+ $this->assertInstanceOf( 'SiteList', $sites );
+
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $this->assertInstanceOf( 'Site', $site );
+ }
+
+ foreach ( $expectedSites as $site ) {
+ if ( $site->getGlobalId() !== null ) {
+ $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) );
+ }
+ }
+ }
+
+ /**
+ * @covers SiteSQLStore::saveSites
+ */
+ public function testSaveSites() {
+ $store = SiteSQLStore::newInstance();
+
+ $sites = array();
+
+ $site = new Site();
+ $site->setGlobalId( 'ertrywuutr' );
+ $site->setLanguageCode( 'en' );
+ $sites[] = $site;
+
+ $site = new MediaWikiSite();
+ $site->setGlobalId( 'sdfhxujgkfpth' );
+ $site->setLanguageCode( 'nl' );
+ $sites[] = $site;
+
+ $this->assertTrue( $store->saveSites( $sites ) );
+
+ $site = $store->getSite( 'ertrywuutr' );
+ $this->assertInstanceOf( 'Site', $site );
+ $this->assertEquals( 'en', $site->getLanguageCode() );
+ $this->assertTrue( is_integer( $site->getInternalId() ) );
+ $this->assertTrue( $site->getInternalId() >= 0 );
+
+ $site = $store->getSite( 'sdfhxujgkfpth' );
+ $this->assertInstanceOf( 'Site', $site );
+ $this->assertEquals( 'nl', $site->getLanguageCode() );
+ $this->assertTrue( is_integer( $site->getInternalId() ) );
+ $this->assertTrue( $site->getInternalId() >= 0 );
+ }
+
+ /**
+ * @covers SiteSQLStore::reset
+ */
+ public function testReset() {
+ $store1 = SiteSQLStore::newInstance();
+ $store2 = SiteSQLStore::newInstance();
+
+ // initialize internal cache
+ $this->assertGreaterThan( 0, $store1->getSites()->count() );
+ $this->assertGreaterThan( 0, $store2->getSites()->count() );
+
+ // Clear actual data. Will purge the external cache and reset the internal
+ // cache in $store1, but not the internal cache in store2.
+ $this->assertTrue( $store1->clear() );
+
+ // sanity check: $store2 should have a stale cache now
+ $this->assertNotNull( $store2->getSite( 'enwiki' ) );
+
+ // purge cache
+ $store2->reset();
+
+ // ...now the internal cache of $store2 should be updated and thus empty.
+ $site = $store2->getSite( 'enwiki' );
+ $this->assertNull( $site );
+ }
+
+ /**
+ * @covers SiteSQLStore::clear
+ */
+ public function testClear() {
+ $store = SiteSQLStore::newInstance();
+ $this->assertTrue( $store->clear() );
+
+ $site = $store->getSite( 'enwiki' );
+ $this->assertNull( $site );
+
+ $sites = $store->getSites();
+ $this->assertEquals( 0, $sites->count() );
+ }
+}
diff --git a/tests/phpunit/includes/site/SiteTest.php b/tests/phpunit/includes/site/SiteTest.php
new file mode 100644
index 00000000..29c1ff33
--- /dev/null
+++ b/tests/phpunit/includes/site/SiteTest.php
@@ -0,0 +1,296 @@
+<?php
+
+/**
+ * Tests for the Site class.
+ *
+ * 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
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class SiteTest extends MediaWikiTestCase {
+
+ public function instanceProvider() {
+ return $this->arrayWrap( TestSites::getSites() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getInterwikiIds
+ */
+ public function testGetInterwikiIds( Site $site ) {
+ $this->assertInternalType( 'array', $site->getInterwikiIds() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getNavigationIds
+ */
+ public function testGetNavigationIds( Site $site ) {
+ $this->assertInternalType( 'array', $site->getNavigationIds() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::addNavigationId
+ */
+ public function testAddNavigationId( Site $site ) {
+ $site->addNavigationId( 'foobar' );
+ $this->assertTrue( in_array( 'foobar', $site->getNavigationIds(), true ) );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::addInterwikiId
+ */
+ public function testAddInterwikiId( Site $site ) {
+ $site->addInterwikiId( 'foobar' );
+ $this->assertTrue( in_array( 'foobar', $site->getInterwikiIds(), true ) );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getLanguageCode
+ */
+ public function testGetLanguageCode( Site $site ) {
+ $this->assertTypeOrValue( 'string', $site->getLanguageCode(), null );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::setLanguageCode
+ */
+ public function testSetLanguageCode( Site $site ) {
+ $site->setLanguageCode( 'en' );
+ $this->assertEquals( 'en', $site->getLanguageCode() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::normalizePageName
+ */
+ public function testNormalizePageName( Site $site ) {
+ $this->assertInternalType( 'string', $site->normalizePageName( 'Foobar' ) );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getGlobalId
+ */
+ public function testGetGlobalId( Site $site ) {
+ $this->assertTypeOrValue( 'string', $site->getGlobalId(), null );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::setGlobalId
+ */
+ public function testSetGlobalId( Site $site ) {
+ $site->setGlobalId( 'foobar' );
+ $this->assertEquals( 'foobar', $site->getGlobalId() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getType
+ */
+ public function testGetType( Site $site ) {
+ $this->assertInternalType( 'string', $site->getType() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getPath
+ */
+ public function testGetPath( Site $site ) {
+ $this->assertTypeOrValue( 'string', $site->getPath( 'page_path' ), null );
+ $this->assertTypeOrValue( 'string', $site->getPath( 'file_path' ), null );
+ $this->assertTypeOrValue( 'string', $site->getPath( 'foobar' ), null );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::getAllPaths
+ */
+ public function testGetAllPaths( Site $site ) {
+ $this->assertInternalType( 'array', $site->getAllPaths() );
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::setPath
+ * @covers Site::removePath
+ */
+ public function testSetAndRemovePath( Site $site ) {
+ $count = count( $site->getAllPaths() );
+
+ $site->setPath( 'spam', 'http://www.wikidata.org/$1' );
+ $site->setPath( 'spam', 'http://www.wikidata.org/foo/$1' );
+ $site->setPath( 'foobar', 'http://www.wikidata.org/bar/$1' );
+
+ $this->assertEquals( $count + 2, count( $site->getAllPaths() ) );
+
+ $this->assertInternalType( 'string', $site->getPath( 'foobar' ) );
+ $this->assertEquals( 'http://www.wikidata.org/foo/$1', $site->getPath( 'spam' ) );
+
+ $site->removePath( 'spam' );
+ $site->removePath( 'foobar' );
+
+ $this->assertEquals( $count, count( $site->getAllPaths() ) );
+
+ $this->assertNull( $site->getPath( 'foobar' ) );
+ $this->assertNull( $site->getPath( 'spam' ) );
+ }
+
+ /**
+ * @covers Site::setLinkPath
+ */
+ public function testSetLinkPath() {
+ $site = new Site();
+ $path = "TestPath/$1";
+
+ $site->setLinkPath( $path );
+ $this->assertEquals( $path, $site->getLinkPath() );
+ }
+
+ /**
+ * @covers Site::getLinkPathType
+ */
+ public function testGetLinkPathType() {
+ $site = new Site();
+
+ $path = 'TestPath/$1';
+ $site->setLinkPath( $path );
+ $this->assertEquals( $path, $site->getPath( $site->getLinkPathType() ) );
+
+ $path = 'AnotherPath/$1';
+ $site->setPath( $site->getLinkPathType(), $path );
+ $this->assertEquals( $path, $site->getLinkPath() );
+ }
+
+ /**
+ * @covers Site::setPath
+ */
+ public function testSetPath() {
+ $site = new Site();
+
+ $path = 'TestPath/$1';
+ $site->setPath( 'foo', $path );
+
+ $this->assertEquals( $path, $site->getPath( 'foo' ) );
+ }
+
+ /**
+ * @covers Site::setPath
+ * @covers Site::getProtocol
+ */
+ public function testProtocolRelativePath() {
+ $site = new Site();
+
+ $type = $site->getLinkPathType();
+ $path = '//acme.com/'; // protocol-relative URL
+ $site->setPath( $type, $path );
+
+ $this->assertEquals( '', $site->getProtocol() );
+ }
+
+ public static function provideGetPageUrl() {
+ //NOTE: the assumption that the URL is built by replacing $1
+ // with the urlencoded version of $page
+ // is true for Site but not guaranteed for subclasses.
+ // Subclasses need to override this provider appropriately.
+
+ return array(
+ array( #0
+ 'http://acme.test/TestPath/$1',
+ 'Foo',
+ '/TestPath/Foo',
+ ),
+ array( #1
+ 'http://acme.test/TestScript?x=$1&y=bla',
+ 'Foo',
+ 'TestScript?x=Foo&y=bla',
+ ),
+ array( #2
+ 'http://acme.test/TestPath/$1',
+ 'foo & bar/xyzzy (quux-shmoox?)',
+ '/TestPath/foo%20%26%20bar%2Fxyzzy%20%28quux-shmoox%3F%29',
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetPageUrl
+ * @covers Site::getPageUrl
+ */
+ public function testGetPageUrl( $path, $page, $expected ) {
+ $site = new Site();
+
+ //NOTE: the assumption that getPageUrl is based on getLinkPath
+ // is true for Site but not guaranteed for subclasses.
+ // Subclasses need to override this test case appropriately.
+ $site->setLinkPath( $path );
+ $this->assertContains( $path, $site->getPageUrl() );
+
+ $this->assertContains( $expected, $site->getPageUrl( $page ) );
+ }
+
+ protected function assertTypeOrFalse( $type, $value ) {
+ if ( $value === false ) {
+ $this->assertTrue( true );
+ } else {
+ $this->assertInternalType( $type, $value );
+ }
+ }
+
+ /**
+ * @dataProvider instanceProvider
+ * @param Site $site
+ * @covers Site::serialize
+ * @covers Site::unserialize
+ */
+ public function testSerialization( Site $site ) {
+ $this->assertInstanceOf( 'Serializable', $site );
+
+ $serialization = serialize( $site );
+ $newInstance = unserialize( $serialization );
+
+ $this->assertInstanceOf( 'Site', $newInstance );
+
+ $this->assertEquals( $serialization, serialize( $newInstance ) );
+ }
+}
diff --git a/tests/phpunit/includes/site/TestSites.php b/tests/phpunit/includes/site/TestSites.php
new file mode 100644
index 00000000..af314ba2
--- /dev/null
+++ b/tests/phpunit/includes/site/TestSites.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * Holds sites for testing purposes.
+ *
+ * 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
+ * @since 1.21
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class TestSites {
+
+ /**
+ * @since 1.21
+ *
+ * @return array
+ */
+ public static function getSites() {
+ $sites = array();
+
+ $site = new Site();
+ $site->setGlobalId( 'foobar' );
+ $sites[] = $site;
+
+ $site = new MediaWikiSite();
+ $site->setGlobalId( 'enwiktionary' );
+ $site->setGroup( 'wiktionary' );
+ $site->setLanguageCode( 'en' );
+ $site->addNavigationId( 'enwiktionary' );
+ $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" );
+ $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" );
+ $sites[] = $site;
+
+ $site = new MediaWikiSite();
+ $site->setGlobalId( 'dewiktionary' );
+ $site->setGroup( 'wiktionary' );
+ $site->setLanguageCode( 'de' );
+ $site->addInterwikiId( 'dewiktionary' );
+ $site->addInterwikiId( 'wiktionaryde' );
+ $site->setPath( MediaWikiSite::PATH_PAGE, "https://de.wiktionary.org/wiki/$1" );
+ $site->setPath( MediaWikiSite::PATH_FILE, "https://de.wiktionary.org/w/$1" );
+ $sites[] = $site;
+
+ $site = new Site();
+ $site->setGlobalId( 'spam' );
+ $site->setGroup( 'spam' );
+ $site->setLanguageCode( 'en' );
+ $site->addNavigationId( 'spam' );
+ $site->addNavigationId( 'spamz' );
+ $site->addInterwikiId( 'spamzz' );
+ $site->setLinkPath( "http://spamzz.test/testing/" );
+ $sites[] = $site;
+
+ /**
+ * Add at least one right-to-left language (current RTL languages in MediaWiki core are:
+ * aeb, ar, arc, arz, azb, bcc, bqi, ckb, dv, en_rtl, fa, glk, he, khw, kk_arab, kk_cn,
+ * ks_arab, ku_arab, lrc, mzn, pnb, ps, sd, ug_arab, ur, yi).
+ */
+ $languageCodes = array(
+ 'de',
+ 'en',
+ 'fa', //right-to-left
+ 'nl',
+ 'nn',
+ 'no',
+ 'sr',
+ 'sv',
+ );
+ foreach ( $languageCodes as $langCode ) {
+ $site = new MediaWikiSite();
+ $site->setGlobalId( $langCode . 'wiki' );
+ $site->setGroup( 'wikipedia' );
+ $site->setLanguageCode( $langCode );
+ $site->addInterwikiId( $langCode );
+ $site->addNavigationId( $langCode );
+ $site->setPath( MediaWikiSite::PATH_PAGE, "https://$langCode.wikipedia.org/wiki/$1" );
+ $site->setPath( MediaWikiSite::PATH_FILE, "https://$langCode.wikipedia.org/w/$1" );
+ $sites[] = $site;
+ }
+
+ return $sites;
+ }
+
+ /**
+ * Inserts sites into the database for the unit tests that need them.
+ *
+ * @since 0.1
+ */
+ public static function insertIntoDb() {
+ $sitesTable = SiteSQLStore::newInstance();
+ $sitesTable->clear();
+ $sitesTable->saveSites( TestSites::getSites() );
+ }
+}
diff --git a/tests/phpunit/includes/skins/SkinFactoryTest.php b/tests/phpunit/includes/skins/SkinFactoryTest.php
new file mode 100644
index 00000000..d3663c84
--- /dev/null
+++ b/tests/phpunit/includes/skins/SkinFactoryTest.php
@@ -0,0 +1,70 @@
+<?php
+
+class SkinFactoryTest extends MediaWikiTestCase {
+
+ /**
+ * @covers SkinFactory::register
+ */
+ public function testRegister() {
+ $factory = new SkinFactory();
+ $factory->register( 'fallback', 'Fallback', function () {
+ return new SkinFallback();
+ } );
+ $this->assertTrue( true ); // No exception thrown
+ $this->setExpectedException( 'InvalidArgumentException' );
+ $factory->register( 'invalid', 'Invalid', 'Invalid callback' );
+ }
+
+ /**
+ * @covers SkinFactory::makeSkin
+ */
+ public function testMakeSkinWithNoBuilders() {
+ $factory = new SkinFactory();
+ $this->setExpectedException( 'SkinException' );
+ $factory->makeSkin( 'nobuilderregistered' );
+ }
+
+ /**
+ * @covers SkinFactory::makeSkin
+ */
+ public function testMakeSkinWithInvalidCallback() {
+ $factory = new SkinFactory();
+ $factory->register( 'unittest', 'Unittest', function () {
+ return true; // Not a Skin object
+ } );
+ $this->setExpectedException( 'UnexpectedValueException' );
+ $factory->makeSkin( 'unittest' );
+ }
+
+ /**
+ * @covers SkinFactory::makeSkin
+ */
+ public function testMakeSkinWithValidCallback() {
+ $factory = new SkinFactory();
+ $factory->register( 'testfallback', 'TestFallback', function () {
+ return new SkinFallback();
+ } );
+
+ $skin = $factory->makeSkin( 'testfallback' );
+ $this->assertInstanceOf( 'Skin', $skin );
+ $this->assertInstanceOf( 'SkinFallback', $skin );
+ }
+
+ /**
+ * @covers SkinFactory::getSkinNames
+ */
+ public function testGetSkinNames() {
+ $factory = new SkinFactory();
+ // A fake callback we can use that will never be called
+ $callback = function () {
+ // NOP
+ };
+ $factory->register( 'skin1', 'Skin1', $callback );
+ $factory->register( 'skin2', 'Skin2', $callback );
+ $names = $factory->getSkinNames();
+ $this->assertArrayHasKey( 'skin1', $names );
+ $this->assertArrayHasKey( 'skin2', $names );
+ $this->assertEquals( 'Skin1', $names['skin1'] );
+ $this->assertEquals( 'Skin2', $names['skin2'] );
+ }
+}
diff --git a/tests/phpunit/includes/skins/SkinTemplateTest.php b/tests/phpunit/includes/skins/SkinTemplateTest.php
new file mode 100644
index 00000000..baa995d4
--- /dev/null
+++ b/tests/phpunit/includes/skins/SkinTemplateTest.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @covers SkinTemplate
+ *
+ * @group Output
+ *
+ * @licence GNU GPL v2+
+ * @author Bene* < benestar.wikimedia@gmail.com >
+ */
+
+class SkinTemplateTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider makeListItemProvider
+ */
+ public function testMakeListItem( $expected, $key, $item, $options, $message ) {
+ $template = $this->getMockForAbstractClass( 'BaseTemplate' );
+
+ $this->assertEquals(
+ $expected,
+ $template->makeListItem( $key, $item, $options ),
+ $message
+ );
+ }
+
+ public function makeListItemProvider() {
+ return array(
+ array(
+ '<li class="class" title="itemtitle"><a href="url" title="title">text</a></li>',
+ '',
+ array(
+ 'class' => 'class',
+ 'itemtitle' => 'itemtitle',
+ 'href' => 'url',
+ 'title' => 'title',
+ 'text' => 'text'
+ ),
+ array(),
+ 'Test makteListItem with normal values'
+ )
+ );
+ }
+}
diff --git a/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php b/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php
new file mode 100644
index 00000000..779fa558
--- /dev/null
+++ b/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php
@@ -0,0 +1,225 @@
+<?php
+/**
+ * Factory for handling the special page list and generating SpecialPage objects.
+ *
+ * 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
+ *
+ * @covers SpecialPageFactory
+ * @group SpecialPage
+ */
+class SpecialPageFactoryTest extends MediaWikiTestCase {
+
+ protected function tearDown() {
+ parent::tearDown();
+
+ SpecialPageFactory::resetList();
+ }
+
+ public function newSpecialAllPages() {
+ return new SpecialAllPages();
+ }
+
+ public function specialPageProvider() {
+ return array(
+ 'class name' => array( 'SpecialAllPages', false ),
+ 'closure' => array( function() {
+ return new SpecialAllPages();
+ }, false ),
+ 'function' => array( array( $this, 'newSpecialAllPages' ), false ),
+ );
+ }
+
+ /**
+ * @dataProvider specialPageProvider
+ */
+ public function testGetPage( $spec, $shouldReuseInstance ) {
+ $this->mergeMwGlobalArrayValue( 'wgSpecialPages', array( 'testdummy' => $spec ) );
+ SpecialPageFactory::resetList();
+
+ $page = SpecialPageFactory::getPage( 'testdummy' );
+ $this->assertInstanceOf( 'SpecialPage', $page );
+
+ $page2 = SpecialPageFactory::getPage( 'testdummy' );
+ $this->assertEquals( $shouldReuseInstance, $page2 === $page, "Should re-use instance:" );
+ }
+
+ public function testGetNames() {
+ $this->mergeMwGlobalArrayValue( 'wgSpecialPages', array( 'testdummy' => 'SpecialAllPages' ) );
+ SpecialPageFactory::resetList();
+
+ $names = SpecialPageFactory::getNames();
+ $this->assertInternalType( 'array', $names );
+ $this->assertContains( 'testdummy', $names );
+ }
+
+ public function testResolveAlias() {
+ $this->setMwGlobals( 'wgContLang', Language::factory( 'de' ) );
+ SpecialPageFactory::resetList();
+
+ list( $name, $param ) = SpecialPageFactory::resolveAlias( 'Spezialseiten/Foo' );
+ $this->assertEquals( 'Specialpages', $name );
+ $this->assertEquals( 'Foo', $param );
+ }
+
+ public function testGetLocalNameFor() {
+ $this->setMwGlobals( 'wgContLang', Language::factory( 'de' ) );
+ SpecialPageFactory::resetList();
+
+ $name = SpecialPageFactory::getLocalNameFor( 'Specialpages', 'Foo' );
+ $this->assertEquals( 'Spezialseiten/Foo', $name );
+ }
+
+ public function testGetTitleForAlias() {
+ $this->setMwGlobals( 'wgContLang', Language::factory( 'de' ) );
+ SpecialPageFactory::resetList();
+
+ $title = SpecialPageFactory::getTitleForAlias( 'Specialpages/Foo' );
+ $this->assertEquals( 'Spezialseiten/Foo', $title->getText() );
+ $this->assertEquals( NS_SPECIAL, $title->getNamespace() );
+ }
+
+ /**
+ * @dataProvider provideTestConflictResolution
+ */
+ public function testConflictResolution(
+ $test, $aliasesList, $alias, $expectedName, $expectedAlias, $expectWarnings
+ ) {
+ global $wgContLang;
+ $lang = clone $wgContLang;
+ $lang->mExtendedSpecialPageAliases = $aliasesList;
+ $this->setMwGlobals( 'wgContLang', $lang );
+ $this->setMwGlobals( 'wgSpecialPages',
+ array_combine( array_keys( $aliasesList ), array_keys( $aliasesList ) )
+ );
+ SpecialPageFactory::resetList();
+
+ // Catch the warnings we expect to be raised
+ $warnings = array();
+ $this->setMwGlobals( 'wgDevelopmentWarnings', true );
+ set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) {
+ if ( preg_match( '/First alias \'[^\']*\' for .*/', $errstr ) ||
+ preg_match( '/Did not find a usable alias for special page .*/', $errstr )
+ ) {
+ $warnings[] = $errstr;
+ return true;
+ }
+ return false;
+ } );
+ $reset = new ScopedCallback( 'restore_error_handler' );
+
+ list( $name, /*...*/ ) = SpecialPageFactory::resolveAlias( $alias );
+ $this->assertEquals( $expectedName, $name, "$test: Alias to name" );
+ $result = SpecialPageFactory::getLocalNameFor( $name );
+ $this->assertEquals( $expectedAlias, $result, "$test: Alias to name to alias" );
+
+ $gotWarnings = count( $warnings );
+ if ( $gotWarnings !== $expectWarnings ) {
+ $this->fail( "Expected $expectWarnings warning(s), but got $gotWarnings:\n" .
+ join( "\n", $warnings )
+ );
+ }
+ }
+
+ /**
+ * @dataProvider provideTestConflictResolution
+ */
+ public function testConflictResolutionReversed(
+ $test, $aliasesList, $alias, $expectedName, $expectedAlias, $expectWarnings
+ ) {
+ // Make sure order doesn't matter by reversing the list
+ $aliasesList = array_reverse( $aliasesList );
+ return $this->testConflictResolution(
+ $test, $aliasesList, $alias, $expectedName, $expectedAlias, $expectWarnings
+ );
+ }
+
+ public function provideTestConflictResolution() {
+ return array(
+ array(
+ 'Canonical name wins',
+ array( 'Foo' => array( 'Foo', 'Bar' ), 'Baz' => array( 'Foo', 'BazPage', 'Baz2' ) ),
+ 'Foo',
+ 'Foo',
+ 'Foo',
+ 1,
+ ),
+
+ array(
+ 'Doesn\'t redirect to a different special page\'s canonical name',
+ array( 'Foo' => array( 'Foo', 'Bar' ), 'Baz' => array( 'Foo', 'BazPage', 'Baz2' ) ),
+ 'Baz',
+ 'Baz',
+ 'BazPage',
+ 1,
+ ),
+
+ array(
+ 'Canonical name wins even if not aliased',
+ array( 'Foo' => array( 'FooPage' ), 'Baz' => array( 'Foo', 'BazPage', 'Baz2' ) ),
+ 'Foo',
+ 'Foo',
+ 'FooPage',
+ 1,
+ ),
+
+ array(
+ 'Doesn\'t redirect to a different special page\'s canonical name even if not aliased',
+ array( 'Foo' => array( 'FooPage' ), 'Baz' => array( 'Foo', 'BazPage', 'Baz2' ) ),
+ 'Baz',
+ 'Baz',
+ 'BazPage',
+ 1,
+ ),
+
+ array(
+ 'First local name beats non-first',
+ array( 'First' => array( 'Foo' ), 'NonFirst' => array( 'Bar', 'Foo' ) ),
+ 'Foo',
+ 'First',
+ 'Foo',
+ 0,
+ ),
+
+ array(
+ 'Doesn\'t redirect to a different special page\'s first alias',
+ array(
+ 'Foo' => array( 'Foo' ),
+ 'First' => array( 'Bar' ),
+ 'Baz' => array( 'Foo', 'Bar', 'BazPage', 'Baz2' )
+ ),
+ 'Baz',
+ 'Baz',
+ 'BazPage',
+ 1,
+ ),
+
+ array(
+ 'Doesn\'t redirect wrong even if all aliases conflict',
+ array(
+ 'Foo' => array( 'Foo' ),
+ 'First' => array( 'Bar' ),
+ 'Baz' => array( 'Foo', 'Bar' )
+ ),
+ 'Baz',
+ 'Baz',
+ 'Baz',
+ 2,
+ ),
+
+ );
+ }
+
+}
diff --git a/tests/phpunit/includes/specials/ImageListPagerTest.php b/tests/phpunit/includes/specials/ImageListPagerTest.php
new file mode 100644
index 00000000..22bdefdf
--- /dev/null
+++ b/tests/phpunit/includes/specials/ImageListPagerTest.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Test class for ImageListPagerTest class.
+ *
+ * Copyright © 2013, Antoine Musso
+ * Copyright © 2013, Siebrand Mazeland
+ * Copyright © 2013, Wikimedia Foundation Inc.
+ *
+ * @group Database
+ */
+
+class ImageListPagerTest extends MediaWikiTestCase {
+ /**
+ * @expectedException MWException
+ * @expectedExceptionMessage invalid_field
+ * @covers ImageListPager::formatValue
+ */
+ public function testFormatValuesThrowException() {
+ $page = new ImageListPager( RequestContext::getMain() );
+ $page->formatValue( 'invalid_field', 'invalid_value' );
+ }
+}
diff --git a/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php
new file mode 100644
index 00000000..f92dc66f
--- /dev/null
+++ b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Test class to run the query of most of all our special pages
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ * @group Database
+ */
+
+/**
+ * @covers QueryPage<extended>
+ */
+class QueryAllSpecialPagesTest extends MediaWikiTestCase {
+
+ /** List query pages that can not be tested automatically */
+ protected $manualTest = array(
+ 'LinkSearchPage'
+ );
+
+ /**
+ * Pages whose query use the same DB table more than once.
+ * This is used to skip testing those pages when run against a MySQL backend
+ * which does not support reopening a temporary table. See upstream bug:
+ * http://bugs.mysql.com/bug.php?id=10327
+ */
+ protected $reopensTempTable = array(
+ 'BrokenRedirects',
+ );
+
+ /**
+ * Initialize all query page objects
+ */
+ function __construct() {
+ parent::__construct();
+
+ foreach ( QueryPage::getPages() as $page ) {
+ $class = $page[0];
+ if ( !in_array( $class, $this->manualTest ) ) {
+ $this->queryPages[$class] = new $class;
+ }
+ }
+ }
+
+ /**
+ * Test SQL for each of our QueryPages objects
+ * @group Database
+ */
+ public function testQuerypageSqlQuery() {
+ global $wgDBtype;
+
+ foreach ( $this->queryPages as $page ) {
+ // With MySQL, skips special pages reopening a temporary table
+ // See http://bugs.mysql.com/bug.php?id=10327
+ if (
+ $wgDBtype === 'mysql'
+ && in_array( $page->getName(), $this->reopensTempTable )
+ ) {
+ $this->markTestSkipped( "SQL query for page {$page->getName()} "
+ . "can not be tested on MySQL backend (it reopens a temporary table)" );
+ continue;
+ }
+
+ $msg = "SQL query for page {$page->getName()} should give a result wrapper object";
+
+ $result = $page->reallyDoQuery( 50 );
+ if ( $result instanceof ResultWrapper ) {
+ $this->assertTrue( true, $msg );
+ } else {
+ $this->assertFalse( false, $msg );
+ }
+ }
+ }
+}
diff --git a/tests/phpunit/includes/specials/SpecialMIMESearchTest.php b/tests/phpunit/includes/specials/SpecialMIMESearchTest.php
new file mode 100644
index 00000000..14d19685
--- /dev/null
+++ b/tests/phpunit/includes/specials/SpecialMIMESearchTest.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * @group Database
+ */
+
+class SpecialMIMESearchTest extends MediaWikiTestCase {
+
+ /** @var MIMESearchPage */
+ private $page;
+
+ function setUp() {
+ $this->page = new MIMESearchPage;
+ $context = new RequestContext();
+ $context->setTitle( Title::makeTitle( NS_SPECIAL, 'MIMESearch' ) );
+ $context->setRequest( new FauxRequest() );
+ $this->page->setContext( $context );
+
+ parent::setUp();
+ }
+
+ /**
+ * @dataProvider providerMimeFiltering
+ * @param string $par Subpage for special page
+ * @param string $major Major MIME type we expect to look for
+ * @param string $minor Minor MIME type we expect to look for
+ */
+ function testMimeFiltering( $par, $major, $minor ) {
+ $this->page->run( $par );
+ $qi = $this->page->getQueryInfo();
+ $this->assertEquals( $qi['conds']['img_major_mime'], $major );
+ if ( $minor !== null ) {
+ $this->assertEquals( $qi['conds']['img_minor_mime'], $minor );
+ } else {
+ $this->assertArrayNotHasKey( 'img_minor_mime', $qi['conds'] );
+ }
+ $this->assertContains( 'image', $qi['tables'] );
+ }
+
+ function providerMimeFiltering() {
+ return array(
+ array( 'image/gif', 'image', 'gif' ),
+ array( 'image/png', 'image', 'png' ),
+ array( 'application/pdf', 'application', 'pdf' ),
+ array( 'image/*', 'image', null ),
+ array( 'multipart/*', 'multipart', null ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/specials/SpecialMyLanguageTest.php b/tests/phpunit/includes/specials/SpecialMyLanguageTest.php
new file mode 100644
index 00000000..4dbfc412
--- /dev/null
+++ b/tests/phpunit/includes/specials/SpecialMyLanguageTest.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @group Database
+ * @covers SpecialMyLanguage
+ */
+class SpecialMyLanguageTest extends MediaWikiTestCase {
+ public function addDBData() {
+ $titles = array(
+ 'Page/Another',
+ 'Page/Another/ru',
+ );
+ foreach ( $titles as $title ) {
+ $page = WikiPage::factory( Title::newFromText( $title ) );
+ if ( $page->getId() == 0 ) {
+ $page->doEditContent(
+ new WikitextContent( 'UTContent' ),
+ 'UTPageSummary',
+ EDIT_NEW,
+ false,
+ User::newFromName( 'UTSysop' ) );
+ }
+ }
+ }
+
+ /**
+ * @covers SpecialMyLanguage::findTitle
+ * @dataProvider provideFindTitle
+ * @param string $expected
+ * @param string $subpage
+ * @param string $langCode
+ * @param string $userLang
+ */
+ public function testFindTitle( $expected, $subpage, $langCode, $userLang ) {
+ $this->setMwGlobals( 'wgLanguageCode', $langCode );
+ $special = new SpecialMyLanguage();
+ $special->getContext()->setLanguage( $userLang );
+ // Test with subpages both enabled and disabled
+ $this->mergeMwGlobalArrayValue( 'wgNamespacesWithSubpages', array( NS_MAIN => true ) );
+ $this->assertTitle( $expected, $special->findTitle( $subpage ) );
+ $this->mergeMwGlobalArrayValue( 'wgNamespacesWithSubpages', array( NS_MAIN => false ) );
+ $this->assertTitle( $expected, $special->findTitle( $subpage ) );
+ }
+
+ /**
+ * @param string $expected
+ * @param Title|null $title
+ */
+ private function assertTitle( $expected, $title ) {
+ if ( $title ) {
+ $title = $title->getPrefixedText();
+ }
+ $this->assertEquals( $expected, $title );
+ }
+
+ public static function provideFindTitle() {
+ return array(
+ array( null, '::Fail', 'en', 'en' ),
+ array( 'Page/Another', 'Page/Another/en', 'en', 'en' ),
+ array( 'Page/Another', 'Page/Another', 'en', 'en' ),
+ array( 'Page/Another/ru', 'Page/Another', 'en', 'ru' ),
+ array( 'Page/Another', 'Page/Another', 'en', 'es' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/specials/SpecialPreferencesTest.php b/tests/phpunit/includes/specials/SpecialPreferencesTest.php
new file mode 100644
index 00000000..4f6c4116
--- /dev/null
+++ b/tests/phpunit/includes/specials/SpecialPreferencesTest.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Test class for SpecialPreferences class.
+ *
+ * Copyright © 2013, Antoine Musso
+ * Copyright © 2013, Wikimedia Foundation Inc.
+ *
+ */
+
+/**
+ * @covers SpecialPreferences
+ */
+class SpecialPreferencesTest extends MediaWikiTestCase {
+
+ /**
+ * Make sure a nickname which is longer than $wgMaxSigChars
+ * is not throwing a fatal error.
+ *
+ * Test specifications by Alexandre "ialex" Emsenhuber.
+ * @todo give this test a real name explaining what is being tested here
+ */
+ public function testBug41337() {
+
+ // Set a low limit
+ $this->setMwGlobals( 'wgMaxSigChars', 2 );
+
+ $user = $this->getMock( 'User' );
+ $user->expects( $this->any() )
+ ->method( 'isAnon' )
+ ->will( $this->returnValue( false ) );
+
+ # Yeah foreach requires an array, not NULL =(
+ $user->expects( $this->any() )
+ ->method( 'getEffectiveGroups' )
+ ->will( $this->returnValue( array() ) );
+
+ # The mocked user has a long nickname
+ $user->expects( $this->any() )
+ ->method( 'getOption' )
+ ->will( $this->returnValueMap( array(
+ array( 'nickname', null, false, 'superlongnickname' ),
+ )
+ ) );
+
+ # Forge a request to call the special page
+ $context = new RequestContext();
+ $context->setRequest( new FauxRequest() );
+ $context->setUser( $user );
+ $context->setTitle( Title::newFromText( 'Test' ) );
+
+ # Do the call, should not spurt a fatal error.
+ $special = new SpecialPreferences();
+ $special->setContext( $context );
+ $this->assertNull( $special->execute( array() ) );
+ }
+
+}
diff --git a/tests/phpunit/includes/specials/SpecialRecentchangesTest.php b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php
new file mode 100644
index 00000000..c3d75aa5
--- /dev/null
+++ b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php
@@ -0,0 +1,123 @@
+<?php
+/**
+ * Test class for SpecialRecentchanges class
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ * @group Database
+ *
+ * @covers SpecialRecentChanges
+ */
+class SpecialRecentchangesTest extends MediaWikiTestCase {
+
+ /**
+ * @var SpecialRecentChanges
+ */
+ protected $rc;
+
+ /** helper to test SpecialRecentchanges::buildMainQueryConds() */
+ private function assertConditions( $expected, $requestOptions = null, $message = '' ) {
+ $context = new RequestContext;
+ $context->setRequest( new FauxRequest( $requestOptions ) );
+
+ # setup the rc object
+ $this->rc = new SpecialRecentChanges();
+ $this->rc->setContext( $context );
+ $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
+ );
+ }
+
+ /** 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_namespace = '0'",
+ ),
+ array(
+ 'namespace' => NS_MAIN,
+ ),
+ "rc conditions with no options (aka default setting)"
+ );
+ }
+
+ public function testRcNsFilterInversion() {
+ $this->assertConditions(
+ array( # expected
+ 'rc_bot' => 0,
+ 0 => 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
+ 'rc_bot' => 0,
+ 0 => 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
+ 'rc_bot' => 0,
+ 0 => 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 static function provideNamespacesAssociations() {
+ return array( # (NS => Associated_NS)
+ array( NS_MAIN, NS_TALK ),
+ array( NS_TALK, NS_MAIN ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/specials/SpecialSearchTest.php b/tests/phpunit/includes/specials/SpecialSearchTest.php
new file mode 100644
index 00000000..83489c65
--- /dev/null
+++ b/tests/phpunit/includes/specials/SpecialSearchTest.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Test class for SpecialSearch class
+ * Copyright © 2012, Antoine Musso
+ *
+ * @author Antoine Musso
+ * @group Database
+ */
+
+class SpecialSearchTest extends MediaWikiTestCase {
+
+ /**
+ * @covers SpecialSearch::load
+ * @dataProvider provideSearchOptionsTests
+ * @param array $requested Request parameters. For example:
+ * array( 'ns5' => true, 'ns6' => true). Null to use default options.
+ * @param array $userOptions User options to test with. For example:
+ * array('searchNs5' => 1 );. Null to use default options.
+ * @param string $expectedProfile An expected search profile name
+ * @param array $expectedNS Expected namespaces
+ * @param string $message
+ */
+ public function testProfileAndNamespaceLoading( $requested, $userOptions,
+ $expectedProfile, $expectedNS, $message = 'Profile name and namespaces mismatches!'
+ ) {
+ $context = new RequestContext;
+ $context->setUser(
+ $this->newUserWithSearchNS( $userOptions )
+ );
+ /*
+ $context->setRequest( new FauxRequest( array(
+ 'ns5'=>true,
+ 'ns6'=>true,
+ ) ));
+ */
+ $context->setRequest( new FauxRequest( $requested ) );
+ $search = new SpecialSearch();
+ $search->setContext( $context );
+ $search->load();
+
+ /**
+ * Verify profile name and namespace in the same assertion to make
+ * sure we will be able to fully compare the above code. PHPUnit stop
+ * after an assertion fail.
+ */
+ $this->assertEquals(
+ array( /** Expected: */
+ 'ProfileName' => $expectedProfile,
+ 'Namespaces' => $expectedNS,
+ ),
+ array( /** Actual: */
+ 'ProfileName' => $search->getProfile(),
+ 'Namespaces' => $search->getNamespaces(),
+ ),
+ $message
+ );
+ }
+
+ public static function provideSearchOptionsTests() {
+ $defaultNS = SearchEngine::defaultNamespaces();
+ $EMPTY_REQUEST = array();
+ $NO_USER_PREF = null;
+
+ return array(
+ /**
+ * Parameters:
+ * <Web Request>, <User options>
+ * Followed by expected values:
+ * <ProfileName>, <NSList>
+ * Then an optional message.
+ */
+ array(
+ $EMPTY_REQUEST, $NO_USER_PREF,
+ 'default', $defaultNS,
+ 'Bug 33270: No request nor user preferences should give default profile'
+ ),
+ array(
+ array( 'ns5' => 1 ), $NO_USER_PREF,
+ 'advanced', array( 5 ),
+ 'Web request with specific NS should override user preference'
+ ),
+ array(
+ $EMPTY_REQUEST, array(
+ 'searchNs2' => 1,
+ 'searchNs14' => 1,
+ ) + array_fill_keys( array_map( function ( $ns ) {
+ return "searchNs$ns";
+ }, $defaultNS ), 0 ),
+ 'advanced', array( 2, 14 ),
+ 'Bug 33583: search with no option should honor User search preferences'
+ . ' and have all other namespace disabled'
+ ),
+ );
+ }
+
+ /**
+ * Helper to create a new User object with given options
+ * User remains anonymous though
+ * @param array|null $opt
+ */
+ function newUserWithSearchNS( $opt = null ) {
+ $u = User::newFromId( 0 );
+ if ( $opt === null ) {
+ return $u;
+ }
+ foreach ( $opt as $name => $value ) {
+ $u->setOption( $name, $value );
+ }
+
+ return $u;
+ }
+
+ /**
+ * Verify we do not expand search term in <title> on search result page
+ * https://gerrit.wikimedia.org/r/4841
+ */
+ public function testSearchTermIsNotExpanded() {
+ $this->setMwGlobals( array(
+ 'wgSearchType' => null,
+ ) );
+
+ # Initialize [[Special::Search]]
+ $search = new SpecialSearch();
+ $search->getContext()->setTitle( Title::newFromText( 'Special:Search' ) );
+ $search->load();
+
+ # Simulate a user searching for a given term
+ $term = '{{SITENAME}}';
+ $search->showResults( $term );
+
+ # Lookup the HTML page title set for that page
+ $pageTitle = $search
+ ->getContext()
+ ->getOutput()
+ ->getHTMLTitle();
+
+ # Compare :-]
+ $this->assertRegExp(
+ '/' . preg_quote( $term ) . '/',
+ $pageTitle,
+ "Search term '{$term}' should not be expanded in Special:Search <title>"
+ );
+ }
+}
diff --git a/tests/phpunit/includes/title/MediaWikiPageLinkRendererTest.php b/tests/phpunit/includes/title/MediaWikiPageLinkRendererTest.php
new file mode 100644
index 00000000..4171c10e
--- /dev/null
+++ b/tests/phpunit/includes/title/MediaWikiPageLinkRendererTest.php
@@ -0,0 +1,169 @@
+<?php
+/**
+ * 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
+ * @license GPL 2+
+ * @author Daniel Kinzler
+ */
+
+/**
+ * @covers MediaWikiPageLinkRenderer
+ *
+ * @group Title
+ * @group Database
+ */
+class MediaWikiPageLinkRendererTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgContLang' => Language::factory( 'en' ),
+ ) );
+ }
+
+ /**
+ * Returns a mock GenderCache that will return "female" always.
+ *
+ * @return GenderCache
+ */
+ private function getGenderCache() {
+ $genderCache = $this->getMockBuilder( 'GenderCache' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $genderCache->expects( $this->any() )
+ ->method( 'getGenderOf' )
+ ->will( $this->returnValue( 'female' ) );
+
+ return $genderCache;
+ }
+
+ public static function provideGetPageUrl() {
+ return array(
+ array(
+ new TitleValue( NS_MAIN, 'Foo_Bar' ),
+ array(),
+ '/Foo_Bar'
+ ),
+ array(
+ new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ),
+ array( 'foo' => 'bar' ),
+ '/User:Hansi_Maier?foo=bar#stuff'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetPageUrl
+ */
+ public function testGetPageUrl( TitleValue $title, $params, $url ) {
+ // NOTE: was of Feb 2014, MediaWikiPageLinkRenderer *ignores* the
+ // WikitextTitleFormatter we pass here, and relies on the Linker
+ // class for generating the link! This may break the test e.g.
+ // of Linker uses a different language for the namespace names.
+
+ $lang = Language::factory( 'en' );
+
+ $formatter = new MediaWikiTitleCodec( $lang, $this->getGenderCache() );
+ $renderer = new MediaWikiPageLinkRenderer( $formatter, '/' );
+ $actual = $renderer->getPageUrl( $title, $params );
+
+ $this->assertEquals( $url, $actual );
+ }
+
+ public static function provideRenderHtmlLink() {
+ return array(
+ array(
+ new TitleValue( NS_MAIN, 'Foo_Bar' ),
+ 'Foo Bar',
+ '!<a .*href=".*?Foo_Bar.*?".*?>Foo Bar</a>!'
+ ),
+ array(
+ //NOTE: Linker doesn't include fragments in "broken" links
+ //NOTE: once this no longer uses Linker, we will get "2" instead of "User" for the namespace.
+ new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ),
+ 'Hansi Maier\'s Stuff',
+ '!<a .*href=".*?User:Hansi_Maier.*?>Hansi Maier\'s Stuff</a>!'
+ ),
+ array(
+ //NOTE: Linker doesn't include fragments in "broken" links
+ //NOTE: once this no longer uses Linker, we will get "2" instead of "User" for the namespace.
+ new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ),
+ null,
+ '!<a .*href=".*?User:Hansi_Maier.*?>User:Hansi Maier#stuff</a>!'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideRenderHtmlLink
+ */
+ public function testRenderHtmlLink( TitleValue $title, $text, $pattern ) {
+ // NOTE: was of Feb 2014, MediaWikiPageLinkRenderer *ignores* the
+ // WikitextTitleFormatter we pass here, and relies on the Linker
+ // class for generating the link! This may break the test e.g.
+ // of Linker uses a different language for the namespace names.
+
+ $lang = Language::factory( 'en' );
+
+ $formatter = new MediaWikiTitleCodec( $lang, $this->getGenderCache() );
+ $renderer = new MediaWikiPageLinkRenderer( $formatter );
+ $actual = $renderer->renderHtmlLink( $title, $text );
+
+ $this->assertRegExp( $pattern, $actual );
+ }
+
+ public static function provideRenderWikitextLink() {
+ return array(
+ array(
+ new TitleValue( NS_MAIN, 'Foo_Bar' ),
+ 'Foo Bar',
+ '[[:0:Foo Bar|Foo Bar]]'
+ ),
+ array(
+ new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ),
+ 'Hansi Maier\'s Stuff',
+ '[[:2:Hansi Maier#stuff|Hansi Maier&#39;s Stuff]]'
+ ),
+ array(
+ new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ),
+ null,
+ '[[:2:Hansi Maier#stuff|2:Hansi Maier#stuff]]'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideRenderWikitextLink
+ */
+ public function testRenderWikitextLink( TitleValue $title, $text, $expected ) {
+ $formatter = $this->getMock( 'TitleFormatter' );
+ $formatter->expects( $this->any() )
+ ->method( 'getFullText' )
+ ->will( $this->returnCallback(
+ function ( TitleValue $title ) {
+ return str_replace( '_', ' ', "$title" );
+ }
+ ));
+
+ $renderer = new MediaWikiPageLinkRenderer( $formatter, '/' );
+ $actual = $renderer->renderWikitextLink( $title, $text );
+
+ $this->assertEquals( $expected, $actual );
+ }
+}
diff --git a/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php b/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php
new file mode 100644
index 00000000..f95b3050
--- /dev/null
+++ b/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php
@@ -0,0 +1,384 @@
+<?php
+/**
+ * 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
+ * @license GPL 2+
+ * @author Daniel Kinzler
+ */
+
+/**
+ * @covers MediaWikiTitleCodec
+ *
+ * @group Title
+ * @group Database
+ * ^--- needed because of global state in
+ */
+class MediaWikiTitleCodecTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => Language::factory( 'en' ),
+ // User language
+ 'wgLang' => Language::factory( 'en' ),
+ 'wgAllowUserJs' => false,
+ 'wgDefaultLanguageVariant' => false,
+ 'wgLocalInterwikis' => array( 'localtestiw' ),
+ 'wgCapitalLinks' => true,
+
+ // NOTE: this is why global state is evil.
+ // TODO: refactor access to the interwiki codes so it can be injected.
+ 'wgHooks' => array(
+ 'InterwikiLoadPrefix' => array(
+ function ( $prefix, &$data ) {
+ if ( $prefix === 'localtestiw' ) {
+ $data = array( 'iw_url' => 'localtestiw' );
+ } elseif ( $prefix === 'remotetestiw' ) {
+ $data = array( 'iw_url' => 'remotetestiw' );
+ }
+ return false;
+ }
+ )
+ )
+ ) );
+ }
+
+ /**
+ * Returns a mock GenderCache that will consider a user "female" if the
+ * first part of the user name ends with "a".
+ *
+ * @return GenderCache
+ */
+ private function getGenderCache() {
+ $genderCache = $this->getMockBuilder( 'GenderCache' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $genderCache->expects( $this->any() )
+ ->method( 'getGenderOf' )
+ ->will( $this->returnCallback( function ( $userName ) {
+ return preg_match( '/^[^- _]+a( |_|$)/u', $userName ) ? 'female' : 'male';
+ } ) );
+
+ return $genderCache;
+ }
+
+ protected function makeCodec( $lang ) {
+ $gender = $this->getGenderCache();
+ $lang = Language::factory( $lang );
+ return new MediaWikiTitleCodec( $lang, $gender );
+ }
+
+ public static function provideFormat() {
+ return array(
+ array( NS_MAIN, 'Foo_Bar', '', 'en', 'Foo Bar' ),
+ array( NS_USER, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'User:Hansi Maier#stuff and so on' ),
+ array( false, 'Hansi_Maier', '', 'en', 'Hansi Maier' ),
+ array(
+ NS_USER_TALK,
+ 'hansi__maier',
+ '',
+ 'en',
+ 'User talk:hansi maier',
+ 'User talk:Hansi maier'
+ ),
+
+ // getGenderCache() provides a mock that considers first
+ // names ending in "a" to be female.
+ array( NS_USER, 'Lisa_Müller', '', 'de', 'Benutzerin:Lisa Müller' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideFormat
+ */
+ public function testFormat( $namespace, $text, $fragment, $lang, $expected, $normalized = null ) {
+ if ( $normalized === null ) {
+ $normalized = $expected;
+ }
+
+ $codec = $this->makeCodec( $lang );
+ $actual = $codec->formatTitle( $namespace, $text, $fragment );
+
+ $this->assertEquals( $expected, $actual, 'formatted' );
+
+ // test round trip
+ $parsed = $codec->parseTitle( $actual, NS_MAIN );
+ $actual2 = $codec->formatTitle(
+ $parsed->getNamespace(),
+ $parsed->getText(),
+ $parsed->getFragment()
+ );
+
+ $this->assertEquals( $normalized, $actual2, 'normalized after round trip' );
+ }
+
+ public static function provideGetText() {
+ return array(
+ array( NS_MAIN, 'Foo_Bar', '', 'en', 'Foo Bar' ),
+ array( NS_USER, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'Hansi Maier' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetText
+ */
+ public function testGetText( $namespace, $dbkey, $fragment, $lang, $expected ) {
+ $codec = $this->makeCodec( $lang );
+ $title = new TitleValue( $namespace, $dbkey, $fragment );
+
+ $actual = $codec->getText( $title );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetPrefixedText() {
+ return array(
+ array( NS_MAIN, 'Foo_Bar', '', 'en', 'Foo Bar' ),
+ array( NS_USER, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'User:Hansi Maier' ),
+
+ // No capitalization or normalization is applied while formatting!
+ array( NS_USER_TALK, 'hansi__maier', '', 'en', 'User talk:hansi maier' ),
+
+ // getGenderCache() provides a mock that considers first
+ // names ending in "a" to be female.
+ array( NS_USER, 'Lisa_Müller', '', 'de', 'Benutzerin:Lisa Müller' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetPrefixedText
+ */
+ public function testGetPrefixedText( $namespace, $dbkey, $fragment, $lang, $expected ) {
+ $codec = $this->makeCodec( $lang );
+ $title = new TitleValue( $namespace, $dbkey, $fragment );
+
+ $actual = $codec->getPrefixedText( $title );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetFullText() {
+ return array(
+ array( NS_MAIN, 'Foo_Bar', '', 'en', 'Foo Bar' ),
+ array( NS_USER, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'User:Hansi Maier#stuff and so on' ),
+
+ // No capitalization or normalization is applied while formatting!
+ array( NS_USER_TALK, 'hansi__maier', '', 'en', 'User talk:hansi maier' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetFullText
+ */
+ public function testGetFullText( $namespace, $dbkey, $fragment, $lang, $expected ) {
+ $codec = $this->makeCodec( $lang );
+ $title = new TitleValue( $namespace, $dbkey, $fragment );
+
+ $actual = $codec->getFullText( $title );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideParseTitle() {
+ //TODO: test capitalization and trimming
+ //TODO: test unicode normalization
+
+ return array(
+ array( ' : Hansi_Maier _ ', NS_MAIN, 'en',
+ new TitleValue( NS_MAIN, 'Hansi_Maier', '' ) ),
+ array( 'User:::1', NS_MAIN, 'de',
+ new TitleValue( NS_USER, '0:0:0:0:0:0:0:1', '' ) ),
+ array( ' lisa Müller', NS_USER, 'de',
+ new TitleValue( NS_USER, 'Lisa_Müller', '' ) ),
+ array( 'benutzerin:lisa Müller#stuff', NS_MAIN, 'de',
+ new TitleValue( NS_USER, 'Lisa_Müller', 'stuff' ) ),
+
+ array( ':Category:Quux', NS_MAIN, 'en',
+ new TitleValue( NS_CATEGORY, 'Quux', '' ) ),
+ array( 'Category:Quux', NS_MAIN, 'en',
+ new TitleValue( NS_CATEGORY, 'Quux', '' ) ),
+ array( 'Category:Quux', NS_CATEGORY, 'en',
+ new TitleValue( NS_CATEGORY, 'Quux', '' ) ),
+ array( 'Quux', NS_CATEGORY, 'en',
+ new TitleValue( NS_CATEGORY, 'Quux', '' ) ),
+ array( ':Quux', NS_CATEGORY, 'en',
+ new TitleValue( NS_MAIN, 'Quux', '' ) ),
+
+ // getGenderCache() provides a mock that considers first
+ // names ending in "a" to be female.
+
+ array( 'a b c', NS_MAIN, 'en',
+ new TitleValue( NS_MAIN, 'A_b_c' ) ),
+ array( ' a b c ', NS_MAIN, 'en',
+ new TitleValue( NS_MAIN, 'A_b_c' ) ),
+ array( ' _ Foo __ Bar_ _', NS_MAIN, 'en',
+ new TitleValue( NS_MAIN, 'Foo_Bar' ) ),
+
+ //NOTE: cases copied from TitleTest::testSecureAndSplit. Keep in sync.
+ array( 'Sandbox', NS_MAIN, 'en', ),
+ array( 'A "B"', NS_MAIN, 'en', ),
+ array( 'A \'B\'', NS_MAIN, 'en', ),
+ array( '.com', NS_MAIN, 'en', ),
+ array( '~', NS_MAIN, 'en', ),
+ array( '"', NS_MAIN, 'en', ),
+ array( '\'', NS_MAIN, 'en', ),
+
+ array( 'Talk:Sandbox', NS_MAIN, 'en',
+ new TitleValue( NS_TALK, 'Sandbox' ) ),
+ array( 'Talk:Foo:Sandbox', NS_MAIN, 'en',
+ new TitleValue( NS_TALK, 'Foo:Sandbox' ) ),
+ array( 'File:Example.svg', NS_MAIN, 'en',
+ new TitleValue( NS_FILE, 'Example.svg' ) ),
+ array( 'File_talk:Example.svg', NS_MAIN, 'en',
+ new TitleValue( NS_FILE_TALK, 'Example.svg' ) ),
+ array( 'Foo/.../Sandbox', NS_MAIN, 'en',
+ 'Foo/.../Sandbox' ),
+ array( 'Sandbox/...', NS_MAIN, 'en',
+ 'Sandbox/...' ),
+ array( 'A~~', NS_MAIN, 'en',
+ 'A~~' ),
+ // Length is 256 total, but only title part matters
+ array( 'Category:' . str_repeat( 'x', 248 ), NS_MAIN, 'en',
+ new TitleValue( NS_CATEGORY,
+ 'X' . str_repeat( 'x', 247 ) ) ),
+ array( str_repeat( 'x', 252 ), NS_MAIN, 'en',
+ 'X' . str_repeat( 'x', 251 ) )
+ );
+ }
+
+ /**
+ * @dataProvider provideParseTitle
+ */
+ public function testParseTitle( $text, $ns, $lang, $title = null ) {
+ if ( $title === null ) {
+ $title = str_replace( ' ', '_', trim( $text ) );
+ }
+
+ if ( is_string( $title ) ) {
+ $title = new TitleValue( NS_MAIN, $title, '' );
+ }
+
+ $codec = $this->makeCodec( $lang );
+ $actual = $codec->parseTitle( $text, $ns );
+
+ $this->assertEquals( $title, $actual );
+ }
+
+ public static function provideParseTitle_invalid() {
+ //TODO: test unicode errors
+
+ return array(
+ array( '#' ),
+ array( '::' ),
+ array( '::xx' ),
+ array( '::##' ),
+ array( ' :: x' ),
+
+ array( 'Talk:File:Foo.jpg' ),
+ array( 'Talk:localtestiw:Foo' ),
+ array( 'remotetestiw:Foo' ),
+ array( '::1' ), // only valid in user namespace
+ array( 'User::x' ), // leading ":" in a user name is only valid of IPv6 addresses
+
+ //NOTE: cases copied from TitleTest::testSecureAndSplit. Keep in sync.
+ array( '' ),
+ array( ':' ),
+ array( '__ __' ),
+ array( ' __ ' ),
+ // Bad characters forbidden regardless of wgLegalTitleChars
+ array( 'A [ B' ),
+ array( 'A ] B' ),
+ array( 'A { B' ),
+ array( 'A } B' ),
+ array( 'A < B' ),
+ array( 'A > B' ),
+ array( 'A | B' ),
+ // URL encoding
+ array( 'A%20B' ),
+ array( 'A%23B' ),
+ array( 'A%2523B' ),
+ // XML/HTML character entity references
+ // Note: Commented out because they are not marked invalid by the PHP test as
+ // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first.
+ //array( 'A &eacute; B' ),
+ //array( 'A &#233; B' ),
+ //array( 'A &#x00E9; B' ),
+ // Subject of NS_TALK does not roundtrip to NS_MAIN
+ array( 'Talk:File:Example.svg' ),
+ // Directory navigation
+ array( '.' ),
+ array( '..' ),
+ array( './Sandbox' ),
+ array( '../Sandbox' ),
+ array( 'Foo/./Sandbox' ),
+ array( 'Foo/../Sandbox' ),
+ array( 'Sandbox/.' ),
+ array( 'Sandbox/..' ),
+ // Tilde
+ array( 'A ~~~ Name' ),
+ array( 'A ~~~~ Signature' ),
+ array( 'A ~~~~~ Timestamp' ),
+ array( str_repeat( 'x', 256 ) ),
+ // Namespace prefix without actual title
+ array( 'Talk:' ),
+ array( 'Category: ' ),
+ array( 'Category: #bar' )
+ );
+ }
+
+ /**
+ * @dataProvider provideParseTitle_invalid
+ */
+ public function testParseTitle_invalid( $text ) {
+ $this->setExpectedException( 'MalformedTitleException' );
+
+ $codec = $this->makeCodec( 'en' );
+ $codec->parseTitle( $text, NS_MAIN );
+ }
+
+ public static function provideGetNamespaceName() {
+ return array(
+ array( NS_MAIN, 'Foo', 'en', '' ),
+ array( NS_USER, 'Foo', 'en', 'User' ),
+ array( NS_USER, 'Hansi Maier', 'de', 'Benutzer' ),
+
+ // getGenderCache() provides a mock that considers first
+ // names ending in "a" to be female.
+ array( NS_USER, 'Lisa Müller', 'de', 'Benutzerin' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetNamespaceName
+ *
+ * @param int $namespace
+ * @param string $text
+ * @param string $lang
+ * @param string $expected
+ *
+ * @internal param \TitleValue $title
+ */
+ public function testGetNamespaceName( $namespace, $text, $lang, $expected ) {
+ $codec = $this->makeCodec( $lang );
+ $name = $codec->getNamespaceName( $namespace, $text );
+
+ $this->assertEquals( $expected, $name );
+ }
+}
diff --git a/tests/phpunit/includes/title/TitleValueTest.php b/tests/phpunit/includes/title/TitleValueTest.php
new file mode 100644
index 00000000..3ba008d6
--- /dev/null
+++ b/tests/phpunit/includes/title/TitleValueTest.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * 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
+ * @license GPL 2+
+ * @author Daniel Kinzler
+ */
+
+/**
+ * @covers TitleValue
+ *
+ * @group Title
+ */
+class TitleValueTest extends MediaWikiTestCase {
+
+ public function testConstruction() {
+ $title = new TitleValue( NS_USER, 'TestThis', 'stuff' );
+
+ $this->assertEquals( NS_USER, $title->getNamespace() );
+ $this->assertEquals( 'TestThis', $title->getText() );
+ $this->assertEquals( 'stuff', $title->getFragment() );
+ }
+
+ public function badConstructorProvider() {
+ return array(
+ array( 'foo', 'title', 'fragment' ),
+ array( null, 'title', 'fragment' ),
+ array( 2.3, 'title', 'fragment' ),
+
+ array( NS_MAIN, 5, 'fragment' ),
+ array( NS_MAIN, null, 'fragment' ),
+ array( NS_MAIN, '', 'fragment' ),
+ array( NS_MAIN, 'foo bar', '' ),
+ array( NS_MAIN, 'bar_', '' ),
+ array( NS_MAIN, '_foo', '' ),
+ array( NS_MAIN, ' eek ', '' ),
+
+ array( NS_MAIN, 'title', 5 ),
+ array( NS_MAIN, 'title', null ),
+ array( NS_MAIN, 'title', array() ),
+ );
+ }
+
+ /**
+ * @dataProvider badConstructorProvider
+ */
+ public function testConstructionErrors( $ns, $text, $fragment ) {
+ $this->setExpectedException( 'InvalidArgumentException' );
+ new TitleValue( $ns, $text, $fragment );
+ }
+
+ public function fragmentTitleProvider() {
+ return array(
+ array( new TitleValue( NS_MAIN, 'Test' ), 'foo' ),
+ array( new TitleValue( NS_TALK, 'Test', 'foo' ), '' ),
+ array( new TitleValue( NS_CATEGORY, 'Test', 'foo' ), 'bar' ),
+ );
+ }
+
+ /**
+ * @dataProvider fragmentTitleProvider
+ */
+ public function testCreateFragmentTitle( TitleValue $title, $fragment ) {
+ $fragmentTitle = $title->createFragmentTitle( $fragment );
+
+ $this->assertEquals( $title->getNamespace(), $fragmentTitle->getNamespace() );
+ $this->assertEquals( $title->getText(), $fragmentTitle->getText() );
+ $this->assertEquals( $fragment, $fragmentTitle->getFragment() );
+ }
+
+ public function getTextProvider() {
+ return array(
+ array( 'Foo', 'Foo' ),
+ array( 'Foo_Bar', 'Foo Bar' ),
+ );
+ }
+
+ /**
+ * @dataProvider getTextProvider
+ */
+ public function testGetText( $dbkey, $text ) {
+ $title = new TitleValue( NS_MAIN, $dbkey );
+
+ $this->assertEquals( $text, $title->getText() );
+ }
+}
diff --git a/tests/phpunit/includes/upload/UploadBaseTest.php b/tests/phpunit/includes/upload/UploadBaseTest.php
new file mode 100644
index 00000000..3d3b0068
--- /dev/null
+++ b/tests/phpunit/includes/upload/UploadBaseTest.php
@@ -0,0 +1,427 @@
+<?php
+
+/**
+ * @group Upload
+ */
+class UploadBaseTest extends MediaWikiTestCase {
+
+ /** @var UploadTestHandler */
+ protected $upload;
+
+ protected function setUp() {
+ global $wgHooks;
+ parent::setUp();
+
+ $this->upload = new UploadTestHandler;
+ $this->hooks = $wgHooks;
+ $wgHooks['InterwikiLoadPrefix'][] = function ( $prefix, &$data ) {
+ return false;
+ };
+ }
+
+ protected function tearDown() {
+ global $wgHooks;
+ $wgHooks = $this->hooks;
+
+ parent::tearDown();
+ }
+
+ /**
+ * First checks the return code
+ * of UploadBase::getTitle() and then the actual returned title
+ *
+ * @dataProvider provideTestTitleValidation
+ * @covers UploadBase::getTitle
+ */
+ public function testTitleValidation( $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 various forms of valid and invalid titles that can be supplied.
+ */
+ public static function provideTestTitleValidation() {
+ return array(
+ /* Test a valid title */
+ array( 'ValidTitle.jpg', 'ValidTitle.jpg', UploadBase::OK,
+ 'upload valid title' ),
+ /* A title with a slash */
+ array( 'A/B.jpg', 'B.jpg', UploadBase::OK,
+ 'upload title with slash' ),
+ /* A title with illegal char */
+ array( 'A:B.jpg', 'A-B.jpg', UploadBase::OK,
+ 'upload title with colon' ),
+ /* Stripping leading File: prefix */
+ array( 'File:C.jpg', 'C.jpg', UploadBase::OK,
+ 'upload title with File prefix' ),
+ /* Test illegal suggested title (r94601) */
+ array( '%281%29.JPG', null, UploadBase::ILLEGAL_FILENAME,
+ 'illegal title for upload' ),
+ /* A title without extension */
+ array( 'A', null, UploadBase::FILETYPE_MISSING,
+ 'upload title without extension' ),
+ /* A title with no basename */
+ array( '.jpg', null, UploadBase::MIN_LENGTH_PARTNAME,
+ 'upload title without basename' ),
+ /* A title that is longer than 255 bytes */
+ array( str_repeat( 'a', 255 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG,
+ 'upload title longer than 255 bytes' ),
+ /* A title that is longer than 240 bytes */
+ array( str_repeat( 'a', 240 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG,
+ 'upload title longer than 240 bytes' ),
+ );
+ }
+
+ /**
+ * Test the upload verification functions
+ * @covers UploadBase::verifyUpload
+ */
+ 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
+ }
+
+
+ /**
+ * @dataProvider provideCheckSvgScriptCallback
+ */
+ public function testCheckSvgScriptCallback( $svg, $wellFormed, $filterMatch, $message ) {
+ list( $formed, $match ) = $this->upload->checkSvgString( $svg );
+ $this->assertSame( $wellFormed, $formed, $message );
+ $this->assertSame( $filterMatch, $match, $message );
+ }
+
+ public static function provideCheckSvgScriptCallback() {
+ return array(
+ // html5sec SVG vectors
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>',
+ true,
+ true,
+ 'Script tag in svg (http://html5sec.org/#47)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"><g onload="javascript:alert(1)"></g></svg>',
+ true,
+ true,
+ 'SVG with onload property (http://html5sec.org/#11)'
+ ),
+ array(
+ '<svg onload="javascript:alert(1)" xmlns="http://www.w3.org/2000/svg"></svg>',
+ true,
+ true,
+ 'SVG with onload property (http://html5sec.org/#65)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <a xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="javascript:alert(1)"><rect width="1000" height="1000" fill="white"/></a> </svg>',
+ true,
+ true,
+ 'SVG with javascript xlink (http://html5sec.org/#87)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><use xlink:href="data:application/xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KPGRlZnM+CjxjaXJjbGUgaWQ9InRlc3QiIHI9IjUwIiBjeD0iMTAwIiBjeT0iMTAwIiBzdHlsZT0iZmlsbDogI0YwMCI+CjxzZXQgYXR0cmlidXRlTmFtZT0iZmlsbCIgYXR0cmlidXRlVHlwZT0iQ1NTIiBvbmJlZ2luPSdhbGVydChkb2N1bWVudC5jb29raWUpJwpvbmVuZD0nYWxlcnQoIm9uZW5kIiknIHRvPSIjMDBGIiBiZWdpbj0iMXMiIGR1cj0iNXMiIC8+CjwvY2lyY2xlPgo8L2RlZnM+Cjx1c2UgeGxpbms6aHJlZj0iI3Rlc3QiLz4KPC9zdmc+#test"/> </svg>',
+ true,
+ true,
+ 'SVG with Opera image xlink (http://html5sec.org/#88 - c)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <animation xlink:href="javascript:alert(1)"/> </svg>',
+ true,
+ true,
+ 'SVG with Opera animation xlink (http://html5sec.org/#88 - a)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <animation xlink:href="data:text/xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' onload=\'alert(1)\'%3E%3C/svg%3E"/> </svg>',
+ true,
+ true,
+ 'SVG with Opera animation xlink (http://html5sec.org/#88 - b)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <image xlink:href="data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' onload=\'alert(1)\'%3E%3C/svg%3E"/> </svg>',
+ true,
+ true,
+ 'SVG with Opera image xlink (http://html5sec.org/#88 - c)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <foreignObject xlink:href="javascript:alert(1)"/> </svg>',
+ true,
+ true,
+ 'SVG with Opera foreignObject xlink (http://html5sec.org/#88 - d)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <foreignObject xlink:href="data:text/xml,%3Cscript xmlns=\'http://www.w3.org/1999/xhtml\'%3Ealert(1)%3C/script%3E"/> </svg>',
+ true,
+ true,
+ 'SVG with Opera foreignObject xlink (http://html5sec.org/#88 - e)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <set attributeName="onmouseover" to="alert(1)"/> </svg>',
+ true,
+ true,
+ 'SVG with event handler set (http://html5sec.org/#89 - a)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <animate attributeName="onunload" to="alert(1)"/> </svg>',
+ true,
+ true,
+ 'SVG with event handler animate (http://html5sec.org/#89 - a)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>',
+ true,
+ true,
+ 'SVG with element handler (http://html5sec.org/#94)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <feImage> <set attributeName="xlink:href" to="data:image/svg+xml;charset=utf-8;base64, PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxzY3JpcHQ%2BYWxlcnQoMSk8L3NjcmlwdD48L3N2Zz4NCg%3D%3D"/> </feImage> </svg>',
+ true,
+ true,
+ 'SVG with href to data: url (http://html5sec.org/#95)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" id="foo"> <x xmlns="http://www.w3.org/2001/xml-events" event="load" observer="foo" handler="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Chandler%20xml%3Aid%3D%22bar%22%20type%3D%22application%2Fecmascript%22%3E alert(1) %3C%2Fhandler%3E%0A%3C%2Fsvg%3E%0A#bar"/> </svg>',
+ true,
+ true,
+ 'SVG with Tiny handler (http://html5sec.org/#104)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <a id="x"><rect fill="white" width="1000" height="1000"/></a> <rect fill="white" style="clip-path:url(test3.svg#a);fill:url(#b);filter:url(#c);marker:url(#d);mask:url(#e);stroke:url(#f);"/> </svg>',
+ true,
+ true,
+ 'SVG with new CSS styles properties (http://html5sec.org/#109)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <a id="x"><rect fill="white" width="1000" height="1000"/></a> <rect clip-path="url(test3.svg#a)" /> </svg>',
+ true,
+ true,
+ 'SVG with new CSS styles properties as attributes'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <a id="x"> <rect fill="white" width="1000" height="1000"/> </a> <rect fill="url(http://html5sec.org/test3.svg#a)" /> </svg>',
+ true,
+ true,
+ 'SVG with new CSS styles properties as attributes (2)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <path d="M0,0" style="marker-start:url(test4.svg#a)"/> </svg>',
+ true,
+ true,
+ 'SVG with path marker-start (http://html5sec.org/#110)'
+ ),
+ array(
+ '<?xml version="1.0"?> <?xml-stylesheet type="text/xml" href="#stylesheet"?> <!DOCTYPE doc [ <!ATTLIST xsl:stylesheet id ID #REQUIRED>]> <svg xmlns="http://www.w3.org/2000/svg"> <xsl:stylesheet id="stylesheet" version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <iframe xmlns="http://www.w3.org/1999/xhtml" src="javascript:alert(1)"></iframe> </xsl:template> </xsl:stylesheet> <circle fill="red" r="40"></circle> </svg>',
+ true,
+ true,
+ 'SVG with embedded stylesheet (http://html5sec.org/#125)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" id="x"> <listener event="load" handler="#y" xmlns="http://www.w3.org/2001/xml-events" observer="x"/> <handler id="y">alert(1)</handler> </svg>',
+ true,
+ true,
+ 'SVG with handler attribute (http://html5sec.org/#127)'
+ ),
+ array(
+ // Haven't found a browser that accepts this particular example, but we
+ // don't want to allow embeded svgs, ever
+ '<svg> <image style=\'filter:url("data:image/svg+xml;charset=utf-8;base64, PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxzY3JpcHQ/YWxlcnQoMSk8L3NjcmlwdD48L3N2Zz4NCg==")\' /> </svg>',
+ true,
+ true,
+ 'SVG with image filter via style (http://html5sec.org/#129)'
+ ),
+ array(
+ // This doesn't seem possible without embedding the svg, but just in case
+ '<svg> <a xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="?"> <circle r="400"></circle> <animate attributeName="xlink:href" begin="0" from="javascript:alert(1)" to="" /> </a></svg>',
+ true,
+ true,
+ 'SVG with animate from (http://html5sec.org/#137)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <a><text y="1em">Click me</text> <animate attributeName="xlink:href" values="javascript:alert(\'Bang!\')" begin="0s" dur="0.1s" fill="freeze" /> </a></svg>',
+ true,
+ true,
+ 'SVG with animate xlink:href (http://html5sec.org/#137)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:y="http://www.w3.org/1999/xlink"> <a y:href="#"> <text y="1em">Click me</text> <animate attributeName="y:href" values="javascript:alert(\'Bang!\')" begin="0s" dur="0.1s" fill="freeze" /> </a> </svg>',
+ true,
+ true,
+ 'SVG with animate y:href (http://html5sec.org/#137)'
+ ),
+
+ // Other hostile SVG's
+ array(
+ '<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:xlink="http://www.w3.org/1999/xlink"> <image xlink:href="https://upload.wikimedia.org/wikipedia/commons/3/34/Bahnstrecke_Zeitz-Camburg_1930.png" /> </svg>',
+ true,
+ true,
+ 'SVG with non-local image href (bug 65839)'
+ ),
+ array(
+ '<?xml version="1.0" ?> <?xml-stylesheet type="text/xsl" href="/w/index.php?title=User:Jeeves/test.xsl&amp;action=raw&amp;format=xml" ?> <svg> <height>50</height> <width>100</width> </svg>',
+ true,
+ true,
+ 'SVG with remote stylesheet (bug 57550)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" viewbox="-1 -1 15 15"> <rect y="0" height="13" width="12" stroke="#179" rx="1" fill="#2ac"/> <text x="1.5" y="11" font-family="courier" stroke="white" font-size="16"><![CDATA[B]]></text> <iframe xmlns="http://www.w3.org/1999/xhtml" srcdoc="&#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x45;&#x44;&#x20;&#x3D;&#x3E;&#x20;&#x44;&#x6F;&#x6D;&#x61;&#x69;&#x6E;&#x28;&#x27;&#x2B;&#x74;&#x6F;&#x70;&#x2E;&#x64;&#x6F;&#x63;&#x75;&#x6D;&#x65;&#x6E;&#x74;&#x2E;&#x64;&#x6F;&#x6D;&#x61;&#x69;&#x6E;&#x2B;&#x27;&#x29;&#x27;&#x29;&#x3B;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;"></iframe> </svg>',
+ true,
+ true,
+ 'SVG with rembeded iframe (bug 60771)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="6 3 177 153" xmlns:xlink="http://www.w3.org/1999/xlink"> <style>@import url("https://fonts.googleapis.com/css?family=Bitter:700&amp;text=WebPlatform.org");</style> <g transform="translate(-.5,-.5)"> <text fill="#474747" x="95" y="150" text-anchor="middle" font-family="Bitter" font-size="20" font-weight="bold">WebPlatform.org</text> </g> </svg>',
+ true,
+ true,
+ 'SVG with @import in style element (bug 69008)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="6 3 177 153" xmlns:xlink="http://www.w3.org/1999/xlink"> <style>@import url("https://fonts.googleapis.com/css?family=Bitter:700&amp;text=WebPlatform.org");<foo/></style> <g transform="translate(-.5,-.5)"> <text fill="#474747" x="95" y="150" text-anchor="middle" font-family="Bitter" font-size="20" font-weight="bold">WebPlatform.org</text> </g> </svg>',
+ true,
+ true,
+ 'SVG with @import in style element and child element (bug 69008#c11)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="6 3 177 153" xmlns:xlink="http://www.w3.org/1999/xlink"> <style>@imporT "https://fonts.googleapis.com/css?family=Bitter:700&amp;text=WebPlatform.org";</style> <g transform="translate(-.5,-.5)"> <text fill="#474747" x="95" y="150" text-anchor="middle" font-family="Bitter" font-size="20" font-weight="bold">WebPlatform.org</text> </g> </svg>',
+ true,
+ true,
+ 'SVG with case-insensitive @import in style element (bug T85349)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <rect width="100" height="100" style="background-image:url(https://www.google.com/images/srpr/logo11w.png)"/> </svg>',
+ true,
+ true,
+ 'SVG with remote background image (bug 69008)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <rect width="100" height="100" style="background-image:\55rl(https://www.google.com/images/srpr/logo11w.png)"/> </svg>',
+ true,
+ true,
+ 'SVG with remote background image, encoded (bug 69008)'
+ ),
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg"> <style> #a { background-image:\55rl(\'https://www.google.com/images/srpr/logo11w.png\'); } </style> <rect width="100" height="100" id="a"/> </svg>',
+ true,
+ true,
+ 'SVG with remote background image, in style element (bug 69008)'
+ ),
+ array(
+ // This currently doesn't seem to work in any browsers, but in case
+ // http://www.w3.org/TR/css3-images/ is implemented for SVG files
+ '<svg xmlns="http://www.w3.org/2000/svg"> <rect width="100" height="100" style="background-image:image(\'sprites.svg#xywh=40,0,20,20\')"/> </svg>',
+ true,
+ true,
+ 'SVG with remote background image using image() (bug 69008)'
+ ),
+ array(
+ // As reported by Cure53
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <a xlink:href="data:text/html;charset=utf-8;base64, PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ%2BDQo%3D"> <circle r="400" fill="red"></circle> </a> </svg>',
+ true,
+ true,
+ 'SVG with data:text/html link target (firefox only)'
+ ),
+ array(
+ '<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ <!ENTITY lol "lol"> <!ENTITY lol2 "&#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x45;&#x44;&#x20;&#x3D;&#x3E;&#x20;&#x27;&#x2B;&#x64;&#x6F;&#x63;&#x75;&#x6D;&#x65;&#x6E;&#x74;&#x2E;&#x64;&#x6F;&#x6D;&#x61;&#x69;&#x6E;&#x29;&#x3B;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;"> ]> <svg xmlns="http://www.w3.org/2000/svg" width="68" height="68" viewBox="-34 -34 68 68" version="1.1"> <circle cx="0" cy="0" r="24" fill="#c8c8c8"/> <text x="0" y="0" fill="black">&lol2;</text> </svg>',
+ true,
+ true,
+ 'SVG with encoded script tag in internal entity (reported by Beyond Security)'
+ ),
+ array(
+ '<?xml version="1.0"?> <!DOCTYPE svg [ <!ENTITY foo SYSTEM "file:///etc/passwd"> ]> <svg xmlns="http://www.w3.org/2000/svg" version="1.1"> <desc>&foo;</desc> <rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,2)" /> </svg>',
+ false,
+ false,
+ 'SVG with external entity'
+ ),
+
+ // Test good, but strange files that we want to allow
+ array(
+ '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <g> <a xlink:href="http://en.wikipedia.org/wiki/Main_Page"> <path transform="translate(0,496)" id="path6706" d="m 112.09375,107.6875 -5.0625,3.625 -4.3125,5.03125 -0.46875,0.5 -4.09375,3.34375 -9.125,5.28125 -8.625,-3.375 z" style="fill:#cccccc;fill-opacity:1;stroke:#6e6e6e;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;display:inline" /> </a> </g> </svg>',
+ true,
+ false,
+ 'SVG with <a> link to a remote site'
+ ),
+ array(
+ '<svg> <defs> <filter id="filter6226" x="-0.93243687" width="2.8648737" y="-0.24250539" height="1.4850108"> <feGaussianBlur stdDeviation="3.2344681" id="feGaussianBlur6228" /> </filter> <clipPath id="clipPath2436"> <path d="M 0,0 L 0,0 L 0,0 L 0,0 z" id="path2438" /> </clipPath> </defs> <g clip-path="url(#clipPath2436)" id="g2460"> <text id="text2466"> <tspan>12345</tspan> </text> </g> <path style="fill:#346733;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:1, 1;stroke-dashoffset:0;filter:url(\'#filter6226\');fill-opacity:1;opacity:0.79807692" d="M 236.82371,332.63732 C 236.92217,332.63732 z" id="path5618" /> </svg>',
+ true,
+ false,
+ 'SVG with local urls, including filter: in style'
+ ),
+ );
+ }
+}
+
+class UploadTestHandler extends UploadBase {
+ public function initializeFromRequest( &$request ) {
+ }
+
+ public function testTitleValidation( $name ) {
+ $this->mTitle = false;
+ $this->mDesiredDestName = $name;
+ $this->mTitleError = UploadBase::OK;
+ $this->getTitle();
+
+ return $this->mTitleError;
+ }
+
+ /**
+ * Almost the same as UploadBase::detectScriptInSvg, except it's
+ * public, works on an xml string instead of filename, and returns
+ * the result instead of interpreting them.
+ */
+ public function checkSvgString( $svg ) {
+ $check = new XmlTypeCheck(
+ $svg,
+ array( $this, 'checkSvgScriptCallback' ),
+ false,
+ array( 'processing_instruction_handler' => 'UploadBase::checkSvgPICallback' )
+ );
+ return array( $check->wellFormed, $check->filterMatch );
+ }
+}
diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php
new file mode 100644
index 00000000..ec56b63e
--- /dev/null
+++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php
@@ -0,0 +1,328 @@
+<?php
+
+/**
+ * @group Broken
+ * @group Upload
+ * @group Database
+ *
+ * @covers UploadFromUrl
+ */
+class UploadFromUrlTest extends ApiTestCase {
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgEnableUploads' => true,
+ 'wgAllowCopyUploads' => true,
+ 'wgAllowAsyncCopyUploads' => true,
+ ) );
+ wfSetupSession();
+
+ if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) {
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+ }
+ }
+
+ protected function doApiRequest( array $params, array $unused = null,
+ $appendModule = false, User $user = null
+ ) {
+ $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() {
+ $job = JobQueueGroup::singleton()->pop();
+ while ( $job ) {
+ $job = JobQueueGroup::singleton()->pop();
+ }
+ $this->assertFalse( $job );
+ }
+
+ /**
+ * @depends testClearQueue
+ */
+ public function testSetupUrlDownload( $data ) {
+ $token = $this->user->getEditToken();
+ $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://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.png',
+ 'asyncdownload' => 1,
+ 'filename' => 'UploadFromUrlTest.png',
+ 'token' => $token,
+ ), $data );
+
+ $this->assertEquals( $data[0]['upload']['result'], 'Queued', 'Queued upload' );
+
+ $job = JobQueueGroup::singleton()->pop();
+ $this->assertThat( $job, $this->isInstanceOf( 'UploadFromUrlJob' ), 'Queued upload inserted' );
+ }
+
+ /**
+ * @depends testClearQueue
+ */
+ public function testAsyncUpload( $data ) {
+ $token = $this->user->getEditToken();
+
+ $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 testClearQueue
+ */
+ public function testAsyncUploadWarning( $data ) {
+ $token = $this->user->getEditToken();
+
+ $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 testClearQueue
+ */
+ public function testSyncDownload( $data ) {
+ $token = $this->user->getEditToken();
+
+ $job = JobQueueGroup::singleton()->pop();
+ $this->assertFalse( $job, 'Starting with an empty jobqueue' );
+
+ $this->user->addGroup( 'users' );
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.png',
+ 'ignorewarnings' => true,
+ 'token' => $token,
+ ), $data );
+
+ $job = JobQueueGroup::singleton()->pop();
+ $this->assertFalse( $job );
+
+ $this->assertEquals( 'Success', $data[0]['upload']['result'] );
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ return $data;
+ }
+
+ public function testLeaveMessage() {
+ $token = $this->user->user->getEditToken();
+
+ $talk = $this->user->user->getTalkPage();
+ if ( $talk->exists() ) {
+ $page = WikiPage::factory( $talk );
+ $page->doDeleteArticle( '' );
+ }
+
+ $this->assertFalse(
+ (bool)$talk->getArticleID( Title::GAID_FOR_UPDATE ),
+ 'User talk does not exist'
+ );
+
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.png',
+ 'asyncdownload' => 1,
+ 'token' => $token,
+ 'leavemessage' => 1,
+ 'ignorewarnings' => 1,
+ ) );
+
+ $job = JobQueueGroup::singleton()->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' );
+
+ $exception = false;
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.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 = JobQueueGroup::singleton()->pop();
+ $this->assertFalse( $job );
+
+ return;
+ /*
+ // Broken until using leavemessage with ignorewarnings is supported
+ $talkRev = Revision::newFromTitle( $talk );
+ $talkSize = $talkRev->getSize();
+
+ $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
+ *
+ * @param string $token
+ * @param bool $ignoreWarnings
+ * @param bool $leaveMessage
+ * @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://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.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 = JobQueueGroup::singleton()->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 );
+ $page = WikiPage::factory( $t );
+ $page->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..d5d1188e
--- /dev/null
+++ b/tests/phpunit/includes/upload/UploadStashTest.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * @group Database
+ *
+ * @covers UploadStash
+ */
+class UploadStashTest extends MediaWikiTestCase {
+ /**
+ * @var array Array of UploadStashTestUser
+ */
+ public static $users;
+
+ /**
+ * @var string
+ */
+ private $bug29408File;
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Setup a file for bug 29408
+ $this->bug29408File = __DIR__ . '/bug29408';
+ file_put_contents( $this->bug29408File, "\x00" );
+
+ self::$users = array(
+ 'sysop' => new TestUser(
+ 'Uploadstashtestsysop',
+ 'Upload Stash Test Sysop',
+ 'upload_stash_test_sysop@example.com',
+ array( 'sysop' )
+ ),
+ 'uploader' => new TestUser(
+ 'Uploadstashtestuser',
+ 'Upload Stash Test User',
+ 'upload_stash_test_user@example.com',
+ array()
+ )
+ );
+ }
+
+ protected function tearDown() {
+ if ( file_exists( $this->bug29408File . "." ) ) {
+ unlink( $this->bug29408File . "." );
+ }
+
+ if ( file_exists( $this->bug29408File ) ) {
+ unlink( $this->bug29408File );
+ }
+
+ parent::tearDown();
+ }
+
+ /**
+ * @todo give this test a real name explaining what is being tested here
+ */
+ public function testBug29408() {
+ $this->setMwGlobals( '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 static function provideInvalidRequests() {
+ return array(
+ 'Check failure on bad wpFileKey' =>
+ array( new FauxRequest( array( 'wpFileKey' => 'foo' ) ) ),
+ 'Check failure on bad wpSessionKey' =>
+ array( new FauxRequest( array( 'wpSessionKey' => 'foo' ) ) ),
+ );
+ }
+
+ /**
+ * @dataProvider provideInvalidRequests
+ */
+ public function testValidRequestWithInvalidRequests( $request ) {
+ $this->assertFalse( UploadFromStash::isValidRequest( $request ) );
+ }
+
+ public static function provideValidRequests() {
+ return array(
+ 'Check good wpFileKey' =>
+ array( new FauxRequest( array( 'wpFileKey' => 'testkey-test.test' ) ) ),
+ 'Check good wpSessionKey' =>
+ array( new FauxRequest( array( 'wpFileKey' => 'testkey-test.test' ) ) ),
+ 'Check key precedence' =>
+ array( new FauxRequest( array(
+ 'wpFileKey' => 'testkey-test.test',
+ 'wpSessionKey' => 'foo'
+ ) ) ),
+ );
+ }
+ /**
+ * @dataProvider provideValidRequests
+ */
+ public function testValidRequestWithValidRequests( $request ) {
+ $this->assertTrue( UploadFromStash::isValidRequest( $request ) );
+ }
+
+}
diff --git a/tests/phpunit/includes/utils/CdbTest.php b/tests/phpunit/includes/utils/CdbTest.php
new file mode 100644
index 00000000..487ee1fc
--- /dev/null
+++ b/tests/phpunit/includes/utils/CdbTest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * Test the CDB reader/writer
+ * @covers CdbWriterPHP
+ * @covers CdbWriterDBA
+ */
+class CdbTest extends MediaWikiTestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ if ( !CdbReader::haveExtension() ) {
+ $this->markTestSkipped( 'Native CDB support is not available' );
+ }
+ }
+
+ /**
+ * @group medium
+ */
+ public function testCdb() {
+ $dir = wfTempDir();
+ if ( !is_writable( $dir ) ) {
+ $this->markTestSkipped( "Temp dir isn't writable" );
+ }
+
+ $phpcdbfile = $this->getNewTempFile();
+ $dbacdbfile = $this->getNewTempFile();
+
+ $w1 = new CdbWriterPHP( $phpcdbfile );
+ $w2 = new CdbWriterDBA( $dbacdbfile );
+
+ $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( $phpcdbfile ),
+ md5_file( $dbacdbfile ),
+ 'same hash'
+ );
+
+ $r1 = new CdbReaderPHP( $phpcdbfile );
+ $r2 = new CdbReaderDBA( $dbacdbfile );
+
+ 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 );
+ }
+ }
+
+ 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/utils/IPTest.php b/tests/phpunit/includes/utils/IPTest.php
new file mode 100644
index 00000000..ebe347fd
--- /dev/null
+++ b/tests/phpunit/includes/utils/IPTest.php
@@ -0,0 +1,580 @@
+<?php
+/**
+ * Tests for IP validity functions.
+ *
+ * Ported from /t/inc/IP.t by avar.
+ *
+ * @group IP
+ * @todo Test methods in this call should be split into a method and a
+ * dataprovider.
+ */
+
+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 :: occurrence' );
+ $this->assertFalse(
+ IP::isIPAddress( '2001:0DB8::A:1::' ),
+ 'IPv6 with a double :: occurrence, last at end'
+ );
+ $this->assertFalse(
+ IP::isIPAddress( '::2001:0DB8::5:1' ),
+ 'IPv6 with a double :: occurrence, 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( ' ' ) );
+ }
+
+ /**
+ * @covers IP::toHex
+ * @dataProvider provideToHex
+ */
+ public function testToHex( $expected, $input ) {
+ $result = IP::toHex( $input );
+ $this->assertTrue( $result === false || is_string( $result ) );
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * Provider for IP::testToHex()
+ */
+ public static function provideToHex() {
+ return array(
+ array( '00000001', '0.0.0.1' ),
+ array( '01020304', '1.2.3.4' ),
+ array( '7F000001', '127.0.0.1' ),
+ array( '80000000', '128.0.0.0' ),
+ array( 'DEADCAFE', '222.173.202.254' ),
+ array( 'FFFFFFFF', '255.255.255.255' ),
+ array( false, 'IN.VA.LI.D' ),
+ array( 'v6-00000000000000000000000000000001', '::1' ),
+ array( 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ),
+ array( 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ),
+ array( false, 'IN:VA::LI:D' ),
+ array( false, ':::1' )
+ );
+ }
+
+ /**
+ * @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', '00FC::' );
+ 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
+ */
+ public 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() */
+ public static 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
+ */
+ public function testSplitHostAndPort( $expected, $input, $description ) {
+ $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description );
+ }
+
+ /**
+ * Provider for IP::splitHostAndPort()
+ */
+ public static 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
+ */
+ public function testCombineHostAndPort( $expected, $input, $description ) {
+ list( $host, $port, $defaultPort ) = $input;
+ $this->assertEquals(
+ $expected,
+ IP::combineHostAndPort( $host, $port, $defaultPort ),
+ $description );
+ }
+
+ /**
+ * Provider for IP::combineHostAndPort()
+ */
+ public static 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
+ */
+ public function testSanitizeRange( $input, $expected, $description ) {
+ $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description );
+ }
+
+ /**
+ * Provider for IP::testSanitizeRange()
+ */
+ public static 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' ),
+ );
+ }
+
+ /**
+ * Test for IP::prettifyIP()
+ * @dataProvider provideIPsToPrettify
+ */
+ public function testPrettifyIP( $ip, $prettified ) {
+ $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" );
+ }
+
+ /**
+ * Provider for IP::testPrettifyIP()
+ */
+ public static function provideIPsToPrettify() {
+ return array(
+ array( '0:0:0:0:0:0:0:0', '::' ),
+ array( '0:0:0::0:0:0', '::' ),
+ array( '0:0:0:1:0:0:0:0', '0:0:0:1::' ),
+ array( '0:0::f', '::f' ),
+ array( '0::0:0:0:33:fef:b', '::33:fef:b' ),
+ array( '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ),
+ array( '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ),
+ array( 'abbc:2004::0:0:0:0', 'abbc:2004::' ),
+ array( 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ),
+ array( '0:0:0:0:0:0:0:0/16', '::/16' ),
+ array( '0:0:0::0:0:0/64', '::/64' ),
+ array( '0:0::f/52', '::f/52' ),
+ array( '::0:0:33:fef:b/52', '::33:fef:b/52' ),
+ array( '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ),
+ array( '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ),
+ array( 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ),
+ array( 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/utils/MWCryptHKDFTest.php b/tests/phpunit/includes/utils/MWCryptHKDFTest.php
new file mode 100644
index 00000000..7e37534a
--- /dev/null
+++ b/tests/phpunit/includes/utils/MWCryptHKDFTest.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ *
+ * @group HKDF
+ */
+
+class MWCryptHKDFTest extends MediaWikiTestCase {
+
+ /**
+ * Test basic usage works
+ */
+ public function testGenerate() {
+ $a = MWCryptHKDF::generateHex( 64 );
+ $b = MWCryptHKDF::generateHex( 64 );
+
+ $this->assertTrue( strlen( $a ) == 64, "MWCryptHKDF produced fewer bytes than expected" );
+ $this->assertTrue( strlen( $b ) == 64, "MWCryptHKDF produced fewer bytes than expected" );
+ $this->assertFalse( $a == $b, "Two runs of MWCryptHKDF produced the same result." );
+ }
+
+ /**
+ * @dataProvider providerRfc5869
+ */
+ public function testRfc5869( $hash, $ikm, $salt, $info, $L, $prk, $okm ) {
+ $ikm = pack( 'H*', $ikm );
+ $salt = pack( 'H*', $salt );
+ $info = pack( 'H*', $info );
+ $okm = pack( 'H*', $okm );
+ $result = MWCryptHKDF::HKDF( $hash, $ikm, $salt, $info, $L );
+ $this->assertEquals( $okm, $result );
+ }
+
+ /**
+ * Test vectors from Appendix A on http://tools.ietf.org/html/rfc5869
+ */
+ public static function providerRfc5869() {
+
+ return array(
+ // A.1
+ array( 'sha256',
+ '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', // ikm
+ '000102030405060708090a0b0c', // salt
+ 'f0f1f2f3f4f5f6f7f8f9', // context
+ 42, // bytes
+ '077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5', // prk
+ '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865' // okm
+ ),
+ // A.2
+ array( 'sha256',
+ '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f',
+ '606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf',
+ 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff',
+ 82,
+ '06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244',
+ 'b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87'
+ ),
+ // A.3
+ array( 'sha256',
+ '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', // ikm
+ '', // salt
+ '', // context
+ 42, // bytes
+ '19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04', // prk
+ '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8' // okm
+ ),
+ // A.4
+ array( 'sha1',
+ '0b0b0b0b0b0b0b0b0b0b0b', // ikm
+ '000102030405060708090a0b0c', // salt
+ 'f0f1f2f3f4f5f6f7f8f9', // context
+ 42, // bytes
+ '9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243', // prk
+ '085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896' // okm
+ ),
+ // A.5
+ array( 'sha1',
+ '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f', // ikm
+ '606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf', // salt
+ 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', // context
+ 82, // bytes
+ '8adae09a2a307059478d309b26c4115a224cfaf6', // prk
+ '0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4' // okm
+ ),
+ );
+
+ }
+
+
+}
diff --git a/tests/phpunit/includes/utils/StringUtilsTest.php b/tests/phpunit/includes/utils/StringUtilsTest.php
new file mode 100644
index 00000000..0fdb8e15
--- /dev/null
+++ b/tests/phpunit/includes/utils/StringUtilsTest.php
@@ -0,0 +1,149 @@
+<?php
+
+class StringUtilsTest extends MediaWikiTestCase {
+
+ /**
+ * This tests StringUtils::isUtf8 whenever we have the mbstring extension
+ * loaded.
+ *
+ * @covers StringUtils::isUtf8
+ * @dataProvider provideStringsForIsUtf8Check
+ */
+ public function testIsUtf8WithMbstring( $expected, $string ) {
+ if ( !function_exists( 'mb_check_encoding' ) ) {
+ $this->markTestSkipped( 'Test requires the mbstring PHP extension' );
+ }
+ $this->assertEquals( $expected,
+ StringUtils::isUtf8( $string ),
+ 'Testing string "' . $this->escaped( $string ) . '" with mb_check_encoding'
+ );
+ }
+
+ /**
+ * This tests StringUtils::isUtf8 making sure we use the pure PHP
+ * implementation used as a fallback when mb_check_encoding() is
+ * not available.
+ *
+ * @covers StringUtils::isUtf8
+ * @dataProvider provideStringsForIsUtf8Check
+ */
+ public function testIsUtf8WithPhpFallbackImplementation( $expected, $string ) {
+ $this->assertEquals( $expected,
+ StringUtils::isUtf8( $string, /** disable mbstring: */true ),
+ 'Testing string "' . $this->escaped( $string ) . '" with pure PHP implementation'
+ );
+ }
+
+ /**
+ * Print high range characters as a hexadecimal
+ * @param string $string
+ * @return string
+ */
+ function escaped( $string ) {
+ $escaped = '';
+ $length = strlen( $string );
+ for ( $i = 0; $i < $length; $i++ ) {
+ $char = $string[$i];
+ $val = ord( $char );
+ if ( $val > 127 ) {
+ $escaped .= '\x' . dechex( $val );
+ } else {
+ $escaped .= $char;
+ }
+ }
+
+ return $escaped;
+ }
+
+ /**
+ * See also "UTF-8 decoder capability and stress test" by
+ * Markus Kuhn:
+ * http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
+ */
+ public static function provideStringsForIsUtf8Check() {
+ // Expected return values for StringUtils::isUtf8()
+ $PASS = true;
+ $FAIL = false;
+
+ return array(
+ 'some ASCII' => array( $PASS, 'Some ASCII' ),
+ 'euro sign' => array( $PASS, "Euro sign €" ),
+
+ 'first possible sequence 1 byte' => array( $PASS, "\x00" ),
+ 'first possible sequence 2 bytes' => array( $PASS, "\xc2\x80" ),
+ 'first possible sequence 3 bytes' => array( $PASS, "\xe0\xa0\x80" ),
+ 'first possible sequence 4 bytes' => array( $PASS, "\xf0\x90\x80\x80" ),
+ 'first possible sequence 5 bytes' => array( $FAIL, "\xf8\x88\x80\x80\x80" ),
+ 'first possible sequence 6 bytes' => array( $FAIL, "\xfc\x84\x80\x80\x80\x80" ),
+
+ 'last possible sequence 1 byte' => array( $PASS, "\x7f" ),
+ 'last possible sequence 2 bytes' => array( $PASS, "\xdf\xbf" ),
+ 'last possible sequence 3 bytes' => array( $PASS, "\xef\xbf\xbf" ),
+ 'last possible sequence 4 bytes (U+1FFFFF)' => array( $FAIL, "\xf7\xbf\xbf\xbf" ),
+ 'last possible sequence 5 bytes' => array( $FAIL, "\xfb\xbf\xbf\xbf\xbf" ),
+ 'last possible sequence 6 bytes' => array( $FAIL, "\xfd\xbf\xbf\xbf\xbf\xbf" ),
+
+ 'boundary 1' => array( $PASS, "\xed\x9f\xbf" ),
+ 'boundary 2' => array( $PASS, "\xee\x80\x80" ),
+ 'boundary 3' => array( $PASS, "\xef\xbf\xbd" ),
+ 'boundary 4' => array( $PASS, "\xf2\x80\x80\x80" ),
+ 'boundary 5 (U+FFFFF)' => array( $PASS, "\xf3\xbf\xbf\xbf" ),
+ 'boundary 6 (U+100000)' => array( $PASS, "\xf4\x80\x80\x80" ),
+ 'boundary 7 (U+10FFFF)' => array( $PASS, "\xf4\x8f\xbf\xbf" ),
+ 'boundary 8 (U+110000)' => array( $FAIL, "\xf4\x90\x80\x80" ),
+
+ 'malformed 1' => array( $FAIL, "\x80" ),
+ 'malformed 2' => array( $FAIL, "\xbf" ),
+ 'malformed 3' => array( $FAIL, "\x80\xbf" ),
+ 'malformed 4' => array( $FAIL, "\x80\xbf\x80" ),
+ 'malformed 5' => array( $FAIL, "\x80\xbf\x80\xbf" ),
+ 'malformed 6' => array( $FAIL, "\x80\xbf\x80\xbf\x80" ),
+ 'malformed 7' => array( $FAIL, "\x80\xbf\x80\xbf\x80\xbf" ),
+ 'malformed 8' => array( $FAIL, "\x80\xbf\x80\xbf\x80\xbf\x80" ),
+
+ 'last byte missing 1' => array( $FAIL, "\xc0" ),
+ 'last byte missing 2' => array( $FAIL, "\xe0\x80" ),
+ 'last byte missing 3' => array( $FAIL, "\xf0\x80\x80" ),
+ 'last byte missing 4' => array( $FAIL, "\xf8\x80\x80\x80" ),
+ 'last byte missing 5' => array( $FAIL, "\xfc\x80\x80\x80\x80" ),
+ 'last byte missing 6' => array( $FAIL, "\xdf" ),
+ 'last byte missing 7' => array( $FAIL, "\xef\xbf" ),
+ 'last byte missing 8' => array( $FAIL, "\xf7\xbf\xbf" ),
+ 'last byte missing 9' => array( $FAIL, "\xfb\xbf\xbf\xbf" ),
+ 'last byte missing 10' => array( $FAIL, "\xfd\xbf\xbf\xbf\xbf" ),
+
+ 'extra continuation byte 1' => array( $FAIL, "e\xaf" ),
+ 'extra continuation byte 2' => array( $FAIL, "\xc3\x89\xaf" ),
+ 'extra continuation byte 3' => array( $FAIL, "\xef\xbc\xa5\xaf" ),
+ 'extra continuation byte 4' => array( $FAIL, "\xf0\x9d\x99\xb4\xaf" ),
+
+ 'impossible bytes 1' => array( $FAIL, "\xfe" ),
+ 'impossible bytes 2' => array( $FAIL, "\xff" ),
+ 'impossible bytes 3' => array( $FAIL, "\xfe\xfe\xff\xff" ),
+
+ 'overlong sequences 1' => array( $FAIL, "\xc0\xaf" ),
+ 'overlong sequences 2' => array( $FAIL, "\xc1\xaf" ),
+ 'overlong sequences 3' => array( $FAIL, "\xe0\x80\xaf" ),
+ 'overlong sequences 4' => array( $FAIL, "\xf0\x80\x80\xaf" ),
+ 'overlong sequences 5' => array( $FAIL, "\xf8\x80\x80\x80\xaf" ),
+ 'overlong sequences 6' => array( $FAIL, "\xfc\x80\x80\x80\x80\xaf" ),
+
+ 'maximum overlong sequences 1' => array( $FAIL, "\xc1\xbf" ),
+ 'maximum overlong sequences 2' => array( $FAIL, "\xe0\x9f\xbf" ),
+ 'maximum overlong sequences 3' => array( $FAIL, "\xf0\x8f\xbf\xbf" ),
+ 'maximum overlong sequences 4' => array( $FAIL, "\xf8\x87\xbf\xbf" ),
+ 'maximum overlong sequences 5' => array( $FAIL, "\xfc\x83\xbf\xbf\xbf\xbf" ),
+
+ 'surrogates 1 (U+D799)' => array( $PASS, "\xed\x9f\xbf" ),
+ 'surrogates 2 (U+E000)' => array( $PASS, "\xee\x80\x80" ),
+ 'surrogates 3 (U+D800)' => array( $FAIL, "\xed\xa0\x80" ),
+ 'surrogates 4 (U+DBFF)' => array( $FAIL, "\xed\xaf\xbf" ),
+ 'surrogates 5 (U+DC00)' => array( $FAIL, "\xed\xb0\x80" ),
+ 'surrogates 6 (U+DFFF)' => array( $FAIL, "\xed\xbf\xbf" ),
+ 'surrogates 7 (U+D800 U+DC00)' => array( $FAIL, "\xed\xa0\x80\xed\xb0\x80" ),
+
+ 'noncharacters 1' => array( $PASS, "\xef\xbf\xbe" ),
+ 'noncharacters 2' => array( $PASS, "\xef\xbf\xbf" ),
+ );
+ }
+}
diff --git a/tests/phpunit/includes/utils/UIDGeneratorTest.php b/tests/phpunit/includes/utils/UIDGeneratorTest.php
new file mode 100644
index 00000000..50fa3849
--- /dev/null
+++ b/tests/phpunit/includes/utils/UIDGeneratorTest.php
@@ -0,0 +1,129 @@
+<?php
+
+class UIDGeneratorTest extends MediaWikiTestCase {
+
+ protected function tearDown() {
+ // Bug: 44850
+ UIDGenerator::unitTestTearDown();
+ parent::tearDown();
+ }
+
+ /**
+ * @dataProvider provider_testTimestampedUID
+ * @covers UIDGenerator::newTimestampedUID128
+ * @covers UIDGenerator::newTimestampedUID88
+ */
+ public function testTimestampedUID( $method, $digitlen, $bits, $tbits, $hostbits ) {
+ $id = call_user_func( array( 'UIDGenerator', $method ) );
+ $this->assertEquals( true, ctype_digit( $id ), "UID made of digit characters" );
+ $this->assertLessThanOrEqual( $digitlen, strlen( $id ),
+ "UID has the right number of digits" );
+ $this->assertLessThanOrEqual( $bits, strlen( wfBaseConvert( $id, 10, 2 ) ),
+ "UID has the right number of bits" );
+
+ $ids = array();
+ for ( $i = 0; $i < 300; $i++ ) {
+ $ids[] = call_user_func( array( 'UIDGenerator', $method ) );
+ }
+
+ $lastId = array_shift( $ids );
+
+ $this->assertArrayEquals( array_unique( $ids ), $ids, "All generated IDs are unique." );
+
+ foreach ( $ids as $id ) {
+ $id_bin = wfBaseConvert( $id, 10, 2 );
+ $lastId_bin = wfBaseConvert( $lastId, 10, 2 );
+
+ $this->assertGreaterThanOrEqual(
+ substr( $id_bin, 0, $tbits ),
+ substr( $lastId_bin, 0, $tbits ),
+ "New ID timestamp ($id_bin) >= prior one ($lastId_bin)." );
+
+ if ( $hostbits ) {
+ $this->assertEquals(
+ substr( $id_bin, 0, -$hostbits ),
+ substr( $lastId_bin, 0, -$hostbits ),
+ "Host ID of ($id_bin) is same as prior one ($lastId_bin)." );
+ }
+
+ $lastId = $id;
+ }
+ }
+
+ /**
+ * array( method, length, bits, hostbits )
+ * NOTE: When adding a new method name here please update the covers tags for the tests!
+ */
+ public static function provider_testTimestampedUID() {
+ return array(
+ array( 'newTimestampedUID128', 39, 128, 46, 48 ),
+ array( 'newTimestampedUID128', 39, 128, 46, 48 ),
+ array( 'newTimestampedUID88', 27, 88, 46, 32 ),
+ );
+ }
+
+ /**
+ * @covers UIDGenerator::newUUIDv4
+ */
+ public function testUUIDv4() {
+ for ( $i = 0; $i < 100; $i++ ) {
+ $id = UIDGenerator::newUUIDv4();
+ $this->assertEquals( true,
+ preg_match( '!^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$!', $id ),
+ "UID $id has the right format" );
+ }
+ }
+
+ /**
+ * @covers UIDGenerator::newRawUUIDv4
+ */
+ public function testRawUUIDv4() {
+ for ( $i = 0; $i < 100; $i++ ) {
+ $id = UIDGenerator::newRawUUIDv4();
+ $this->assertEquals( true,
+ preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
+ "UID $id has the right format" );
+ }
+ }
+
+ /**
+ * @covers UIDGenerator::newRawUUIDv4
+ */
+ public function testRawUUIDv4QuickRand() {
+ for ( $i = 0; $i < 100; $i++ ) {
+ $id = UIDGenerator::newRawUUIDv4( UIDGenerator::QUICK_RAND );
+ $this->assertEquals( true,
+ preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
+ "UID $id has the right format" );
+ }
+ }
+
+ /**
+ * @covers UIDGenerator::newSequentialPerNodeID
+ */
+ public function testNewSequentialID() {
+ $id1 = UIDGenerator::newSequentialPerNodeID( 'test', 32 );
+ $id2 = UIDGenerator::newSequentialPerNodeID( 'test', 32 );
+
+ $this->assertType( 'float', $id1, "ID returned as float" );
+ $this->assertType( 'float', $id2, "ID returned as float" );
+ $this->assertGreaterThan( 0, $id1, "ID greater than 1" );
+ $this->assertGreaterThan( $id1, $id2, "IDs increasing in value" );
+ }
+
+ /**
+ * @covers UIDGenerator::newSequentialPerNodeIDs
+ */
+ public function testNewSequentialIDs() {
+ $ids = UIDGenerator::newSequentialPerNodeIDs( 'test', 32, 5 );
+ $lastId = null;
+ foreach ( $ids as $id ) {
+ $this->assertType( 'float', $id, "ID returned as float" );
+ $this->assertGreaterThan( 0, $id, "ID greater than 1" );
+ if ( $lastId ) {
+ $this->assertGreaterThan( $lastId, $id, "IDs increasing in value" );
+ }
+ $lastId = $id;
+ }
+ }
+}
diff --git a/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php
new file mode 100644
index 00000000..34ffb535
--- /dev/null
+++ b/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @covers ZipDirectoryReader
+ * NOTE: this test is more like an integration test than a unit test
+ */
+class ZipDirectoryReaderTest extends MediaWikiTestCase {
+ protected $zipDir;
+ protected $entries;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->zipDir = __DIR__ . '/../../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 );
+ }
+
+ public function testEmpty() {
+ $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
+ }
+
+ public function testMultiDisk0() {
+ $this->readZipAssertError( 'split.zip', 'zip-unsupported',
+ 'Split zip error' );
+ }
+
+ public function testNoSignature() {
+ $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
+ 'No signature should give "wrong format" error' );
+ }
+
+ public function testSimple() {
+ $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
+ $this->assertEquals( $this->entries, array( array(
+ 'name' => 'Class.class',
+ 'mtime' => '20010115000000',
+ 'size' => 1,
+ ) ) );
+ }
+
+ public function testBadCentralEntrySignature() {
+ $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
+ 'Bad central entry error' );
+ }
+
+ public function testTrailingBytes() {
+ $this->readZipAssertError( 'trail.zip', 'zip-bad',
+ 'Trailing bytes error' );
+ }
+
+ public function testWrongCDStart() {
+ $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
+ 'Wrong CD start disk error' );
+ }
+
+ public function testCentralDirectoryGap() {
+ $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
+ 'CD gap error' );
+ }
+
+ public function testCentralDirectoryTruncated() {
+ $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
+ 'CD truncated error (should hit unpack() overrun)' );
+ }
+
+ public 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/install-phpunit.sh b/tests/phpunit/install-phpunit.sh
new file mode 100644
index 00000000..022f998e
--- /dev/null
+++ b/tests/phpunit/install-phpunit.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+has_binary () {
+ if [ -z `which $1` ]; then
+ return 1
+ fi
+ return 0
+}
+
+if [ `id -u` -ne 0 ]; then
+ echo '*** ERROR: Must be root to run'
+ exit 1
+fi
+
+if ( has_binary phpunit ); then
+ echo PHPUnit already installed
+else if ( has_binary pear ); then
+ echo Installing phpunit with pear
+ pear channel-discover pear.phpunit.de
+ pear channel-discover components.ez.no
+ pear channel-discover pear.symfony.com
+ pear update-channels
+ #Temporary fix for 64597
+ pear install --alldeps phpunit/PHPUnit-3.7.35
+else if ( has_binary apt-get ); then
+ echo Installing phpunit with apt-get
+ apt-get install phpunit
+else if ( has_binary yum ); then
+ echo Installing phpunit with yum
+ yum install phpunit
+else if ( has_binary port ); then
+ echo Installing phpunit with macports
+ port install php5-unit
+fi
+fi
+fi
+fi
+fi
diff --git a/tests/phpunit/languages/LanguageAmTest.php b/tests/phpunit/languages/LanguageAmTest.php
new file mode 100644
index 00000000..a644f5e0
--- /dev/null
+++ b/tests/phpunit/languages/LanguageAmTest.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageAm.php */
+class LanguageAmTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageArTest.php b/tests/phpunit/languages/LanguageArTest.php
new file mode 100644
index 00000000..7b48f236
--- /dev/null
+++ b/tests/phpunit/languages/LanguageArTest.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Based on LanguagMlTest
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageAr.php */
+class LanguageArTest extends LanguageClassesTestCase {
+ /**
+ * @covers Language::formatNum
+ * @todo split into a test and a dataprovider
+ */
+ public function testFormatNum() {
+ $this->assertEquals( '١٬٢٣٤٬٥٦٧', $this->getLang()->formatNum( '1234567' ) );
+ $this->assertEquals( '-١٢٫٨٩', $this->getLang()->formatNum( -12.89 ) );
+ }
+
+ /**
+ * Mostly to test the raw ascii feature.
+ * @dataProvider providerSprintfDate
+ * @covers Language::sprintfDate
+ */
+ public function testSprintfDate( $format, $date, $expected ) {
+ $this->assertEquals( $expected, $this->getLang()->sprintfDate( $format, $date ) );
+ }
+
+ public static function providerSprintfDate() {
+ return array(
+ array(
+ 'xg "vs" g',
+ '20120102030410',
+ 'يناير vs ٣'
+ ),
+ array(
+ 'xmY',
+ '20120102030410',
+ '١٤٣٣'
+ ),
+ array(
+ 'xnxmY',
+ '20120102030410',
+ '1433'
+ ),
+ array(
+ 'xN xmj xmn xN xmY',
+ '20120102030410',
+ ' 7 2 ١٤٣٣'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'zero', 'one', 'two', 'few', 'many', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'zero', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 9 ),
+ array( 'few', 110 ),
+ array( 'many', 11 ),
+ array( 'many', 15 ),
+ array( 'many', 99 ),
+ array( 'many', 9999 ),
+ array( 'other', 100 ),
+ array( 'other', 102 ),
+ array( 'other', 1000 ),
+ array( 'other', 1.7 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageArqTest.php b/tests/phpunit/languages/LanguageArqTest.php
new file mode 100644
index 00000000..3fa56d78
--- /dev/null
+++ b/tests/phpunit/languages/LanguageArqTest.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Based on LanguageMlTest
+ * @author Joel Sahleen
+ * @copyright Copyright © 2014, Joel Sahleen
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageArq.php */
+class LanguageArqTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider provideNumber
+ * @covers Language::formatNum
+ */
+ public function testFormatNum( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->formatNum( $value ) );
+ }
+
+ public static function provideNumber() {
+ return array(
+ array( '1.234.567', '1234567'),
+ array( '-12,89', -12.89 ),
+ );
+ }
+
+}
diff --git a/tests/phpunit/languages/LanguageBeTest.php b/tests/phpunit/languages/LanguageBeTest.php
new file mode 100644
index 00000000..7bd586af
--- /dev/null
+++ b/tests/phpunit/languages/LanguageBeTest.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageBe.php */
+class LanguageBeTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'many', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 1 ),
+ array( 'many', 11 ),
+ array( 'one', 91 ),
+ array( 'one', 121 ),
+ array( 'few', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 4 ),
+ array( 'few', 334 ),
+ array( 'many', 5 ),
+ array( 'many', 15 ),
+ array( 'many', 120 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageBe_taraskTest.php b/tests/phpunit/languages/LanguageBe_taraskTest.php
new file mode 100644
index 00000000..4dd5cdd7
--- /dev/null
+++ b/tests/phpunit/languages/LanguageBe_taraskTest.php
@@ -0,0 +1,97 @@
+<?php
+
+// @codingStandardsIgnoreStart Ignore Squiz.Classes.ValidClassName.NotCamelCaps
+class LanguageBe_taraskTest extends LanguageClassesTestCase {
+ // @codingStandardsIgnoreEnd
+ /**
+ * Make sure the language code we are given is indeed
+ * be-tarask. This is to ensure LanguageClassesTestCase
+ * does not give us the wrong language.
+ */
+ public function testBeTaraskTestsUsesBeTaraskCode() {
+ $this->assertEquals( 'be-tarask',
+ $this->getLang()->getCode()
+ );
+ }
+
+ /**
+ * @see bug 23156 & r64981
+ * @covers Language::commafy
+ */
+ public function testSearchRightSingleQuotationMarkAsApostroph() {
+ $this->assertEquals(
+ "'",
+ $this->getLang()->normalizeForSearch( '’' ),
+ 'bug 23156: U+2019 conversion to U+0027'
+ );
+ }
+
+ /**
+ * @see bug 23156 & r64981
+ * @covers Language::commafy
+ */
+ public function testCommafy() {
+ $this->assertEquals( '1,234,567', $this->getLang()->commafy( '1234567' ) );
+ $this->assertEquals( '12,345', $this->getLang()->commafy( '12345' ) );
+ }
+
+ /**
+ * @see bug 23156 & r64981
+ * @covers Language::commafy
+ */
+ public function testDoesNotCommafyFourDigitsNumber() {
+ $this->assertEquals( '1234', $this->getLang()->commafy( '1234' ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'many', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 1 ),
+ array( 'many', 11 ),
+ array( 'one', 91 ),
+ array( 'one', 121 ),
+ array( 'few', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 4 ),
+ array( 'few', 334 ),
+ array( 'many', 5 ),
+ array( 'many', 15 ),
+ array( 'many', 120 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralTwoForms( $result, $value ) {
+ $forms = array( '1=one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 11 ),
+ array( 'other', 91 ),
+ array( 'other', 121 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageBhoTest.php b/tests/phpunit/languages/LanguageBhoTest.php
new file mode 100644
index 00000000..187bfbbc
--- /dev/null
+++ b/tests/phpunit/languages/LanguageBhoTest.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageBho.php */
+class LanguageBhoTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageBsTest.php b/tests/phpunit/languages/LanguageBsTest.php
new file mode 100644
index 00000000..7aca2ab1
--- /dev/null
+++ b/tests/phpunit/languages/LanguageBsTest.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for Croatian (hrvatski) */
+class LanguageBsTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 4 ),
+ array( 'other', 5 ),
+ array( 'other', 11 ),
+ array( 'other', 20 ),
+ array( 'one', 21 ),
+ array( 'few', 24 ),
+ array( 'other', 25 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageClassesTestCase.php b/tests/phpunit/languages/LanguageClassesTestCase.php
new file mode 100644
index 00000000..f93ff7d3
--- /dev/null
+++ b/tests/phpunit/languages/LanguageClassesTestCase.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Helping class to run tests using a clean language instance.
+ *
+ * This is intended for the MediaWiki language class tests under
+ * tests/phpunit/languages.
+ *
+ * Before each tests, a new language object is build which you
+ * can retrieve in your test using the $this->getLang() method:
+ *
+ * @par Using the crafted language object:
+ * @code
+ * function testHasLanguageObject() {
+ * $langObject = $this->getLang();
+ * $this->assertInstanceOf( 'LanguageFoo',
+ * $langObject
+ * );
+ * }
+ * @endcode
+ */
+abstract class LanguageClassesTestCase extends MediaWikiTestCase {
+ /**
+ * Internal language object
+ *
+ * A new object is created before each tests thanks to PHPUnit
+ * setUp() method, it is deleted after each test too. To get
+ * this object you simply use the getLang method.
+ *
+ * You must have setup a language code first. See $LanguageClassCode
+ * @code
+ * function testWeAreTheChampions() {
+ * $this->getLang(); # language object
+ * }
+ * @endcode
+ */
+ private $languageObject;
+
+ /**
+ * @return Language
+ */
+ protected function getLang() {
+ return $this->languageObject;
+ }
+
+ /**
+ * Create a new language object before each test.
+ */
+ protected function setUp() {
+ parent::setUp();
+ $found = preg_match( '/Language(.+)Test/', get_called_class(), $m );
+ if ( $found ) {
+ # Normalize language code since classes uses underscores
+ $m[1] = strtolower( str_replace( '_', '-', $m[1] ) );
+ } else {
+ # Fallback to english language
+ $m[1] = 'en';
+ wfDebug(
+ __METHOD__ . " could not extract a language name "
+ . "out of " . get_called_class() . " failling back to 'en'\n"
+ );
+ }
+ // @todo validate $m[1] which should be a valid language code
+ $this->languageObject = Language::factory( $m[1] );
+ }
+
+ /**
+ * Delete the internal language object so each test start
+ * out with a fresh language instance.
+ */
+ protected function tearDown() {
+ unset( $this->languageObject );
+ parent::tearDown();
+ }
+}
diff --git a/tests/phpunit/languages/LanguageCsTest.php b/tests/phpunit/languages/LanguageCsTest.php
new file mode 100644
index 00000000..da9e6b88
--- /dev/null
+++ b/tests/phpunit/languages/LanguageCsTest.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/Languagecs.php */
+class LanguageCsTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 4 ),
+ array( 'other', 5 ),
+ array( 'other', 11 ),
+ array( 'other', 20 ),
+ array( 'other', 25 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageCuTest.php b/tests/phpunit/languages/LanguageCuTest.php
new file mode 100644
index 00000000..07193172
--- /dev/null
+++ b/tests/phpunit/languages/LanguageCuTest.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageCu.php */
+class LanguageCuTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 4 ),
+ array( 'other', 5 ),
+ array( 'one', 11 ),
+ array( 'other', 20 ),
+ array( 'two', 22 ),
+ array( 'few', 223 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageCyTest.php b/tests/phpunit/languages/LanguageCyTest.php
new file mode 100644
index 00000000..eaf663a8
--- /dev/null
+++ b/tests/phpunit/languages/LanguageCyTest.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageCy.php */
+class LanguageCyTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'zero', 'one', 'two', 'few', 'many', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'zero', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'few', 3 ),
+ array( 'many', 6 ),
+ array( 'other', 4 ),
+ array( 'other', 5 ),
+ array( 'other', 11 ),
+ array( 'other', 20 ),
+ array( 'other', 22 ),
+ array( 'other', 223 ),
+ array( 'other', 200.00 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageDsbTest.php b/tests/phpunit/languages/LanguageDsbTest.php
new file mode 100644
index 00000000..94c11bcc
--- /dev/null
+++ b/tests/phpunit/languages/LanguageDsbTest.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageDsb.php */
+class LanguageDsbTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'one', 101 ),
+ array( 'one', 90001 ),
+ array( 'two', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 203 ),
+ array( 'few', 4 ),
+ array( 'other', 99 ),
+ array( 'other', 555 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageFrTest.php b/tests/phpunit/languages/LanguageFrTest.php
new file mode 100644
index 00000000..46b65011
--- /dev/null
+++ b/tests/phpunit/languages/LanguageFrTest.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageFr.php */
+class LanguageFrTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageGaTest.php b/tests/phpunit/languages/LanguageGaTest.php
new file mode 100644
index 00000000..c009f56b
--- /dev/null
+++ b/tests/phpunit/languages/LanguageGaTest.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageGa.php */
+class LanguageGaTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageGdTest.php b/tests/phpunit/languages/LanguageGdTest.php
new file mode 100644
index 00000000..b89b4df9
--- /dev/null
+++ b/tests/phpunit/languages/LanguageGdTest.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012-2013, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageGd.php */
+class LanguageGdTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providerPlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providerPlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'one', 11 ),
+ array( 'two', 12 ),
+ array( 'few', 3 ),
+ array( 'few', 19 ),
+ array( 'other', 200 ),
+ );
+ }
+
+ /**
+ * @dataProvider providerPluralExplicit
+ * @covers Language::convertPlural
+ */
+ public function testExplicitPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'few', 'other', '11=Form11', '12=Form12' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providerPluralExplicit() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'Form11', 11 ),
+ array( 'Form12', 12 ),
+ array( 'few', 3 ),
+ array( 'few', 19 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageGvTest.php b/tests/phpunit/languages/LanguageGvTest.php
new file mode 100644
index 00000000..fc58022a
--- /dev/null
+++ b/tests/phpunit/languages/LanguageGvTest.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Test for Manx (Gaelg) language
+ *
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2013, Santhosh Thottingal
+ * @file
+ */
+
+class LanguageGvTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'few', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'other', 3 ),
+ array( 'few', 20 ),
+ array( 'one', 21 ),
+ array( 'two', 22 ),
+ array( 'other', 23 ),
+ array( 'other', 50 ),
+ array( 'few', 60 ),
+ array( 'other', 80 ),
+ array( 'few', 100 )
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageHeTest.php b/tests/phpunit/languages/LanguageHeTest.php
new file mode 100644
index 00000000..c382244f
--- /dev/null
+++ b/tests/phpunit/languages/LanguageHeTest.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageHe.php */
+class LanguageHeTest extends LanguageClassesTestCase {
+ /**
+ * The most common usage for the plural forms is two forms,
+ * for singular and plural. In this case, the second form
+ * is technically dual, but in practice it's used as plural.
+ * In some cases, usually with expressions of time, three forms
+ * are needed - singular, dual and plural.
+ * CLDR also specifies a fourth form for multiples of 10,
+ * which is very rare. It also has a mistake, because
+ * the number 10 itself is supposed to be just plural,
+ * so currently it's overridden in MediaWiki.
+ */
+
+ // @todo the below test*PluralForms test methods can be refactored
+ // to use a single test method and data provider..
+
+ /**
+ * @dataProvider provideTwoPluralForms
+ * @covers Language::convertPlural
+ */
+ public function testTwoPluralForms( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider provideThreePluralForms
+ * @covers Language::convertPlural
+ */
+ public function testThreePluralForms( $result, $value ) {
+ $forms = array( 'one', 'two', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider provideFourPluralForms
+ * @covers Language::convertPlural
+ */
+ public function testFourPluralForms( $result, $value ) {
+ $forms = array( 'one', 'two', 'many', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider provideFourPluralForms
+ * @covers Language::convertPlural
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function provideTwoPluralForms() {
+ return array(
+ array( 'other', 0 ), // Zero - plural
+ array( 'one', 1 ), // Singular
+ array( 'other', 2 ), // No third form provided, use it as plural
+ array( 'other', 3 ), // Plural - other
+ array( 'other', 10 ), // No fourth form provided, use it as plural
+ array( 'other', 20 ), // No fourth form provided, use it as plural
+ );
+ }
+
+ public static function provideThreePluralForms() {
+ return array(
+ array( 'other', 0 ), // Zero - plural
+ array( 'one', 1 ), // Singular
+ array( 'two', 2 ), // Dual
+ array( 'other', 3 ), // Plural - other
+ array( 'other', 10 ), // No fourth form provided, use it as plural
+ array( 'other', 20 ), // No fourth form provided, use it as plural
+ );
+ }
+
+ public static function provideFourPluralForms() {
+ return array(
+ array( 'other', 0 ), // Zero - plural
+ array( 'one', 1 ), // Singular
+ array( 'two', 2 ), // Dual
+ array( 'other', 3 ), // Plural - other
+ array( 'other', 10 ), // 10 is supposed to be plural (other), not "many"
+ array( 'many', 20 ), // Fourth form provided - rare, but supported by CLDR
+ );
+ }
+
+ /**
+ * @dataProvider provideGrammar
+ * @covers Language::convertGrammar
+ */
+ public function testGrammar( $result, $word, $case ) {
+ $this->assertEquals( $result, $this->getLang()->convertGrammar( $word, $case ) );
+ }
+
+ // The comments in the beginning of the line help avoid RTL problems
+ // with text editors.
+ public static function provideGrammar() {
+ return array(
+ array(
+ /* result */'וויקיפדיה',
+ /* word */'ויקיפדיה',
+ /* case */'תחילית',
+ ),
+ array(
+ /* result */'וולפגנג',
+ /* word */'וולפגנג',
+ /* case */'prefixed',
+ ),
+ array(
+ /* result */'קובץ',
+ /* word */'הקובץ',
+ /* case */'תחילית',
+ ),
+ array(
+ /* result */'־Wikipedia',
+ /* word */'Wikipedia',
+ /* case */'תחילית',
+ ),
+ array(
+ /* result */'־1995',
+ /* word */'1995',
+ /* case */'תחילית',
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageHiTest.php b/tests/phpunit/languages/LanguageHiTest.php
new file mode 100644
index 00000000..f6d2c9e9
--- /dev/null
+++ b/tests/phpunit/languages/LanguageHiTest.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageHi.php */
+class LanguageHiTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageHrTest.php b/tests/phpunit/languages/LanguageHrTest.php
new file mode 100644
index 00000000..644c5255
--- /dev/null
+++ b/tests/phpunit/languages/LanguageHrTest.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageHr.php */
+class LanguageHrTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 4 ),
+ array( 'other', 5 ),
+ array( 'other', 11 ),
+ array( 'other', 20 ),
+ array( 'one', 21 ),
+ array( 'few', 24 ),
+ array( 'other', 25 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageHsbTest.php b/tests/phpunit/languages/LanguageHsbTest.php
new file mode 100644
index 00000000..f95a43bf
--- /dev/null
+++ b/tests/phpunit/languages/LanguageHsbTest.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageHsb.php */
+class LanguageHsbTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'one', 101 ),
+ array( 'one', 90001 ),
+ array( 'two', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 203 ),
+ array( 'few', 4 ),
+ array( 'other', 99 ),
+ array( 'other', 555 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageHuTest.php b/tests/phpunit/languages/LanguageHuTest.php
new file mode 100644
index 00000000..ee9197d7
--- /dev/null
+++ b/tests/phpunit/languages/LanguageHuTest.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageHu.php */
+class LanguageHuTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageHyTest.php b/tests/phpunit/languages/LanguageHyTest.php
new file mode 100644
index 00000000..92e0ef94
--- /dev/null
+++ b/tests/phpunit/languages/LanguageHyTest.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for Armenian (Հայերեն) */
+class LanguageHyTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageKshTest.php b/tests/phpunit/languages/LanguageKshTest.php
new file mode 100644
index 00000000..568a3780
--- /dev/null
+++ b/tests/phpunit/languages/LanguageKshTest.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageKsh.php */
+class LanguageKshTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other', 'zero' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'zero', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageLnTest.php b/tests/phpunit/languages/LanguageLnTest.php
new file mode 100644
index 00000000..10b3234f
--- /dev/null
+++ b/tests/phpunit/languages/LanguageLnTest.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageLn.php */
+class LanguageLnTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageLtTest.php b/tests/phpunit/languages/LanguageLtTest.php
new file mode 100644
index 00000000..30642f62
--- /dev/null
+++ b/tests/phpunit/languages/LanguageLtTest.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageLt.php */
+class LanguageLtTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 9 ),
+ array( 'other', 10 ),
+ array( 'other', 11 ),
+ array( 'other', 20 ),
+ array( 'one', 21 ),
+ array( 'few', 32 ),
+ array( 'one', 41 ),
+ array( 'one', 40001 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testOneFewPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ // This fails for 21, but not sure why.
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 15 ),
+ array( 'other', 20 ),
+ array( 'one', 21 ),
+ array( 'other', 22 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageLvTest.php b/tests/phpunit/languages/LanguageLvTest.php
new file mode 100644
index 00000000..7120cfe3
--- /dev/null
+++ b/tests/phpunit/languages/LanguageLvTest.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for Latvian */
+class LanguageLvTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'zero', 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'zero', 0 ),
+ array( 'one', 1 ),
+ array( 'zero', 11 ),
+ array( 'one', 21 ),
+ array( 'zero', 411 ),
+ array( 'other', 2 ),
+ array( 'other', 9 ),
+ array( 'zero', 12 ),
+ array( 'other', 12.345 ),
+ array( 'zero', 20 ),
+ array( 'other', 22 ),
+ array( 'one', 31 ),
+ array( 'zero', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageMgTest.php b/tests/phpunit/languages/LanguageMgTest.php
new file mode 100644
index 00000000..65e8fd7b
--- /dev/null
+++ b/tests/phpunit/languages/LanguageMgTest.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageMg.php */
+class LanguageMgTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 200 ),
+ array( 'other', 123.3434 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageMkTest.php b/tests/phpunit/languages/LanguageMkTest.php
new file mode 100644
index 00000000..ed155263
--- /dev/null
+++ b/tests/phpunit/languages/LanguageMkTest.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for македонски/Macedonian */
+class LanguageMkTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'one', 11 ),
+ array( 'one', 21 ),
+ array( 'one', 411 ),
+ array( 'other', 12.345 ),
+ array( 'other', 20 ),
+ array( 'one', 31 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageMlTest.php b/tests/phpunit/languages/LanguageMlTest.php
new file mode 100644
index 00000000..4fa45ce3
--- /dev/null
+++ b/tests/phpunit/languages/LanguageMlTest.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2011, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageMl.php */
+class LanguageMlTest extends LanguageClassesTestCase {
+
+ /**
+ * @dataProvider providerFormatNum
+ * @see bug 29495
+ * @covers Language::formatNum
+ */
+ public function testFormatNum( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->formatNum( $value ) );
+ }
+
+ public static function providerFormatNum() {
+ return array(
+ array( '12,34,567', '1234567' ),
+ array( '12,345', '12345' ),
+ array( '1', '1' ),
+ array( '123', '123' ),
+ array( '1,234', '1234' ),
+ array( '12,345.56', '12345.56' ),
+ array( '12,34,56,79,81,23,45,678', '12345679812345678' ),
+ array( '.12345', '.12345' ),
+ array( '-12,00,000', '-1200000' ),
+ array( '-98', '-98' ),
+ array( '-98', -98 ),
+ array( '-1,23,45,678', -12345678 ),
+ array( '', '' ),
+ array( '', null ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageMoTest.php b/tests/phpunit/languages/LanguageMoTest.php
new file mode 100644
index 00000000..e0e54ca8
--- /dev/null
+++ b/tests/phpunit/languages/LanguageMoTest.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageMo.php */
+class LanguageMoTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'few', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 19 ),
+ array( 'other', 20 ),
+ array( 'other', 99 ),
+ array( 'other', 100 ),
+ array( 'few', 101 ),
+ array( 'few', 119 ),
+ array( 'other', 120 ),
+ array( 'other', 200 ),
+ array( 'few', 201 ),
+ array( 'few', 219 ),
+ array( 'other', 220 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageMtTest.php b/tests/phpunit/languages/LanguageMtTest.php
new file mode 100644
index 00000000..96d2bc92
--- /dev/null
+++ b/tests/phpunit/languages/LanguageMtTest.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageMt.php */
+class LanguageMtTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'many', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'few', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 10 ),
+ array( 'many', 11 ),
+ array( 'many', 19 ),
+ array( 'other', 20 ),
+ array( 'other', 99 ),
+ array( 'other', 100 ),
+ array( 'other', 101 ),
+ array( 'few', 102 ),
+ array( 'few', 110 ),
+ array( 'many', 111 ),
+ array( 'many', 119 ),
+ array( 'other', 120 ),
+ array( 'other', 201 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralTwoForms( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 10 ),
+ array( 'other', 11 ),
+ array( 'other', 19 ),
+ array( 'other', 20 ),
+ array( 'other', 99 ),
+ array( 'other', 100 ),
+ array( 'other', 101 ),
+ array( 'other', 102 ),
+ array( 'other', 110 ),
+ array( 'other', 111 ),
+ array( 'other', 119 ),
+ array( 'other', 120 ),
+ array( 'other', 201 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageNlTest.php b/tests/phpunit/languages/LanguageNlTest.php
new file mode 100644
index 00000000..26bd691a
--- /dev/null
+++ b/tests/phpunit/languages/LanguageNlTest.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2011, Santhosh Thottingal
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageNl.php */
+class LanguageNlTest extends LanguageClassesTestCase {
+
+ /**
+ * @covers Language::formatNum
+ * @todo split into a test and a dataprovider
+ */
+ public function testFormatNum() {
+ $this->assertEquals( '1.234.567', $this->getLang()->formatNum( '1234567' ) );
+ $this->assertEquals( '12.345', $this->getLang()->formatNum( '12345' ) );
+ $this->assertEquals( '1', $this->getLang()->formatNum( '1' ) );
+ $this->assertEquals( '123', $this->getLang()->formatNum( '123' ) );
+ $this->assertEquals( '1.234', $this->getLang()->formatNum( '1234' ) );
+ $this->assertEquals( '12.345,56', $this->getLang()->formatNum( '12345.56' ) );
+ $this->assertEquals( ',1234556', $this->getLang()->formatNum( '.1234556' ) );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageNsoTest.php b/tests/phpunit/languages/LanguageNsoTest.php
new file mode 100644
index 00000000..18efd736
--- /dev/null
+++ b/tests/phpunit/languages/LanguageNsoTest.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageNso.php */
+class LanguageNsoTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguagePlTest.php b/tests/phpunit/languages/LanguagePlTest.php
new file mode 100644
index 00000000..d180037b
--- /dev/null
+++ b/tests/phpunit/languages/LanguagePlTest.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguagePl.php */
+class LanguagePlTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'many' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'many', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 4 ),
+ array( 'many', 5 ),
+ array( 'many', 9 ),
+ array( 'many', 10 ),
+ array( 'many', 11 ),
+ array( 'many', 21 ),
+ array( 'few', 22 ),
+ array( 'few', 23 ),
+ array( 'few', 24 ),
+ array( 'many', 25 ),
+ array( 'many', 200 ),
+ array( 'many', 201 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralTwoForms( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 3 ),
+ array( 'other', 4 ),
+ array( 'other', 5 ),
+ array( 'other', 9 ),
+ array( 'other', 10 ),
+ array( 'other', 11 ),
+ array( 'other', 21 ),
+ array( 'other', 22 ),
+ array( 'other', 23 ),
+ array( 'other', 24 ),
+ array( 'other', 25 ),
+ array( 'other', 200 ),
+ array( 'other', 201 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageRoTest.php b/tests/phpunit/languages/LanguageRoTest.php
new file mode 100644
index 00000000..ae7816bc
--- /dev/null
+++ b/tests/phpunit/languages/LanguageRoTest.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageRo.php */
+class LanguageRoTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'few', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 19 ),
+ array( 'other', 20 ),
+ array( 'other', 99 ),
+ array( 'other', 100 ),
+ array( 'few', 101 ),
+ array( 'few', 119 ),
+ array( 'other', 120 ),
+ array( 'other', 200 ),
+ array( 'few', 201 ),
+ array( 'few', 219 ),
+ array( 'other', 220 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageRuTest.php b/tests/phpunit/languages/LanguageRuTest.php
new file mode 100644
index 00000000..f64fc722
--- /dev/null
+++ b/tests/phpunit/languages/LanguageRuTest.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * based on LanguageBe_tarask.php
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageRu.php */
+class LanguageRuTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'many', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * Test explicit plural forms - n=FormN forms
+ * @covers Language::convertPlural
+ */
+ public function testExplicitPlural() {
+ $forms = array( 'one', 'many', 'other', '12=dozen' );
+ $this->assertEquals( 'dozen', $this->getLang()->convertPlural( 12, $forms ) );
+ $forms = array( 'one', 'many', '100=hundred', 'other', '12=dozen' );
+ $this->assertEquals( 'hundred', $this->getLang()->convertPlural( 100, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 1 ),
+ array( 'many', 11 ),
+ array( 'one', 91 ),
+ array( 'one', 121 ),
+ array( 'other', 2 ),
+ array( 'other', 3 ),
+ array( 'other', 4 ),
+ array( 'other', 334 ),
+ array( 'many', 5 ),
+ array( 'many', 15 ),
+ array( 'many', 120 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralTwoForms( $result, $value ) {
+ $forms = array( '1=one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'one', 1 ),
+ array( 'other', 11 ),
+ array( 'other', 91 ),
+ array( 'other', 121 ),
+ );
+ }
+
+ /**
+ * @dataProvider providerGrammar
+ * @covers Language::convertGrammar
+ */
+ public function testGrammar( $result, $word, $case ) {
+ $this->assertEquals( $result, $this->getLang()->convertGrammar( $word, $case ) );
+ }
+
+ public static function providerGrammar() {
+ return array(
+ array(
+ 'Википедии',
+ 'Википедия',
+ 'genitive',
+ ),
+ array(
+ 'Викитеки',
+ 'Викитека',
+ 'genitive',
+ ),
+ array(
+ 'Викитеке',
+ 'Викитека',
+ 'prepositional',
+ ),
+ array(
+ 'Викисклада',
+ 'Викисклад',
+ 'genitive',
+ ),
+ array(
+ 'Викискладе',
+ 'Викисклад',
+ 'prepositional',
+ ),
+ array(
+ 'Викиданных',
+ 'Викиданные',
+ 'prepositional',
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageSeTest.php b/tests/phpunit/languages/LanguageSeTest.php
new file mode 100644
index 00000000..533aa2bc
--- /dev/null
+++ b/tests/phpunit/languages/LanguageSeTest.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageSe.php */
+class LanguageSeTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'other', 3 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralTwoForms( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 3 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageSgsTest.php b/tests/phpunit/languages/LanguageSgsTest.php
new file mode 100644
index 00000000..fa49a4dd
--- /dev/null
+++ b/tests/phpunit/languages/LanguageSgsTest.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for Samogitian */
+class LanguageSgsTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePluralAllForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralAllForms( $result, $value ) {
+ $forms = array( 'one', 'two', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePluralAllForms
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePluralAllForms() {
+ return array(
+ array( 'few', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'other', 3 ),
+ array( 'few', 10 ),
+ array( 'few', 11 ),
+ array( 'few', 12 ),
+ array( 'few', 19 ),
+ array( 'other', 20 ),
+ array( 'few', 100 ),
+ array( 'one', 101 ),
+ array( 'few', 111 ),
+ array( 'few', 112 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralTwoForms( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 3 ),
+ array( 'other', 10 ),
+ array( 'other', 11 ),
+ array( 'other', 12 ),
+ array( 'other', 19 ),
+ array( 'other', 20 ),
+ array( 'other', 100 ),
+ array( 'one', 101 ),
+ array( 'other', 111 ),
+ array( 'other', 112 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageShTest.php b/tests/phpunit/languages/LanguageShTest.php
new file mode 100644
index 00000000..1b390872
--- /dev/null
+++ b/tests/phpunit/languages/LanguageShTest.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for srpskohrvatski / српскохрватски / Serbocroatian */
+class LanguageShTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 4 ),
+ array( 'other', 5 ),
+ array( 'other', 10 ),
+ array( 'other', 11 ),
+ array( 'other', 12 ),
+ array( 'one', 101 ),
+ array( 'few', 102 ),
+ array( 'other', 111 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageSkTest.php b/tests/phpunit/languages/LanguageSkTest.php
new file mode 100644
index 00000000..cb8a13b8
--- /dev/null
+++ b/tests/phpunit/languages/LanguageSkTest.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * based on LanguageSkTest.php
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageSk.php */
+class LanguageSkTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'few', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 4 ),
+ array( 'other', 5 ),
+ array( 'other', 11 ),
+ array( 'other', 20 ),
+ array( 'other', 25 ),
+ array( 'other', 200 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageSlTest.php b/tests/phpunit/languages/LanguageSlTest.php
new file mode 100644
index 00000000..9783dd80
--- /dev/null
+++ b/tests/phpunit/languages/LanguageSlTest.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * based on LanguageSkTest.php
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageSl.php */
+class LanguageSlTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providerPlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providerPlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providerPlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 4 ),
+ array( 'other', 5 ),
+ array( 'other', 99 ),
+ array( 'other', 100 ),
+ array( 'one', 101 ),
+ array( 'two', 102 ),
+ array( 'few', 103 ),
+ array( 'one', 201 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageSmaTest.php b/tests/phpunit/languages/LanguageSmaTest.php
new file mode 100644
index 00000000..95cb333c
--- /dev/null
+++ b/tests/phpunit/languages/LanguageSmaTest.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageSma.php */
+class LanguageSmaTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'two', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'two', 2 ),
+ array( 'other', 3 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralTwoForms( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'other', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ array( 'other', 3 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageSrTest.php b/tests/phpunit/languages/LanguageSrTest.php
new file mode 100644
index 00000000..bfb199f3
--- /dev/null
+++ b/tests/phpunit/languages/LanguageSrTest.php
@@ -0,0 +1,249 @@
+<?php
+/**
+ * PHPUnit tests for the Serbian language.
+ * The language can be represented using two scripts:
+ * - Latin (SR_el)
+ * - Cyrillic (SR_ec)
+ * Both representations seems to be bijective, hence MediaWiki can convert
+ * from one script to the other.
+ *
+ * @author Antoine Musso <hashar at free dot fr>
+ * @copyright Copyright © 2011, Antoine Musso <hashar at free dot fr>
+ * @file
+ *
+ * @todo methods in test class should be tidied:
+ * - Should be split into separate test methods and data providers
+ * - Tests for LanguageConverter and Language should probably be separate..
+ */
+
+/** Tests for MediaWiki languages/LanguageSr.php */
+class LanguageSrTest extends LanguageClassesTestCase {
+ /**
+ * @covers LanguageConverter::convertTo
+ */
+ public function testEasyConversions() {
+ $this->assertCyrillic(
+ 'шђчћжШЂЧЋЖ',
+ 'Cyrillic guessing characters'
+ );
+ $this->assertLatin(
+ 'šđč枊ĐČĆŽ',
+ 'Latin guessing characters'
+ );
+ }
+
+ /**
+ * @covers LanguageConverter::convertTo
+ */
+ public function testMixedConversions() {
+ $this->assertCyrillic(
+ 'шђчћжШЂЧЋЖ - šđčćž',
+ 'Mostly cyrillic characters'
+ );
+ $this->assertLatin(
+ 'šđč枊ĐČĆŽ - шђчћж',
+ 'Mostly latin characters'
+ );
+ }
+
+ /**
+ * @covers LanguageConverter::convertTo
+ */
+ public function testSameAmountOfLatinAndCyrillicGetConverted() {
+ $this->assertConverted(
+ '4 latin: šđčć | 4 cyrillic: шђчћ',
+ 'sr-ec'
+ );
+ $this->assertConverted(
+ '4 latin: šđčć | 4 cyrillic: шђчћ',
+ 'sr-el'
+ );
+ }
+
+ /**
+ * @author Nikola Smolenski
+ * @covers LanguageConverter::convertTo
+ */
+ public function testConversionToCyrillic() {
+ //A simple convertion of Latin to Cyrillic
+ $this->assertEquals( 'абвг',
+ $this->convertToCyrillic( 'abvg' )
+ );
+ //Same as above, but assert that -{}-s must be removed and not converted
+ $this->assertEquals( 'ljабnjвгdž',
+ $this->convertToCyrillic( '-{lj}-ab-{nj}-vg-{dž}-' )
+ );
+ //A simple convertion of Cyrillic to Cyrillic
+ $this->assertEquals( 'абвг',
+ $this->convertToCyrillic( 'абвг' )
+ );
+ //Same as above, but assert that -{}-s must be removed and not converted
+ $this->assertEquals( 'ljабnjвгdž',
+ $this->convertToCyrillic( '-{lj}-аб-{nj}-вг-{dž}-' )
+ );
+ //This text has some Latin, but is recognized as Cyrillic, so it should not be converted
+ $this->assertEquals( 'abvgшђжчћ',
+ $this->convertToCyrillic( 'abvgшђжчћ' )
+ );
+ //Same as above, but assert that -{}-s must be removed
+ $this->assertEquals( 'љabvgњшђжчћџ',
+ $this->convertToCyrillic( '-{љ}-abvg-{њ}-шђжчћ-{џ}-' )
+ );
+ //This text has some Cyrillic, but is recognized as Latin, so it should be converted
+ $this->assertEquals( 'абвгшђжчћ',
+ $this->convertToCyrillic( 'абвгšđžčć' )
+ );
+ //Same as above, but assert that -{}-s must be removed and not converted
+ $this->assertEquals( 'ljабвгnjшђжчћdž',
+ $this->convertToCyrillic( '-{lj}-абвг-{nj}-šđžčć-{dž}-' )
+ );
+ // Roman numerals are not converted
+ $this->assertEquals( 'а I б II в III г IV шђжчћ',
+ $this->convertToCyrillic( 'a I b II v III g IV šđžčć' )
+ );
+ }
+
+ /**
+ * @covers LanguageConverter::convertTo
+ */
+ public function testConversionToLatin() {
+ //A simple convertion of Latin to Latin
+ $this->assertEquals( 'abcd',
+ $this->convertToLatin( 'abcd' )
+ );
+ //A simple convertion of Cyrillic to Latin
+ $this->assertEquals( 'abcd',
+ $this->convertToLatin( 'абцд' )
+ );
+ //This text has some Latin, but is recognized as Cyrillic, so it should be converted
+ $this->assertEquals( 'abcdšđžčć',
+ $this->convertToLatin( 'abcdшђжчћ' )
+ );
+ //This text has some Cyrillic, but is recognized as Latin, so it should not be converted
+ $this->assertEquals( 'абцдšđžčć',
+ $this->convertToLatin( 'абцдšđžčć' )
+ );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 1 ),
+ array( 'other', 11 ),
+ array( 'one', 91 ),
+ array( 'one', 121 ),
+ array( 'few', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 4 ),
+ array( 'few', 334 ),
+ array( 'other', 5 ),
+ array( 'other', 15 ),
+ array( 'other', 120 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralTwoForms( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'one', 1 ),
+ array( 'other', 11 ),
+ array( 'other', 4 ),
+ array( 'one', 91 ),
+ array( 'one', 121 ),
+ );
+ }
+
+ ##### HELPERS #####################################################
+ /**
+ *Wrapper to verify text stay the same after applying conversion
+ * @param string $text Text to convert
+ * @param string $variant Language variant 'sr-ec' or 'sr-el'
+ * @param string $msg Optional message
+ */
+ protected function assertUnConverted( $text, $variant, $msg = '' ) {
+ $this->assertEquals(
+ $text,
+ $this->convertTo( $text, $variant ),
+ $msg
+ );
+ }
+
+ /**
+ * Wrapper to verify a text is different once converted to a variant.
+ * @param string $text Text to convert
+ * @param string $variant Language variant 'sr-ec' or 'sr-el'
+ * @param string $msg Optional message
+ */
+ protected function assertConverted( $text, $variant, $msg = '' ) {
+ $this->assertNotEquals(
+ $text,
+ $this->convertTo( $text, $variant ),
+ $msg
+ );
+ }
+
+ /**
+ * Verifiy the given Cyrillic text is not converted when using
+ * using the cyrillic variant and converted to Latin when using
+ * the Latin variant.
+ * @param string $text Text to convert
+ * @param string $msg Optional message
+ */
+ protected function assertCyrillic( $text, $msg = '' ) {
+ $this->assertUnConverted( $text, 'sr-ec', $msg );
+ $this->assertConverted( $text, 'sr-el', $msg );
+ }
+
+ /**
+ * Verifiy the given Latin text is not converted when using
+ * using the Latin variant and converted to Cyrillic when using
+ * the Cyrillic variant.
+ * @param string $text Text to convert
+ * @param string $msg Optional message
+ */
+ protected function assertLatin( $text, $msg = '' ) {
+ $this->assertUnConverted( $text, 'sr-el', $msg );
+ $this->assertConverted( $text, 'sr-ec', $msg );
+ }
+
+ /** Wrapper for converter::convertTo() method*/
+ protected function convertTo( $text, $variant ) {
+ return $this->getLang()
+ ->mConverter
+ ->convertTo(
+ $text, $variant
+ );
+ }
+
+ protected function convertToCyrillic( $text ) {
+ return $this->convertTo( $text, 'sr-ec' );
+ }
+
+ protected function convertToLatin( $text ) {
+ return $this->convertTo( $text, 'sr-el' );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php
new file mode 100644
index 00000000..cff2e8fd
--- /dev/null
+++ b/tests/phpunit/languages/LanguageTest.php
@@ -0,0 +1,1635 @@
+<?php
+
+class LanguageTest extends LanguageClassesTestCase {
+ /**
+ * @covers Language::convertDoubleWidth
+ * @covers Language::normalizeForSearch
+ */
+ public function testLanguageConvertDoubleWidthToSingleWidth() {
+ $this->assertEquals(
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+ $this->getLang()->normalizeForSearch(
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ ),
+ 'convertDoubleWidth() with the full alphabet and digits'
+ );
+ }
+
+ /**
+ * @dataProvider provideFormattableTimes
+ * @covers Language::formatTimePeriod
+ */
+ public function testFormatTimePeriod( $seconds, $format, $expected, $desc ) {
+ $this->assertEquals( $expected, $this->getLang()->formatTimePeriod( $seconds, $format ), $desc );
+ }
+
+ public static function provideFormattableTimes() {
+ return array(
+ array(
+ 9.45,
+ array(),
+ '9.5 s',
+ 'formatTimePeriod() rounding (<10s)'
+ ),
+ array(
+ 9.45,
+ array( 'noabbrevs' => true ),
+ '9.5 seconds',
+ 'formatTimePeriod() rounding (<10s)'
+ ),
+ array(
+ 9.95,
+ array(),
+ '10 s',
+ 'formatTimePeriod() rounding (<10s)'
+ ),
+ array(
+ 9.95,
+ array( 'noabbrevs' => true ),
+ '10 seconds',
+ 'formatTimePeriod() rounding (<10s)'
+ ),
+ array(
+ 59.55,
+ array(),
+ '1 min 0 s',
+ 'formatTimePeriod() rounding (<60s)'
+ ),
+ array(
+ 59.55,
+ array( 'noabbrevs' => true ),
+ '1 minute 0 seconds',
+ 'formatTimePeriod() rounding (<60s)'
+ ),
+ array(
+ 119.55,
+ array(),
+ '2 min 0 s',
+ 'formatTimePeriod() rounding (<1h)'
+ ),
+ array(
+ 119.55,
+ array( 'noabbrevs' => true ),
+ '2 minutes 0 seconds',
+ 'formatTimePeriod() rounding (<1h)'
+ ),
+ array(
+ 3599.55,
+ array(),
+ '1 h 0 min 0 s',
+ 'formatTimePeriod() rounding (<1h)'
+ ),
+ array(
+ 3599.55,
+ array( 'noabbrevs' => true ),
+ '1 hour 0 minutes 0 seconds',
+ 'formatTimePeriod() rounding (<1h)'
+ ),
+ array(
+ 7199.55,
+ array(),
+ '2 h 0 min 0 s',
+ 'formatTimePeriod() rounding (>=1h)'
+ ),
+ array(
+ 7199.55,
+ array( 'noabbrevs' => true ),
+ '2 hours 0 minutes 0 seconds',
+ 'formatTimePeriod() rounding (>=1h)'
+ ),
+ array(
+ 7199.55,
+ 'avoidseconds',
+ '2 h 0 min',
+ 'formatTimePeriod() rounding (>=1h), avoidseconds'
+ ),
+ array(
+ 7199.55,
+ array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+ '2 hours 0 minutes',
+ 'formatTimePeriod() rounding (>=1h), avoidseconds'
+ ),
+ array(
+ 7199.55,
+ 'avoidminutes',
+ '2 h 0 min',
+ 'formatTimePeriod() rounding (>=1h), avoidminutes'
+ ),
+ array(
+ 7199.55,
+ array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ),
+ '2 hours 0 minutes',
+ 'formatTimePeriod() rounding (>=1h), avoidminutes'
+ ),
+ array(
+ 172799.55,
+ 'avoidseconds',
+ '48 h 0 min',
+ 'formatTimePeriod() rounding (=48h), avoidseconds'
+ ),
+ array(
+ 172799.55,
+ array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+ '48 hours 0 minutes',
+ 'formatTimePeriod() rounding (=48h), avoidseconds'
+ ),
+ array(
+ 259199.55,
+ 'avoidminutes',
+ '3 d 0 h',
+ 'formatTimePeriod() rounding (>48h), avoidminutes'
+ ),
+ array(
+ 259199.55,
+ array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ),
+ '3 days 0 hours',
+ 'formatTimePeriod() rounding (>48h), avoidminutes'
+ ),
+ array(
+ 176399.55,
+ 'avoidseconds',
+ '2 d 1 h 0 min',
+ 'formatTimePeriod() rounding (>48h), avoidseconds'
+ ),
+ array(
+ 176399.55,
+ array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+ '2 days 1 hour 0 minutes',
+ 'formatTimePeriod() rounding (>48h), avoidseconds'
+ ),
+ array(
+ 176399.55,
+ 'avoidminutes',
+ '2 d 1 h',
+ 'formatTimePeriod() rounding (>48h), avoidminutes'
+ ),
+ array(
+ 176399.55,
+ array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ),
+ '2 days 1 hour',
+ 'formatTimePeriod() rounding (>48h), avoidminutes'
+ ),
+ array(
+ 259199.55,
+ 'avoidseconds',
+ '3 d 0 h 0 min',
+ 'formatTimePeriod() rounding (>48h), avoidseconds'
+ ),
+ array(
+ 259199.55,
+ array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+ '3 days 0 hours 0 minutes',
+ 'formatTimePeriod() rounding (>48h), avoidseconds'
+ ),
+ array(
+ 172801.55,
+ 'avoidseconds',
+ '2 d 0 h 0 min',
+ 'formatTimePeriod() rounding, (>48h), avoidseconds'
+ ),
+ array(
+ 172801.55,
+ array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+ '2 days 0 hours 0 minutes',
+ 'formatTimePeriod() rounding, (>48h), avoidseconds'
+ ),
+ array(
+ 176460.55,
+ array(),
+ '2 d 1 h 1 min 1 s',
+ 'formatTimePeriod() rounding, recursion, (>48h)'
+ ),
+ array(
+ 176460.55,
+ array( 'noabbrevs' => true ),
+ '2 days 1 hour 1 minute 1 second',
+ 'formatTimePeriod() rounding, recursion, (>48h)'
+ ),
+ );
+ }
+
+ /**
+ * @covers Language::truncate
+ */
+ public function testTruncate() {
+ $this->assertEquals(
+ "XXX",
+ $this->getLang()->truncate( "1234567890", 0, 'XXX' ),
+ 'truncate prefix, len 0, small ellipsis'
+ );
+
+ $this->assertEquals(
+ "12345XXX",
+ $this->getLang()->truncate( "1234567890", 8, 'XXX' ),
+ 'truncate prefix, small ellipsis'
+ );
+
+ $this->assertEquals(
+ "123456789",
+ $this->getLang()->truncate( "123456789", 5, 'XXXXXXXXXXXXXXX' ),
+ 'truncate prefix, large ellipsis'
+ );
+
+ $this->assertEquals(
+ "XXX67890",
+ $this->getLang()->truncate( "1234567890", -8, 'XXX' ),
+ 'truncate suffix, small ellipsis'
+ );
+
+ $this->assertEquals(
+ "123456789",
+ $this->getLang()->truncate( "123456789", -5, 'XXXXXXXXXXXXXXX' ),
+ 'truncate suffix, large ellipsis'
+ );
+ $this->assertEquals(
+ "123XXX",
+ $this->getLang()->truncate( "123 ", 9, 'XXX' ),
+ 'truncate prefix, with spaces'
+ );
+ $this->assertEquals(
+ "12345XXX",
+ $this->getLang()->truncate( "12345 8", 11, 'XXX' ),
+ 'truncate prefix, with spaces and non-space ending'
+ );
+ $this->assertEquals(
+ "XXX234",
+ $this->getLang()->truncate( "1 234", -8, 'XXX' ),
+ 'truncate suffix, with spaces'
+ );
+ $this->assertEquals(
+ "12345XXX",
+ $this->getLang()->truncate( "1234567890", 5, 'XXX', false ),
+ 'truncate without adjustment'
+ );
+ }
+
+ /**
+ * @dataProvider provideHTMLTruncateData
+ * @covers Language::truncateHTML
+ */
+ public function testTruncateHtml( $len, $ellipsis, $input, $expected ) {
+ // Actual HTML...
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->truncateHTML( $input, $len, $ellipsis )
+ );
+ }
+
+ /**
+ * @return array Format is ($len, $ellipsis, $input, $expected)
+ */
+ public static function provideHTMLTruncateData() {
+ return array(
+ array( 0, 'XXX', "1234567890", "XXX" ),
+ array( 8, 'XXX', "1234567890", "12345XXX" ),
+ array( 5, 'XXXXXXXXXXXXXXX', '1234567890', "1234567890" ),
+ array( 2, '***',
+ '<p><span style="font-weight:bold;"></span></p>',
+ '<p><span style="font-weight:bold;"></span></p>',
+ ),
+ array( 2, '***',
+ '<p><span style="font-weight:bold;">123456789</span></p>',
+ '<p><span style="font-weight:bold;">***</span></p>',
+ ),
+ array( 2, '***',
+ '<p><span style="font-weight:bold;">&nbsp;23456789</span></p>',
+ '<p><span style="font-weight:bold;">***</span></p>',
+ ),
+ array( 3, '***',
+ '<p><span style="font-weight:bold;">123456789</span></p>',
+ '<p><span style="font-weight:bold;">***</span></p>',
+ ),
+ array( 4, '***',
+ '<p><span style="font-weight:bold;">123456789</span></p>',
+ '<p><span style="font-weight:bold;">1***</span></p>',
+ ),
+ array( 5, '***',
+ '<tt><span style="font-weight:bold;">123456789</span></tt>',
+ '<tt><span style="font-weight:bold;">12***</span></tt>',
+ ),
+ array( 6, '***',
+ '<p><a href="www.mediawiki.org">123456789</a></p>',
+ '<p><a href="www.mediawiki.org">123***</a></p>',
+ ),
+ array( 6, '***',
+ '<p><a href="www.mediawiki.org">12&nbsp;456789</a></p>',
+ '<p><a href="www.mediawiki.org">12&nbsp;***</a></p>',
+ ),
+ array( 7, '***',
+ '<small><span style="font-weight:bold;">123<p id="#moo">456</p>789</span></small>',
+ '<small><span style="font-weight:bold;">123<p id="#moo">4***</p></span></small>',
+ ),
+ array( 8, '***',
+ '<div><span style="font-weight:bold;">123<span>4</span>56789</span></div>',
+ '<div><span style="font-weight:bold;">123<span>4</span>5***</span></div>',
+ ),
+ array( 9, '***',
+ '<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>',
+ '<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>',
+ ),
+ array( 10, '***',
+ '<p><font style="font-weight:bold;">123456789</font></p>',
+ '<p><font style="font-weight:bold;">123456789</font></p>',
+ ),
+ );
+ }
+
+ /**
+ * Test Language::isWellFormedLanguageTag()
+ * @dataProvider provideWellFormedLanguageTags
+ * @covers Language::isWellFormedLanguageTag
+ */
+ public function testWellFormedLanguageTag( $code, $message = '' ) {
+ $this->assertTrue(
+ Language::isWellFormedLanguageTag( $code ),
+ "validating code $code $message"
+ );
+ }
+
+ /**
+ * The test cases are based on the tests in the GaBuZoMeu parser
+ * written by Stéphane Bortzmeyer <bortzmeyer@nic.fr>
+ * and distributed as free software, under the GNU General Public Licence.
+ * http://www.bortzmeyer.org/gabuzomeu-parsing-language-tags.html
+ */
+ public static function provideWellFormedLanguageTags() {
+ return array(
+ array( 'fr', 'two-letter code' ),
+ array( 'fr-latn', 'two-letter code with lower case script code' ),
+ array( 'fr-Latn-FR', 'two-letter code with title case script code and uppercase country code' ),
+ array( 'fr-Latn-419', 'two-letter code with title case script code and region number' ),
+ array( 'fr-FR', 'two-letter code with uppercase' ),
+ array( 'ax-TZ', 'Not in the registry, but well-formed' ),
+ array( 'fr-shadok', 'two-letter code with variant' ),
+ array( 'fr-y-myext-myext2', 'non-x singleton' ),
+ array( 'fra-Latn', 'ISO 639 can be 3-letters' ),
+ array( 'fra', 'three-letter language code' ),
+ array( 'fra-FX', 'three-letter language code with country code' ),
+ array( 'i-klingon', 'grandfathered with singleton' ),
+ array( 'I-kLINgon', 'tags are case-insensitive...' ),
+ array( 'no-bok', 'grandfathered without singleton' ),
+ array( 'i-enochian', 'Grandfathered' ),
+ array( 'x-fr-CH', 'private use' ),
+ array( 'es-419', 'two-letter code with region number' ),
+ array( 'en-Latn-GB-boont-r-extended-sequence-x-private', 'weird, but well-formed' ),
+ array( 'ab-x-abc-x-abc', 'anything goes after x' ),
+ array( 'ab-x-abc-a-a', 'anything goes after x, including several non-x singletons' ),
+ array( 'i-default', 'grandfathered' ),
+ array( 'abcd-Latn', 'Language of 4 chars reserved for future use' ),
+ array( 'AaBbCcDd-x-y-any-x', 'Language of 5-8 chars, registered' ),
+ array( 'de-CH-1901', 'with country and year' ),
+ array( 'en-US-x-twain', 'with country and singleton' ),
+ array( 'zh-cmn', 'three-letter variant' ),
+ array( 'zh-cmn-Hant', 'three-letter variant and script' ),
+ array( 'zh-cmn-Hant-HK', 'three-letter variant, script and country' ),
+ array( 'xr-p-lze', 'Extension' ),
+ );
+ }
+
+ /**
+ * Negative test for Language::isWellFormedLanguageTag()
+ * @dataProvider provideMalformedLanguageTags
+ * @covers Language::isWellFormedLanguageTag
+ */
+ public function testMalformedLanguageTag( $code, $message = '' ) {
+ $this->assertFalse(
+ Language::isWellFormedLanguageTag( $code ),
+ "validating that code $code is a malformed language tag - $message"
+ );
+ }
+
+ /**
+ * The test cases are based on the tests in the GaBuZoMeu parser
+ * written by Stéphane Bortzmeyer <bortzmeyer@nic.fr>
+ * and distributed as free software, under the GNU General Public Licence.
+ * http://www.bortzmeyer.org/gabuzomeu-parsing-language-tags.html
+ */
+ public static function provideMalformedLanguageTags() {
+ return array(
+ array( 'f', 'language too short' ),
+ array( 'f-Latn', 'language too short with script' ),
+ array( 'xr-lxs-qut', 'variants too short' ), # extlangS
+ array( 'fr-Latn-F', 'region too short' ),
+ array( 'a-value', 'language too short with region' ),
+ array( 'tlh-a-b-foo', 'valid three-letter with wrong variant' ),
+ array(
+ 'i-notexist',
+ 'grandfathered but not registered: invalid, even if we only test well-formedness'
+ ),
+ array( 'abcdefghi-012345678', 'numbers too long' ),
+ array( 'ab-abc-abc-abc-abc', 'invalid extensions' ),
+ array( 'ab-abcd-abc', 'invalid extensions' ),
+ array( 'ab-ab-abc', 'invalid extensions' ),
+ array( 'ab-123-abc', 'invalid extensions' ),
+ array( 'a-Hant-ZH', 'short language with valid extensions' ),
+ array( 'a1-Hant-ZH', 'invalid character in language' ),
+ array( 'ab-abcde-abc', 'invalid extensions' ),
+ array( 'ab-1abc-abc', 'invalid characters in extensions' ),
+ array( 'ab-ab-abcd', 'invalid order of extensions' ),
+ array( 'ab-123-abcd', 'invalid order of extensions' ),
+ array( 'ab-abcde-abcd', 'invalid extensions' ),
+ array( 'ab-1abc-abcd', 'invalid characters in extensions' ),
+ array( 'ab-a-b', 'extensions too short' ),
+ array( 'ab-a-x', 'extensions too short, even with singleton' ),
+ array( 'ab--ab', 'two separators' ),
+ array( 'ab-abc-', 'separator in the end' ),
+ array( '-ab-abc', 'separator in the beginning' ),
+ array( 'abcd-efg', 'language too long' ),
+ array( 'aabbccddE', 'tag too long' ),
+ array( 'pa_guru', 'A tag with underscore is invalid in strict mode' ),
+ array( 'de-f', 'subtag too short' ),
+ );
+ }
+
+ /**
+ * Negative test for Language::isWellFormedLanguageTag()
+ * @covers Language::isWellFormedLanguageTag
+ */
+ public function testLenientLanguageTag() {
+ $this->assertTrue(
+ Language::isWellFormedLanguageTag( 'pa_guru', true ),
+ 'pa_guru is a well-formed language tag in lenient mode'
+ );
+ }
+
+ /**
+ * Test Language::isValidBuiltInCode()
+ * @dataProvider provideLanguageCodes
+ * @covers Language::isValidBuiltInCode
+ */
+ public function testBuiltInCodeValidation( $code, $expected, $message = '' ) {
+ $this->assertEquals( $expected,
+ (bool)Language::isValidBuiltInCode( $code ),
+ "validating code $code $message"
+ );
+ }
+
+ public static function provideLanguageCodes() {
+ return array(
+ array( 'fr', true, 'Two letters, minor case' ),
+ array( 'EN', false, 'Two letters, upper case' ),
+ array( 'tyv', true, 'Three letters' ),
+ array( 'tokipona', true, 'long language code' ),
+ array( 'be-tarask', true, 'With dash' ),
+ array( 'be-x-old', true, 'With extension (two dashes)' ),
+ array( 'be_tarask', false, 'Reject underscores' ),
+ );
+ }
+
+ /**
+ * Test Language::isKnownLanguageTag()
+ * @dataProvider provideKnownLanguageTags
+ * @covers Language::isKnownLanguageTag
+ */
+ public function testKnownLanguageTag( $code, $message = '' ) {
+ $this->assertTrue(
+ (bool)Language::isKnownLanguageTag( $code ),
+ "validating code $code - $message"
+ );
+ }
+
+ public static function provideKnownLanguageTags() {
+ return array(
+ array( 'fr', 'simple code' ),
+ array( 'bat-smg', 'an MW legacy tag' ),
+ array( 'sgs', 'an internal standard MW name, for which a legacy tag is used externally' ),
+ );
+ }
+
+ /**
+ * @covers Language::isKnownLanguageTag
+ */
+ public function testKnownCldrLanguageTag() {
+ if ( !class_exists( 'LanguageNames' ) ) {
+ $this->markTestSkipped( 'The LanguageNames class is not available. '
+ . 'The CLDR extension is probably not installed.' );
+ }
+
+ $this->assertTrue(
+ (bool)Language::isKnownLanguageTag( 'pal' ),
+ 'validating code "pal" an ancient language, which probably will '
+ . 'not appear in Names.php, but appears in CLDR in English'
+ );
+ }
+
+ /**
+ * Negative tests for Language::isKnownLanguageTag()
+ * @dataProvider provideUnKnownLanguageTags
+ * @covers Language::isKnownLanguageTag
+ */
+ public function testUnknownLanguageTag( $code, $message = '' ) {
+ $this->assertFalse(
+ (bool)Language::isKnownLanguageTag( $code ),
+ "checking that code $code is invalid - $message"
+ );
+ }
+
+ public static function provideUnknownLanguageTags() {
+ return array(
+ array( 'mw', 'non-existent two-letter code' ),
+ array( 'foo"<bar', 'very invalid language code' ),
+ );
+ }
+
+ /**
+ * Test too short timestamp
+ * @expectedException MWException
+ * @covers Language::sprintfDate
+ */
+ public function testSprintfDateTooShortTimestamp() {
+ $this->getLang()->sprintfDate( 'xiY', '1234567890123' );
+ }
+
+ /**
+ * Test too long timestamp
+ * @expectedException MWException
+ * @covers Language::sprintfDate
+ */
+ public function testSprintfDateTooLongTimestamp() {
+ $this->getLang()->sprintfDate( 'xiY', '123456789012345' );
+ }
+
+ /**
+ * Test too short timestamp
+ * @expectedException MWException
+ * @covers Language::sprintfDate
+ */
+ public function testSprintfDateNotAllDigitTimestamp() {
+ $this->getLang()->sprintfDate( 'xiY', '-1234567890123' );
+ }
+
+ /**
+ * @dataProvider provideSprintfDateSamples
+ * @covers Language::sprintfDate
+ */
+ public function testSprintfDate( $format, $ts, $expected, $msg ) {
+ $ttl = null;
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->sprintfDate( $format, $ts, null, $ttl ),
+ "sprintfDate('$format', '$ts'): $msg"
+ );
+ if ( $ttl ) {
+ $dt = new DateTime( $ts );
+ $lastValidTS = $dt->add( new DateInterval( 'PT' . ( $ttl - 1 ) . 'S' ) )->format( 'YmdHis' );
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->sprintfDate( $format, $lastValidTS, null ),
+ "sprintfDate('$format', '$ts'): TTL $ttl too high (output was different at $lastValidTS)"
+ );
+ } else {
+ // advance the time enough to make all of the possible outputs different (except possibly L)
+ $dt = new DateTime( $ts );
+ $newTS = $dt->add( new DateInterval( 'P1Y1M8DT13H1M1S' ) )->format( 'YmdHis' );
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->sprintfDate( $format, $newTS, null ),
+ "sprintfDate('$format', '$ts'): Missing TTL (output was different at $newTS)"
+ );
+ }
+ }
+
+ /**
+ * sprintfDate should always use UTC when no zone is given.
+ * @dataProvider provideSprintfDateSamples
+ * @covers Language::sprintfDate
+ */
+ public function testSprintfDateNoZone( $format, $ts, $expected, $ignore, $msg ) {
+ $oldTZ = date_default_timezone_get();
+ $res = date_default_timezone_set( 'Asia/Seoul' );
+ if ( !$res ) {
+ $this->markTestSkipped( "Error setting Timezone" );
+ }
+
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->sprintfDate( $format, $ts ),
+ "sprintfDate('$format', '$ts'): $msg"
+ );
+
+ date_default_timezone_set( $oldTZ );
+ }
+
+ /**
+ * sprintfDate should use passed timezone
+ * @dataProvider provideSprintfDateSamples
+ * @covers Language::sprintfDate
+ */
+ public function testSprintfDateTZ( $format, $ts, $ignore, $expected, $msg ) {
+ $tz = new DateTimeZone( 'Asia/Seoul' );
+ if ( !$tz ) {
+ $this->markTestSkipped( "Error getting Timezone" );
+ }
+
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->sprintfDate( $format, $ts, $tz ),
+ "sprintfDate('$format', '$ts', 'Asia/Seoul'): $msg"
+ );
+ }
+
+ public static function provideSprintfDateSamples() {
+ return array(
+ array(
+ 'xiY',
+ '20111212000000',
+ '1390', // note because we're testing English locale we get Latin-standard digits
+ '1390',
+ 'Iranian calendar full year'
+ ),
+ array(
+ 'xiy',
+ '20111212000000',
+ '90',
+ '90',
+ 'Iranian calendar short year'
+ ),
+ array(
+ 'o',
+ '20120101235000',
+ '2011',
+ '2011',
+ 'ISO 8601 (week) year'
+ ),
+ array(
+ 'W',
+ '20120101235000',
+ '52',
+ '52',
+ 'Week number'
+ ),
+ array(
+ 'W',
+ '20120102235000',
+ '1',
+ '1',
+ 'Week number'
+ ),
+ array(
+ 'o-\\WW-N',
+ '20091231235000',
+ '2009-W53-4',
+ '2009-W53-4',
+ 'leap week'
+ ),
+ // What follows is mostly copied from
+ // https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
+ array(
+ 'Y',
+ '20120102090705',
+ '2012',
+ '2012',
+ 'Full year'
+ ),
+ array(
+ 'y',
+ '20120102090705',
+ '12',
+ '12',
+ '2 digit year'
+ ),
+ array(
+ 'L',
+ '20120102090705',
+ '1',
+ '1',
+ 'Leap year'
+ ),
+ array(
+ 'n',
+ '20120102090705',
+ '1',
+ '1',
+ 'Month index, not zero pad'
+ ),
+ array(
+ 'N',
+ '20120102090705',
+ '01',
+ '01',
+ 'Month index. Zero pad'
+ ),
+ array(
+ 'M',
+ '20120102090705',
+ 'Jan',
+ 'Jan',
+ 'Month abbrev'
+ ),
+ array(
+ 'F',
+ '20120102090705',
+ 'January',
+ 'January',
+ 'Full month'
+ ),
+ array(
+ 'xg',
+ '20120102090705',
+ 'January',
+ 'January',
+ 'Genitive month name (same in EN)'
+ ),
+ array(
+ 'j',
+ '20120102090705',
+ '2',
+ '2',
+ 'Day of month (not zero pad)'
+ ),
+ array(
+ 'd',
+ '20120102090705',
+ '02',
+ '02',
+ 'Day of month (zero-pad)'
+ ),
+ array(
+ 'z',
+ '20120102090705',
+ '1',
+ '1',
+ 'Day of year (zero-indexed)'
+ ),
+ array(
+ 'D',
+ '20120102090705',
+ 'Mon',
+ 'Mon',
+ 'Day of week (abbrev)'
+ ),
+ array(
+ 'l',
+ '20120102090705',
+ 'Monday',
+ 'Monday',
+ 'Full day of week'
+ ),
+ array(
+ 'N',
+ '20120101090705',
+ '7',
+ '7',
+ 'Day of week (Mon=1, Sun=7)'
+ ),
+ array(
+ 'w',
+ '20120101090705',
+ '0',
+ '0',
+ 'Day of week (Sun=0, Sat=6)'
+ ),
+ array(
+ 'N',
+ '20120102090705',
+ '1',
+ '1',
+ 'Day of week'
+ ),
+ array(
+ 'a',
+ '20120102090705',
+ 'am',
+ 'am',
+ 'am vs pm'
+ ),
+ array(
+ 'A',
+ '20120102120000',
+ 'PM',
+ 'PM',
+ 'AM vs PM'
+ ),
+ array(
+ 'a',
+ '20120102000000',
+ 'am',
+ 'am',
+ 'AM vs PM'
+ ),
+ array(
+ 'g',
+ '20120102090705',
+ '9',
+ '9',
+ '12 hour, not Zero'
+ ),
+ array(
+ 'h',
+ '20120102090705',
+ '09',
+ '09',
+ '12 hour, zero padded'
+ ),
+ array(
+ 'G',
+ '20120102090705',
+ '9',
+ '9',
+ '24 hour, not zero'
+ ),
+ array(
+ 'H',
+ '20120102090705',
+ '09',
+ '09',
+ '24 hour, zero'
+ ),
+ array(
+ 'H',
+ '20120102110705',
+ '11',
+ '11',
+ '24 hour, zero'
+ ),
+ array(
+ 'i',
+ '20120102090705',
+ '07',
+ '07',
+ 'Minutes'
+ ),
+ array(
+ 's',
+ '20120102090705',
+ '05',
+ '05',
+ 'seconds'
+ ),
+ array(
+ 'U',
+ '20120102090705',
+ '1325495225',
+ '1325462825',
+ 'unix time'
+ ),
+ array(
+ 't',
+ '20120102090705',
+ '31',
+ '31',
+ 'Days in current month'
+ ),
+ array(
+ 'c',
+ '20120102090705',
+ '2012-01-02T09:07:05+00:00',
+ '2012-01-02T09:07:05+09:00',
+ 'ISO 8601 timestamp'
+ ),
+ array(
+ 'r',
+ '20120102090705',
+ 'Mon, 02 Jan 2012 09:07:05 +0000',
+ 'Mon, 02 Jan 2012 09:07:05 +0900',
+ 'RFC 5322'
+ ),
+ array(
+ 'e',
+ '20120102090705',
+ 'UTC',
+ 'Asia/Seoul',
+ 'Timezone identifier'
+ ),
+ array(
+ 'I',
+ '19880602090705',
+ '0',
+ '1',
+ 'DST indicator'
+ ),
+ array(
+ 'O',
+ '20120102090705',
+ '+0000',
+ '+0900',
+ 'Timezone offset'
+ ),
+ array(
+ 'P',
+ '20120102090705',
+ '+00:00',
+ '+09:00',
+ 'Timezone offset with colon'
+ ),
+ array(
+ 'T',
+ '20120102090705',
+ 'UTC',
+ 'KST',
+ 'Timezone abbreviation'
+ ),
+ array(
+ 'Z',
+ '20120102090705',
+ '0',
+ '32400',
+ 'Timezone offset in seconds'
+ ),
+ array(
+ 'xmj xmF xmn xmY',
+ '20120102090705',
+ '7 Safar 2 1433',
+ '7 Safar 2 1433',
+ 'Islamic'
+ ),
+ array(
+ 'xij xiF xin xiY',
+ '20120102090705',
+ '12 Dey 10 1390',
+ '12 Dey 10 1390',
+ 'Iranian'
+ ),
+ array(
+ 'xjj xjF xjn xjY',
+ '20120102090705',
+ '7 Tevet 4 5772',
+ '7 Tevet 4 5772',
+ 'Hebrew'
+ ),
+ array(
+ 'xjt',
+ '20120102090705',
+ '29',
+ '29',
+ 'Hebrew number of days in month'
+ ),
+ array(
+ 'xjx',
+ '20120102090705',
+ 'Tevet',
+ 'Tevet',
+ 'Hebrew genitive month name (No difference in EN)'
+ ),
+ array(
+ 'xkY',
+ '20120102090705',
+ '2555',
+ '2555',
+ 'Thai year'
+ ),
+ array(
+ 'xoY',
+ '20120102090705',
+ '101',
+ '101',
+ 'Minguo'
+ ),
+ array(
+ 'xtY',
+ '20120102090705',
+ '平成24',
+ '平成24',
+ 'nengo'
+ ),
+ array(
+ 'xrxkYY',
+ '20120102090705',
+ 'MMDLV2012',
+ 'MMDLV2012',
+ 'Roman numerals'
+ ),
+ array(
+ 'xhxjYY',
+ '20120102090705',
+ 'ה\'תשע"ב2012',
+ 'ה\'תשע"ב2012',
+ 'Hebrew numberals'
+ ),
+ array(
+ 'xnY',
+ '20120102090705',
+ '2012',
+ '2012',
+ 'Raw numerals (doesn\'t mean much in EN)'
+ ),
+ array(
+ '[[Y "(yea"\\r)]] \\"xx\\"',
+ '20120102090705',
+ '[[2012 (year)]] "x"',
+ '[[2012 (year)]] "x"',
+ 'Various escaping'
+ ),
+
+ );
+ }
+
+ /**
+ * @dataProvider provideFormatSizes
+ * @covers Language::formatSize
+ */
+ public function testFormatSize( $size, $expected, $msg ) {
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->formatSize( $size ),
+ "formatSize('$size'): $msg"
+ );
+ }
+
+ public static function provideFormatSizes() {
+ return array(
+ array(
+ 0,
+ "0 B",
+ "Zero bytes"
+ ),
+ array(
+ 1024,
+ "1 KB",
+ "1 kilobyte"
+ ),
+ array(
+ 1024 * 1024,
+ "1 MB",
+ "1,024 megabytes"
+ ),
+ array(
+ 1024 * 1024 * 1024,
+ "1 GB",
+ "1 gigabytes"
+ ),
+ array(
+ pow( 1024, 4 ),
+ "1 TB",
+ "1 terabyte"
+ ),
+ array(
+ pow( 1024, 5 ),
+ "1 PB",
+ "1 petabyte"
+ ),
+ array(
+ pow( 1024, 6 ),
+ "1 EB",
+ "1,024 exabyte"
+ ),
+ array(
+ pow( 1024, 7 ),
+ "1 ZB",
+ "1 zetabyte"
+ ),
+ array(
+ pow( 1024, 8 ),
+ "1 YB",
+ "1 yottabyte"
+ ),
+ // How big!? THIS BIG!
+ );
+ }
+
+ /**
+ * @dataProvider provideFormatBitrate
+ * @covers Language::formatBitrate
+ */
+ public function testFormatBitrate( $bps, $expected, $msg ) {
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->formatBitrate( $bps ),
+ "formatBitrate('$bps'): $msg"
+ );
+ }
+
+ public static function provideFormatBitrate() {
+ return array(
+ array(
+ 0,
+ "0 bps",
+ "0 bits per second"
+ ),
+ array(
+ 999,
+ "999 bps",
+ "999 bits per second"
+ ),
+ array(
+ 1000,
+ "1 kbps",
+ "1 kilobit per second"
+ ),
+ array(
+ 1000 * 1000,
+ "1 Mbps",
+ "1 megabit per second"
+ ),
+ array(
+ pow( 10, 9 ),
+ "1 Gbps",
+ "1 gigabit per second"
+ ),
+ array(
+ pow( 10, 12 ),
+ "1 Tbps",
+ "1 terabit per second"
+ ),
+ array(
+ pow( 10, 15 ),
+ "1 Pbps",
+ "1 petabit per second"
+ ),
+ array(
+ pow( 10, 18 ),
+ "1 Ebps",
+ "1 exabit per second"
+ ),
+ array(
+ pow( 10, 21 ),
+ "1 Zbps",
+ "1 zetabit per second"
+ ),
+ array(
+ pow( 10, 24 ),
+ "1 Ybps",
+ "1 yottabit per second"
+ ),
+ array(
+ pow( 10, 27 ),
+ "1,000 Ybps",
+ "1,000 yottabits per second"
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideFormatDuration
+ * @covers Language::formatDuration
+ */
+ public function testFormatDuration( $duration, $expected, $intervals = array() ) {
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->formatDuration( $duration, $intervals ),
+ "formatDuration('$duration'): $expected"
+ );
+ }
+
+ public static function provideFormatDuration() {
+ return array(
+ array(
+ 0,
+ '0 seconds',
+ ),
+ array(
+ 1,
+ '1 second',
+ ),
+ array(
+ 2,
+ '2 seconds',
+ ),
+ array(
+ 60,
+ '1 minute',
+ ),
+ array(
+ 2 * 60,
+ '2 minutes',
+ ),
+ array(
+ 3600,
+ '1 hour',
+ ),
+ array(
+ 2 * 3600,
+ '2 hours',
+ ),
+ array(
+ 24 * 3600,
+ '1 day',
+ ),
+ array(
+ 2 * 86400,
+ '2 days',
+ ),
+ array(
+ // ( 365 + ( 24 * 3 + 25 ) / 400 ) * 86400 = 31556952
+ ( 365 + ( 24 * 3 + 25 ) / 400.0 ) * 86400,
+ '1 year',
+ ),
+ array(
+ 2 * 31556952,
+ '2 years',
+ ),
+ array(
+ 10 * 31556952,
+ '1 decade',
+ ),
+ array(
+ 20 * 31556952,
+ '2 decades',
+ ),
+ array(
+ 100 * 31556952,
+ '1 century',
+ ),
+ array(
+ 200 * 31556952,
+ '2 centuries',
+ ),
+ array(
+ 1000 * 31556952,
+ '1 millennium',
+ ),
+ array(
+ 2000 * 31556952,
+ '2 millennia',
+ ),
+ array(
+ 9001,
+ '2 hours, 30 minutes and 1 second'
+ ),
+ array(
+ 3601,
+ '1 hour and 1 second'
+ ),
+ array(
+ 31556952 + 2 * 86400 + 9000,
+ '1 year, 2 days, 2 hours and 30 minutes'
+ ),
+ array(
+ 42 * 1000 * 31556952 + 42,
+ '42 millennia and 42 seconds'
+ ),
+ array(
+ 60,
+ '60 seconds',
+ array( 'seconds' ),
+ ),
+ array(
+ 61,
+ '61 seconds',
+ array( 'seconds' ),
+ ),
+ array(
+ 1,
+ '1 second',
+ array( 'seconds' ),
+ ),
+ array(
+ 31556952 + 2 * 86400 + 9000,
+ '1 year, 2 days and 150 minutes',
+ array( 'years', 'days', 'minutes' ),
+ ),
+ array(
+ 42,
+ '0 days',
+ array( 'years', 'days' ),
+ ),
+ array(
+ 31556952 + 2 * 86400 + 9000,
+ '1 year, 2 days and 150 minutes',
+ array( 'minutes', 'days', 'years' ),
+ ),
+ array(
+ 42,
+ '0 days',
+ array( 'days', 'years' ),
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideCheckTitleEncodingData
+ * @covers Language::checkTitleEncoding
+ */
+ public function testCheckTitleEncoding( $s ) {
+ $this->assertEquals(
+ $s,
+ $this->getLang()->checkTitleEncoding( $s ),
+ "checkTitleEncoding('$s')"
+ );
+ }
+
+ public static function provideCheckTitleEncodingData() {
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ return array(
+ array( "" ),
+ array( "United States of America" ), // 7bit ASCII
+ array( rawurldecode( "S%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e" ) ),
+ array(
+ rawurldecode(
+ "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn"
+ )
+ ),
+ // The following two data sets come from bug 36839. They fail if checkTitleEncoding uses a regexp to test for
+ // valid UTF-8 encoding and the pcre.recursion_limit is low (like, say, 1024). They succeed if checkTitleEncoding
+ // uses mb_check_encoding for its test.
+ array(
+ rawurldecode(
+ "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn%7C"
+ . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C"
+ . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C"
+ . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C"
+ . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C"
+ . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C"
+ . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C"
+ . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C"
+ . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C"
+ . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C"
+ . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C"
+ . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C"
+ . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C"
+ . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis"
+ ),
+ ),
+ array(
+ rawurldecode(
+ "Mod%C3%A8le%3AArrondissements%20homonymes%7CMod%C3%A8le%3ABandeau%20standard%20pour%20page%20d'homonymie%7C"
+ . "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C"
+ . "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C"
+ . "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C"
+ . "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou"
+ . "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C"
+ . "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C"
+ . "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C"
+ . "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C"
+ . "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C"
+ . "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C"
+ . "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C"
+ . "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C"
+ . "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C"
+ . "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes"
+ )
+ )
+ );
+ // @codingStandardsIgnoreEnd
+ }
+
+ /**
+ * @dataProvider provideRomanNumeralsData
+ * @covers Language::romanNumeral
+ */
+ public function testRomanNumerals( $num, $numerals ) {
+ $this->assertEquals(
+ $numerals,
+ Language::romanNumeral( $num ),
+ "romanNumeral('$num')"
+ );
+ }
+
+ public static function provideRomanNumeralsData() {
+ return array(
+ array( 1, 'I' ),
+ array( 2, 'II' ),
+ array( 3, 'III' ),
+ array( 4, 'IV' ),
+ array( 5, 'V' ),
+ array( 6, 'VI' ),
+ array( 7, 'VII' ),
+ array( 8, 'VIII' ),
+ array( 9, 'IX' ),
+ array( 10, 'X' ),
+ array( 20, 'XX' ),
+ array( 30, 'XXX' ),
+ array( 40, 'XL' ),
+ array( 49, 'XLIX' ),
+ array( 50, 'L' ),
+ array( 60, 'LX' ),
+ array( 70, 'LXX' ),
+ array( 80, 'LXXX' ),
+ array( 90, 'XC' ),
+ array( 99, 'XCIX' ),
+ array( 100, 'C' ),
+ array( 200, 'CC' ),
+ array( 300, 'CCC' ),
+ array( 400, 'CD' ),
+ array( 500, 'D' ),
+ array( 600, 'DC' ),
+ array( 700, 'DCC' ),
+ array( 800, 'DCCC' ),
+ array( 900, 'CM' ),
+ array( 999, 'CMXCIX' ),
+ array( 1000, 'M' ),
+ array( 1989, 'MCMLXXXIX' ),
+ array( 2000, 'MM' ),
+ array( 3000, 'MMM' ),
+ array( 4000, 'MMMM' ),
+ array( 5000, 'MMMMM' ),
+ array( 6000, 'MMMMMM' ),
+ array( 7000, 'MMMMMMM' ),
+ array( 8000, 'MMMMMMMM' ),
+ array( 9000, 'MMMMMMMMM' ),
+ array( 9999, 'MMMMMMMMMCMXCIX' ),
+ array( 10000, 'MMMMMMMMMM' ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralData
+ * @covers Language::convertPlural
+ */
+ public function testConvertPlural( $expected, $number, $forms ) {
+ $chosen = $this->getLang()->convertPlural( $number, $forms );
+ $this->assertEquals( $expected, $chosen );
+ }
+
+ public static function providePluralData() {
+ // Params are: [expected text, number given, [the plural forms]]
+ return array(
+ array( 'plural', 0, array(
+ 'singular', 'plural'
+ ) ),
+ array( 'explicit zero', 0, array(
+ '0=explicit zero', 'singular', 'plural'
+ ) ),
+ array( 'explicit one', 1, array(
+ 'singular', 'plural', '1=explicit one',
+ ) ),
+ array( 'singular', 1, array(
+ 'singular', 'plural', '0=explicit zero',
+ ) ),
+ array( 'plural', 3, array(
+ '0=explicit zero', '1=explicit one', 'singular', 'plural'
+ ) ),
+ array( 'explicit eleven', 11, array(
+ 'singular', 'plural', '11=explicit eleven',
+ ) ),
+ array( 'plural', 12, array(
+ 'singular', 'plural', '11=explicit twelve',
+ ) ),
+ array( 'plural', 12, array(
+ 'singular', 'plural', '=explicit form',
+ ) ),
+ array( 'other', 2, array(
+ 'kissa=kala', '1=2=3', 'other',
+ ) ),
+ array( '', 2, array(
+ '0=explicit zero', '1=explicit one',
+ ) ),
+ );
+ }
+
+ /**
+ * @covers Language::translateBlockExpiry()
+ * @dataProvider provideTranslateBlockExpiry
+ */
+ public function testTranslateBlockExpiry( $expectedData, $str, $desc ) {
+ $lang = $this->getLang();
+ if ( is_array( $expectedData ) ) {
+ list( $func, $arg ) = $expectedData;
+ $expected = $lang->$func( $arg );
+ } else {
+ $expected = $expectedData;
+ }
+ $this->assertEquals( $expected, $lang->translateBlockExpiry( $str ), $desc );
+ }
+
+ public static function provideTranslateBlockExpiry() {
+ return array(
+ array( '2 hours', '2 hours', 'simple data from ipboptions' ),
+ array( 'indefinite', 'infinite', 'infinite from ipboptions' ),
+ array( 'indefinite', 'infinity', 'alternative infinite from ipboptions' ),
+ array( 'indefinite', 'indefinite', 'another alternative infinite from ipboptions' ),
+ array( array( 'formatDuration', 1023 * 60 * 60 ), '1023 hours', 'relative' ),
+ array( array( 'formatDuration', -1023 ), '-1023 seconds', 'negative relative' ),
+ array( array( 'formatDuration', 0 ), 'now', 'now' ),
+ array(
+ array( 'timeanddate', '20120102070000' ),
+ '2012-1-1 7:00 +1 day',
+ 'mixed, handled as absolute'
+ ),
+ array( array( 'timeanddate', '19910203040506' ), '1991-2-3 4:05:06', 'absolute' ),
+ array( array( 'timeanddate', '19700101000000' ), '1970-1-1 0:00:00', 'absolute at epoch' ),
+ array( array( 'timeanddate', '19691231235959' ), '1969-12-31 23:59:59', 'time before epoch' ),
+ array( 'dummy', 'dummy', 'return garbage as is' ),
+ );
+ }
+
+ /**
+ * @dataProvider parseFormattedNumberProvider
+ */
+ public function testParseFormattedNumber( $langCode, $number ) {
+ $lang = Language::factory( $langCode );
+
+ $localisedNum = $lang->formatNum( $number );
+ $normalisedNum = $lang->parseFormattedNumber( $localisedNum );
+
+ $this->assertEquals( $number, $normalisedNum );
+ }
+
+ public function parseFormattedNumberProvider() {
+ return array(
+ array( 'de', 377.01 ),
+ array( 'fa', 334 ),
+ array( 'fa', 382.772 ),
+ array( 'ar', 1844 ),
+ array( 'lzh', 3731 ),
+ array( 'zh-classical', 7432 )
+ );
+ }
+
+ /**
+ * @covers Language::commafy()
+ * @dataProvider provideCommafyData
+ */
+ public function testCommafy( $number, $numbersWithCommas ) {
+ $this->assertEquals(
+ $numbersWithCommas,
+ $this->getLang()->commafy( $number ),
+ "commafy('$number')"
+ );
+ }
+
+ public static function provideCommafyData() {
+ return array(
+ array( -1, '-1' ),
+ array( 10, '10' ),
+ array( 100, '100' ),
+ array( 1000, '1,000' ),
+ array( 10000, '10,000' ),
+ array( 100000, '100,000' ),
+ array( 1000000, '1,000,000' ),
+ array( -1.0001, '-1.0001' ),
+ array( 1.0001, '1.0001' ),
+ array( 10.0001, '10.0001' ),
+ array( 100.0001, '100.0001' ),
+ array( 1000.0001, '1,000.0001' ),
+ array( 10000.0001, '10,000.0001' ),
+ array( 100000.0001, '100,000.0001' ),
+ array( 1000000.0001, '1,000,000.0001' ),
+ array( '200000000000000000000', '200,000,000,000,000,000,000' ),
+ array( '-200000000000000000000', '-200,000,000,000,000,000,000' ),
+ );
+ }
+
+ /**
+ * @covers Language::listToText
+ */
+ public function testListToText() {
+ $lang = $this->getLang();
+ $and = $lang->getMessageFromDB( 'and' );
+ $s = $lang->getMessageFromDB( 'word-separator' );
+ $c = $lang->getMessageFromDB( 'comma-separator' );
+
+ $this->assertEquals( '', $lang->listToText( array() ) );
+ $this->assertEquals( 'a', $lang->listToText( array( 'a' ) ) );
+ $this->assertEquals( "a{$and}{$s}b", $lang->listToText( array( 'a', 'b' ) ) );
+ $this->assertEquals( "a{$c}b{$and}{$s}c", $lang->listToText( array( 'a', 'b', 'c' ) ) );
+ $this->assertEquals( "a{$c}b{$c}c{$and}{$s}d", $lang->listToText( array( 'a', 'b', 'c', 'd' ) ) );
+ }
+
+ /**
+ * @dataProvider provideIsSupportedLanguage
+ * @covers Language::isSupportedLanguage
+ */
+ public function testIsSupportedLanguage( $code, $expected, $comment ) {
+ $this->assertEquals( $expected, Language::isSupportedLanguage( $code ), $comment );
+ }
+
+ public static function provideIsSupportedLanguage() {
+ return array(
+ array( 'en', true, 'is supported language' ),
+ array( 'fi', true, 'is supported language' ),
+ array( 'bunny', false, 'is not supported language' ),
+ array( 'FI', false, 'is not supported language, input should be in lower case' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetParentLanguage
+ * @covers Language::getParentLanguage
+ */
+ public function testGetParentLanguage( $code, $expected, $comment ) {
+ $lang = Language::factory( $code );
+ if ( is_null( $expected ) ) {
+ $this->assertNull( $lang->getParentLanguage(), $comment );
+ } else {
+ $this->assertEquals( $expected, $lang->getParentLanguage()->getCode(), $comment );
+ }
+ }
+
+ public static function provideGetParentLanguage() {
+ return array(
+ array( 'zh-cn', 'zh', 'zh is the parent language of zh-cn' ),
+ array( 'zh', 'zh', 'zh is defined as the parent language of zh, '
+ . 'because zh converter can convert zh-cn to zh' ),
+ array( 'zh-invalid', null, 'do not be fooled by arbitrarily composed language codes' ),
+ array( 'en-gb', null, 'en does not have converter' ),
+ array( 'en', null, 'en does not have converter. Although FakeConverter '
+ . 'handles en -> en conversion but it is useless' ),
+ );
+ }
+
+ /**
+ * @dataProvider provideGetNamespaceAliases
+ * @covers Language::getNamespaceAliases
+ */
+ public function testGetNamespaceAliases( $languageCode, $subset ) {
+ $language = Language::factory( $languageCode );
+ $aliases = $language->getNamespaceAliases();
+ foreach ( $subset as $alias => $nsId ) {
+ $this->assertEquals( $nsId, $aliases[$alias] );
+ }
+ }
+
+ public static function provideGetNamespaceAliases() {
+ // TODO: Add tests for NS_PROJECT_TALK and GenderNamespaces
+ return array(
+ array(
+ 'zh',
+ array(
+ '文件' => NS_FILE,
+ '檔案' => NS_FILE,
+ ),
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageTiTest.php b/tests/phpunit/languages/LanguageTiTest.php
new file mode 100644
index 00000000..e225af97
--- /dev/null
+++ b/tests/phpunit/languages/LanguageTiTest.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageTi.php */
+class LanguageTiTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageTlTest.php b/tests/phpunit/languages/LanguageTlTest.php
new file mode 100644
index 00000000..7ac51c69
--- /dev/null
+++ b/tests/phpunit/languages/LanguageTlTest.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageTl.php */
+class LanguageTlTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageTrTest.php b/tests/phpunit/languages/LanguageTrTest.php
new file mode 100644
index 00000000..2c9905f7
--- /dev/null
+++ b/tests/phpunit/languages/LanguageTrTest.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * @author Antoine Musso
+ * @copyright Copyright © 2011, Antoine Musso
+ * @file
+ */
+
+/** Tests for MediaWiki languages/LanguageTr.php */
+class LanguageTrTest extends LanguageClassesTestCase {
+
+ /**
+ * See @bug 28040
+ * Credits to irc://irc.freenode.net/wikipedia-tr users:
+ * - berm
+ * - []LuCkY[]
+ * - Emperyan
+ * @see http://en.wikipedia.org/wiki/Dotted_and_dotless_I
+ * @dataProvider provideDottedAndDotlessI
+ * @covers Language::ucfirst
+ * @covers Language::lcfirst
+ */
+ public function testDottedAndDotlessI( $func, $input, $inputCase, $expected ) {
+ if ( $func == 'ucfirst' ) {
+ $res = $this->getLang()->ucfirst( $input );
+ } elseif ( $func == 'lcfirst' ) {
+ $res = $this->getLang()->lcfirst( $input );
+ } else {
+ throw new MWException( __METHOD__ . " given an invalid function name '$func'" );
+ }
+
+ $msg = "Converting $inputCase case '$input' with $func should give '$expected'";
+
+ $this->assertEquals( $expected, $res, $msg );
+ }
+
+ public static function provideDottedAndDotlessI() {
+ return array(
+ # function, input, input case, expected
+ # Case changed:
+ array( 'ucfirst', 'ı', 'lower', 'I' ),
+ array( 'ucfirst', 'i', 'lower', 'İ' ),
+ array( 'lcfirst', 'I', 'upper', 'ı' ),
+ array( 'lcfirst', 'İ', 'upper', 'i' ),
+
+ # Already using the correct case
+ array( 'ucfirst', 'I', 'upper', 'I' ),
+ array( 'ucfirst', 'İ', 'upper', 'İ' ),
+ array( 'lcfirst', 'ı', 'lower', 'ı' ),
+ array( 'lcfirst', 'i', 'lower', 'i' ),
+
+ # A real example taken from bug 28040 using
+ # http://tr.wikipedia.org/wiki/%C4%B0Phone
+ array( 'lcfirst', 'iPhone', 'lower', 'iPhone' ),
+
+ # next case is valid in Turkish but are different words if we
+ # consider IPhone is English!
+ array( 'lcfirst', 'IPhone', 'upper', 'ıPhone' ),
+
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageUkTest.php b/tests/phpunit/languages/LanguageUkTest.php
new file mode 100644
index 00000000..9051bcff
--- /dev/null
+++ b/tests/phpunit/languages/LanguageUkTest.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * based on LanguageBe_tarask.php
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for Ukrainian */
+class LanguageUkTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'few', 'many', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * Test explicit plural forms - n=FormN forms
+ * @covers Language::convertPlural
+ */
+ public function testExplicitPlural() {
+ $forms = array( 'one', 'few', 'many', 'other', '12=dozen' );
+ $this->assertEquals( 'dozen', $this->getLang()->convertPlural( 12, $forms ) );
+ $forms = array( 'one', 'few', 'many', '100=hundred', 'other', '12=dozen' );
+ $this->assertEquals( 'hundred', $this->getLang()->convertPlural( 100, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 1 ),
+ array( 'many', 11 ),
+ array( 'one', 91 ),
+ array( 'one', 121 ),
+ array( 'few', 2 ),
+ array( 'few', 3 ),
+ array( 'few', 4 ),
+ array( 'few', 334 ),
+ array( 'many', 5 ),
+ array( 'many', 15 ),
+ array( 'many', 120 ),
+ );
+ }
+
+ /**
+ * @dataProvider providePluralTwoForms
+ * @covers Language::convertPlural
+ */
+ public function testPluralTwoForms( $result, $value ) {
+ $forms = array( '1=one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ public static function providePluralTwoForms() {
+ return array(
+ array( 'one', 1 ),
+ array( 'other', 11 ),
+ array( 'other', 91 ),
+ array( 'other', 121 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageUzTest.php b/tests/phpunit/languages/LanguageUzTest.php
new file mode 100644
index 00000000..4881103f
--- /dev/null
+++ b/tests/phpunit/languages/LanguageUzTest.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * PHPUnit tests for the Uzbek language.
+ * The language can be represented using two scripts:
+ * - Latin (uz-latn)
+ * - Cyrillic (uz-cyrl)
+ *
+ * @author Robin Pepermans
+ * @author Antoine Musso <hashar at free dot fr>
+ * @copyright Copyright © 2012, Robin Pepermans
+ * @copyright Copyright © 2011, Antoine Musso <hashar at free dot fr>
+ * @file
+ *
+ * @todo methods in test class should be tidied:
+ * - Should be split into separate test methods and data providers
+ * - Tests for LanguageConverter and Language should probably be separate..
+ */
+
+/** Tests for MediaWiki languages/LanguageUz.php */
+class LanguageUzTest extends LanguageClassesTestCase {
+
+ /**
+ * @author Nikola Smolenski
+ * @covers LanguageConverter::convertTo
+ */
+ public function testConversionToCyrillic() {
+ // A convertion of Latin to Cyrillic
+ $this->assertEquals( 'абвгғ',
+ $this->convertToCyrillic( 'abvggʻ' )
+ );
+ // Same as above, but assert that -{}-s must be removed and not converted
+ $this->assertEquals( 'ljабnjвгўоdb',
+ $this->convertToCyrillic( '-{lj}-ab-{nj}-vgoʻo-{db}-' )
+ );
+ // A simple convertion of Cyrillic to Cyrillic
+ $this->assertEquals( 'абвг',
+ $this->convertToCyrillic( 'абвг' )
+ );
+ // Same as above, but assert that -{}-s must be removed and not converted
+ $this->assertEquals( 'ljабnjвгdaž',
+ $this->convertToCyrillic( '-{lj}-аб-{nj}-вг-{da}-ž' )
+ );
+ }
+
+ /**
+ * @covers LanguageConverter::convertTo
+ */
+ public function testConversionToLatin() {
+ // A simple convertion of Latin to Latin
+ $this->assertEquals( 'abdef',
+ $this->convertToLatin( 'abdef' )
+ );
+ // A convertion of Cyrillic to Latin
+ $this->assertEquals( 'gʻabtsdOʻQyo',
+ $this->convertToLatin( 'ғабцдЎҚё' )
+ );
+ }
+
+ ##### HELPERS #####################################################
+ /**
+ * Wrapper to verify text stay the same after applying conversion
+ * @param string $text Text to convert
+ * @param string $variant Language variant 'uz-cyrl' or 'uz-latn'
+ * @param string $msg Optional message
+ */
+ protected function assertUnConverted( $text, $variant, $msg = '' ) {
+ $this->assertEquals(
+ $text,
+ $this->convertTo( $text, $variant ),
+ $msg
+ );
+ }
+
+ /**
+ * Wrapper to verify a text is different once converted to a variant.
+ * @param string $text Text to convert
+ * @param string $variant Language variant 'uz-cyrl' or 'uz-latn'
+ * @param string $msg Optional message
+ */
+ protected function assertConverted( $text, $variant, $msg = '' ) {
+ $this->assertNotEquals(
+ $text,
+ $this->convertTo( $text, $variant ),
+ $msg
+ );
+ }
+
+ /**
+ * Verifiy the given Cyrillic text is not converted when using
+ * using the cyrillic variant and converted to Latin when using
+ * the Latin variant.
+ * @param string $text Text to convert
+ * @param string $msg Optional message
+ */
+ protected function assertCyrillic( $text, $msg = '' ) {
+ $this->assertUnConverted( $text, 'uz-cyrl', $msg );
+ $this->assertConverted( $text, 'uz-latn', $msg );
+ }
+
+ /**
+ * Verifiy the given Latin text is not converted when using
+ * using the Latin variant and converted to Cyrillic when using
+ * the Cyrillic variant.
+ * @param string $text Text to convert
+ * @param string $msg Optional message
+ */
+ protected function assertLatin( $text, $msg = '' ) {
+ $this->assertUnConverted( $text, 'uz-latn', $msg );
+ $this->assertConverted( $text, 'uz-cyrl', $msg );
+ }
+
+ /** Wrapper for converter::convertTo() method*/
+ protected function convertTo( $text, $variant ) {
+ return $this->getLang()->mConverter->convertTo( $text, $variant );
+ }
+
+ protected function convertToCyrillic( $text ) {
+ return $this->convertTo( $text, 'uz-cyrl' );
+ }
+
+ protected function convertToLatin( $text ) {
+ return $this->convertTo( $text, 'uz-latn' );
+ }
+}
diff --git a/tests/phpunit/languages/LanguageWaTest.php b/tests/phpunit/languages/LanguageWaTest.php
new file mode 100644
index 00000000..d05196c0
--- /dev/null
+++ b/tests/phpunit/languages/LanguageWaTest.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * @author Amir E. Aharoni
+ * @copyright Copyright © 2012, Amir E. Aharoni
+ * @file
+ */
+
+/** Tests for MediaWiki languages/classes/LanguageWa.php */
+class LanguageWaTest extends LanguageClassesTestCase {
+ /**
+ * @dataProvider providePlural
+ * @covers Language::convertPlural
+ */
+ public function testPlural( $result, $value ) {
+ $forms = array( 'one', 'other' );
+ $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
+ }
+
+ /**
+ * @dataProvider providePlural
+ * @covers Language::getPluralRuleType
+ */
+ public function testGetPluralRuleType( $result, $value ) {
+ $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) );
+ }
+
+ public static function providePlural() {
+ return array(
+ array( 'one', 0 ),
+ array( 'one', 1 ),
+ array( 'other', 2 ),
+ );
+ }
+}
diff --git a/tests/phpunit/languages/SpecialPageAliasTest.php b/tests/phpunit/languages/SpecialPageAliasTest.php
new file mode 100644
index 00000000..f6d6bc96
--- /dev/null
+++ b/tests/phpunit/languages/SpecialPageAliasTest.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * Verifies that special page aliases are valid, with no slashes.
+ *
+ * @group Language
+ * @group SpecialPageAliases
+ * @group SystemTest
+ * @group medium
+ *
+ * @licence GNU GPL v2+
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class SpecialPageAliasTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider validSpecialPageAliasesProvider
+ */
+ public function testValidSpecialPageAliases( $code, $specialPageAliases ) {
+ foreach ( $specialPageAliases as $specialPage => $aliases ) {
+ foreach ( $aliases as $alias ) {
+ $msg = "$specialPage alias '$alias' in $code is valid with no slashes";
+ $this->assertRegExp( '/^[^\/]*$/', $msg );
+ }
+ }
+ }
+
+ public function validSpecialPageAliasesProvider() {
+ $codes = array_keys( Language::fetchLanguageNames( 'mwfile' ) );
+
+ $data = array();
+
+ foreach ( $codes as $code ) {
+ $specialPageAliases = $this->getSpecialPageAliases( $code );
+
+ if ( $specialPageAliases !== array() ) {
+ $data[] = array( $code, $specialPageAliases );
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param string $code
+ *
+ * @return array
+ */
+ protected function getSpecialPageAliases( $code ) {
+ $file = Language::getMessagesFileName( $code );
+
+ if ( is_readable( $file ) ) {
+ include $file;
+
+ if ( isset( $specialPageAliases ) && $specialPageAliases !== null ) {
+ return $specialPageAliases;
+ }
+ }
+
+ return array();
+ }
+
+}
diff --git a/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php
new file mode 100644
index 00000000..8e3b1145
--- /dev/null
+++ b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * @author Niklas Laxström
+ * @file
+ */
+
+/**
+ * @covers CLDRPluralRuleEvaluator
+ */
+class CLDRPluralRuleEvaluatorTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider validTestCases
+ */
+ function testValidRules( $expected, $rules, $number, $comment ) {
+ $result = CLDRPluralRuleEvaluator::evaluate( $number, (array)$rules );
+ $this->assertEquals( $expected, $result, $comment );
+ }
+
+ /**
+ * @dataProvider invalidTestCases
+ * @expectedException CLDRPluralRuleError
+ */
+ function testInvalidRules( $rules, $comment ) {
+ CLDRPluralRuleEvaluator::evaluate( 1, (array)$rules );
+ }
+
+ function validTestCases() {
+ $tests = array(
+ # expected, rule, number, comment
+ array( 0, 'n is 1', 1, 'integer number and is' ),
+ array( 0, 'n is 1', "1", 'string integer number and is' ),
+ array( 0, 'n is 1', 1.0, 'float number and is' ),
+ array( 0, 'n is 1', "1.0", 'string float number and is' ),
+ array( 1, 'n is 1', 1.1, 'float number and is' ),
+ array( 1, 'n is 1', 2, 'float number and is' ),
+
+ array( 0, 'n in 1,3,5', 3, '' ),
+ array( 1, 'n not in 1,3,5', 5, '' ),
+
+ array( 1, 'n in 1,3,5', 2, '' ),
+ array( 0, 'n not in 1,3,5', 4, '' ),
+
+ array( 0, 'n in 1..3', 2, '' ),
+ array( 0, 'n in 1..3', 3, 'in is inclusive' ),
+ array( 1, 'n in 1..3', 0, '' ),
+
+ array( 1, 'n not in 1..3', 2, '' ),
+ array( 1, 'n not in 1..3', 3, 'in is inclusive' ),
+ array( 0, 'n not in 1..3', 0, '' ),
+
+ array( 1, 'n is not 1 and n is not 2 and n is not 3', 1, 'and relation' ),
+ array( 0, 'n is not 1 and n is not 2 and n is not 4', 3, 'and relation' ),
+
+ array( 0, 'n is not 1 or n is 1', 1, 'or relation' ),
+ array( 1, 'n is 1 or n is 2', 3, 'or relation' ),
+
+ array( 0, 'n is 1', 1, 'extra whitespace' ),
+
+ array( 0, 'n mod 3 is 1', 7, 'mod' ),
+ array( 0, 'n mod 3 is not 1', 4.3, 'mod with floats' ),
+
+ array( 0, 'n within 1..3', 2, 'within with integer' ),
+ array( 0, 'n within 1..3', 2.5, 'within with float' ),
+ array( 0, 'n in 1..3', 2, 'in with integer' ),
+ array( 1, 'n in 1..3', 2.5, 'in with float' ),
+
+ array( 0, 'n in 3 or n is 4 and n is 5', 3, 'and binds more tightly than or' ),
+ array( 1, 'n is 3 or n is 4 and n is 5', 4, 'and binds more tightly than or' ),
+
+ array( 0, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 24, 'breton rule' ),
+ array( 1, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 25, 'breton rule' ),
+
+ array( 0, 'n within 0..2 and n is not 2', 0, 'french rule' ),
+ array( 0, 'n within 0..2 and n is not 2', 1, 'french rule' ),
+ array( 0, 'n within 0..2 and n is not 2', 1.2, 'french rule' ),
+ array( 1, 'n within 0..2 and n is not 2', 2, 'french rule' ),
+
+ array( 1, 'n in 3..10,13..19', 2, 'scottish rule - ranges with comma' ),
+ array( 0, 'n in 3..10,13..19', 4, 'scottish rule - ranges with comma' ),
+ array( 1, 'n in 3..10,13..19', 12.999, 'scottish rule - ranges with comma' ),
+ array( 0, 'n in 3..10,13..19', 13, 'scottish rule - ranges with comma' ),
+
+ array( 0, '5 mod 3 is n', 2, 'n as result of mod - no need to pass' ),
+
+ # Revision 33 new operand examples
+ # expected, rule, number, comment
+ array( 0, 'i is 1', '1.00', 'new operand i' ),
+ array( 0, 'v is 2', '1.00', 'new operand v' ),
+ array( 0, 'w is 0', '1.00', 'new operand w' ),
+ array( 0, 'f is 0', '1.00', 'new operand f' ),
+ array( 0, 't is 0', '1.00', 'new operand t' ),
+
+ array( 0, 'i is 1', '1.30', 'new operand i' ),
+ array( 0, 'v is 2', '1.30', 'new operand v' ),
+ array( 0, 'w is 1', '1.30', 'new operand w' ),
+ array( 0, 'f is 30', '1.30', 'new operand f' ),
+ array( 0, 't is 3', '1.30', 'new operand t' ),
+
+ array( 0, 'i is 1', '1.03', 'new operand i' ),
+ array( 0, 'v is 2', '1.03', 'new operand v' ),
+ array( 0, 'w is 2', '1.03', 'new operand w' ),
+ array( 0, 'f is 3', '1.03', 'new operand f' ),
+ array( 0, 't is 3', '1.03', 'new operand t' ),
+
+ # Revision 33 new operator aliases
+ # expected, rule, number, comment
+ array( 0, 'n % 3 is 1', 7, 'new % operator' ),
+ array( 0, 'n = 1,3,5', 3, 'new = operator' ),
+ array( 1, 'n != 1,3,5', 5, 'new != operator' ),
+
+ # Revision 33 samples
+ # expected, rule, number, comment
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ array( 0, 'n in 1,3,5@integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …', 3, 'samples' ),
+ // @codingStandardsIgnoreEnd
+
+ # Revision 33 some test cases from CLDR
+ array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.1', 'pt one' ),
+ array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.01', 'pt one' ),
+ array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.10', 'pt one' ),
+ array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.010', 'pt one' ),
+ array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.100', 'pt one' ),
+ array( 1, 'i = 1 and v = 0 or i = 0 and t = 1', '0.0', 'pt other' ),
+ array( 1, 'i = 1 and v = 0 or i = 0 and t = 1', '0.2', 'pt other' ),
+ array( 1, 'i = 1 and v = 0 or i = 0 and t = 1', '10.0', 'pt other' ),
+ array( 1, 'i = 1 and v = 0 or i = 0 and t = 1', '100.0', 'pt other' ),
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '2', 'bs few' ),
+ array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '4', 'bs few' ),
+ array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '22', 'bs few' ),
+ array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '102', 'bs few' ),
+ array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '0.2', 'bs few' ),
+ array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '0.4', 'bs few' ),
+ array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '10.2', 'bs few' ),
+ array( 1, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '10.0', 'bs other' ),
+ // @codingStandardsIgnoreEnd
+ );
+
+ return $tests;
+ }
+
+ function invalidTestCases() {
+ $tests = array(
+ array( 'n mod mod 5 is 1', 'mod mod' ),
+ array( 'n', 'just n' ),
+ array( 'n is in 5', 'is in' ),
+ );
+
+ return $tests;
+ }
+}
diff --git a/tests/phpunit/maintenance/DumpTestCase.php b/tests/phpunit/maintenance/DumpTestCase.php
new file mode 100644
index 00000000..8b6aef53
--- /dev/null
+++ b/tests/phpunit/maintenance/DumpTestCase.php
@@ -0,0 +1,386 @@
+<?php
+
+/**
+ * Base TestCase for dumps
+ */
+abstract class DumpTestCase extends MediaWikiLangTestCase {
+
+ /**
+ * exception to be rethrown once in sound PHPUnit surrounding
+ *
+ * As the current MediaWikiTestCase::run is not robust enough to recover
+ * from thrown exceptions directly, we cannot throw frow within
+ * self::addDBData, although it would be appropriate. Hence, we catch the
+ * exception and store it until we are in setUp and may finally rethrow
+ * the exception without crashing the test suite.
+ *
+ * @var Exception|null
+ */
+ protected $exceptionFromAddDBData = null;
+
+ /**
+ * Holds the xmlreader used for analyzing an xml dump
+ *
+ * @var XMLReader|null
+ */
+ protected $xml = null;
+
+ /**
+ * Adds a revision to a page, while returning the resuting revision's id
+ *
+ * @param Page $page Page to add the revision to
+ * @param string $text Revisions text
+ * @param string $summary Revisions summare
+ * @return array
+ * @throws MWException
+ */
+ protected function addRevision( Page $page, $text, $summary ) {
+ $status = $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle() ),
+ $summary
+ );
+
+ if ( $status->isGood() ) {
+ $value = $status->getValue();
+ $revision = $value['revision'];
+ $revision_id = $revision->getId();
+ $text_id = $revision->getTextId();
+
+ if ( ( $revision_id > 0 ) && ( $text_id > 0 ) ) {
+ return array( $revision_id, $text_id );
+ }
+ }
+
+ throw new MWException( "Could not determine revision id (" . $status->getWikiText() . ")" );
+ }
+
+ /**
+ * gunzips the given file and stores the result in the original file name
+ *
+ * @param string $fname Filename to read the gzipped data from and stored
+ * the gunzipped data into
+ */
+ protected function gunzip( $fname ) {
+ $gzipped_contents = file_get_contents( $fname );
+ if ( $gzipped_contents === false ) {
+ $this->fail( "Could not get contents of $fname" );
+ }
+
+ $contents = gzdecode( $gzipped_contents );
+
+ $this->assertEquals(
+ strlen( $contents ),
+ file_put_contents( $fname, $contents ),
+ '# bytes written'
+ );
+ }
+
+ /**
+ * Default set up function.
+ *
+ * Clears $wgUser, and reports errors from addDBData to PHPUnit
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ // Check if any Exception is stored for rethrowing from addDBData
+ // @see self::exceptionFromAddDBData
+ if ( $this->exceptionFromAddDBData !== null ) {
+ throw $this->exceptionFromAddDBData;
+ }
+
+ $this->setMwGlobals( 'wgUser', new User() );
+ }
+
+ /**
+ * Checks for test output consisting only of lines containing ETA announcements
+ */
+ function expectETAOutput() {
+ // Newer PHPUnits require assertion about the output using PHPUnit's own
+ // expectOutput[...] functions. However, the PHPUnit shipped prediactes
+ // do not allow to check /each/ line of the output using /readable/ REs.
+ // So we ...
+ //
+ // 1. ... add a dummy output checking to make PHPUnit not complain
+ // about unchecked test output
+ $this->expectOutputRegex( '//' );
+
+ // 2. Do the real output checking on our own.
+ $lines = explode( "\n", $this->getActualOutput() );
+ $this->assertGreaterThan( 1, count( $lines ), "Minimal lines of produced output" );
+ $this->assertEquals( '', array_pop( $lines ), "Output ends in LF" );
+ $timestamp_re = "[0-9]{4}-[01][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-6][0-9]";
+ foreach ( $lines as $line ) {
+ $this->assertRegExp(
+ "/$timestamp_re: .* \(ID [0-9]+\) [0-9]* pages .*, [0-9]* revs .*, ETA/",
+ $line
+ );
+ }
+ }
+
+ /**
+ * Step the current XML reader until node end of given name is found.
+ *
+ * @param string $name Name of the closing element to look for
+ * (e.g.: "mediawiki" when looking for </mediawiki>)
+ *
+ * @return bool True if the end node could be found. false otherwise.
+ */
+ protected function skipToNodeEnd( $name ) {
+ while ( $this->xml->read() ) {
+ if ( $this->xml->nodeType == XMLReader::END_ELEMENT &&
+ $this->xml->name == $name
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Step the current XML reader to the first element start after the node
+ * end of a given name.
+ *
+ * @param string $name Name of the closing element to look for
+ * (e.g.: "mediawiki" when looking for </mediawiki>)
+ *
+ * @return bool True if new element after the closing of $name could be
+ * found. false otherwise.
+ */
+ protected function skipPastNodeEnd( $name ) {
+ $this->assertTrue( $this->skipToNodeEnd( $name ),
+ "Skipping to end of $name" );
+ while ( $this->xml->read() ) {
+ if ( $this->xml->nodeType == XMLReader::ELEMENT ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Opens an XML file to analyze and optionally skips past siteinfo.
+ *
+ * @param string $fname Name of file to analyze
+ * @param bool $skip_siteinfo (optional) If true, step the xml reader
+ * to the first element after </siteinfo>
+ */
+ protected function assertDumpStart( $fname, $skip_siteinfo = true ) {
+ $this->xml = new XMLReader();
+ $this->assertTrue( $this->xml->open( $fname ),
+ "Opening temporary file $fname via XMLReader failed" );
+ if ( $skip_siteinfo ) {
+ $this->assertTrue( $this->skipPastNodeEnd( "siteinfo" ),
+ "Skipping past end of siteinfo" );
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at the final closing tag of an xml file and
+ * closes the reader.
+ *
+ * @param string $name (optional) the name of the final tag
+ * (e.g.: "mediawiki" for </mediawiki>)
+ */
+ protected function assertDumpEnd( $name = "mediawiki" ) {
+ $this->assertNodeEnd( $name, false );
+ if ( $this->xml->read() ) {
+ $this->skipWhitespace();
+ }
+ $this->assertEquals( $this->xml->nodeType, XMLReader::NONE,
+ "No proper entity left to parse" );
+ $this->xml->close();
+ }
+
+ /**
+ * Steps the xml reader over white space
+ */
+ protected function skipWhitespace() {
+ $cont = true;
+ while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE )
+ || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) {
+ $cont = $this->xml->read();
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at an element of given name, and optionally
+ * skips past it.
+ *
+ * @param string $name The name of the element to check for
+ * (e.g.: "mediawiki" for <mediawiki>)
+ * @param bool $skip (optional) if true, skip past the found element
+ */
+ protected function assertNodeStart( $name, $skip = true ) {
+ $this->assertEquals( $name, $this->xml->name, "Node name" );
+ $this->assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" );
+ if ( $skip ) {
+ $this->assertTrue( $this->xml->read(), "Skipping past start tag" );
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at an closing element of given name, and optionally
+ * skips past it.
+ *
+ * @param string $name The name of the closing element to check for
+ * (e.g.: "mediawiki" for </mediawiki>)
+ * @param bool $skip (optional) if true, skip past the found element
+ */
+ protected function assertNodeEnd( $name, $skip = true ) {
+ $this->assertEquals( $name, $this->xml->name, "Node name" );
+ $this->assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" );
+ if ( $skip ) {
+ $this->assertTrue( $this->xml->read(), "Skipping past end tag" );
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at an element of given tag that contains a given text,
+ * and skips over the element.
+ *
+ * @param string $name The name of the element to check for
+ * (e.g.: "mediawiki" for <mediawiki>...</mediawiki>)
+ * @param string|bool $text If string, check if it equals the elements text.
+ * If false, ignore the element's text
+ * @param bool $skip_ws (optional) if true, skip past white spaces that trail the
+ * closing element.
+ */
+ protected function assertTextNode( $name, $text, $skip_ws = true ) {
+ $this->assertNodeStart( $name );
+
+ if ( $text !== false ) {
+ $this->assertEquals( $text, $this->xml->value, "Text of node " . $name );
+ }
+ $this->assertTrue( $this->xml->read(), "Skipping past processed text of " . $name );
+ $this->assertNodeEnd( $name );
+
+ if ( $skip_ws ) {
+ $this->skipWhitespace();
+ }
+ }
+
+ /**
+ * Asserts that the xml reader is at the start of a page element and skips over the first
+ * tags, after checking them.
+ *
+ * Besides the opening page element, this function also checks for and skips over the
+ * title, ns, and id tags. Hence after this function, the xml reader is at the first
+ * revision of the current page.
+ *
+ * @param int $id Id of the page to assert
+ * @param int $ns Number of namespage to assert
+ * @param string $name Title of the current page
+ */
+ protected function assertPageStart( $id, $ns, $name ) {
+
+ $this->assertNodeStart( "page" );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "title", $name );
+ $this->assertTextNode( "ns", $ns );
+ $this->assertTextNode( "id", $id );
+ }
+
+ /**
+ * Asserts that the xml reader is at the page's closing element and skips to the next
+ * element.
+ */
+ protected function assertPageEnd() {
+ $this->assertNodeEnd( "page" );
+ $this->skipWhitespace();
+ }
+
+ /**
+ * Asserts that the xml reader is at a revision and checks its representation before
+ * skipping over it.
+ *
+ * @param int $id Id of the revision
+ * @param string $summary Summary of the revision
+ * @param int $text_id Id of the revision's text
+ * @param int $text_bytes Number of bytes in the revision's text
+ * @param string $text_sha1 The base36 SHA-1 of the revision's text
+ * @param string|bool $text (optional) The revision's string, or false to check for a
+ * revision stub
+ * @param int|bool $parentid (optional) id of the parent revision
+ * @param string $model The expected content model id (default: CONTENT_MODEL_WIKITEXT)
+ * @param string $format The expected format model id (default: CONTENT_FORMAT_WIKITEXT)
+ */
+ protected function assertRevision( $id, $summary, $text_id, $text_bytes,
+ $text_sha1, $text = false, $parentid = false,
+ $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT
+ ) {
+ $this->assertNodeStart( "revision" );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "id", $id );
+ if ( $parentid !== false ) {
+ $this->assertTextNode( "parentid", $parentid );
+ }
+ $this->assertTextNode( "timestamp", false );
+
+ $this->assertNodeStart( "contributor" );
+ $this->skipWhitespace();
+ $this->assertTextNode( "ip", false );
+ $this->assertNodeEnd( "contributor" );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "comment", $summary );
+ $this->skipWhitespace();
+
+ if ( $this->xml->name == "text" ) {
+ // note: <text> tag may occur here or at the very end.
+ $text_found = true;
+ $this->assertText( $id, $text_id, $text_bytes, $text );
+ } else {
+ $text_found = false;
+ }
+
+ $this->assertTextNode( "sha1", $text_sha1 );
+
+ $this->assertTextNode( "model", $model );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "format", $format );
+ $this->skipWhitespace();
+
+ if ( !$text_found ) {
+ $this->assertText( $id, $text_id, $text_bytes, $text );
+ }
+
+ $this->assertNodeEnd( "revision" );
+ $this->skipWhitespace();
+ }
+
+ protected function assertText( $id, $text_id, $text_bytes, $text ) {
+ $this->assertNodeStart( "text", false );
+ if ( $text_bytes !== false ) {
+ $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
+ "Attribute 'bytes' of revision " . $id );
+ }
+
+ if ( $text === false ) {
+ // Testing for a stub
+ $this->assertEquals( $this->xml->getAttribute( "id" ), $text_id,
+ "Text id of revision " . $id );
+ $this->assertFalse( $this->xml->hasValue, "Revision has text" );
+ $this->assertTrue( $this->xml->read(), "Skipping text start tag" );
+ if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT )
+ && ( $this->xml->name == "text" )
+ ) {
+
+ $this->xml->read();
+ }
+ $this->skipWhitespace();
+ } else {
+ // Testing for a real dump
+ $this->assertTrue( $this->xml->read(), "Skipping text start tag" );
+ $this->assertEquals( $text, $this->xml->value, "Text of revision " . $id );
+ $this->assertTrue( $this->xml->read(), "Skipping past text" );
+ $this->assertNodeEnd( "text" );
+ $this->skipWhitespace();
+ }
+ }
+}
diff --git a/tests/phpunit/maintenance/MaintenanceTest.php b/tests/phpunit/maintenance/MaintenanceTest.php
new file mode 100644
index 00000000..e2fc8247
--- /dev/null
+++ b/tests/phpunit/maintenance/MaintenanceTest.php
@@ -0,0 +1,830 @@
+<?php
+
+// It would be great if we were able to use PHPUnit's getMockForAbstractClass
+// instead of the MaintenanceFixup hack below. However, we cannot do
+// without changing the visibility and without working around hacks in
+// Maintenance.php
+//
+// For the same reason, we cannot just use FakeMaintenance.
+
+/**
+ * makes parts of the API of Maintenance that is hidden by protected visibily
+ * visible for testing, and makes up for a stream closing hack in Maintenance.php.
+ *
+ * This class is solely used for being able to test Maintenance right now
+ * without having to apply major refactorings to fix some design issues in
+ * Maintenance.php. Before adding more functions here, please consider whether
+ * this approach is correct, or a refactoring Maintenance to separate concers
+ * is more appropriate.
+ *
+ * Upon refactoring, keep in mind that besides the maintenance scrits themselves
+ * and tests right here, also at least Extension:Maintenance make use of
+ * Maintenance.
+ *
+ * Due to a hack in Maintenance.php using register_shutdown_function, be sure to
+ * finally call simulateShutdown on MaintenanceFixup instance before a test
+ * ends.
+ *
+ */
+class MaintenanceFixup extends Maintenance {
+
+ // --- Making up for the register_shutdown_function hack in Maintenance.php
+
+ /**
+ * The test case that generated this instance.
+ *
+ * This member is motivated by allowing the destructor to check whether or not
+ * the test failed, in order to avoid unnecessary nags about omitted shutdown
+ * simulation.
+ * But as it is already available, we also usi it to flagging tests as failed
+ *
+ * @var MediaWikiTestCase
+ */
+ private $testCase;
+
+ /**
+ * shutdownSimulated === true if simulateShutdown has done it's work
+ *
+ * @var bool
+ */
+ private $shutdownSimulated = false;
+
+ /**
+ * Simulates what Maintenance wants to happen at script's end.
+ */
+ public function simulateShutdown() {
+
+ if ( $this->shutdownSimulated ) {
+ $this->testCase->fail( __METHOD__ . " called more than once" );
+ }
+
+ // The cleanup action.
+ $this->outputChanneled( false );
+
+ // Bookkeeping that we simulated the clean up.
+ $this->shutdownSimulated = true;
+ }
+
+ // Note that the "public" here does not change visibility
+ public function outputChanneled( $msg, $channel = null ) {
+ if ( $this->shutdownSimulated ) {
+ if ( $msg !== false ) {
+ $this->testCase->fail( "Already past simulated shutdown, but msg is "
+ . "not false. Did the hack in Maintenance.php change? Please "
+ . "adapt the test case or Maintenance.php" );
+ }
+
+ // The current call is the one registered via register_shutdown_function.
+ // We can safely ignore it, as we simulated this one via simulateShutdown
+ // before (if we did not, the destructor of this instance will warn about
+ // it)
+ return;
+ }
+
+ call_user_func_array( array( "parent", __FUNCTION__ ), func_get_args() );
+ }
+
+ /**
+ * Safety net around register_shutdown_function of Maintenance.php
+ */
+ public function __destruct() {
+ if ( !$this->shutdownSimulated ) {
+ // Someone generated a MaintenanceFixup instance without calling
+ // simulateShutdown. We'd have to raise a PHPUnit exception to correctly
+ // flag this illegal usage. However, we are already in a destruktor, which
+ // would trigger undefined behavior. Hence, we can only report to the
+ // error output :( Hopefully people read the PHPUnit output.
+ $name = $this->testCase->getName();
+ fwrite( STDERR, "ERROR! Instance of " . __CLASS__ . " for test $name "
+ . "destructed without calling simulateShutdown method. Call "
+ . "simulateShutdown on the instance before it gets destructed." );
+ }
+
+ // The following guard is required, as PHP does not offer default destructors :(
+ if ( is_callable( "parent::__destruct" ) ) {
+ parent::__destruct();
+ }
+ }
+
+ public function __construct( MediaWikiTestCase $testCase ) {
+ parent::__construct();
+ $this->testCase = $testCase;
+ }
+
+ // --- Making protected functions visible for test
+
+ public function output( $out, $channel = null ) {
+ // Just to make PHP not nag about signature mismatches, we copied
+ // Maintenance::output signature. However, we do not use (or rely on)
+ // those variables. Instead we pass to Maintenance::output whatever we
+ // receive at runtime.
+ return call_user_func_array( array( "parent", __FUNCTION__ ), func_get_args() );
+ }
+
+ // --- Requirements for getting instance of abstract class
+
+ public function execute() {
+ $this->testCase->fail( __METHOD__ . " called unexpectedly" );
+ }
+}
+
+/**
+ * @covers Maintenance
+ */
+class MaintenanceTest extends MediaWikiTestCase {
+
+ /**
+ * The main Maintenance instance that is used for testing.
+ *
+ * @var MaintenanceFixup
+ */
+ private $m;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->m = new MaintenanceFixup( $this );
+ }
+
+ protected function tearDown() {
+ if ( $this->m ) {
+ $this->m->simulateShutdown();
+ $this->m = null;
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * asserts the output before and after simulating shutdown
+ *
+ * This function simulates shutdown of self::m.
+ *
+ * @param string $preShutdownOutput Expected output before simulating shutdown
+ * @param bool $expectNLAppending Whether or not shutdown simulation is expected
+ * to add a newline to the output. If false, $preShutdownOutput is the
+ * expected output after shutdown simulation. Otherwise,
+ * $preShutdownOutput with an appended newline is the expected output
+ * after shutdown simulation.
+ */
+ private function assertOutputPrePostShutdown( $preShutdownOutput, $expectNLAppending ) {
+
+ $this->assertEquals( $preShutdownOutput, $this->getActualOutput(),
+ "Output before shutdown simulation" );
+
+ $this->m->simulateShutdown();
+ $this->m = null;
+
+ $postShutdownOutput = $preShutdownOutput . ( $expectNLAppending ? "\n" : "" );
+ $this->expectOutputString( $postShutdownOutput );
+ }
+
+ // Although the following tests do not seem to be too consistent (compare for
+ // example the newlines within the test.*StringString tests, or the
+ // test.*Intermittent.* tests), the objective of these tests is not to describe
+ // consistent behavior, but rather currently existing behavior.
+
+ function testOutputEmpty() {
+ $this->m->output( "" );
+ $this->assertOutputPrePostShutdown( "", false );
+ }
+
+ function testOutputString() {
+ $this->m->output( "foo" );
+ $this->assertOutputPrePostShutdown( "foo", false );
+ }
+
+ function testOutputStringString() {
+ $this->m->output( "foo" );
+ $this->m->output( "bar" );
+ $this->assertOutputPrePostShutdown( "foobar", false );
+ }
+
+ function testOutputStringNL() {
+ $this->m->output( "foo\n" );
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testOutputStringNLNL() {
+ $this->m->output( "foo\n\n" );
+ $this->assertOutputPrePostShutdown( "foo\n\n", false );
+ }
+
+ function testOutputStringNLString() {
+ $this->m->output( "foo\nbar" );
+ $this->assertOutputPrePostShutdown( "foo\nbar", false );
+ }
+
+ function testOutputStringNLStringNL() {
+ $this->m->output( "foo\nbar\n" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputStringNLStringNLLinewise() {
+ $this->m->output( "foo\n" );
+ $this->m->output( "bar\n" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputStringNLStringNLArbitrary() {
+ $this->m->output( "" );
+ $this->m->output( "foo" );
+ $this->m->output( "" );
+ $this->m->output( "\n" );
+ $this->m->output( "ba" );
+ $this->m->output( "" );
+ $this->m->output( "r\n" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputStringNLStringNLArbitraryAgain() {
+ $this->m->output( "" );
+ $this->m->output( "foo" );
+ $this->m->output( "" );
+ $this->m->output( "\nb" );
+ $this->m->output( "a" );
+ $this->m->output( "" );
+ $this->m->output( "r\n" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputWNullChannelEmpty() {
+ $this->m->output( "", null );
+ $this->assertOutputPrePostShutdown( "", false );
+ }
+
+ function testOutputWNullChannelString() {
+ $this->m->output( "foo", null );
+ $this->assertOutputPrePostShutdown( "foo", false );
+ }
+
+ function testOutputWNullChannelStringString() {
+ $this->m->output( "foo", null );
+ $this->m->output( "bar", null );
+ $this->assertOutputPrePostShutdown( "foobar", false );
+ }
+
+ function testOutputWNullChannelStringNL() {
+ $this->m->output( "foo\n", null );
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testOutputWNullChannelStringNLNL() {
+ $this->m->output( "foo\n\n", null );
+ $this->assertOutputPrePostShutdown( "foo\n\n", false );
+ }
+
+ function testOutputWNullChannelStringNLString() {
+ $this->m->output( "foo\nbar", null );
+ $this->assertOutputPrePostShutdown( "foo\nbar", false );
+ }
+
+ function testOutputWNullChannelStringNLStringNL() {
+ $this->m->output( "foo\nbar\n", null );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputWNullChannelStringNLStringNLLinewise() {
+ $this->m->output( "foo\n", null );
+ $this->m->output( "bar\n", null );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputWNullChannelStringNLStringNLArbitrary() {
+ $this->m->output( "", null );
+ $this->m->output( "foo", null );
+ $this->m->output( "", null );
+ $this->m->output( "\n", null );
+ $this->m->output( "ba", null );
+ $this->m->output( "", null );
+ $this->m->output( "r\n", null );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputWNullChannelStringNLStringNLArbitraryAgain() {
+ $this->m->output( "", null );
+ $this->m->output( "foo", null );
+ $this->m->output( "", null );
+ $this->m->output( "\nb", null );
+ $this->m->output( "a", null );
+ $this->m->output( "", null );
+ $this->m->output( "r\n", null );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputWChannelString() {
+ $this->m->output( "foo", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo", true );
+ }
+
+ function testOutputWChannelStringNL() {
+ $this->m->output( "foo\n", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo", true );
+ }
+
+ function testOutputWChannelStringNLNL() {
+ // If this test fails, note that output takes strings with double line
+ // endings (although output's implementation in this situation calls
+ // outputChanneled with a string ending in a nl ... which is not allowed
+ // according to the documentation of outputChanneled)
+ $this->m->output( "foo\n\n", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo\n", true );
+ }
+
+ function testOutputWChannelStringNLString() {
+ $this->m->output( "foo\nbar", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo\nbar", true );
+ }
+
+ function testOutputWChannelStringNLStringNL() {
+ $this->m->output( "foo\nbar\n", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo\nbar", true );
+ }
+
+ function testOutputWChannelStringNLStringNLLinewise() {
+ $this->m->output( "foo\n", "bazChannel" );
+ $this->m->output( "bar\n", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar", true );
+ }
+
+ function testOutputWChannelStringNLStringNLArbitrary() {
+ $this->m->output( "", "bazChannel" );
+ $this->m->output( "foo", "bazChannel" );
+ $this->m->output( "", "bazChannel" );
+ $this->m->output( "\n", "bazChannel" );
+ $this->m->output( "ba", "bazChannel" );
+ $this->m->output( "", "bazChannel" );
+ $this->m->output( "r\n", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar", true );
+ }
+
+ function testOutputWChannelStringNLStringNLArbitraryAgain() {
+ $this->m->output( "", "bazChannel" );
+ $this->m->output( "foo", "bazChannel" );
+ $this->m->output( "", "bazChannel" );
+ $this->m->output( "\nb", "bazChannel" );
+ $this->m->output( "a", "bazChannel" );
+ $this->m->output( "", "bazChannel" );
+ $this->m->output( "r\n", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo\nbar", true );
+ }
+
+ function testOutputWMultipleChannelsChannelChange() {
+ $this->m->output( "foo", "bazChannel" );
+ $this->m->output( "bar", "bazChannel" );
+ $this->m->output( "qux", "quuxChannel" );
+ $this->m->output( "corge", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true );
+ }
+
+ function testOutputWMultipleChannelsChannelChangeNL() {
+ $this->m->output( "foo", "bazChannel" );
+ $this->m->output( "bar\n", "bazChannel" );
+ $this->m->output( "qux\n", "quuxChannel" );
+ $this->m->output( "corge", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true );
+ }
+
+ function testOutputWAndWOChannelStringStartWO() {
+ $this->m->output( "foo" );
+ $this->m->output( "bar", "bazChannel" );
+ $this->m->output( "qux" );
+ $this->m->output( "quux", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar\nquxquux", true );
+ }
+
+ function testOutputWAndWOChannelStringStartW() {
+ $this->m->output( "foo", "bazChannel" );
+ $this->m->output( "bar" );
+ $this->m->output( "qux", "bazChannel" );
+ $this->m->output( "quux" );
+ $this->assertOutputPrePostShutdown( "foo\nbarqux\nquux", false );
+ }
+
+ function testOutputWChannelTypeSwitch() {
+ $this->m->output( "foo", 1 );
+ $this->m->output( "bar", 1.0 );
+ $this->assertOutputPrePostShutdown( "foo\nbar", true );
+ }
+
+ function testOutputIntermittentEmpty() {
+ $this->m->output( "foo" );
+ $this->m->output( "" );
+ $this->m->output( "bar" );
+ $this->assertOutputPrePostShutdown( "foobar", false );
+ }
+
+ function testOutputIntermittentFalse() {
+ $this->m->output( "foo" );
+ $this->m->output( false );
+ $this->m->output( "bar" );
+ $this->assertOutputPrePostShutdown( "foobar", false );
+ }
+
+ function testOutputIntermittentFalseAfterOtherChannel() {
+ $this->m->output( "qux", "quuxChannel" );
+ $this->m->output( "foo" );
+ $this->m->output( false );
+ $this->m->output( "bar" );
+ $this->assertOutputPrePostShutdown( "qux\nfoobar", false );
+ }
+
+ function testOutputWNullChannelIntermittentEmpty() {
+ $this->m->output( "foo", null );
+ $this->m->output( "", null );
+ $this->m->output( "bar", null );
+ $this->assertOutputPrePostShutdown( "foobar", false );
+ }
+
+ function testOutputWNullChannelIntermittentFalse() {
+ $this->m->output( "foo", null );
+ $this->m->output( false, null );
+ $this->m->output( "bar", null );
+ $this->assertOutputPrePostShutdown( "foobar", false );
+ }
+
+ function testOutputWChannelIntermittentEmpty() {
+ $this->m->output( "foo", "bazChannel" );
+ $this->m->output( "", "bazChannel" );
+ $this->m->output( "bar", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar", true );
+ }
+
+ function testOutputWChannelIntermittentFalse() {
+ $this->m->output( "foo", "bazChannel" );
+ $this->m->output( false, "bazChannel" );
+ $this->m->output( "bar", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar", true );
+ }
+
+ // Note that (per documentation) outputChanneled does take strings that end
+ // in \n, hence we do not test such strings.
+
+ function testOutputChanneledEmpty() {
+ $this->m->outputChanneled( "" );
+ $this->assertOutputPrePostShutdown( "\n", false );
+ }
+
+ function testOutputChanneledString() {
+ $this->m->outputChanneled( "foo" );
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testOutputChanneledStringString() {
+ $this->m->outputChanneled( "foo" );
+ $this->m->outputChanneled( "bar" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputChanneledStringNLString() {
+ $this->m->outputChanneled( "foo\nbar" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputChanneledStringNLStringNLArbitraryAgain() {
+ $this->m->outputChanneled( "" );
+ $this->m->outputChanneled( "foo" );
+ $this->m->outputChanneled( "" );
+ $this->m->outputChanneled( "\nb" );
+ $this->m->outputChanneled( "a" );
+ $this->m->outputChanneled( "" );
+ $this->m->outputChanneled( "r" );
+ $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", false );
+ }
+
+ function testOutputChanneledWNullChannelEmpty() {
+ $this->m->outputChanneled( "", null );
+ $this->assertOutputPrePostShutdown( "\n", false );
+ }
+
+ function testOutputChanneledWNullChannelString() {
+ $this->m->outputChanneled( "foo", null );
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testOutputChanneledWNullChannelStringString() {
+ $this->m->outputChanneled( "foo", null );
+ $this->m->outputChanneled( "bar", null );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputChanneledWNullChannelStringNLString() {
+ $this->m->outputChanneled( "foo\nbar", null );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputChanneledWNullChannelStringNLStringNLArbitraryAgain() {
+ $this->m->outputChanneled( "", null );
+ $this->m->outputChanneled( "foo", null );
+ $this->m->outputChanneled( "", null );
+ $this->m->outputChanneled( "\nb", null );
+ $this->m->outputChanneled( "a", null );
+ $this->m->outputChanneled( "", null );
+ $this->m->outputChanneled( "r", null );
+ $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", false );
+ }
+
+ function testOutputChanneledWChannelString() {
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo", true );
+ }
+
+ function testOutputChanneledWChannelStringNLString() {
+ $this->m->outputChanneled( "foo\nbar", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo\nbar", true );
+ }
+
+ function testOutputChanneledWChannelStringString() {
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->m->outputChanneled( "bar", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar", true );
+ }
+
+ function testOutputChanneledWChannelStringNLStringNLArbitraryAgain() {
+ $this->m->outputChanneled( "", "bazChannel" );
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->m->outputChanneled( "", "bazChannel" );
+ $this->m->outputChanneled( "\nb", "bazChannel" );
+ $this->m->outputChanneled( "a", "bazChannel" );
+ $this->m->outputChanneled( "", "bazChannel" );
+ $this->m->outputChanneled( "r", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo\nbar", true );
+ }
+
+ function testOutputChanneledWMultipleChannelsChannelChange() {
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->m->outputChanneled( "bar", "bazChannel" );
+ $this->m->outputChanneled( "qux", "quuxChannel" );
+ $this->m->outputChanneled( "corge", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true );
+ }
+
+ function testOutputChanneledWMultipleChannelsChannelChangeEnclosedNull() {
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->m->outputChanneled( "bar", null );
+ $this->m->outputChanneled( "qux", null );
+ $this->m->outputChanneled( "corge", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", true );
+ }
+
+ function testOutputChanneledWMultipleChannelsChannelAfterNullChange() {
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->m->outputChanneled( "bar", null );
+ $this->m->outputChanneled( "qux", null );
+ $this->m->outputChanneled( "corge", "quuxChannel" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", true );
+ }
+
+ function testOutputChanneledWAndWOChannelStringStartWO() {
+ $this->m->outputChanneled( "foo" );
+ $this->m->outputChanneled( "bar", "bazChannel" );
+ $this->m->outputChanneled( "qux" );
+ $this->m->outputChanneled( "quux", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux", true );
+ }
+
+ function testOutputChanneledWAndWOChannelStringStartW() {
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->m->outputChanneled( "bar" );
+ $this->m->outputChanneled( "qux", "bazChannel" );
+ $this->m->outputChanneled( "quux" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux\n", false );
+ }
+
+ function testOutputChanneledWChannelTypeSwitch() {
+ $this->m->outputChanneled( "foo", 1 );
+ $this->m->outputChanneled( "bar", 1.0 );
+ $this->assertOutputPrePostShutdown( "foo\nbar", true );
+ }
+
+ function testOutputChanneledWOChannelIntermittentEmpty() {
+ $this->m->outputChanneled( "foo" );
+ $this->m->outputChanneled( "" );
+ $this->m->outputChanneled( "bar" );
+ $this->assertOutputPrePostShutdown( "foo\n\nbar\n", false );
+ }
+
+ function testOutputChanneledWOChannelIntermittentFalse() {
+ $this->m->outputChanneled( "foo" );
+ $this->m->outputChanneled( false );
+ $this->m->outputChanneled( "bar" );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputChanneledWNullChannelIntermittentEmpty() {
+ $this->m->outputChanneled( "foo", null );
+ $this->m->outputChanneled( "", null );
+ $this->m->outputChanneled( "bar", null );
+ $this->assertOutputPrePostShutdown( "foo\n\nbar\n", false );
+ }
+
+ function testOutputChanneledWNullChannelIntermittentFalse() {
+ $this->m->outputChanneled( "foo", null );
+ $this->m->outputChanneled( false, null );
+ $this->m->outputChanneled( "bar", null );
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testOutputChanneledWChannelIntermittentEmpty() {
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->m->outputChanneled( "", "bazChannel" );
+ $this->m->outputChanneled( "bar", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foobar", true );
+ }
+
+ function testOutputChanneledWChannelIntermittentFalse() {
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->m->outputChanneled( false, "bazChannel" );
+ $this->m->outputChanneled( "bar", "bazChannel" );
+ $this->assertOutputPrePostShutdown( "foo\nbar", true );
+ }
+
+ function testCleanupChanneledClean() {
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "", false );
+ }
+
+ function testCleanupChanneledAfterOutput() {
+ $this->m->output( "foo" );
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "foo", false );
+ }
+
+ function testCleanupChanneledAfterOutputWNullChannel() {
+ $this->m->output( "foo", null );
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "foo", false );
+ }
+
+ function testCleanupChanneledAfterOutputWChannel() {
+ $this->m->output( "foo", "bazChannel" );
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testCleanupChanneledAfterNLOutput() {
+ $this->m->output( "foo\n" );
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testCleanupChanneledAfterNLOutputWNullChannel() {
+ $this->m->output( "foo\n", null );
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testCleanupChanneledAfterNLOutputWChannel() {
+ $this->m->output( "foo\n", "bazChannel" );
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testCleanupChanneledAfterOutputChanneledWOChannel() {
+ $this->m->outputChanneled( "foo" );
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testCleanupChanneledAfterOutputChanneledWNullChannel() {
+ $this->m->outputChanneled( "foo", null );
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testCleanupChanneledAfterOutputChanneledWChannel() {
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $this->m->cleanupChanneled();
+ $this->assertOutputPrePostShutdown( "foo\n", false );
+ }
+
+ function testMultipleMaintenanceObjectsInteractionOutput() {
+ $m2 = new MaintenanceFixup( $this );
+
+ $this->m->output( "foo" );
+ $m2->output( "bar" );
+
+ $this->assertEquals( "foobar", $this->getActualOutput(),
+ "Output before shutdown simulation (m2)" );
+ $m2->simulateShutdown();
+ $this->assertOutputPrePostShutdown( "foobar", false );
+ }
+
+ function testMultipleMaintenanceObjectsInteractionOutputWNullChannel() {
+ $m2 = new MaintenanceFixup( $this );
+
+ $this->m->output( "foo", null );
+ $m2->output( "bar", null );
+
+ $this->assertEquals( "foobar", $this->getActualOutput(),
+ "Output before shutdown simulation (m2)" );
+ $m2->simulateShutdown();
+ $this->assertOutputPrePostShutdown( "foobar", false );
+ }
+
+ function testMultipleMaintenanceObjectsInteractionOutputWChannel() {
+ $m2 = new MaintenanceFixup( $this );
+
+ $this->m->output( "foo", "bazChannel" );
+ $m2->output( "bar", "bazChannel" );
+
+ $this->assertEquals( "foobar", $this->getActualOutput(),
+ "Output before shutdown simulation (m2)" );
+ $m2->simulateShutdown();
+ $this->assertOutputPrePostShutdown( "foobar\n", true );
+ }
+
+ function testMultipleMaintenanceObjectsInteractionOutputWNullChannelNL() {
+ $m2 = new MaintenanceFixup( $this );
+
+ $this->m->output( "foo\n", null );
+ $m2->output( "bar\n", null );
+
+ $this->assertEquals( "foo\nbar\n", $this->getActualOutput(),
+ "Output before shutdown simulation (m2)" );
+ $m2->simulateShutdown();
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testMultipleMaintenanceObjectsInteractionOutputWChannelNL() {
+ $m2 = new MaintenanceFixup( $this );
+
+ $this->m->output( "foo\n", "bazChannel" );
+ $m2->output( "bar\n", "bazChannel" );
+
+ $this->assertEquals( "foobar", $this->getActualOutput(),
+ "Output before shutdown simulation (m2)" );
+ $m2->simulateShutdown();
+ $this->assertOutputPrePostShutdown( "foobar\n", true );
+ }
+
+ function testMultipleMaintenanceObjectsInteractionOutputChanneled() {
+ $m2 = new MaintenanceFixup( $this );
+
+ $this->m->outputChanneled( "foo" );
+ $m2->outputChanneled( "bar" );
+
+ $this->assertEquals( "foo\nbar\n", $this->getActualOutput(),
+ "Output before shutdown simulation (m2)" );
+ $m2->simulateShutdown();
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testMultipleMaintenanceObjectsInteractionOutputChanneledWNullChannel() {
+ $m2 = new MaintenanceFixup( $this );
+
+ $this->m->outputChanneled( "foo", null );
+ $m2->outputChanneled( "bar", null );
+
+ $this->assertEquals( "foo\nbar\n", $this->getActualOutput(),
+ "Output before shutdown simulation (m2)" );
+ $m2->simulateShutdown();
+ $this->assertOutputPrePostShutdown( "foo\nbar\n", false );
+ }
+
+ function testMultipleMaintenanceObjectsInteractionOutputChanneledWChannel() {
+ $m2 = new MaintenanceFixup( $this );
+
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $m2->outputChanneled( "bar", "bazChannel" );
+
+ $this->assertEquals( "foobar", $this->getActualOutput(),
+ "Output before shutdown simulation (m2)" );
+ $m2->simulateShutdown();
+ $this->assertOutputPrePostShutdown( "foobar\n", true );
+ }
+
+ function testMultipleMaintenanceObjectsInteractionCleanupChanneledWChannel() {
+ $m2 = new MaintenanceFixup( $this );
+
+ $this->m->outputChanneled( "foo", "bazChannel" );
+ $m2->outputChanneled( "bar", "bazChannel" );
+
+ $this->assertEquals( "foobar", $this->getActualOutput(),
+ "Output before first cleanup" );
+ $this->m->cleanupChanneled();
+ $this->assertEquals( "foobar\n", $this->getActualOutput(),
+ "Output after first cleanup" );
+ $m2->cleanupChanneled();
+ $this->assertEquals( "foobar\n\n", $this->getActualOutput(),
+ "Output after second cleanup" );
+
+ $m2->simulateShutdown();
+ $this->assertOutputPrePostShutdown( "foobar\n\n", false );
+ }
+
+ /**
+ * @covers Maintenance::getConfig
+ */
+ public function testGetConfig() {
+ $this->assertInstanceOf( 'Config', $this->m->getConfig() );
+ $this->assertSame( ConfigFactory::getDefaultInstance()->makeConfig( 'main' ), $this->m->getConfig() );
+ }
+
+ /**
+ * @covers Maintenance::setConfig
+ */
+ public function testSetConfig() {
+ $conf = $this->getMock( 'Config' );
+ $this->m->setConfig( $conf );
+ $this->assertSame( $conf, $this->m->getConfig() );
+ }
+}
diff --git a/tests/phpunit/maintenance/backupPrefetchTest.php b/tests/phpunit/maintenance/backupPrefetchTest.php
new file mode 100644
index 00000000..5e0fe89d
--- /dev/null
+++ b/tests/phpunit/maintenance/backupPrefetchTest.php
@@ -0,0 +1,277 @@
+<?php
+
+require_once __DIR__ . "/../../../maintenance/backupPrefetch.inc";
+
+/**
+ * Tests for BaseDump
+ *
+ * @group Dump
+ * @covers BaseDump
+ */
+class BaseDumpTest extends MediaWikiTestCase {
+
+ /**
+ * @var BaseDump The BaseDump instance used within a test.
+ *
+ * If set, this BaseDump gets automatically closed in tearDown.
+ */
+ private $dump = null;
+
+ protected function tearDown() {
+ if ( $this->dump !== null ) {
+ $this->dump->close();
+ }
+
+ // Bug 37458, parent teardown need to be done after closing the
+ // dump or it might cause some permissions errors.
+ parent::tearDown();
+ }
+
+ /**
+ * asserts that a prefetch yields an expected string
+ *
+ * @param string|null $expected The exepcted result of the prefetch
+ * @param int $page The page number to prefetch the text for
+ * @param int $revision The revision number to prefetch the text for
+ */
+ private function assertPrefetchEquals( $expected, $page, $revision ) {
+ $this->assertEquals( $expected, $this->dump->prefetch( $page, $revision ),
+ "Prefetch of page $page revision $revision" );
+ }
+
+ function testSequential() {
+ $fname = $this->setUpPrefetch();
+ $this->dump = new BaseDump( $fname );
+
+ $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 );
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 );
+ $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 );
+ }
+
+ function testSynchronizeRevisionMissToRevision() {
+ $fname = $this->setUpPrefetch();
+ $this->dump = new BaseDump( $fname );
+
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ $this->assertPrefetchEquals( null, 2, 3 );
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 );
+ }
+
+ function testSynchronizeRevisionMissToPage() {
+ $fname = $this->setUpPrefetch();
+ $this->dump = new BaseDump( $fname );
+
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ $this->assertPrefetchEquals( null, 2, 40 );
+ $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 );
+ }
+
+ function testSynchronizePageMiss() {
+ $fname = $this->setUpPrefetch();
+ $this->dump = new BaseDump( $fname );
+
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ $this->assertPrefetchEquals( null, 3, 40 );
+ $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 );
+ }
+
+ function testPageMissAtEnd() {
+ $fname = $this->setUpPrefetch();
+ $this->dump = new BaseDump( $fname );
+
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ $this->assertPrefetchEquals( null, 6, 40 );
+ }
+
+ function testRevisionMissAtEnd() {
+ $fname = $this->setUpPrefetch();
+ $this->dump = new BaseDump( $fname );
+
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ $this->assertPrefetchEquals( null, 4, 40 );
+ }
+
+ function testSynchronizePageMissAtStart() {
+ $fname = $this->setUpPrefetch();
+ $this->dump = new BaseDump( $fname );
+
+ $this->assertPrefetchEquals( null, 0, 2 );
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ }
+
+ function testSynchronizeRevisionMissAtStart() {
+ $fname = $this->setUpPrefetch();
+ $this->dump = new BaseDump( $fname );
+
+ $this->assertPrefetchEquals( null, 1, -2 );
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ }
+
+ function testSequentialAcrossFiles() {
+ $fname1 = $this->setUpPrefetch( array( 1 ) );
+ $fname2 = $this->setUpPrefetch( array( 2, 4 ) );
+ $this->dump = new BaseDump( $fname1 . ";" . $fname2 );
+
+ $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 );
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 );
+ $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 );
+ }
+
+ function testSynchronizeSkipAcrossFile() {
+ $fname1 = $this->setUpPrefetch( array( 1 ) );
+ $fname2 = $this->setUpPrefetch( array( 2 ) );
+ $fname3 = $this->setUpPrefetch( array( 4 ) );
+ $this->dump = new BaseDump( $fname1 . ";" . $fname2 . ";" . $fname3 );
+
+ $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 );
+ $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 );
+ }
+
+ function testSynchronizeMissInWholeFirstFile() {
+ $fname1 = $this->setUpPrefetch( array( 1 ) );
+ $fname2 = $this->setUpPrefetch( array( 2 ) );
+ $this->dump = new BaseDump( $fname1 . ";" . $fname2 );
+
+ $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 );
+ }
+
+ /**
+ * Constructs a temporary file that can be used for prefetching
+ *
+ * The temporary file is removed by DumpBackup upon tearDown.
+ *
+ * @param array $requested_pages The indices of the page parts that should
+ * go into the prefetch file. 1,2,4 are available.
+ * @return string The file name of the created temporary file
+ */
+ private function setUpPrefetch( $requested_pages = array( 1, 2, 4 ) ) {
+ // The file name, where we store the prepared prefetch file
+ $fname = $this->getNewTempFile();
+
+ // The header of every prefetch file
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en">
+ <siteinfo>
+ <sitename>wikisvn</sitename>
+ <base>http://localhost/wiki-svn/index.php/Main_Page</base>
+ <generator>MediaWiki 1.21alpha</generator>
+ <case>first-letter</case>
+ <namespaces>
+ <namespace key="-2" case="first-letter">Media</namespace>
+ <namespace key="-1" case="first-letter">Special</namespace>
+ <namespace key="0" case="first-letter" />
+ <namespace key="1" case="first-letter">Talk</namespace>
+ <namespace key="2" case="first-letter">User</namespace>
+ <namespace key="3" case="first-letter">User talk</namespace>
+ <namespace key="4" case="first-letter">Wikisvn</namespace>
+ <namespace key="5" case="first-letter">Wikisvn talk</namespace>
+ <namespace key="6" case="first-letter">File</namespace>
+ <namespace key="7" case="first-letter">File talk</namespace>
+ <namespace key="8" case="first-letter">MediaWiki</namespace>
+ <namespace key="9" case="first-letter">MediaWiki talk</namespace>
+ <namespace key="10" case="first-letter">Template</namespace>
+ <namespace key="11" case="first-letter">Template talk</namespace>
+ <namespace key="12" case="first-letter">Help</namespace>
+ <namespace key="13" case="first-letter">Help talk</namespace>
+ <namespace key="14" case="first-letter">Category</namespace>
+ <namespace key="15" case="first-letter">Category talk</namespace>
+ </namespaces>
+ </siteinfo>
+';
+ // @codingStandardsIgnoreEnd
+
+ // An array holding the pages that are available for prefetch
+ $available_pages = array();
+
+ // Simple plain page
+ $available_pages[1] = ' <page>
+ <title>BackupDumperTestP1</title>
+ <ns>0</ns>
+ <id>1</id>
+ <revision>
+ <id>1</id>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>BackupDumperTestP1Summary1</comment>
+ <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
+ <text xml:space="preserve">BackupDumperTestP1Text1</text>
+ <model name="wikitext">1</model>
+ <format mime="text/x-wiki">1</format>
+ </revision>
+ </page>
+';
+ // Page with more than one revisions. Hole in rev ids.
+ $available_pages[2] = ' <page>
+ <title>BackupDumperTestP2</title>
+ <ns>0</ns>
+ <id>2</id>
+ <revision>
+ <id>2</id>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>BackupDumperTestP2Summary1</comment>
+ <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
+ <text xml:space="preserve">BackupDumperTestP2Text1</text>
+ <model name="wikitext">1</model>
+ <format mime="text/x-wiki">1</format>
+ </revision>
+ <revision>
+ <id>5</id>
+ <parentid>2</parentid>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>BackupDumperTestP2Summary4 extra</comment>
+ <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
+ <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text>
+ <model name="wikitext">1</model>
+ <format mime="text/x-wiki">1</format>
+ </revision>
+ </page>
+';
+ // Page with id higher than previous id + 1
+ $available_pages[4] = ' <page>
+ <title>Talk:BackupDumperTestP1</title>
+ <ns>1</ns>
+ <id>4</id>
+ <revision>
+ <id>8</id>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>Talk BackupDumperTestP1 Summary1</comment>
+ <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
+ <model name="wikitext">1</model>
+ <format mime="text/x-wiki">1</format>
+ <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text>
+ </revision>
+ </page>
+';
+
+ // The common ending for all files
+ $tail = '</mediawiki>
+';
+
+ // Putting together the content of the prefetch files
+ $content = $header;
+ foreach ( $requested_pages as $i ) {
+ $this->assertTrue( array_key_exists( $i, $available_pages ),
+ "Check for availability of requested page " . $i );
+ $content .= $available_pages[$i];
+ }
+ $content .= $tail;
+
+ $this->assertEquals( strlen( $content ), file_put_contents(
+ $fname, $content ), "Length of prepared prefetch" );
+
+ return $fname;
+ }
+}
diff --git a/tests/phpunit/maintenance/backupTextPassTest.php b/tests/phpunit/maintenance/backupTextPassTest.php
new file mode 100644
index 00000000..a37a97c7
--- /dev/null
+++ b/tests/phpunit/maintenance/backupTextPassTest.php
@@ -0,0 +1,584 @@
+<?php
+
+require_once __DIR__ . "/../../../maintenance/backupTextPass.inc";
+
+/**
+ * Tests for page dumps of BackupDumper
+ *
+ * @group Database
+ * @group Dump
+ * @covers TextPassDumper
+ */
+class TextPassDumperTest extends DumpTestCase {
+
+ // We'll add several pages, revision and texts. The following variables hold the
+ // corresponding ids.
+ private $pageId1, $pageId2, $pageId3, $pageId4;
+ private static $numOfPages = 4;
+ private $revId1_1, $textId1_1;
+ private $revId2_1, $textId2_1, $revId2_2, $textId2_2;
+ private $revId2_3, $textId2_3, $revId2_4, $textId2_4;
+ private $revId3_1, $textId3_1, $revId3_2, $textId3_2;
+ private $revId4_1, $textId4_1;
+ private static $numOfRevs = 8;
+
+ function addDBData() {
+ $this->tablesUsed[] = 'page';
+ $this->tablesUsed[] = 'revision';
+ $this->tablesUsed[] = 'text';
+
+ $ns = $this->getDefaultWikitextNS();
+
+ try {
+ // Simple page
+ $title = Title::newFromText( 'BackupDumperTestP1', $ns );
+ $page = WikiPage::factory( $title );
+ list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page,
+ "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" );
+ $this->pageId1 = $page->getId();
+
+ // Page with more than one revision
+ $title = Title::newFromText( 'BackupDumperTestP2', $ns );
+ $page = WikiPage::factory( $title );
+ list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page,
+ "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" );
+ list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page,
+ "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" );
+ list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page,
+ "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" );
+ list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page,
+ "BackupDumperTestP2Text4 some additional Text ",
+ "BackupDumperTestP2Summary4 extra " );
+ $this->pageId2 = $page->getId();
+
+ // Deleted page.
+ $title = Title::newFromText( 'BackupDumperTestP3', $ns );
+ $page = WikiPage::factory( $title );
+ list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page,
+ "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" );
+ list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page,
+ "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" );
+ $this->pageId3 = $page->getId();
+ $page->doDeleteArticle( "Testing ;)" );
+
+ // Page from non-default namespace
+
+ if ( $ns === NS_TALK ) {
+ // @todo work around this.
+ throw new MWException( "The default wikitext namespace is the talk namespace. "
+ . " We can't currently deal with that." );
+ }
+
+ $title = Title::newFromText( 'BackupDumperTestP1', NS_TALK );
+ $page = WikiPage::factory( $title );
+ list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page,
+ "Talk about BackupDumperTestP1 Text1",
+ "Talk BackupDumperTestP1 Summary1" );
+ $this->pageId4 = $page->getId();
+ } catch ( Exception $e ) {
+ // We'd love to pass $e directly. However, ... see
+ // documentation of exceptionFromAddDBData in
+ // DumpTestCase
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Since we will restrict dumping by page ranges (to allow
+ // working tests, even if the db gets prepopulated by a base
+ // class), we have to assert, that the page id are consecutively
+ // increasing
+ $this->assertEquals(
+ array( $this->pageId2, $this->pageId3, $this->pageId4 ),
+ array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ),
+ "Page ids increasing without holes" );
+ }
+
+ function testPlain() {
+ // Setting up the dump
+ $nameStub = $this->setUpStub();
+ $nameFull = $this->getNewTempFile();
+ $dumper = new TextPassDumper( array( "--stub=file:" . $nameStub,
+ "--output=file:" . $nameFull ) );
+ $dumper->reporting = false;
+ $dumper->setDb( $this->db );
+
+ // Performing the dump
+ $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
+
+ // Checking for correctness of the dumped data
+ $this->assertDumpStart( $nameFull );
+
+ // Page 1
+ $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
+ $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
+ "BackupDumperTestP1Text1" );
+ $this->assertPageEnd();
+
+ // Page 2
+ $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
+ $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+ $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
+ "BackupDumperTestP2Text1" );
+ $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+ $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+ "BackupDumperTestP2Text2", $this->revId2_1 );
+ $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+ $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
+ "BackupDumperTestP2Text3", $this->revId2_2 );
+ $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
+ $this->assertPageEnd();
+
+ // Page 3
+ // -> Page is marked deleted. Hence not visible
+
+ // Page 4
+ $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
+ $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
+ "Talk about BackupDumperTestP1 Text1" );
+ $this->assertPageEnd();
+
+ $this->assertDumpEnd();
+ }
+
+ function testPrefetchPlain() {
+ // The mapping between ids and text, for the hits of the prefetch mock
+ $prefetchMap = array(
+ array( $this->pageId1, $this->revId1_1, "Prefetch_________1Text1" ),
+ array( $this->pageId2, $this->revId2_3, "Prefetch_________2Text3" )
+ );
+
+ // The mock itself
+ $prefetchMock = $this->getMock( 'BaseDump', array( 'prefetch' ), array(), '', false );
+ $prefetchMock->expects( $this->exactly( 6 ) )
+ ->method( 'prefetch' )
+ ->will( $this->returnValueMap( $prefetchMap ) );
+
+ // Setting up of the dump
+ $nameStub = $this->setUpStub();
+ $nameFull = $this->getNewTempFile();
+ $dumper = new TextPassDumper( array( "--stub=file:"
+ . $nameStub, "--output=file:" . $nameFull ) );
+ $dumper->prefetch = $prefetchMock;
+ $dumper->reporting = false;
+ $dumper->setDb( $this->db );
+
+ // Performing the dump
+ $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
+
+ // Checking for correctness of the dumped data
+ $this->assertDumpStart( $nameFull );
+
+ // Page 1
+ $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" );
+ // Prefetch kicks in. This is still the SHA-1 of the original text,
+ // But the actual text (with different SHA-1) comes from prefetch.
+ $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
+ "Prefetch_________1Text1" );
+ $this->assertPageEnd();
+
+ // Page 2
+ $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
+ $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+ $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
+ "BackupDumperTestP2Text1" );
+ $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+ $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+ "BackupDumperTestP2Text2", $this->revId2_1 );
+ // Prefetch kicks in. This is still the SHA-1 of the original text,
+ // But the actual text (with different SHA-1) comes from prefetch.
+ $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+ $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
+ "Prefetch_________2Text3", $this->revId2_2 );
+ $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
+ $this->assertPageEnd();
+
+ // Page 3
+ // -> Page is marked deleted. Hence not visible
+
+ // Page 4
+ $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" );
+ $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
+ "Talk about BackupDumperTestP1 Text1" );
+ $this->assertPageEnd();
+
+ $this->assertDumpEnd();
+ }
+
+ /**
+ * Ensures that checkpoint dumps are used and written, by successively increasing the
+ * stub size and dumping until the duration crosses a threshold.
+ *
+ * @param string $checkpointFormat Either "file" for plain text or "gzip" for gzipped
+ * checkpoint files.
+ */
+ private function checkpointHelper( $checkpointFormat = "file" ) {
+ // Getting temporary names
+ $nameStub = $this->getNewTempFile();
+ $nameOutputDir = $this->getNewTempDirectory();
+
+ $stderr = fopen( 'php://output', 'a' );
+ if ( $stderr === false ) {
+ $this->fail( "Could not open stream for stderr" );
+ }
+
+ $iterations = 32; // We'll start with that many iterations of revisions in stub
+ $lastDuration = 0;
+ $minDuration = 2; // We want the dump to take at least this many seconds
+ $checkpointAfter = 0.5; // Generate checkpoint after this many seconds
+
+ // Until a dump takes at least $minDuration seconds, perform a dump and check
+ // duration. If the dump did not take long enough increase the iteration
+ // count, to generate a bigger stub file next time.
+ while ( $lastDuration < $minDuration ) {
+
+ // Setting up the dump
+ wfRecursiveRemoveDir( $nameOutputDir );
+ $this->assertTrue( wfMkdirParents( $nameOutputDir ),
+ "Creating temporary output directory " );
+ $this->setUpStub( $nameStub, $iterations );
+ $dumper = new TextPassDumper( array( "--stub=file:" . $nameStub,
+ "--output=" . $checkpointFormat . ":" . $nameOutputDir . "/full",
+ "--maxtime=1" /*This is in minutes. Fixup is below*/,
+ "--checkpointfile=checkpoint-%s-%s.xml.gz" ) );
+ $dumper->setDb( $this->db );
+ $dumper->maxTimeAllowed = $checkpointAfter; // Patching maxTime from 1 minute
+ $dumper->stderr = $stderr;
+
+ // The actual dump and taking time
+ $ts_before = microtime( true );
+ $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
+ $ts_after = microtime( true );
+ $lastDuration = $ts_after - $ts_before;
+
+ // Handling increasing the iteration count for the stubs
+ if ( $lastDuration < $minDuration ) {
+ $old_iterations = $iterations;
+ if ( $lastDuration > 0.2 ) {
+ // lastDuration is big enough, to allow an educated guess
+ $factor = ( $minDuration + 0.5 ) / $lastDuration;
+ if ( ( $factor > 1.1 ) && ( $factor < 100 ) ) {
+ // educated guess is reasonable
+ $iterations = (int)( $iterations * $factor );
+ }
+ }
+
+ if ( $old_iterations == $iterations ) {
+ // Heuristics were not applied, so we just *2.
+ $iterations *= 2;
+ }
+
+ $this->assertLessThan( 50000, $iterations,
+ "Emergency stop against infinitely increasing iteration "
+ . "count ( last duration: $lastDuration )" );
+ }
+ }
+
+ // The dump (hopefully) did take long enough to produce more than one
+ // checkpoint file.
+ //
+ // We now check all the checkpoint files for validity.
+
+ $files = scandir( $nameOutputDir );
+ $this->assertTrue( asort( $files ), "Sorting files in temporary directory" );
+ $fileOpened = false;
+ $lookingForPage = 1;
+ $checkpointFiles = 0;
+
+ // Each run of the following loop body tries to handle exactly 1 /page/ (not
+ // iteration of stub content). $i is only increased after having treated page 4.
+ for ( $i = 0; $i < $iterations; ) {
+
+ // 1. Assuring a file is opened and ready. Skipping across header if
+ // necessary.
+ if ( !$fileOpened ) {
+ $this->assertNotEmpty( $files, "No more existing dump files, "
+ . "but not yet all pages found" );
+ $fname = array_shift( $files );
+ while ( $fname == "." || $fname == ".." ) {
+ $this->assertNotEmpty( $files, "No more existing dump"
+ . " files, but not yet all pages found" );
+ $fname = array_shift( $files );
+ }
+ if ( $checkpointFormat == "gzip" ) {
+ $this->gunzip( $nameOutputDir . "/" . $fname );
+ }
+ $this->assertDumpStart( $nameOutputDir . "/" . $fname );
+ $fileOpened = true;
+ $checkpointFiles++;
+ }
+
+ // 2. Performing a single page check
+ switch ( $lookingForPage ) {
+ case 1:
+ // Page 1
+ $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN,
+ "BackupDumperTestP1" );
+ $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1",
+ $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
+ "BackupDumperTestP1Text1" );
+ $this->assertPageEnd();
+
+ $lookingForPage = 2;
+ break;
+
+ case 2:
+ // Page 2
+ $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN,
+ "BackupDumperTestP2" );
+ $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1",
+ $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
+ "BackupDumperTestP2Text1" );
+ $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2",
+ $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+ "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs );
+ $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3",
+ $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
+ "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs );
+ $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs,
+ "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ "BackupDumperTestP2Text4 some additional Text",
+ $this->revId2_3 + $i * self::$numOfRevs );
+ $this->assertPageEnd();
+
+ $lookingForPage = 4;
+ break;
+
+ case 4:
+ // Page 4
+ $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK,
+ "Talk:BackupDumperTestP1" );
+ $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs,
+ "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
+ "Talk about BackupDumperTestP1 Text1" );
+ $this->assertPageEnd();
+
+ $lookingForPage = 1;
+
+ // We dealt with the whole iteration.
+ $i++;
+ break;
+
+ default:
+ $this->fail( "Bad setting for lookingForPage ($lookingForPage)" );
+ }
+
+ // 3. Checking for the end of the current checkpoint file
+ if ( $this->xml->nodeType == XMLReader::END_ELEMENT
+ && $this->xml->name == "mediawiki"
+ ) {
+ $this->assertDumpEnd();
+ $fileOpened = false;
+ }
+ }
+
+ // Assuring we completely read all files ...
+ $this->assertFalse( $fileOpened, "Currently read file still open?" );
+ $this->assertEmpty( $files, "Remaining unchecked files" );
+
+ // ... and have dealt with more than one checkpoint file
+ $this->assertGreaterThan(
+ 1,
+ $checkpointFiles,
+ "expected more than 1 checkpoint to have been created. "
+ . "Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?"
+ );
+
+ $this->expectETAOutput();
+ }
+
+ /**
+ * @group large
+ */
+ function testCheckpointPlain() {
+ $this->checkpointHelper();
+ }
+
+ /**
+ * tests for working checkpoint generation in gzip format work.
+ *
+ * We keep this test in addition to the simpler self::testCheckpointPlain, as there
+ * were once problems when the used sinks were DumpPipeOutputs.
+ *
+ * xmldumps-backup typically uses bzip2 instead of gzip. However, as bzip2 requires
+ * PHP extensions, we go for gzip instead, which triggers the same relevant code
+ * paths while still being testable on more systems.
+ *
+ * @group large
+ */
+ function testCheckpointGzip() {
+ $this->checkHasGzip();
+ $this->checkpointHelper( "gzip" );
+ }
+
+ /**
+ * Creates a stub file that is used for testing the text pass of dumps
+ *
+ * @param string $fname (Optional) Absolute name of the file to write
+ * the stub into. If this parameter is null, a new temporary
+ * file is generated that is automatically removed upon tearDown.
+ * @param int $iterations (Optional) specifies how often the block
+ * of 3 pages should go into the stub file. The page and
+ * revision id increase further and further, while the text
+ * id of the first iteration is reused. The pages and revision
+ * of iteration > 1 have no corresponding representation in the database.
+ * @return string Absolute filename of the stub
+ */
+ private function setUpStub( $fname = null, $iterations = 1 ) {
+ if ( $fname === null ) {
+ $fname = $this->getNewTempFile();
+ }
+ $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" '
+ . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
+ . 'xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ '
+ . 'http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en">
+ <siteinfo>
+ <sitename>wikisvn</sitename>
+ <base>http://localhost/wiki-svn/index.php/Main_Page</base>
+ <generator>MediaWiki 1.21alpha</generator>
+ <case>first-letter</case>
+ <namespaces>
+ <namespace key="-2" case="first-letter">Media</namespace>
+ <namespace key="-1" case="first-letter">Special</namespace>
+ <namespace key="0" case="first-letter" />
+ <namespace key="1" case="first-letter">Talk</namespace>
+ <namespace key="2" case="first-letter">User</namespace>
+ <namespace key="3" case="first-letter">User talk</namespace>
+ <namespace key="4" case="first-letter">Wikisvn</namespace>
+ <namespace key="5" case="first-letter">Wikisvn talk</namespace>
+ <namespace key="6" case="first-letter">File</namespace>
+ <namespace key="7" case="first-letter">File talk</namespace>
+ <namespace key="8" case="first-letter">MediaWiki</namespace>
+ <namespace key="9" case="first-letter">MediaWiki talk</namespace>
+ <namespace key="10" case="first-letter">Template</namespace>
+ <namespace key="11" case="first-letter">Template talk</namespace>
+ <namespace key="12" case="first-letter">Help</namespace>
+ <namespace key="13" case="first-letter">Help talk</namespace>
+ <namespace key="14" case="first-letter">Category</namespace>
+ <namespace key="15" case="first-letter">Category talk</namespace>
+ </namespaces>
+ </siteinfo>
+';
+ $tail = '</mediawiki>
+';
+
+ $content = $header;
+ $iterations = intval( $iterations );
+ for ( $i = 0; $i < $iterations; $i++ ) {
+
+ $page1 = ' <page>
+ <title>BackupDumperTestP1</title>
+ <ns>0</ns>
+ <id>' . ( $this->pageId1 + $i * self::$numOfPages ) . '</id>
+ <revision>
+ <id>' . ( $this->revId1_1 + $i * self::$numOfRevs ) . '</id>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>BackupDumperTestP1Summary1</comment>
+ <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ <text id="' . $this->textId1_1 . '" bytes="23" />
+ </revision>
+ </page>
+';
+ $page2 = ' <page>
+ <title>BackupDumperTestP2</title>
+ <ns>0</ns>
+ <id>' . ( $this->pageId2 + $i * self::$numOfPages ) . '</id>
+ <revision>
+ <id>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</id>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>BackupDumperTestP2Summary1</comment>
+ <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ <text id="' . $this->textId2_1 . '" bytes="23" />
+ </revision>
+ <revision>
+ <id>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</id>
+ <parentid>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</parentid>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>BackupDumperTestP2Summary2</comment>
+ <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ <text id="' . $this->textId2_2 . '" bytes="23" />
+ </revision>
+ <revision>
+ <id>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</id>
+ <parentid>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</parentid>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>BackupDumperTestP2Summary3</comment>
+ <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ <text id="' . $this->textId2_3 . '" bytes="23" />
+ </revision>
+ <revision>
+ <id>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</id>
+ <parentid>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</parentid>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>BackupDumperTestP2Summary4 extra</comment>
+ <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ <text id="' . $this->textId2_4 . '" bytes="44" />
+ </revision>
+ </page>
+';
+ // page 3 not in stub
+
+ $page4 = ' <page>
+ <title>Talk:BackupDumperTestP1</title>
+ <ns>1</ns>
+ <id>' . ( $this->pageId4 + $i * self::$numOfPages ) . '</id>
+ <revision>
+ <id>' . ( $this->revId4_1 + $i * self::$numOfRevs ) . '</id>
+ <timestamp>2012-04-01T16:46:05Z</timestamp>
+ <contributor>
+ <ip>127.0.0.1</ip>
+ </contributor>
+ <comment>Talk BackupDumperTestP1 Summary1</comment>
+ <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ <text id="' . $this->textId4_1 . '" bytes="35" />
+ </revision>
+ </page>
+';
+ $content .= $page1 . $page2 . $page4;
+ }
+ $content .= $tail;
+ $this->assertEquals( strlen( $content ), file_put_contents(
+ $fname, $content ), "Length of prepared stub" );
+
+ return $fname;
+ }
+}
diff --git a/tests/phpunit/maintenance/backup_LogTest.php b/tests/phpunit/maintenance/backup_LogTest.php
new file mode 100644
index 00000000..7ca45960
--- /dev/null
+++ b/tests/phpunit/maintenance/backup_LogTest.php
@@ -0,0 +1,225 @@
+<?php
+/**
+ * Tests for log dumps of BackupDumper
+ *
+ * @group Database
+ * @group Dump
+ * @covers BackupDumper
+ */
+class BackupDumperLoggerTest extends DumpTestCase {
+
+ // We'll add several log entries and users for this test. The following
+ // variables hold the corresponding ids.
+ private $userId1, $userId2;
+ private $logId1, $logId2, $logId3;
+
+ /**
+ * adds a log entry to the database.
+ *
+ * @param string $type Type of the log entry
+ * @param string $subtype Subtype of the log entry
+ * @param User $user User that performs the logged operation
+ * @param int $ns Number of the namespace for the entry's target's title
+ * @param string $title Title of the entry's target
+ * @param string $comment Comment of the log entry
+ * @param array $parameters (optional) accompanying data that is attached to the entry
+ *
+ * @return int Id of the added log entry
+ */
+ private function addLogEntry( $type, $subtype, User $user, $ns, $title,
+ $comment = null, $parameters = null
+ ) {
+ $logEntry = new ManualLogEntry( $type, $subtype );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( Title::newFromText( $title, $ns ) );
+ if ( $comment !== null ) {
+ $logEntry->setComment( $comment );
+ }
+ if ( $parameters !== null ) {
+ $logEntry->setParameters( $parameters );
+ }
+
+ return $logEntry->insert();
+ }
+
+ function addDBData() {
+ $this->tablesUsed[] = 'logging';
+ $this->tablesUsed[] = 'user';
+
+ try {
+ $user1 = User::newFromName( 'BackupDumperLogUserA' );
+ $this->userId1 = $user1->getId();
+ if ( $this->userId1 === 0 ) {
+ $user1->addToDatabase();
+ $this->userId1 = $user1->getId();
+ }
+ $this->assertGreaterThan( 0, $this->userId1 );
+
+ $user2 = User::newFromName( 'BackupDumperLogUserB' );
+ $this->userId2 = $user2->getId();
+ if ( $this->userId2 === 0 ) {
+ $user2->addToDatabase();
+ $this->userId2 = $user2->getId();
+ }
+ $this->assertGreaterThan( 0, $this->userId2 );
+
+ $this->logId1 = $this->addLogEntry( 'type', 'subtype',
+ $user1, NS_MAIN, "PageA" );
+ $this->assertGreaterThan( 0, $this->logId1 );
+
+ $this->logId2 = $this->addLogEntry( 'supress', 'delete',
+ $user2, NS_TALK, "PageB", "SomeComment" );
+ $this->assertGreaterThan( 0, $this->logId2 );
+
+ $this->logId3 = $this->addLogEntry( 'move', 'delete',
+ $user2, NS_MAIN, "PageA", "SomeOtherComment",
+ array( 'key1' => 1, 3 => 'value3' ) );
+ $this->assertGreaterThan( 0, $this->logId3 );
+ } catch ( Exception $e ) {
+ // We'd love to pass $e directly. However, ... see
+ // documentation of exceptionFromAddDBData in
+ // DumpTestCase
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ /**
+ * asserts that the xml reader is at the beginning of a log entry and skips over
+ * it while analyzing it.
+ *
+ * @param int $id Id of the log entry
+ * @param string $user_name User name of the log entry's performer
+ * @param int $user_id User id of the log entry 's performer
+ * @param string|null $comment Comment of the log entry. If null, the comment text is ignored.
+ * @param string $type Type of the log entry
+ * @param string $subtype Subtype of the log entry
+ * @param string $title Title of the log entry's target
+ * @param array $parameters (optional) unserialized data accompanying the log entry
+ */
+ private function assertLogItem( $id, $user_name, $user_id, $comment, $type,
+ $subtype, $title, $parameters = array()
+ ) {
+
+ $this->assertNodeStart( "logitem" );
+ $this->skipWhitespace();
+
+ $this->assertTextNode( "id", $id );
+ $this->assertTextNode( "timestamp", false );
+
+ $this->assertNodeStart( "contributor" );
+ $this->skipWhitespace();
+ $this->assertTextNode( "username", $user_name );
+ $this->assertTextNode( "id", $user_id );
+ $this->assertNodeEnd( "contributor" );
+ $this->skipWhitespace();
+
+ if ( $comment !== null ) {
+ $this->assertTextNode( "comment", $comment );
+ }
+ $this->assertTextNode( "type", $type );
+ $this->assertTextNode( "action", $subtype );
+ $this->assertTextNode( "logtitle", $title );
+
+ $this->assertNodeStart( "params" );
+ $parameters_xml = unserialize( $this->xml->value );
+ $this->assertEquals( $parameters, $parameters_xml );
+ $this->assertTrue( $this->xml->read(), "Skipping past processed text of params" );
+ $this->assertNodeEnd( "params" );
+ $this->skipWhitespace();
+
+ $this->assertNodeEnd( "logitem" );
+ $this->skipWhitespace();
+ }
+
+ function testPlain() {
+ global $wgContLang;
+
+ // Preparing the dump
+ $fname = $this->getNewTempFile();
+ $dumper = new BackupDumper( array( "--output=file:" . $fname ) );
+ $dumper->startId = $this->logId1;
+ $dumper->endId = $this->logId3 + 1;
+ $dumper->reporting = false;
+ $dumper->setDb( $this->db );
+
+ // Performing the dump
+ $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT );
+
+ // Analyzing the dumped data
+ $this->assertDumpStart( $fname );
+
+ $this->assertLogItem( $this->logId1, "BackupDumperLogUserA",
+ $this->userId1, null, "type", "subtype", "PageA" );
+
+ $this->assertNotNull( $wgContLang, "Content language object validation" );
+ $namespace = $wgContLang->getNsText( NS_TALK );
+ $this->assertInternalType( 'string', $namespace );
+ $this->assertGreaterThan( 0, strlen( $namespace ) );
+ $this->assertLogItem( $this->logId2, "BackupDumperLogUserB",
+ $this->userId2, "SomeComment", "supress", "delete",
+ $namespace . ":PageB" );
+
+ $this->assertLogItem( $this->logId3, "BackupDumperLogUserB",
+ $this->userId2, "SomeOtherComment", "move", "delete",
+ "PageA", array( 'key1' => 1, 3 => 'value3' ) );
+
+ $this->assertDumpEnd();
+ }
+
+ function testXmlDumpsBackupUseCaseLogging() {
+ global $wgContLang;
+
+ $this->checkHasGzip();
+
+ // Preparing the dump
+ $fname = $this->getNewTempFile();
+ $dumper = new BackupDumper( array( "--output=gzip:" . $fname,
+ "--reporting=2" ) );
+ $dumper->startId = $this->logId1;
+ $dumper->endId = $this->logId3 + 1;
+ $dumper->setDb( $this->db );
+
+ // xmldumps-backup demands reporting, although this is currently not
+ // implemented in BackupDumper, when dumping logging data. We
+ // nevertheless capture the output of the dump process already now,
+ // to be able to alert (once dumping produces reports) that this test
+ // needs updates.
+ $dumper->stderr = fopen( 'php://output', 'a' );
+ if ( $dumper->stderr === false ) {
+ $this->fail( "Could not open stream for stderr" );
+ }
+
+ // Performing the dump
+ $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT );
+
+ $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" );
+
+ // Analyzing the dumped data
+ $this->gunzip( $fname );
+
+ $this->assertDumpStart( $fname );
+
+ $this->assertLogItem( $this->logId1, "BackupDumperLogUserA",
+ $this->userId1, null, "type", "subtype", "PageA" );
+
+ $this->assertNotNull( $wgContLang, "Content language object validation" );
+ $namespace = $wgContLang->getNsText( NS_TALK );
+ $this->assertInternalType( 'string', $namespace );
+ $this->assertGreaterThan( 0, strlen( $namespace ) );
+ $this->assertLogItem( $this->logId2, "BackupDumperLogUserB",
+ $this->userId2, "SomeComment", "supress", "delete",
+ $namespace . ":PageB" );
+
+ $this->assertLogItem( $this->logId3, "BackupDumperLogUserB",
+ $this->userId2, "SomeOtherComment", "move", "delete",
+ "PageA", array( 'key1' => 1, 3 => 'value3' ) );
+
+ $this->assertDumpEnd();
+
+ // Currently, no reporting is implemented. Alert via failure, once
+ // this changes.
+ // If reporting for log dumps has been implemented, please update
+ // the following statement to catch good output
+ $this->expectOutputString( '' );
+ }
+}
diff --git a/tests/phpunit/maintenance/backup_PageTest.php b/tests/phpunit/maintenance/backup_PageTest.php
new file mode 100644
index 00000000..0cb0cdb6
--- /dev/null
+++ b/tests/phpunit/maintenance/backup_PageTest.php
@@ -0,0 +1,428 @@
+<?php
+/**
+ * Tests for page dumps of BackupDumper
+ *
+ * @group Database
+ * @group Dump
+ * @covers BackupDumper
+ */
+class BackupDumperPageTest extends DumpTestCase {
+
+ // We'll add several pages, revision and texts. The following variables hold the
+ // corresponding ids.
+ private $pageId1, $pageId2, $pageId3, $pageId4, $pageId5;
+ private $pageTitle1, $pageTitle2, $pageTitle3, $pageTitle4, $pageTitle5;
+ private $revId1_1, $textId1_1;
+ private $revId2_1, $textId2_1, $revId2_2, $textId2_2;
+ private $revId2_3, $textId2_3, $revId2_4, $textId2_4;
+ private $revId3_1, $textId3_1, $revId3_2, $textId3_2;
+ private $revId4_1, $textId4_1;
+ private $namespace, $talk_namespace;
+
+ function addDBData() {
+ // be sure, titles created here using english namespace names
+ $this->setMwGlobals( array(
+ 'wgLanguageCode' => 'en',
+ 'wgContLang' => Language::factory( 'en' ),
+ ) );
+
+ $this->tablesUsed[] = 'page';
+ $this->tablesUsed[] = 'revision';
+ $this->tablesUsed[] = 'text';
+
+ try {
+ $this->namespace = $this->getDefaultWikitextNS();
+ $this->talk_namespace = NS_TALK;
+
+ if ( $this->namespace === $this->talk_namespace ) {
+ // @todo work around this.
+ throw new MWException( "The default wikitext namespace is the talk namespace. "
+ . " We can't currently deal with that." );
+ }
+
+ $this->pageTitle1 = Title::newFromText( 'BackupDumperTestP1', $this->namespace );
+ $page = WikiPage::factory( $this->pageTitle1 );
+ list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page,
+ "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" );
+ $this->pageId1 = $page->getId();
+
+ $this->pageTitle2 = Title::newFromText( 'BackupDumperTestP2', $this->namespace );
+ $page = WikiPage::factory( $this->pageTitle2 );
+ list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page,
+ "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" );
+ list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page,
+ "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" );
+ list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page,
+ "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" );
+ list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page,
+ "BackupDumperTestP2Text4 some additional Text ",
+ "BackupDumperTestP2Summary4 extra " );
+ $this->pageId2 = $page->getId();
+
+ $this->pageTitle3 = Title::newFromText( 'BackupDumperTestP3', $this->namespace );
+ $page = WikiPage::factory( $this->pageTitle3 );
+ list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page,
+ "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" );
+ list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page,
+ "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" );
+ $this->pageId3 = $page->getId();
+ $page->doDeleteArticle( "Testing ;)" );
+
+ $this->pageTitle4 = Title::newFromText( 'BackupDumperTestP1', $this->talk_namespace );
+ $page = WikiPage::factory( $this->pageTitle4 );
+ list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page,
+ "Talk about BackupDumperTestP1 Text1",
+ "Talk BackupDumperTestP1 Summary1" );
+ $this->pageId4 = $page->getId();
+ } catch ( Exception $e ) {
+ // We'd love to pass $e directly. However, ... see
+ // documentation of exceptionFromAddDBData in
+ // DumpTestCase
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Since we will restrict dumping by page ranges (to allow
+ // working tests, even if the db gets prepopulated by a base
+ // class), we have to assert, that the page id are consecutively
+ // increasing
+ $this->assertEquals(
+ array( $this->pageId2, $this->pageId3, $this->pageId4 ),
+ array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ),
+ "Page ids increasing without holes" );
+ }
+
+ function testFullTextPlain() {
+ // Preparing the dump
+ $fname = $this->getNewTempFile();
+ $dumper = new BackupDumper( array( "--output=file:" . $fname ) );
+ $dumper->startId = $this->pageId1;
+ $dumper->endId = $this->pageId4 + 1;
+ $dumper->reporting = false;
+ $dumper->setDb( $this->db );
+
+ // Performing the dump
+ $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
+
+ // Checking the dumped data
+ $this->assertDumpStart( $fname );
+
+ // Page 1
+ $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
+ $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
+ "BackupDumperTestP1Text1" );
+ $this->assertPageEnd();
+
+ // Page 2
+ $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
+ $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+ $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2",
+ "BackupDumperTestP2Text1" );
+ $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+ $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
+ "BackupDumperTestP2Text2", $this->revId2_1 );
+ $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+ $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r",
+ "BackupDumperTestP2Text3", $this->revId2_2 );
+ $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv",
+ "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
+ $this->assertPageEnd();
+
+ // Page 3
+ // -> Page is marked deleted. Hence not visible
+
+ // Page 4
+ $this->assertPageStart(
+ $this->pageId4,
+ $this->talk_namespace,
+ $this->pageTitle4->getPrefixedText()
+ );
+ $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe",
+ "Talk about BackupDumperTestP1 Text1" );
+ $this->assertPageEnd();
+
+ $this->assertDumpEnd();
+ }
+
+ function testFullStubPlain() {
+ // Preparing the dump
+ $fname = $this->getNewTempFile();
+ $dumper = new BackupDumper( array( "--output=file:" . $fname ) );
+ $dumper->startId = $this->pageId1;
+ $dumper->endId = $this->pageId4 + 1;
+ $dumper->reporting = false;
+ $dumper->setDb( $this->db );
+
+ // Performing the dump
+ $dumper->dump( WikiExporter::FULL, WikiExporter::STUB );
+
+ // Checking the dumped data
+ $this->assertDumpStart( $fname );
+
+ // Page 1
+ $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
+ $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
+ $this->assertPageEnd();
+
+ // Page 2
+ $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
+ $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+ $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" );
+ $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+ $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 );
+ $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+ $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 );
+ $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
+ $this->assertPageEnd();
+
+ // Page 3
+ // -> Page is marked deleted. Hence not visible
+
+ // Page 4
+ $this->assertPageStart(
+ $this->pageId4,
+ $this->talk_namespace,
+ $this->pageTitle4->getPrefixedText()
+ );
+ $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
+ $this->assertPageEnd();
+
+ $this->assertDumpEnd();
+ }
+
+ function testCurrentStubPlain() {
+ // Preparing the dump
+ $fname = $this->getNewTempFile();
+ $dumper = new BackupDumper( array( "--output=file:" . $fname ) );
+ $dumper->startId = $this->pageId1;
+ $dumper->endId = $this->pageId4 + 1;
+ $dumper->reporting = false;
+ $dumper->setDb( $this->db );
+
+ // Performing the dump
+ $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB );
+
+ // Checking the dumped data
+ $this->assertDumpStart( $fname );
+
+ // Page 1
+ $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
+ $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
+ $this->assertPageEnd();
+
+ // Page 2
+ $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
+ $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
+ $this->assertPageEnd();
+
+ // Page 3
+ // -> Page is marked deleted. Hence not visible
+
+ // Page 4
+ $this->assertPageStart(
+ $this->pageId4,
+ $this->talk_namespace,
+ $this->pageTitle4->getPrefixedText()
+ );
+ $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
+ $this->assertPageEnd();
+
+ $this->assertDumpEnd();
+ }
+
+ function testCurrentStubGzip() {
+ $this->checkHasGzip();
+
+ // Preparing the dump
+ $fname = $this->getNewTempFile();
+ $dumper = new BackupDumper( array( "--output=gzip:" . $fname ) );
+ $dumper->startId = $this->pageId1;
+ $dumper->endId = $this->pageId4 + 1;
+ $dumper->reporting = false;
+ $dumper->setDb( $this->db );
+
+ // Performing the dump
+ $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB );
+
+ // Checking the dumped data
+ $this->gunzip( $fname );
+ $this->assertDumpStart( $fname );
+
+ // Page 1
+ $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
+ $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
+ $this->assertPageEnd();
+
+ // Page 2
+ $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
+ $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
+ $this->assertPageEnd();
+
+ // Page 3
+ // -> Page is marked deleted. Hence not visible
+
+ // Page 4
+ $this->assertPageStart(
+ $this->pageId4,
+ $this->talk_namespace,
+ $this->pageTitle4->getPrefixedText()
+ );
+ $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
+ $this->assertPageEnd();
+
+ $this->assertDumpEnd();
+ }
+
+ function testXmlDumpsBackupUseCase() {
+ // xmldumps-backup typically performs a single dump that that writes
+ // out three files
+ // * gzipped stubs of everything (meta-history)
+ // * gzipped stubs of latest revisions of all pages (meta-current)
+ // * gzipped stubs of latest revisions of all pages of namespage 0
+ // (articles)
+ //
+ // We reproduce such a setup with our mini fixture, although we omit
+ // chunks, and all the other gimmicks of xmldumps-backup.
+ //
+ $this->checkHasGzip();
+
+ $fnameMetaHistory = $this->getNewTempFile();
+ $fnameMetaCurrent = $this->getNewTempFile();
+ $fnameArticles = $this->getNewTempFile();
+
+ $dumper = new BackupDumper( array( "--output=gzip:" . $fnameMetaHistory,
+ "--output=gzip:" . $fnameMetaCurrent, "--filter=latest",
+ "--output=gzip:" . $fnameArticles, "--filter=latest",
+ "--filter=notalk", "--filter=namespace:!NS_USER",
+ "--reporting=1000" ) );
+ $dumper->startId = $this->pageId1;
+ $dumper->endId = $this->pageId4 + 1;
+ $dumper->setDb( $this->db );
+
+ // xmldumps-backup uses reporting. We will not check the exact reported
+ // message, as they are dependent on the processing power of the used
+ // computer. We only check that reporting does not crash the dumping
+ // and that something is reported
+ $dumper->stderr = fopen( 'php://output', 'a' );
+ if ( $dumper->stderr === false ) {
+ $this->fail( "Could not open stream for stderr" );
+ }
+
+ // Performing the dump
+ $dumper->dump( WikiExporter::FULL, WikiExporter::STUB );
+
+ $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" );
+
+ // Checking meta-history -------------------------------------------------
+
+ $this->gunzip( $fnameMetaHistory );
+ $this->assertDumpStart( $fnameMetaHistory );
+
+ // Page 1
+ $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
+ $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
+ $this->assertPageEnd();
+
+ // Page 2
+ $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
+ $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+ $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" );
+ $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+ $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 );
+ $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+ $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 );
+ $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
+ $this->assertPageEnd();
+
+ // Page 3
+ // -> Page is marked deleted. Hence not visible
+
+ // Page 4
+ $this->assertPageStart(
+ $this->pageId4,
+ $this->talk_namespace,
+ $this->pageTitle4->getPrefixedText()
+ );
+ $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
+ $this->assertPageEnd();
+
+ $this->assertDumpEnd();
+
+ // Checking meta-current -------------------------------------------------
+
+ $this->gunzip( $fnameMetaCurrent );
+ $this->assertDumpStart( $fnameMetaCurrent );
+
+ // Page 1
+ $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
+ $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
+ $this->assertPageEnd();
+
+ // Page 2
+ $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
+ $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
+ $this->assertPageEnd();
+
+ // Page 3
+ // -> Page is marked deleted. Hence not visible
+
+ // Page 4
+ $this->assertPageStart(
+ $this->pageId4,
+ $this->talk_namespace,
+ $this->pageTitle4->getPrefixedText()
+ );
+ $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1",
+ $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" );
+ $this->assertPageEnd();
+
+ $this->assertDumpEnd();
+
+ // Checking articles -------------------------------------------------
+
+ $this->gunzip( $fnameArticles );
+ $this->assertDumpStart( $fnameArticles );
+
+ // Page 1
+ $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() );
+ $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+ $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" );
+ $this->assertPageEnd();
+
+ // Page 2
+ $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() );
+ $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
+ $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 );
+ $this->assertPageEnd();
+
+ // Page 3
+ // -> Page is marked deleted. Hence not visible
+
+ // Page 4
+ // -> Page is not in $this->namespace. Hence not visible
+
+ $this->assertDumpEnd();
+
+ $this->expectETAOutput();
+ }
+}
diff --git a/tests/phpunit/maintenance/fetchTextTest.php b/tests/phpunit/maintenance/fetchTextTest.php
new file mode 100644
index 00000000..4e38418a
--- /dev/null
+++ b/tests/phpunit/maintenance/fetchTextTest.php
@@ -0,0 +1,261 @@
+<?php
+
+require_once __DIR__ . "/../../../maintenance/fetchText.php";
+
+/**
+ * Mock for the input/output of FetchText
+ *
+ * FetchText internally tries to access stdin and stdout. We mock those aspects
+ * for testing.
+ */
+class SemiMockedFetchText extends FetchText {
+
+ /**
+ * @var string|null Text to pass as stdin
+ */
+ private $mockStdinText = null;
+
+ /**
+ * @var bool Whether or not a text for stdin has been provided
+ */
+ private $mockSetUp = false;
+
+ /**
+ * @var array Invocation counters for the mocked aspects
+ */
+ private $mockInvocations = array( 'getStdin' => 0 );
+
+ /**
+ * Data for the fake stdin
+ *
+ * @param string $stdin The string to be used instead of stdin
+ */
+ function mockStdin( $stdin ) {
+ $this->mockStdinText = $stdin;
+ $this->mockSetUp = true;
+ }
+
+ /**
+ * Gets invocation counters for mocked methods.
+ *
+ * @return array An array, whose keys are function names. The corresponding values
+ * denote the number of times the function has been invoked.
+ */
+ function mockGetInvocations() {
+ return $this->mockInvocations;
+ }
+
+ // -----------------------------------------------------------------
+ // Mocked functions from FetchText follow.
+
+ function getStdin( $len = null ) {
+ $this->mockInvocations['getStdin']++;
+ if ( $len !== null ) {
+ throw new PHPUnit_Framework_ExpectationFailedException(
+ "Tried to get stdin with non null parameter" );
+ }
+
+ if ( !$this->mockSetUp ) {
+ throw new PHPUnit_Framework_ExpectationFailedException(
+ "Tried to get stdin before setting up rerouting" );
+ }
+
+ return fopen( 'data://text/plain,' . $this->mockStdinText, 'r' );
+ }
+}
+
+/**
+ * TestCase for FetchText
+ *
+ * @group Database
+ * @group Dump
+ * @covers FetchText
+ */
+class FetchTextTest extends MediaWikiTestCase {
+
+ // We add 5 Revisions for this test. Their corresponding text id's
+ // are stored in the following 5 variables.
+ private $textId1;
+ private $textId2;
+ private $textId3;
+ private $textId4;
+ private $textId5;
+
+ /**
+ * @var Exception|null As the current MediaWikiTestCase::run is not
+ * robust enough to recover from thrown exceptions directly, we cannot
+ * throw frow within addDBData, although it would be appropriate. Hence,
+ * we catch the exception and store it until we are in setUp and may
+ * finally rethrow the exception without crashing the test suite.
+ */
+ private $exceptionFromAddDBData;
+
+ /**
+ * @var FetchText The (mocked) FetchText that is to test
+ */
+ private $fetchText;
+
+ /**
+ * Adds a revision to a page, while returning the resuting text's id
+ *
+ * @param WikiPage $page The page to add the revision to
+ * @param string $text The revisions text
+ * @param string $summary The revisions summare
+ * @return int
+ * @throws MWException
+ */
+ private function addRevision( $page, $text, $summary ) {
+ $status = $page->doEditContent(
+ ContentHandler::makeContent( $text, $page->getTitle() ),
+ $summary
+ );
+
+ if ( $status->isGood() ) {
+ $value = $status->getValue();
+ $revision = $value['revision'];
+ $id = $revision->getTextId();
+
+ if ( $id > 0 ) {
+ return $id;
+ }
+ }
+
+ throw new MWException( "Could not determine text id" );
+ }
+
+ function addDBData() {
+ $this->tablesUsed[] = 'page';
+ $this->tablesUsed[] = 'revision';
+ $this->tablesUsed[] = 'text';
+
+ $wikitextNamespace = $this->getDefaultWikitextNS();
+
+ try {
+ $title = Title::newFromText( 'FetchTextTestPage1', $wikitextNamespace );
+ $page = WikiPage::factory( $title );
+ $this->textId1 = $this->addRevision(
+ $page,
+ "FetchTextTestPage1Text1",
+ "FetchTextTestPage1Summary1"
+ );
+
+ $title = Title::newFromText( 'FetchTextTestPage2', $wikitextNamespace );
+ $page = WikiPage::factory( $title );
+ $this->textId2 = $this->addRevision(
+ $page,
+ "FetchTextTestPage2Text1",
+ "FetchTextTestPage2Summary1"
+ );
+ $this->textId3 = $this->addRevision(
+ $page,
+ "FetchTextTestPage2Text2",
+ "FetchTextTestPage2Summary2"
+ );
+ $this->textId4 = $this->addRevision(
+ $page,
+ "FetchTextTestPage2Text3",
+ "FetchTextTestPage2Summary3"
+ );
+ $this->textId5 = $this->addRevision(
+ $page,
+ "FetchTextTestPage2Text4 some additional Text ",
+ "FetchTextTestPage2Summary4 extra "
+ );
+ } catch ( Exception $e ) {
+ // We'd love to pass $e directly. However, ... see
+ // documentation of exceptionFromAddDBData
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Check if any Exception is stored for rethrowing from addDBData
+ if ( $this->exceptionFromAddDBData !== null ) {
+ throw $this->exceptionFromAddDBData;
+ }
+
+ $this->fetchText = new SemiMockedFetchText();
+ }
+
+ /**
+ * Helper to relate FetchText's input and output
+ * @param string $input
+ * @param string $expectedOutput
+ */
+ private function assertFilter( $input, $expectedOutput ) {
+ $this->fetchText->mockStdin( $input );
+ $this->fetchText->execute();
+ $invocations = $this->fetchText->mockGetInvocations();
+ $this->assertEquals( 1, $invocations['getStdin'],
+ "getStdin invocation counter" );
+ $this->expectOutputString( $expectedOutput );
+ }
+
+ // Instead of the following functions, a data provider would be great.
+ // However, as data providers are evaluated /before/ addDBData, a data
+ // provider would not know the required ids.
+
+ function testExistingSimple() {
+ $this->assertFilter( $this->textId2,
+ $this->textId2 . "\n23\nFetchTextTestPage2Text1" );
+ }
+
+ function testExistingSimpleWithNewline() {
+ $this->assertFilter( $this->textId2 . "\n",
+ $this->textId2 . "\n23\nFetchTextTestPage2Text1" );
+ }
+
+ function testExistingSeveral() {
+ $this->assertFilter( "$this->textId1\n$this->textId5\n"
+ . "$this->textId3\n$this->textId3",
+ implode( "", array(
+ $this->textId1 . "\n23\nFetchTextTestPage1Text1",
+ $this->textId5 . "\n44\nFetchTextTestPage2Text4 "
+ . "some additional Text",
+ $this->textId3 . "\n23\nFetchTextTestPage2Text2",
+ $this->textId3 . "\n23\nFetchTextTestPage2Text2"
+ ) ) );
+ }
+
+ function testEmpty() {
+ $this->assertFilter( "", null );
+ }
+
+ function testNonExisting() {
+ $this->assertFilter( $this->textId5 + 10, ( $this->textId5 + 10 ) . "\n-1\n" );
+ }
+
+ function testNegativeInteger() {
+ $this->assertFilter( "-42", "-42\n-1\n" );
+ }
+
+ function testFloatingPointNumberExisting() {
+ // float -> int -> revision
+ $this->assertFilter( $this->textId3 + 0.14159,
+ $this->textId3 . "\n23\nFetchTextTestPage2Text2" );
+ }
+
+ function testFloatingPointNumberNonExisting() {
+ $this->assertFilter( $this->textId5 + 3.14159,
+ ( $this->textId5 + 3 ) . "\n-1\n" );
+ }
+
+ function testCharacters() {
+ $this->assertFilter( "abc", "0\n-1\n" );
+ }
+
+ function testMix() {
+ $this->assertFilter( "ab\n" . $this->textId4 . ".5cd\n\nefg\n" . $this->textId2
+ . "\n" . $this->textId3,
+ implode( "", array(
+ "0\n-1\n",
+ $this->textId4 . "\n23\nFetchTextTestPage2Text3",
+ "0\n-1\n",
+ "0\n-1\n",
+ $this->textId2 . "\n23\nFetchTextTestPage2Text1",
+ $this->textId3 . "\n23\nFetchTextTestPage2Text2"
+ ) ) );
+ }
+}
diff --git a/tests/phpunit/mocks/filebackend/MockFSFile.php b/tests/phpunit/mocks/filebackend/MockFSFile.php
new file mode 100644
index 00000000..e0463281
--- /dev/null
+++ b/tests/phpunit/mocks/filebackend/MockFSFile.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Mock of a filesystem file.
+ *
+ * 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
+ * @ingroup FileBackend
+ */
+
+/**
+ * Class representing an in memory fake file.
+ * This is intended for unit testing / developement when you do not want
+ * to hit the filesystem.
+ *
+ * It reimplements abstract methods with some hardcoded values. Might
+ * not be suitable for all tests but is good enough for the parser tests.
+ *
+ * @ingroup FileBackend
+ */
+class MockFSFile extends FSFile {
+ protected $sha1Base36 = null; // File Sha1Base36
+
+ public function exists() {
+ return true;
+ }
+
+ /**
+ * August 22 – The theft of the Mona Lisa is discovered in the Louvre."
+ * @bug 20281
+ */
+ public function getSize() {
+ return 1911;
+ }
+
+ public function getTimestamp() {
+ return wfTimestamp( TS_MW );
+ }
+
+ public function getMimeType() {
+ return 'text/mock';
+ }
+
+ public function getProps( $ext = true ) {
+ return array(
+ 'fileExists' => $this->exists(),
+ 'size' => $this->getSize(),
+ 'file-mime' => $this->getMimeType(),
+ 'sha1' => $this->getSha1Base36(),
+ );
+ }
+
+ public function getSha1Base36( $recache = false ) {
+ return '1234567890123456789012345678901';
+ }
+}
diff --git a/tests/phpunit/mocks/filebackend/MockFileBackend.php b/tests/phpunit/mocks/filebackend/MockFileBackend.php
new file mode 100644
index 00000000..de8590e3
--- /dev/null
+++ b/tests/phpunit/mocks/filebackend/MockFileBackend.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Simulation (mock) of a backend storage.
+ *
+ * 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
+ * @ingroup FileBackend
+ * @author Antoine Musso <hashar@free.fr>
+ */
+
+/**
+ * Class simulating a backend store.
+ *
+ * @ingroup FileBackend
+ * @since 1.22
+ */
+class MockFileBackend extends MemoryFileBackend {
+ protected function doGetLocalCopyMulti( array $params ) {
+ $tmpFiles = array(); // (path => MockFSFile)
+ foreach ( $params['srcs'] as $src ) {
+ $tmpFiles[$src] = new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) );
+ }
+ return $tmpFiles;
+ }
+}
diff --git a/tests/phpunit/mocks/media/MockBitmapHandler.php b/tests/phpunit/mocks/media/MockBitmapHandler.php
new file mode 100644
index 00000000..38cacf9f
--- /dev/null
+++ b/tests/phpunit/mocks/media/MockBitmapHandler.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Fake handler for Bitmap images.
+ *
+ * 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
+ * @ingroup Media
+ */
+
+class MockBitmapHandler extends BitmapHandler {
+ function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ return MockImageHandler::doFakeTransform( $this, $image, $dstPath, $dstUrl, $params, $flags );
+ }
+
+ function doClientImage( $image, $scalerParams ) {
+ return $this->getClientScalingThumbnailImage( $image, $scalerParams );
+ }
+}
diff --git a/tests/phpunit/mocks/media/MockDjVuHandler.php b/tests/phpunit/mocks/media/MockDjVuHandler.php
new file mode 100644
index 00000000..31cb13dc
--- /dev/null
+++ b/tests/phpunit/mocks/media/MockDjVuHandler.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Fake handler for DjVu images.
+ *
+ * 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
+ * @ingroup Media
+ */
+
+class MockDjVuHandler extends DjVuHandler {
+ function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ if ( !$this->normaliseParams( $image, $params ) ) {
+ return new TransformParameterError( $params );
+ }
+ $width = $params['width'];
+ $height = $params['height'];
+ $page = $params['page'];
+ if ( $page > $this->pageCount( $image ) ) {
+ return new MediaTransformError(
+ 'thumbnail_error',
+ $width,
+ $height,
+ wfMessage( 'djvu_page_error' )->text()
+ );
+ }
+
+ $params = array(
+ 'width' => $width,
+ 'height' => $height,
+ 'page' => $page
+ );
+
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
+ }
+}
diff --git a/tests/phpunit/mocks/media/MockImageHandler.php b/tests/phpunit/mocks/media/MockImageHandler.php
new file mode 100644
index 00000000..e0a72fd6
--- /dev/null
+++ b/tests/phpunit/mocks/media/MockImageHandler.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Fake handler for images.
+ *
+ * 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
+ * @ingroup Media
+ */
+
+/**
+ * Mock handler for images.
+ *
+ * This is really intended for unit testing.
+ *
+ * @ingroup Media
+ */
+class MockImageHandler {
+
+ /**
+ * Override BitmapHandler::doTransform() making sure we do not generate
+ * a thumbnail at all. That is merely returning a ThumbnailImage that
+ * will be consumed by the unit test. There is no need to create a real
+ * thumbnail on the filesystem.
+ * @param ImageHandler $that
+ * @param File $image
+ * @param string $dstPath
+ * @param string $dstUrl
+ * @param array $params
+ * @param int $flags
+ * @return ThumbnailImage
+ */
+ static function doFakeTransform( $that, $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ # Example of what we receive:
+ # $image: LocalFile
+ # $dstPath: /tmp/transform_7d0a7a2f1a09-1.jpg
+ # $dstUrl : http://example.com/images/thumb/0/09/Bad.jpg/320px-Bad.jpg
+ # $params: width: 320, descriptionUrl http://trunk.dev/wiki/File:Bad.jpg
+
+ $that->normaliseParams( $image, $params );
+
+ $scalerParams = array(
+ # The size to which the image will be resized
+ 'physicalWidth' => $params['physicalWidth'],
+ 'physicalHeight' => $params['physicalHeight'],
+ 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
+ # The size of the image on the page
+ 'clientWidth' => $params['width'],
+ 'clientHeight' => $params['height'],
+ # Comment as will be added to the EXIF of the thumbnail
+ 'comment' => isset( $params['descriptionUrl'] ) ?
+ "File source: {$params['descriptionUrl']}" : '',
+ # Properties of the original image
+ 'srcWidth' => $image->getWidth(),
+ 'srcHeight' => $image->getHeight(),
+ 'mimeType' => $image->getMimeType(),
+ 'dstPath' => $dstPath,
+ 'dstUrl' => $dstUrl,
+ );
+
+ # In some cases, we do not bother generating a thumbnail.
+ if ( !$image->mustRender() &&
+ $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
+ && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
+ ) {
+ wfDebug( __METHOD__ . ": returning unscaled image\n" );
+ // getClientScalingThumbnailImage is protected
+ return $that->doClientImage( $image, $scalerParams );
+ }
+
+ return new ThumbnailImage( $image, $dstUrl, false, $params );
+ }
+}
diff --git a/tests/phpunit/mocks/media/MockSvgHandler.php b/tests/phpunit/mocks/media/MockSvgHandler.php
new file mode 100644
index 00000000..21520c44
--- /dev/null
+++ b/tests/phpunit/mocks/media/MockSvgHandler.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Fake handler for SVG images.
+ *
+ * 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
+ * @ingroup Media
+ */
+
+class MockSvgHandler extends SvgHandler {
+ function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ return MockImageHandler::doFakeTransform( $this, $image, $dstPath, $dstUrl, $params, $flags );
+ }
+}
diff --git a/tests/phpunit/phpunit.php b/tests/phpunit/phpunit.php
new file mode 100644
index 00000000..11255043
--- /dev/null
+++ b/tests/phpunit/phpunit.php
@@ -0,0 +1,233 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Bootstrapping for MediaWiki PHPUnit tests
+ *
+ * @file
+ */
+
+// Set a flag which can be used to detect when other scripts have been entered
+// through this entry point or not.
+define( 'MW_PHPUNIT_TEST', true );
+
+// Start up MediaWiki in command-line mode
+require_once dirname( dirname( __DIR__ ) ) . "/maintenance/Maintenance.php";
+
+class PHPUnitMaintClass extends Maintenance {
+
+ public static $additionalOptions = array(
+ 'regex' => false,
+ 'file' => false,
+ 'use-filebackend' => false,
+ 'use-bagostuff' => false,
+ 'use-jobqueue' => false,
+ 'keep-uploads' => false,
+ 'use-normal-tables' => false,
+ 'reuse-db' => false,
+ 'wiki' => false,
+ );
+
+ public function __construct() {
+ parent::__construct();
+ $this->addOption(
+ 'with-phpunitdir',
+ 'Directory to include PHPUnit from, for example when using a git '
+ . 'fetchout from upstream. Path will be prepended to PHP `include_path`.',
+ false, # not required
+ true # need arg
+ );
+ $this->addOption(
+ 'debug-tests',
+ 'Log testing activity to the PHPUnitCommand log channel.',
+ false, # not required
+ false # no arg needed
+ );
+ $this->addOption( 'regex', 'Only run parser tests that match the given regex.', false, true );
+ $this->addOption( 'file', 'File describing parser tests.', false, true );
+ $this->addOption( 'use-filebackend', 'Use filebackend', false, true );
+ $this->addOption( 'use-bagostuff', 'Use bagostuff', false, true );
+ $this->addOption( 'use-jobqueue', 'Use jobqueue', false, true );
+ $this->addOption( 'keep-uploads', 'Re-use the same upload directory for each test, don\'t delete it.', false, false );
+ $this->addOption( 'use-normal-tables', 'Use normal DB tables.', false, false );
+ $this->addOption( 'reuse-db', 'Init DB only if tables are missing and keep after finish.', false, false );
+ }
+
+ public function finalSetup() {
+ parent::finalSetup();
+
+ global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType;
+ global $wgLanguageConverterCacheType, $wgUseDatabaseMessages;
+ global $wgLocaltimezone, $wgLocalisationCacheConf;
+ global $wgDevelopmentWarnings;
+
+ // Inject test autoloader
+ require_once __DIR__ . '/../TestsAutoLoader.php';
+
+ // wfWarn should cause tests to fail
+ $wgDevelopmentWarnings = true;
+
+ $wgMainCacheType = CACHE_NONE;
+ $wgMessageCacheType = CACHE_NONE;
+ $wgParserCacheType = CACHE_NONE;
+ $wgLanguageConverterCacheType = CACHE_NONE;
+
+ $wgUseDatabaseMessages = false; # Set for future resets
+
+ // Assume UTC for testing purposes
+ $wgLocaltimezone = 'UTC';
+
+ $wgLocalisationCacheConf['storeClass'] = 'LCStoreNull';
+
+ // Bug 44192 Do not attempt to send a real e-mail
+ Hooks::clear( 'AlternateUserMailer' );
+ Hooks::register(
+ 'AlternateUserMailer',
+ function () {
+ return false;
+ }
+ );
+ // xdebug's default of 100 is too low for MediaWiki
+ ini_set( 'xdebug.max_nesting_level', 1000 );
+ }
+
+ public function execute() {
+ global $IP;
+
+ $this->forceFormatServerArgv();
+
+ # Make sure we have --configuration or PHPUnit might complain
+ if ( !in_array( '--configuration', $_SERVER['argv'] ) ) {
+ //Hack to eliminate the need to use the Makefile (which sucks ATM)
+ array_splice( $_SERVER['argv'], 1, 0,
+ array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) );
+ }
+
+ # --with-phpunitdir let us override the default PHPUnit version
+ # Can use with either or phpunit.phar in the directory or the
+ # full PHPUnit code base.
+ if ( $this->hasOption( 'with-phpunitdir' ) ) {
+ $phpunitDir = $this->getOption( 'with-phpunitdir' );
+
+ # prepends provided PHPUnit directory or phar
+ $this->output( "Will attempt loading PHPUnit from `$phpunitDir`\n" );
+ set_include_path( $phpunitDir . PATH_SEPARATOR . get_include_path() );
+
+ # Cleanup $args array so the option and its value do not
+ # pollute PHPUnit
+ $key = array_search( '--with-phpunitdir', $_SERVER['argv'] );
+ unset( $_SERVER['argv'][$key] ); // the option
+ unset( $_SERVER['argv'][$key + 1] ); // its value
+ $_SERVER['argv'] = array_values( $_SERVER['argv'] );
+ }
+
+ if ( !wfIsWindows() ) {
+ # If we are not running on windows then we can enable phpunit colors
+ # Windows does not come anymore with ANSI.SYS loaded by default
+ # PHPUnit uses the suite.xml parameters to enable/disable colors
+ # which can be then forced to be enabled with --colors.
+ # The below code injects a parameter just like if the user called
+ # Probably fix bug 29226
+ $key = array_search( '--colors', $_SERVER['argv'] );
+ if ( $key === false ) {
+ array_splice( $_SERVER['argv'], 1, 0, '--colors' );
+ }
+ }
+
+ # Makes MediaWiki PHPUnit directory includable so the PHPUnit will
+ # be able to resolve relative files inclusion such as suites/*
+ # PHPUnit uses stream_resolve_include_path() internally
+ # See bug 32022
+ $key = array_search( '--include-path', $_SERVER['argv'] );
+ if ( $key === false ) {
+ array_splice( $_SERVER['argv'], 1, 0,
+ __DIR__
+ . PATH_SEPARATOR
+ . get_include_path()
+ );
+ array_splice( $_SERVER['argv'], 1, 0, '--include-path' );
+ }
+
+ $key = array_search( '--debug-tests', $_SERVER['argv'] );
+ if ( $key !== false && array_search( '--printer', $_SERVER['argv'] ) === false ) {
+ unset( $_SERVER['argv'][$key] );
+ array_splice( $_SERVER['argv'], 1, 0, 'MediaWikiPHPUnitTestListener' );
+ array_splice( $_SERVER['argv'], 1, 0, '--printer' );
+ }
+
+ foreach ( self::$additionalOptions as $option => $default ) {
+ $key = array_search( '--' . $option, $_SERVER['argv'] );
+ if ( $key !== false ) {
+ unset( $_SERVER['argv'][$key] );
+ if ( $this->mParams[$option]['withArg'] ) {
+ self::$additionalOptions[$option] = $_SERVER['argv'][$key + 1];
+ unset( $_SERVER['argv'][$key + 1] );
+ } else {
+ self::$additionalOptions[$option] = true;
+ }
+ }
+ }
+
+ }
+
+ public function getDbType() {
+ return Maintenance::DB_ADMIN;
+ }
+
+ /**
+ * Force the format of elements in $_SERVER['argv']
+ * - Split args such as "wiki=enwiki" into two separate arg elements "wiki" and "enwiki"
+ */
+ private function forceFormatServerArgv() {
+ $argv = array();
+ foreach ( $_SERVER['argv'] as $key => $arg ) {
+ if ( $key === 0 ) {
+ $argv[0] = $arg;
+ } elseif ( strstr( $arg, '=' ) ) {
+ foreach ( explode( '=', $arg, 2 ) as $argPart ) {
+ $argv[] = $argPart;
+ }
+ } else {
+ $argv[] = $arg;
+ }
+ }
+ $_SERVER['argv'] = $argv;
+ }
+
+}
+
+$maintClass = 'PHPUnitMaintClass';
+require RUN_MAINTENANCE_IF_MAIN;
+
+// Prevent segfault when we have lots of unit tests (bug 62623)
+if ( version_compare( PHP_VERSION, '5.4.0', '<' ) ) {
+ register_shutdown_function( function () {
+ gc_collect_cycles();
+ gc_disable();
+ } );
+}
+
+
+$ok = false;
+
+foreach ( array(
+ stream_resolve_include_path( 'phpunit.phar' ),
+ 'PHPUnit/Runner/Version.php',
+ 'PHPUnit/Autoload.php'
+) as $includePath ) {
+ @include_once $includePath;
+ if ( class_exists( 'PHPUnit_TextUI_Command' ) ) {
+ $ok = true;
+ break;
+ }
+}
+
+if ( !$ok ) {
+ die( "Couldn't find a usable PHPUnit.\n" );
+}
+
+$puVersion = PHPUnit_Runner_Version::id();
+if ( $puVersion !== '@package_version@' && version_compare( $puVersion, '3.7.0', '<' ) ) {
+ die( "PHPUnit 3.7.0 or later required; you have {$puVersion}.\n" );
+}
+
+PHPUnit_TextUI_Command::main();
diff --git a/tests/phpunit/run-tests.bat b/tests/phpunit/run-tests.bat
new file mode 100644
index 00000000..e6eb3e0c
--- /dev/null
+++ b/tests/phpunit/run-tests.bat
@@ -0,0 +1 @@
+php phpunit.php --configuration suite.xml %*
diff --git a/tests/phpunit/skins/SideBarTest.php b/tests/phpunit/skins/SideBarTest.php
new file mode 100644
index 00000000..a3122b94
--- /dev/null
+++ b/tests/phpunit/skins/SideBarTest.php
@@ -0,0 +1,219 @@
+<?php
+
+/**
+ * @group Skin
+ */
+class SideBarTest extends MediaWikiLangTestCase {
+
+ /**
+ * A skin template, reinitialized before each test
+ * @var SkinTemplate
+ */
+ private $skin;
+ /** Local cache for sidebar messages */
+ private $messages;
+
+ /** Build $this->messages array */
+ private function initMessagesHref() {
+ # List of default messages for the sidebar. The sidebar doesn't care at
+ # all whether they are full URLs, interwiki links or local titles.
+ $URL_messages = array(
+ 'mainpage',
+ 'portal-url',
+ 'currentevents-url',
+ 'recentchanges-url',
+ 'randompage-url',
+ 'helppage',
+ );
+
+ # We're assuming that isValidURI works as advertised: it's also
+ # tested separately, in tests/phpunit/includes/HttpTest.php.
+ foreach ( $URL_messages as $m ) {
+ $titleName = MessageCache::singleton()->get( $m );
+ if ( Http::isValidURI( $titleName ) ) {
+ $this->messages[$m]['href'] = $titleName;
+ } else {
+ $title = Title::newFromText( $titleName );
+ $this->messages[$m]['href'] = $title->getLocalURL();
+ }
+ }
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ $this->initMessagesHref();
+ $this->skin = new SkinTemplate();
+ $this->skin->getContext()->setLanguage( Language::factory( 'en' ) );
+ }
+
+ /**
+ * Internal helper to test the sidebar
+ * @param array $expected
+ * @param string $text
+ * @param string $message (Default: '')
+ * @todo this assert method to should be converted to a test using a dataprovider..
+ */
+ private function assertSideBar( $expected, $text, $message = '' ) {
+ $bar = array();
+ $this->skin->addToSidebarPlain( $bar, $text );
+ $this->assertEquals( $expected, $bar, $message );
+ }
+
+ /**
+ * @covers SkinTemplate::addToSidebarPlain
+ */
+ public function testSidebarWithOnlyTwoTitles() {
+ $this->assertSideBar(
+ array(
+ 'Title1' => array(),
+ 'Title2' => array(),
+ ),
+ '* Title1
+* Title2
+'
+ );
+ }
+
+ /**
+ * @covers SkinTemplate::addToSidebarPlain
+ */
+ public function testExpandMessages() {
+ $this->assertSidebar(
+ array( 'Title' => array(
+ array(
+ 'text' => 'Help',
+ 'href' => $this->messages['helppage']['href'],
+ 'id' => 'n-help',
+ 'active' => null
+ )
+ ) ),
+ '* Title
+** helppage|help
+'
+ );
+ }
+
+ /**
+ * @covers SkinTemplate::addToSidebarPlain
+ */
+ public function testExternalUrlsRequireADescription() {
+ $this->setMwGlobals( array(
+ 'wgNoFollowLinks' => true,
+ 'wgNoFollowDomainExceptions' => array(),
+ 'wgNoFollowNsExceptions' => array(),
+ ) );
+ $this->assertSidebar(
+ array( 'Title' => array(
+ # ** http://www.mediawiki.org/| Home
+ array(
+ 'text' => 'Home',
+ 'href' => 'http://www.mediawiki.org/',
+ 'id' => 'n-Home',
+ 'active' => null,
+ 'rel' => 'nofollow',
+ ),
+ # ** http://valid.no.desc.org/
+ # ... skipped since it is missing a pipe with a description
+ ) ),
+ '* Title
+** http://www.mediawiki.org/| Home
+** http://valid.no.desc.org/
+'
+ );
+ }
+
+ /**
+ * bug 33321 - Make sure there's a | after transforming.
+ * @group Database
+ * @covers SkinTemplate::addToSidebarPlain
+ */
+ public function testTrickyPipe() {
+ $this->assertSidebar(
+ array( 'Title' => array(
+ # The first 2 are skipped
+ # Doesn't really test the url properly
+ # because it will vary with $wgArticlePath et al.
+ # ** Baz|Fred
+ array(
+ 'text' => 'Fred',
+ 'href' => Title::newFromText( 'Baz' )->getLocalURL(),
+ 'id' => 'n-Fred',
+ 'active' => null,
+ ),
+ array(
+ 'text' => 'title-to-display',
+ 'href' => Title::newFromText( 'page-to-go-to' )->getLocalURL(),
+ 'id' => 'n-title-to-display',
+ 'active' => null,
+ ),
+ ) ),
+ '* Title
+** {{PAGENAME|Foo}}
+** Bar
+** Baz|Fred
+** {{PLURAL:1|page-to-go-to{{int:pipe-separator/en}}title-to-display|branch not taken}}
+'
+ );
+ }
+
+ #### Attributes for external links ##########################
+ private function getAttribs() {
+ # Sidebar text we will use everytime
+ $text = '* Title
+** http://www.mediawiki.org/| Home';
+
+ $bar = array();
+ $this->skin->addToSideBarPlain( $bar, $text );
+
+ return $bar['Title'][0];
+ }
+
+ /**
+ * Simple test to verify our helper assertAttribs() is functional
+ */
+ public function testTestAttributesAssertionHelper() {
+ $this->setMwGlobals( array(
+ 'wgNoFollowLinks' => true,
+ 'wgNoFollowDomainExceptions' => array(),
+ 'wgNoFollowNsExceptions' => array(),
+ 'wgExternalLinkTarget' => false,
+ ) );
+ $attribs = $this->getAttribs();
+
+ $this->assertArrayHasKey( 'rel', $attribs );
+ $this->assertEquals( 'nofollow', $attribs['rel'] );
+
+ $this->assertArrayNotHasKey( 'target', $attribs );
+ }
+
+ /**
+ * Test $wgNoFollowLinks in sidebar
+ */
+ public function testRespectWgnofollowlinks() {
+ $this->setMwGlobals( 'wgNoFollowLinks', false );
+
+ $attribs = $this->getAttribs();
+ $this->assertArrayNotHasKey( 'rel', $attribs,
+ 'External URL in sidebar do not have rel=nofollow when $wgNoFollowLinks = false'
+ );
+ }
+
+ /**
+ * Test $wgExternaLinkTarget in sidebar
+ * @dataProvider dataRespectExternallinktarget
+ */
+ public function testRespectExternallinktarget( $externalLinkTarget ) {
+ $this->setMwGlobals( 'wgExternalLinkTarget', $externalLinkTarget );
+
+ $attribs = $this->getAttribs();
+ $this->assertArrayHasKey( 'target', $attribs );
+ $this->assertEquals( $attribs['target'], $externalLinkTarget );
+ }
+
+ public static function dataRespectExternallinktarget() {
+ return array(
+ array( '_blank' ),
+ array( '_self' ),
+ );
+ }
+}
diff --git a/tests/phpunit/structure/AutoLoaderTest.php b/tests/phpunit/structure/AutoLoaderTest.php
new file mode 100644
index 00000000..2bdc9c9a
--- /dev/null
+++ b/tests/phpunit/structure/AutoLoaderTest.php
@@ -0,0 +1,135 @@
+<?php
+
+class AutoLoaderTest extends MediaWikiTestCase {
+ protected function setUp() {
+ global $wgAutoloadLocalClasses, $wgAutoloadClasses;
+
+ parent::setUp();
+
+ // Fancy dance to trigger a rebuild of AutoLoader::$autoloadLocalClassesLower
+ $this->testLocalClasses = array(
+ 'TestAutoloadedLocalClass' => __DIR__ . '/../data/autoloader/TestAutoloadedLocalClass.php',
+ 'TestAutoloadedCamlClass' => __DIR__ . '/../data/autoloader/TestAutoloadedCamlClass.php',
+ 'TestAutoloadedSerializedClass' =>
+ __DIR__ . '/../data/autoloader/TestAutoloadedSerializedClass.php',
+ );
+ $this->setMwGlobals(
+ 'wgAutoloadLocalClasses',
+ $this->testLocalClasses + $wgAutoloadLocalClasses
+ );
+ AutoLoader::resetAutoloadLocalClassesLower();
+
+ $this->testExtensionClasses = array(
+ 'TestAutoloadedClass' => __DIR__ . '/../data/autoloader/TestAutoloadedClass.php',
+ );
+ $this->setMwGlobals( 'wgAutoloadClasses', $this->testExtensionClasses + $wgAutoloadClasses );
+ }
+
+ /**
+ * Assert that there were no classes loaded that are not registered with the AutoLoader.
+ *
+ * For example foo.php having class Foo and class Bar but only registering Foo.
+ * This is important because we should not be relying on Foo being used before Bar.
+ */
+ public function testAutoLoadConfig() {
+ $results = self::checkAutoLoadConf();
+
+ $this->assertEquals(
+ $results['expected'],
+ $results['actual']
+ );
+ }
+
+ protected static function checkAutoLoadConf() {
+ global $wgAutoloadLocalClasses, $wgAutoloadClasses, $IP;
+
+ // wgAutoloadLocalClasses has precedence, just like in includes/AutoLoader.php
+ $expected = $wgAutoloadLocalClasses + $wgAutoloadClasses;
+ $actual = array();
+
+ $files = array_unique( $expected );
+
+ foreach ( $files as $file ) {
+ // Only prefix $IP if it doesn't have it already.
+ // Generally local classes don't have it, and those from extensions and test suites do.
+ if ( substr( $file, 0, 1 ) != '/' && substr( $file, 1, 1 ) != ':' ) {
+ $filePath = "$IP/$file";
+ } else {
+ $filePath = $file;
+ }
+
+ $contents = file_get_contents( $filePath );
+
+ // We could use token_get_all() here, but this is faster
+ $matches = array();
+ preg_match_all( '/
+ ^ [\t ]* (?:
+ (?:final\s+)? (?:abstract\s+)? (?:class|interface) \s+
+ (?P<class> [a-zA-Z0-9_]+)
+ |
+ class_alias \s* \( \s*
+ ([\'"]) (?P<original> [^\'"]+) \g{-2} \s* , \s*
+ ([\'"]) (?P<alias> [^\'"]+ ) \g{-2} \s*
+ \) \s* ;
+ )
+ /imx', $contents, $matches, PREG_SET_ORDER );
+
+ $namespaceMatch = array();
+ preg_match( '/
+ ^ [\t ]*
+ namespace \s+
+ ([a-zA-Z0-9_]+(\\\\[a-zA-Z0-9_]+)*)
+ \s* ;
+ /imx', $contents, $namespaceMatch );
+ $fileNamespace = $namespaceMatch ? $namespaceMatch[1] . '\\' : '';
+
+ $classesInFile = array();
+ $aliasesInFile = array();
+
+ foreach ( $matches as $match ) {
+ if ( !empty( $match['class'] ) ) {
+ $class = $fileNamespace . $match['class'];
+ $actual[$class] = $file;
+ $classesInFile[$class] = true;
+ } else {
+ $aliasesInFile[$match['alias']] = $match['original'];
+ }
+ }
+
+ // Only accept aliases for classes in the same file, because for correct
+ // behavior, all aliases for a class must be set up when the class is loaded
+ // (see <https://bugs.php.net/bug.php?id=61422>).
+ foreach ( $aliasesInFile as $alias => $class ) {
+ if ( isset( $classesInFile[$class] ) ) {
+ $actual[$alias] = $file;
+ } else {
+ $actual[$alias] = "[original class not in $file]";
+ }
+ }
+ }
+
+ return array(
+ 'expected' => $expected,
+ 'actual' => $actual,
+ );
+ }
+
+ function testCoreClass() {
+ $this->assertTrue( class_exists( 'TestAutoloadedLocalClass' ) );
+ }
+
+ function testExtensionClass() {
+ $this->assertTrue( class_exists( 'TestAutoloadedClass' ) );
+ }
+
+ function testWrongCaseClass() {
+ $this->assertTrue( class_exists( 'testautoLoadedcamlCLASS' ) );
+ }
+
+ function testWrongCaseSerializedClass() {
+ $dummyCereal = 'O:29:"testautoloadedserializedclass":0:{}';
+ $uncerealized = unserialize( $dummyCereal );
+ $this->assertFalse( $uncerealized instanceof __PHP_Incomplete_Class,
+ "unserialize() can load classes case-insensitively." );
+ }
+}
diff --git a/tests/phpunit/structure/ResourcesTest.php b/tests/phpunit/structure/ResourcesTest.php
new file mode 100644
index 00000000..2396ea29
--- /dev/null
+++ b/tests/phpunit/structure/ResourcesTest.php
@@ -0,0 +1,269 @@
+<?php
+/**
+ * Sanity checks for making sure registered resources are sane.
+ *
+ * @file
+ * @author Antoine Musso
+ * @author Niklas Laxström
+ * @author Santhosh Thottingal
+ * @author Timo Tijhof
+ * @copyright © 2012, Antoine Musso
+ * @copyright © 2012, Niklas Laxström
+ * @copyright © 2012, Santhosh Thottingal
+ * @copyright © 2012, Timo Tijhof
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class ResourcesTest extends MediaWikiTestCase {
+
+ /**
+ * @dataProvider provideResourceFiles
+ */
+ public function testFileExistence( $filename, $module, $resource ) {
+ $this->assertFileExists( $filename,
+ "File '$resource' referenced by '$module' must exist."
+ );
+ }
+
+ /**
+ * @dataProvider provideMediaStylesheets
+ */
+ public function testStyleMedia( $moduleName, $media, $filename, $css ) {
+ $cssText = CSSMin::minify( $css->cssText );
+
+ $this->assertTrue(
+ strpos( $cssText, '@media' ) === false,
+ 'Stylesheets should not both specify "media" and contain @media'
+ );
+ }
+
+ /**
+ * Verify that nothing explicitly depends on the 'jquery' and 'mediawiki' modules.
+ * They are always loaded, depending on them is unsupported and leads to unexpected behaviour.
+ */
+ public function testIllegalDependencies() {
+ $data = self::getAllModules();
+ $illegalDeps = array( 'jquery', 'mediawiki' );
+
+ /** @var ResourceLoaderModule $module */
+ foreach ( $data['modules'] as $moduleName => $module ) {
+ foreach ( $illegalDeps as $illegalDep ) {
+ $this->assertNotContains(
+ $illegalDep,
+ $module->getDependencies(),
+ "Module '$moduleName' must not depend on '$illegalDep'"
+ );
+ }
+ }
+ }
+
+ /**
+ * Verify that all modules specified as dependencies of other modules actually exist.
+ */
+ public function testMissingDependencies() {
+ $data = self::getAllModules();
+ $validDeps = array_keys( $data['modules'] );
+
+ /** @var ResourceLoaderModule $module */
+ foreach ( $data['modules'] as $moduleName => $module ) {
+ foreach ( $module->getDependencies() as $dep ) {
+ $this->assertContains(
+ $dep,
+ $validDeps,
+ "The module '$dep' required by '$moduleName' must exist"
+ );
+ }
+ }
+ }
+
+ /**
+ * Verify that all dependencies of all modules are always satisfiable with the 'targets' defined
+ * for the involved modules.
+ *
+ * Example: A depends on B. A has targets: mobile, desktop. B has targets: desktop. Therefore the
+ * dependency is sometimes unsatisfiable: it's impossible to load module A on mobile.
+ */
+ public function testUnsatisfiableDependencies() {
+ $data = self::getAllModules();
+ $validDeps = array_keys( $data['modules'] );
+
+ /** @var ResourceLoaderModule $module */
+ foreach ( $data['modules'] as $moduleName => $module ) {
+ $moduleTargets = $module->getTargets();
+ foreach ( $module->getDependencies() as $dep ) {
+ $targets = $data['modules'][$dep]->getTargets();
+ foreach ( $moduleTargets as $moduleTarget ) {
+ $this->assertContains(
+ $moduleTarget,
+ $targets,
+ "The module '$moduleName' must not have target '$moduleTarget' "
+ . "because its dependency '$dep' does not have it"
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Get all registered modules from ResouceLoader.
+ * @return array
+ */
+ protected static function getAllModules() {
+ global $wgEnableJavaScriptTest;
+
+ // Test existance of test suite files as well
+ // (can't use setUp or setMwGlobals because providers are static)
+ $org_wgEnableJavaScriptTest = $wgEnableJavaScriptTest;
+ $wgEnableJavaScriptTest = true;
+
+ // Initialize ResourceLoader
+ $rl = new ResourceLoader();
+
+ $modules = array();
+
+ foreach ( $rl->getModuleNames() as $moduleName ) {
+ $modules[$moduleName] = $rl->getModule( $moduleName );
+ }
+
+ // Restore settings
+ $wgEnableJavaScriptTest = $org_wgEnableJavaScriptTest;
+
+ return array(
+ 'modules' => $modules,
+ 'resourceloader' => $rl,
+ 'context' => new ResourceLoaderContext( $rl, new FauxRequest() )
+ );
+ }
+
+ /**
+ * Get all stylesheet files from modules that are an instance of
+ * ResourceLoaderFileModule (or one of its subclasses).
+ */
+ public static function provideMediaStylesheets() {
+ $data = self::getAllModules();
+ $cases = array();
+
+ foreach ( $data['modules'] as $moduleName => $module ) {
+ if ( !$module instanceof ResourceLoaderFileModule ) {
+ continue;
+ }
+
+ $reflectedModule = new ReflectionObject( $module );
+
+ $getStyleFiles = $reflectedModule->getMethod( 'getStyleFiles' );
+ $getStyleFiles->setAccessible( true );
+
+ $readStyleFile = $reflectedModule->getMethod( 'readStyleFile' );
+ $readStyleFile->setAccessible( true );
+
+ $styleFiles = $getStyleFiles->invoke( $module, $data['context'] );
+
+ $flip = $module->getFlip( $data['context'] );
+
+ foreach ( $styleFiles as $media => $files ) {
+ if ( $media && $media !== 'all' ) {
+ foreach ( $files as $file ) {
+ $cases[] = array(
+ $moduleName,
+ $media,
+ $file,
+ // XXX: Wrapped in an object to keep it out of PHPUnit output
+ (object)array( 'cssText' => $readStyleFile->invoke( $module, $file, $flip ) ),
+ );
+ }
+ }
+ }
+ }
+
+ return $cases;
+ }
+
+ /**
+ * Get all resource files from modules that are an instance of
+ * ResourceLoaderFileModule (or one of its subclasses).
+ *
+ * Since the raw data is stored in protected properties, we have to
+ * overrride this through ReflectionObject methods.
+ */
+ public static function provideResourceFiles() {
+ $data = self::getAllModules();
+ $cases = array();
+
+ // See also ResourceLoaderFileModule::__construct
+ $filePathProps = array(
+ // Lists of file paths
+ 'lists' => array(
+ 'scripts',
+ 'debugScripts',
+ 'loaderScripts',
+ 'styles',
+ ),
+
+ // Collated lists of file paths
+ 'nested-lists' => array(
+ 'languageScripts',
+ 'skinScripts',
+ 'skinStyles',
+ ),
+ );
+
+ foreach ( $data['modules'] as $moduleName => $module ) {
+ if ( !$module instanceof ResourceLoaderFileModule ) {
+ continue;
+ }
+
+ $reflectedModule = new ReflectionObject( $module );
+
+ $files = array();
+
+ foreach ( $filePathProps['lists'] as $propName ) {
+ $property = $reflectedModule->getProperty( $propName );
+ $property->setAccessible( true );
+ $list = $property->getValue( $module );
+ foreach ( $list as $key => $value ) {
+ // 'scripts' are numeral arrays.
+ // 'styles' can be numeral or associative.
+ // In case of associative the key is the file path
+ // and the value is the 'media' attribute.
+ if ( is_int( $key ) ) {
+ $files[] = $value;
+ } else {
+ $files[] = $key;
+ }
+ }
+ }
+
+ foreach ( $filePathProps['nested-lists'] as $propName ) {
+ $property = $reflectedModule->getProperty( $propName );
+ $property->setAccessible( true );
+ $lists = $property->getValue( $module );
+ foreach ( $lists as $list ) {
+ foreach ( $list as $key => $value ) {
+ // We need the same filter as for 'lists',
+ // due to 'skinStyles'.
+ if ( is_int( $key ) ) {
+ $files[] = $value;
+ } else {
+ $files[] = $key;
+ }
+ }
+ }
+ }
+
+ // Get method for resolving the paths to full paths
+ $method = $reflectedModule->getMethod( 'getLocalPath' );
+ $method->setAccessible( true );
+
+ // Populate cases
+ foreach ( $files as $file ) {
+ $cases[] = array(
+ $method->invoke( $module, $file ),
+ $moduleName,
+ ( $file instanceof ResourceLoaderFilePath ? $file->getPath() : $file ),
+ );
+ }
+ }
+
+ return $cases;
+ }
+}
diff --git a/tests/phpunit/structure/StructureTest.php b/tests/phpunit/structure/StructureTest.php
new file mode 100644
index 00000000..14461be6
--- /dev/null
+++ b/tests/phpunit/structure/StructureTest.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * The tests here verify the structure of the code. This is for outright bugs,
+ * not just style issues.
+ */
+
+class StructureTest extends MediaWikiTestCase {
+ /**
+ * Verify all files that appear to be tests have file names ending in
+ * Test. If the file names do not end in Test, they will not be run.
+ * @group medium
+ */
+ public function testUnitTestFileNamesEndWithTest() {
+ if ( wfIsWindows() ) {
+ $this->markTestSkipped( 'This test does not work on Windows' );
+ }
+ $rootPath = escapeshellarg( __DIR__ . '/..' );
+ $testClassRegex = implode( '|', array(
+ 'ApiFormatTestBase',
+ 'ApiTestCase',
+ 'ApiQueryTestBase',
+ 'ApiQueryContinueTestBase',
+ 'MediaWikiLangTestCase',
+ 'MediaWikiMediaTestCase',
+ 'MediaWikiTestCase',
+ 'ResourceLoaderTestCase',
+ 'PHPUnit_Framework_TestCase',
+ 'DumpTestCase',
+ ) );
+ $testClassRegex = "^class .* extends ($testClassRegex)";
+ $finder = "find $rootPath -name '*.php' '!' -name '*Test.php'" .
+ " | xargs grep -El '$testClassRegex|function suite\('";
+
+ $results = null;
+ $exitCode = null;
+ exec( $finder, $results, $exitCode );
+
+ $this->assertEquals(
+ 0,
+ $exitCode,
+ 'Verify find/grep command succeeds.'
+ );
+
+ $results = array_filter(
+ $results,
+ array( $this, 'filterSuites' )
+ );
+ $strip = strlen( $rootPath ) - 1;
+ foreach ( $results as $k => $v ) {
+ $results[$k] = substr( $v, $strip );
+ }
+ $this->assertEquals(
+ array(),
+ $results,
+ "Unit test file in $rootPath must end with Test."
+ );
+ }
+
+ /**
+ * Filter to remove testUnitTestFileNamesEndWithTest false positives.
+ * @param string $filename
+ * @return bool
+ */
+ public function filterSuites( $filename ) {
+ return strpos( $filename, __DIR__ . '/../suites/' ) !== 0;
+ }
+}
diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml
new file mode 100644
index 00000000..574c11e4
--- /dev/null
+++ b/tests/phpunit/suite.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Colors don't work on Windows!
+phpunit.php enables colors for other OSs at runtime
+-->
+<phpunit bootstrap="./bootstrap.php"
+ colors="false"
+ backupGlobals="false"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ forceCoversAnnotation="true"
+ stopOnFailure="false"
+ timeoutForSmallTests="10"
+ timeoutForMediumTests="30"
+ timeoutForLargeTests="60"
+ strict="true"
+ verbose="true">
+ <testsuites>
+ <testsuite name="includes">
+ <directory>includes</directory>
+ </testsuite>
+ <testsuite name="languages">
+ <directory>languages</directory>
+ </testsuite>
+ <testsuite name="skins">
+ <directory>skins</directory>
+ </testsuite>
+ <!-- As there is a class Maintenance, we cannot use the
+ name "maintenance" directly -->
+ <testsuite name="maintenance_suite">
+ <directory>maintenance</directory>
+ </testsuite>
+ <testsuite name="structure">
+ <directory>structure</directory>
+ </testsuite>
+ <testsuite name="uploadfromurl">
+ <file>suites/UploadFromUrlTestSuite.php</file>
+ </testsuite>
+ <testsuite name="extensions">
+ <file>suites/ExtensionsTestSuite.php</file>
+ <file>suites/ExtensionsParserTestSuite.php</file>
+ <file>suites/LessTestSuite.php</file>
+ </testsuite>
+ </testsuites>
+ <groups>
+ <exclude>
+ <group>Utility</group>
+ <group>Broken</group>
+ <group>ParserFuzz</group>
+ <group>Stub</group>
+ </exclude>
+ </groups>
+ <filter>
+ <whitelist addUncoveredFilesFromWhitelist="true">
+ <directory suffix=".php">../../includes</directory>
+ <directory suffix=".php">../../languages</directory>
+ <directory suffix=".php">../../maintenance</directory>
+ <directory suffix=".php">../../resources</directory>
+ <directory suffix=".php">../../skins</directory>
+ </whitelist>
+ </filter>
+</phpunit>
diff --git a/tests/phpunit/suites/ExtensionsParserTestSuite.php b/tests/phpunit/suites/ExtensionsParserTestSuite.php
new file mode 100644
index 00000000..3d68b241
--- /dev/null
+++ b/tests/phpunit/suites/ExtensionsParserTestSuite.php
@@ -0,0 +1,8 @@
+<?php
+class ExtensionsParserTestSuite extends PHPUnit_Framework_TestSuite {
+
+ public static function suite() {
+ return MediaWikiParserTest::suite( MediaWikiParserTest::NO_CORE );
+ }
+
+}
diff --git a/tests/phpunit/suites/ExtensionsTestSuite.php b/tests/phpunit/suites/ExtensionsTestSuite.php
new file mode 100644
index 00000000..116065f8
--- /dev/null
+++ b/tests/phpunit/suites/ExtensionsTestSuite.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * This test suite runs unit tests registered by extensions.
+ * See https://www.mediawiki.org/wiki/Manual:Hooks/UnitTestsList for details of
+ * how to register your tests.
+ */
+
+class ExtensionsTestSuite extends PHPUnit_Framework_TestSuite {
+ public function __construct() {
+ parent::__construct();
+ $paths = array();
+ // Extensions can return a list of files or directories
+ wfRunHooks( 'UnitTestsList', array( &$paths ) );
+ foreach ( $paths as $path ) {
+ if ( is_dir( $path ) ) {
+ // If the path is a directory, search for test cases.
+ // @since 1.24
+ $suffixes = array(
+ 'Test.php',
+ );
+ $fileIterator = new File_Iterator_Facade();
+ $matchingFiles = $fileIterator->getFilesAsArray( $path, $suffixes );
+ $this->addTestFiles( $matchingFiles );
+ } else {
+ // Add a single test case or suite class
+ $this->addTestFile( $path );
+ }
+ }
+ if ( !count( $paths ) ) {
+ $this->addTest( new DummyExtensionsTest( 'testNothing' ) );
+ }
+ }
+
+ public static function suite() {
+ return new self;
+ }
+}
+
+/**
+ * Needed to avoid warnings like 'No tests found in class "ExtensionsTestSuite".'
+ * when no extensions with tests are used.
+ */
+class DummyExtensionsTest extends MediaWikiTestCase {
+ public function testNothing() {
+ $this->assertTrue( true );
+ }
+}
diff --git a/tests/phpunit/suites/LessTestSuite.php b/tests/phpunit/suites/LessTestSuite.php
new file mode 100644
index 00000000..26a784ad
--- /dev/null
+++ b/tests/phpunit/suites/LessTestSuite.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @author Sam Smith <samsmith@wikimedia.org>
+ */
+class LessTestSuite extends PHPUnit_Framework_TestSuite {
+ public function __construct() {
+ parent::__construct();
+
+ $resourceLoader = new ResourceLoader();
+
+ foreach ( $resourceLoader->getModuleNames() as $name ) {
+ $module = $resourceLoader->getModule( $name );
+ if ( !$module || !$module instanceof ResourceLoaderFileModule ) {
+ continue;
+ }
+
+ foreach ( $module->getAllStyleFiles() as $styleFile ) {
+ // TODO (phuedx, 2014-03-19) The
+ // ResourceLoaderFileModule class shouldn't
+ // know how to get a file's extension.
+ if ( $module->getStyleSheetLang( $styleFile ) !== 'less' ) {
+ continue;
+ }
+
+ $this->addTest( new LessFileCompilationTest( $styleFile, $module ) );
+ }
+ }
+ }
+
+ public static function suite() {
+ return new static;
+ }
+}
diff --git a/tests/phpunit/suites/UploadFromUrlTestSuite.php b/tests/phpunit/suites/UploadFromUrlTestSuite.php
new file mode 100644
index 00000000..d4a7bd36
--- /dev/null
+++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php
@@ -0,0 +1,207 @@
+<?php
+
+require_once dirname( __DIR__ ) . '/includes/upload/UploadFromUrlTest.php';
+
+class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
+ public $savedGlobals = array();
+
+ public static function addTables( &$tables ) {
+ $tables[] = 'user_properties';
+ $tables[] = 'filearchive';
+ $tables[] = 'logging';
+ $tables[] = 'updatelog';
+ $tables[] = 'iwlinks';
+
+ return true;
+ }
+
+ protected function setUp() {
+ global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc, $wgUser,
+ $wgLang, $wgOut, $wgRequest, $wgStyleDirectory,
+ $wgEnableParserCache, $wgNamespaceAliases, $wgNamespaceProtection,
+ $parserMemc;
+
+ $tmpGlobals = array();
+
+ $tmpGlobals['wgScript'] = '/index.php';
+ $tmpGlobals['wgScriptPath'] = '/';
+ $tmpGlobals['wgArticlePath'] = '/wiki/$1';
+ $tmpGlobals['wgStylePath'] = '/skins';
+ $tmpGlobals['wgThumbnailScriptPath'] = false;
+ $tmpGlobals['wgLocalFileRepo'] = array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => new FSFileBackend( array(
+ 'name' => 'local-backend',
+ 'wikiId' => wfWikiId(),
+ 'containerPaths' => array(
+ 'local-public' => wfTempDir() . '/test-repo/public',
+ 'local-thumb' => wfTempDir() . '/test-repo/thumb',
+ 'local-temp' => wfTempDir() . '/test-repo/temp',
+ 'local-deleted' => wfTempDir() . '/test-repo/delete',
+ )
+ ) ),
+ );
+ foreach ( $tmpGlobals as $var => $val ) {
+ if ( array_key_exists( $var, $GLOBALS ) ) {
+ $this->savedGlobals[$var] = $GLOBALS[$var];
+ }
+ $GLOBALS[$var] = $val;
+ }
+
+ $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
+ $wgNamespaceAliases['Image'] = NS_FILE;
+ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
+
+ $wgEnableParserCache = false;
+ DeferredUpdates::clearPendingUpdates();
+ $wgMemc = wfGetMainCache();
+ $messageMemc = wfGetMessageCacheStorage();
+ $parserMemc = wfGetParserCacheStorage();
+
+ $wgUser = new User;
+ $context = new RequestContext;
+ $wgLang = $context->getLanguage();
+ $wgOut = $context->getOutput();
+ $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
+ $wgRequest = $context->getRequest();
+
+ if ( $wgStyleDirectory === false ) {
+ $wgStyleDirectory = "$IP/skins";
+ }
+
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+ }
+
+ protected function tearDown() {
+ foreach ( $this->savedGlobals as $var => $val ) {
+ $GLOBALS[$var] = $val;
+ }
+ // Restore backends
+ RepoGroup::destroySingleton();
+ FileBackendGroup::destroySingleton();
+
+ $this->teardownUploadDir( $this->uploadDir );
+
+ parent::tearDown();
+ }
+
+ private $uploadDir;
+ private $keepUploads;
+
+ /**
+ * Remove the dummy uploads directory
+ * @param string $dir
+ */
+ 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",
+ )
+ );
+
+ 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",
+ )
+ );
+ }
+
+ /**
+ * Delete the specified files, if they exist.
+ *
+ * @param array $files 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 array $dirs Full paths to directories to delete.
+ */
+ private static function deleteDirs( $dirs ) {
+ foreach ( $dirs as $dir ) {
+ if ( is_dir( $dir ) ) {
+ rmdir( $dir );
+ }
+ }
+ }
+
+ /**
+ * Create a dummy uploads directory which will contain a couple
+ * of files in order to pass existence tests.
+ *
+ * @return string The directory
+ */
+ private 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', null, __METHOD__ );
+ copy( "$IP/tests/phpunit/data/upload/headbg.jpg", "$dir/3/3a/Foobar.jpg" );
+
+ wfMkdirParents( $dir . '/0/09', null, __METHOD__ );
+ copy( "$IP/tests/phpunit/data/upload/headbg.jpg", "$dir/0/09/Bad.jpg" );
+
+ return $dir;
+ }
+
+ public static function suite() {
+ // Hack to invoke the autoloader required to get phpunit to recognize
+ // the UploadFromUrlTest class
+ class_exists( 'UploadFromUrlTest' );
+ $suite = new UploadFromUrlTestSuite( 'UploadFromUrlTest' );
+
+ return $suite;
+ }
+}
diff --git a/tests/phpunit/tests/MediaWikiTestCaseTest.php b/tests/phpunit/tests/MediaWikiTestCaseTest.php
new file mode 100644
index 00000000..2846fde0
--- /dev/null
+++ b/tests/phpunit/tests/MediaWikiTestCaseTest.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @covers MediaWikiTestCase
+ * @author Adam Shorland
+ */
+class MediaWikiTestCaseTest extends MediaWikiTestCase {
+
+ const GLOBAL_KEY_EXISTING = 'MediaWikiTestCaseTestGLOBAL-Existing';
+ const GLOBAL_KEY_NONEXISTING = 'MediaWikiTestCaseTestGLOBAL-NONExisting';
+
+ public static function setUpBeforeClass() {
+ parent::setUpBeforeClass();
+ $GLOBALS[self::GLOBAL_KEY_EXISTING] = 'foo';
+ }
+
+ public static function tearDownAfterClass() {
+ parent::tearDownAfterClass();
+ unset( $GLOBALS[self::GLOBAL_KEY_EXISTING] );
+ }
+
+ /**
+ * @covers MediaWikiTestCase::setMwGlobals
+ * @covers MediaWikiTestCase::tearDown
+ */
+ public function testSetGlobalsAreRestoredOnTearDown() {
+ $this->setMwGlobals( self::GLOBAL_KEY_EXISTING, 'bar' );
+ $this->assertEquals(
+ 'bar',
+ $GLOBALS[self::GLOBAL_KEY_EXISTING],
+ 'Global failed to correctly set'
+ );
+
+ $this->tearDown();
+
+ $this->assertEquals(
+ 'foo',
+ $GLOBALS[self::GLOBAL_KEY_EXISTING],
+ 'Global failed to be restored on tearDown'
+ );
+ }
+
+ /**
+ * @covers MediaWikiTestCase::stashMwGlobals
+ * @covers MediaWikiTestCase::tearDown
+ */
+ public function testStashedGlobalsAreRestoredOnTearDown() {
+ $this->stashMwGlobals( self::GLOBAL_KEY_EXISTING );
+ $GLOBALS[self::GLOBAL_KEY_EXISTING] = 'bar';
+ $this->assertEquals(
+ 'bar',
+ $GLOBALS[self::GLOBAL_KEY_EXISTING],
+ 'Global failed to correctly set'
+ );
+
+ $this->tearDown();
+
+ $this->assertEquals(
+ 'foo',
+ $GLOBALS[self::GLOBAL_KEY_EXISTING],
+ 'Global failed to be restored on tearDown'
+ );
+ }
+
+ /**
+ * @covers MediaWikiTestCase::stashMwGlobals
+ */
+ public function testExceptionThrownWhenStashingNonExistentGlobals() {
+ $this->setExpectedException(
+ 'Exception',
+ 'Global with key ' . self::GLOBAL_KEY_NONEXISTING . ' doesn\'t exist and cant be stashed'
+ );
+
+ $this->stashMwGlobals( self::GLOBAL_KEY_NONEXISTING );
+ }
+
+}
diff --git a/tests/qunit/.htaccess b/tests/qunit/.htaccess
new file mode 100644
index 00000000..605d2f4c
--- /dev/null
+++ b/tests/qunit/.htaccess
@@ -0,0 +1 @@
+Allow from all
diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php
new file mode 100644
index 00000000..15c33f64
--- /dev/null
+++ b/tests/qunit/QUnitTestResources.php
@@ -0,0 +1,117 @@
+<?php
+
+/* Modules registered when $wgEnableJavaScriptTest is true */
+
+return array(
+
+ /* Utilities */
+
+ 'test.sinonjs' => array(
+ 'scripts' => array(
+ 'resources/lib/sinonjs/sinon-1.10.3.js',
+ // We want tests to work in IE, but can't include this as it
+ // will break the placeholders in Sinon because the hack it uses
+ // to hijack IE globals relies on running in the global scope
+ // and in ResourceLoader this won't be running in the global scope.
+ // Including it results (among other things) in sandboxed timers
+ // being broken due to Date inheritance being undefined.
+ // 'resources/lib/sinonjs/sinon-ie-1.10.3.js',
+ ),
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
+
+ 'test.mediawiki.qunit.testrunner' => array(
+ 'scripts' => array(
+ 'tests/qunit/data/testrunner.js',
+ ),
+ 'dependencies' => array(
+ // Test runner configures QUnit but can't have it as dependency,
+ // see SpecialJavaScriptTest::viewQUnit.
+ 'jquery.getAttrs',
+ 'mediawiki.page.ready',
+ 'mediawiki.page.startup',
+ 'test.sinonjs',
+ ),
+ 'position' => 'top',
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
+
+ /*
+ Test suites for MediaWiki core modules
+ These must have a dependency on test.mediawiki.qunit.testrunner!
+ */
+
+ 'test.mediawiki.qunit.suites' => array(
+ 'scripts' => array(
+ 'tests/qunit/suites/resources/startup.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.byteLength.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.client.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.color.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.hidpi.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.highlightText.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.localize.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.placeholder.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js',
+ 'tests/qunit/suites/resources/jquery/jquery.textSelection.test.js',
+ 'tests/qunit/data/mediawiki.jqueryMsg.data.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js',
+ 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js',
+ 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js',
+ 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js',
+ 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js',
+ 'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js',
+ ),
+ 'dependencies' => array(
+ 'jquery.accessKeyLabel',
+ 'jquery.autoEllipsis',
+ 'jquery.byteLength',
+ 'jquery.byteLimit',
+ 'jquery.client',
+ 'jquery.color',
+ 'jquery.colorUtil',
+ 'jquery.getAttrs',
+ 'jquery.hidpi',
+ 'jquery.highlightText',
+ 'jquery.localize',
+ 'jquery.makeCollapsible',
+ 'jquery.mwExtension',
+ 'jquery.placeholder',
+ 'jquery.tabIndex',
+ 'jquery.tablesorter',
+ 'jquery.textSelection',
+ 'mediawiki.api',
+ 'mediawiki.api.category',
+ 'mediawiki.api.parse',
+ 'mediawiki.api.watch',
+ 'mediawiki.jqueryMsg',
+ 'mediawiki.Title',
+ 'mediawiki.toc',
+ 'mediawiki.Uri',
+ 'mediawiki.user',
+ 'mediawiki.util',
+ 'mediawiki.special.recentchanges',
+ 'mediawiki.language',
+ 'mediawiki.cldr',
+ 'mediawiki.cookie',
+ 'test.mediawiki.qunit.testrunner',
+ ),
+ )
+);
diff --git a/tests/qunit/data/callMwLoaderTestCallback.js b/tests/qunit/data/callMwLoaderTestCallback.js
new file mode 100644
index 00000000..dd034115
--- /dev/null
+++ b/tests/qunit/data/callMwLoaderTestCallback.js
@@ -0,0 +1 @@
+mediaWiki.loader.testCallback();
diff --git a/tests/qunit/data/generateJqueryMsgData.php b/tests/qunit/data/generateJqueryMsgData.php
new file mode 100644
index 00000000..61ebbf8f
--- /dev/null
+++ b/tests/qunit/data/generateJqueryMsgData.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * This PHP script defines the spec that the mediawiki.jqueryMsg module should conform to.
+ *
+ * It does this by looking up the results of various kinds of string parsing, with various
+ * languages, in the current installation of MediaWiki. It then outputs a static specification,
+ * mapping expected inputs to outputs, which can be used fed into a unit test framework.
+ * (QUnit, Jasmine, anything, it just outputs an object with key/value pairs).
+ *
+ * This is similar to Michael Dale (mdale@mediawiki.org)'s parser tests, except that it doesn't
+ * look up the API results while doing the test, so the test run is much faster (at the cost
+ * of being out of date in rare circumstances. But mostly the parsing that we are doing in
+ * Javascript doesn't change much).
+ */
+
+/*
+ * @example QUnit
+ * <code>
+ QUnit.test( 'Output matches PHP parser', mw.libs.phpParserData.tests.length, function ( assert ) {
+ mw.messages.set( mw.libs.phpParserData.messages );
+ $.each( mw.libs.phpParserData.tests, function ( i, test ) {
+ QUnit.stop();
+ getMwLanguage( test.lang, function ( langClass ) {
+ var parser = new mw.jqueryMsg.parser( { language: langClass } );
+ assert.equal(
+ parser.parse( test.key, test.args ).html(),
+ test.result,
+ test.name
+ );
+ QUnit.start();
+ } );
+ } );
+ });
+ * </code>
+ *
+ * @example Jasmine
+ * <code>
+ describe( 'match output to output from PHP parser', function () {
+ mw.messages.set( mw.libs.phpParserData.messages );
+ $.each( mw.libs.phpParserData.tests, function ( i, test ) {
+ it( 'should parse ' + test.name, function () {
+ var langClass;
+ runs( function () {
+ getMwLanguage( test.lang, function ( gotIt ) {
+ langClass = gotIt;
+ });
+ });
+ waitsFor( function () {
+ return langClass !== undefined;
+ }, 'Language class should be loaded', 1000 );
+ runs( function () {
+ console.log( test.lang, 'running tests' );
+ var parser = new mw.jqueryMsg.parser( { language: langClass } );
+ expect(
+ parser.parse( test.key, test.args ).html()
+ ).toEqual( test.result );
+ } );
+ } );
+ } );
+ } );
+ * </code>
+ */
+
+require __DIR__ . '/../../../maintenance/Maintenance.php';
+
+class GenerateJqueryMsgData extends Maintenance {
+
+ public static $keyToTestArgs = array(
+ 'undelete_short' => array(
+ array( 0 ),
+ array( 1 ),
+ array( 2 ),
+ array( 5 ),
+ array( 21 ),
+ array( 101 )
+ ),
+ 'category-subcat-count' => array(
+ array( 0, 10 ),
+ array( 1, 1 ),
+ array( 1, 2 ),
+ array( 3, 30 )
+ )
+ );
+
+ public function __construct() {
+ parent::__construct();
+ $this->mDescription = 'Create a specification for message parsing ini JSON format';
+ // add any other options here
+ }
+
+ public function execute() {
+ list( $messages, $tests ) = $this->getMessagesAndTests();
+ $this->writeJavascriptFile( $messages, $tests, __DIR__ . '/mediawiki.jqueryMsg.data.js' );
+ }
+
+ private function getMessagesAndTests() {
+ $messages = array();
+ $tests = array();
+ foreach ( array( 'en', 'fr', 'ar', 'jp', 'zh' ) as $languageCode ) {
+ foreach ( self::$keyToTestArgs as $key => $testArgs ) {
+ foreach ( $testArgs as $args ) {
+ // Get the raw message, without any transformations.
+ $template = wfMessage( $key )->inLanguage( $languageCode )->plain();
+
+ // Get the magic-parsed version with args.
+ $result = wfMessage( $key, $args )->inLanguage( $languageCode )->text();
+
+ // Record the template, args, language, and expected result
+ // fake multiple languages by flattening them together.
+ $langKey = $languageCode . '_' . $key;
+ $messages[$langKey] = $template;
+ $tests[] = array(
+ 'name' => $languageCode . ' ' . $key . ' ' . join( ',', $args ),
+ 'key' => $langKey,
+ 'args' => $args,
+ 'result' => $result,
+ 'lang' => $languageCode
+ );
+ }
+ }
+ }
+ return array( $messages, $tests );
+ }
+
+ private function writeJavascriptFile( $messages, $tests, $dataSpecFile ) {
+ $phpParserData = array(
+ 'messages' => $messages,
+ 'tests' => $tests,
+ );
+
+ $output =
+ "// This file stores the output from the PHP parser for various messages, arguments,\n"
+ . "// languages, and parser modes. Intended for use by a unit test framework by looping\n"
+ . "// through the object and comparing its parser return value with the 'result' property.\n"
+ . '// Last generated with ' . basename( __FILE__ ) . ' at ' . gmdate( 'r' ) . "\n"
+ // This file will contain unquoted JSON strings as javascript native object literals,
+ // flip the quotemark convention for this file.
+ . "\n"
+ . 'mediaWiki.libs.phpParserData = ' . FormatJson::encode( $phpParserData, true ) . ";\n";
+
+ $fp = file_put_contents( $dataSpecFile, $output );
+ if ( $fp === false ) {
+ die( "Couldn't write to $dataSpecFile." );
+ }
+ }
+}
+
+$maintClass = "GenerateJqueryMsgData";
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/tests/qunit/data/load.mock.php b/tests/qunit/data/load.mock.php
new file mode 100644
index 00000000..ee729e60
--- /dev/null
+++ b/tests/qunit/data/load.mock.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Mock load.php with pre-defined test modules.
+ *
+ * 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
+ * @package MediaWiki
+ * @author Lupo
+ * @since 1.20
+ */
+header( 'Content-Type: text/javascript; charset=utf-8' );
+
+require_once __DIR__ . '/../../../includes/json/FormatJson.php';
+require_once __DIR__ . '/../../../includes/Xml.php';
+
+$moduleImplementations = array(
+ 'testUsesMissing' => "
+mw.loader.implement( 'testUsesMissing', function () {
+ QUnit.ok( false, 'Module usesMissing script should not run.' );
+ QUnit.start();
+}, {}, {});
+",
+
+ 'testUsesNestedMissing' => "
+mw.loader.implement( 'testUsesNestedMissing', function () {
+ QUnit.ok( false, 'Module testUsesNestedMissing script should not run.' );
+ QUnit.start();
+}, {}, {});
+",
+
+ 'testSkipped' =>"
+mw.loader.implement( 'testSkipped', function () {
+ QUnit.ok( false, 'Module testSkipped was supposed to be skipped.' );
+}, {}, {});
+",
+
+ 'testNotSkipped' =>"
+mw.loader.implement( 'testNotSkipped', function () {}, {}, {});
+",
+
+ 'testUsesSkippable' =>"
+mw.loader.implement( 'testUsesSkippable', function () {}, {}, {});
+",
+);
+
+$response = '';
+
+// Only support for non-encoded module names, full module names expected
+if ( isset( $_GET['modules'] ) ) {
+ $modules = explode( ',', $_GET['modules'] );
+ foreach ( $modules as $module ) {
+ if ( isset( $moduleImplementations[$module] ) ) {
+ $response .= $moduleImplementations[$module];
+ } else {
+ $response .= Xml::encodeJsCall( 'mw.loader.state', array( $module, 'missing' ), true );
+ }
+ }
+}
+
+echo $response;
diff --git a/tests/qunit/data/mediawiki.jqueryMsg.data.js b/tests/qunit/data/mediawiki.jqueryMsg.data.js
new file mode 100644
index 00000000..4ab5f146
--- /dev/null
+++ b/tests/qunit/data/mediawiki.jqueryMsg.data.js
@@ -0,0 +1,491 @@
+// This file stores the output from the PHP parser for various messages, arguments,
+// languages, and parser modes. Intended for use by a unit test framework by looping
+// through the object and comparing its parser return value with the 'result' property.
+// Last generated with generateJqueryMsgData.php at Thu, 30 Jan 2014 04:04:41 +0000
+
+mediaWiki.libs.phpParserData = {
+ "messages": {
+ "en_undelete_short": "Undelete {{PLURAL:$1|one edit|$1 edits}}",
+ "en_category-subcat-count": "{{PLURAL:$2|This category has only the following subcategory.|This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
+ "fr_undelete_short": "Restaurer $1 modification{{PLURAL:$1||s}}",
+ "fr_category-subcat-count": "Cette cat\u00e9gorie comprend {{PLURAL:$2|la sous-cat\u00e9gorie|$2 sous-cat\u00e9gories, dont {{PLURAL:$1|celle|les $1}}}} ci-dessous.",
+ "ar_undelete_short": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 {{PLURAL:$1||\u062a\u0639\u062f\u064a\u0644 \u0648\u0627\u062d\u062f|\u062a\u0639\u062f\u064a\u0644\u064a\u0646|$1 \u062a\u0639\u062f\u064a\u0644\u0627\u062a|$1 \u062a\u0639\u062f\u064a\u0644\u0627\u064b|$1 \u062a\u0639\u062f\u064a\u0644}}",
+ "ar_category-subcat-count": "{{PLURAL:$2|\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a \u0627\u0644\u062a\u0627\u0644\u064a|\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a {{PLURAL:$1||\u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a|\u062a\u0635\u0646\u064a\u0641\u064a\u0646 \u0641\u0631\u0639\u064a\u064a\u0646|$1 \u062a\u0635\u0646\u064a\u0641\u0627\u062a \u0641\u0631\u0639\u064a\u0629}}\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a $2.}}",
+ "jp_undelete_short": "Undelete {{PLURAL:$1|one edit|$1 edits}}",
+ "jp_category-subcat-count": "{{PLURAL:$2|This category has only the following subcategory.|This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
+ "zh_undelete_short": "\u8fd8\u539f{{PLURAL:$1|$1\u4e2a\u7f16\u8f91}}",
+ "zh_category-subcat-count": "{{PLURAL:$2|\u672c\u5206\u7c7b\u53ea\u6709\u4ee5\u4e0b\u5b50\u5206\u7c7b\u3002|\u672c\u5206\u7c7b\u6709\u4ee5\u4e0b$1\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u6709$2\u4e2a\u5b50\u5206\u7c7b\u3002}}"
+ },
+ "tests": [
+ {
+ "name": "en undelete_short 0",
+ "key": "en_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "Undelete 0 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 1",
+ "key": "en_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "Undelete one edit",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 2",
+ "key": "en_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "Undelete 2 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 5",
+ "key": "en_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "Undelete 5 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 21",
+ "key": "en_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "Undelete 21 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en undelete_short 101",
+ "key": "en_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "Undelete 101 edits",
+ "lang": "en"
+ },
+ {
+ "name": "en category-subcat-count 0,10",
+ "key": "en_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "This category has the following 0 subcategories, out of 10 total.",
+ "lang": "en"
+ },
+ {
+ "name": "en category-subcat-count 1,1",
+ "key": "en_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "This category has only the following subcategory.",
+ "lang": "en"
+ },
+ {
+ "name": "en category-subcat-count 1,2",
+ "key": "en_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "This category has the following subcategory, out of 2 total.",
+ "lang": "en"
+ },
+ {
+ "name": "en category-subcat-count 3,30",
+ "key": "en_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "This category has the following 3 subcategories, out of 30 total.",
+ "lang": "en"
+ },
+ {
+ "name": "fr undelete_short 0",
+ "key": "fr_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "Restaurer 0 modification",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 1",
+ "key": "fr_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "Restaurer 1 modification",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 2",
+ "key": "fr_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "Restaurer 2 modifications",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 5",
+ "key": "fr_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "Restaurer 5 modifications",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 21",
+ "key": "fr_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "Restaurer 21 modifications",
+ "lang": "fr"
+ },
+ {
+ "name": "fr undelete_short 101",
+ "key": "fr_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "Restaurer 101 modifications",
+ "lang": "fr"
+ },
+ {
+ "name": "fr category-subcat-count 0,10",
+ "key": "fr_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "Cette cat\u00e9gorie comprend 10 sous-cat\u00e9gories, dont celle ci-dessous.",
+ "lang": "fr"
+ },
+ {
+ "name": "fr category-subcat-count 1,1",
+ "key": "fr_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "Cette cat\u00e9gorie comprend la sous-cat\u00e9gorie ci-dessous.",
+ "lang": "fr"
+ },
+ {
+ "name": "fr category-subcat-count 1,2",
+ "key": "fr_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "Cette cat\u00e9gorie comprend 2 sous-cat\u00e9gories, dont celle ci-dessous.",
+ "lang": "fr"
+ },
+ {
+ "name": "fr category-subcat-count 3,30",
+ "key": "fr_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "Cette cat\u00e9gorie comprend 30 sous-cat\u00e9gories, dont les 3 ci-dessous.",
+ "lang": "fr"
+ },
+ {
+ "name": "ar undelete_short 0",
+ "key": "ar_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 ",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 1",
+ "key": "ar_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u062a\u0639\u062f\u064a\u0644 \u0648\u0627\u062d\u062f",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 2",
+ "key": "ar_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u062a\u0639\u062f\u064a\u0644\u064a\u0646",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 5",
+ "key": "ar_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 5 \u062a\u0639\u062f\u064a\u0644\u0627\u062a",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 21",
+ "key": "ar_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 21 \u062a\u0639\u062f\u064a\u0644\u0627\u064b",
+ "lang": "ar"
+ },
+ {
+ "name": "ar undelete_short 101",
+ "key": "ar_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "\u0627\u0633\u062a\u0631\u062c\u0627\u0639 101 \u062a\u0639\u062f\u064a\u0644",
+ "lang": "ar"
+ },
+ {
+ "name": "ar category-subcat-count 0,10",
+ "key": "ar_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a \u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 10.",
+ "lang": "ar"
+ },
+ {
+ "name": "ar category-subcat-count 1,1",
+ "key": "ar_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 1.",
+ "lang": "ar"
+ },
+ {
+ "name": "ar category-subcat-count 1,2",
+ "key": "ar_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u0627\u0644\u0641\u0631\u0639\u064a\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 2.",
+ "lang": "ar"
+ },
+ {
+ "name": "ar category-subcat-count 3,30",
+ "key": "ar_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "\u0647\u0630\u0627 \u0627\u0644\u062a\u0635\u0646\u064a\u0641 \u064a\u062d\u0648\u064a 3 \u062a\u0635\u0646\u064a\u0641\u0627\u062a \u0641\u0631\u0639\u064a\u0629\u060c \u0645\u0646 \u0625\u062c\u0645\u0627\u0644\u064a 30.",
+ "lang": "ar"
+ },
+ {
+ "name": "jp undelete_short 0",
+ "key": "jp_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "Undelete 0 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 1",
+ "key": "jp_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "Undelete one edit",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 2",
+ "key": "jp_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "Undelete 2 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 5",
+ "key": "jp_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "Undelete 5 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 21",
+ "key": "jp_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "Undelete 21 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp undelete_short 101",
+ "key": "jp_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "Undelete 101 edits",
+ "lang": "jp"
+ },
+ {
+ "name": "jp category-subcat-count 0,10",
+ "key": "jp_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "This category has the following 0 subcategories, out of 10 total.",
+ "lang": "jp"
+ },
+ {
+ "name": "jp category-subcat-count 1,1",
+ "key": "jp_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "This category has only the following subcategory.",
+ "lang": "jp"
+ },
+ {
+ "name": "jp category-subcat-count 1,2",
+ "key": "jp_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "This category has the following subcategory, out of 2 total.",
+ "lang": "jp"
+ },
+ {
+ "name": "jp category-subcat-count 3,30",
+ "key": "jp_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "This category has the following 3 subcategories, out of 30 total.",
+ "lang": "jp"
+ },
+ {
+ "name": "zh undelete_short 0",
+ "key": "zh_undelete_short",
+ "args": [
+ 0
+ ],
+ "result": "\u8fd8\u539f0\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 1",
+ "key": "zh_undelete_short",
+ "args": [
+ 1
+ ],
+ "result": "\u8fd8\u539f1\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 2",
+ "key": "zh_undelete_short",
+ "args": [
+ 2
+ ],
+ "result": "\u8fd8\u539f2\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 5",
+ "key": "zh_undelete_short",
+ "args": [
+ 5
+ ],
+ "result": "\u8fd8\u539f5\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 21",
+ "key": "zh_undelete_short",
+ "args": [
+ 21
+ ],
+ "result": "\u8fd8\u539f21\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh undelete_short 101",
+ "key": "zh_undelete_short",
+ "args": [
+ 101
+ ],
+ "result": "\u8fd8\u539f101\u4e2a\u7f16\u8f91",
+ "lang": "zh"
+ },
+ {
+ "name": "zh category-subcat-count 0,10",
+ "key": "zh_category-subcat-count",
+ "args": [
+ 0,
+ 10
+ ],
+ "result": "\u672c\u5206\u7c7b\u6709\u4ee5\u4e0b0\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u670910\u4e2a\u5b50\u5206\u7c7b\u3002",
+ "lang": "zh"
+ },
+ {
+ "name": "zh category-subcat-count 1,1",
+ "key": "zh_category-subcat-count",
+ "args": [
+ 1,
+ 1
+ ],
+ "result": "\u672c\u5206\u7c7b\u53ea\u6709\u4ee5\u4e0b\u5b50\u5206\u7c7b\u3002",
+ "lang": "zh"
+ },
+ {
+ "name": "zh category-subcat-count 1,2",
+ "key": "zh_category-subcat-count",
+ "args": [
+ 1,
+ 2
+ ],
+ "result": "\u672c\u5206\u7c7b\u6709\u4ee5\u4e0b1\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u67092\u4e2a\u5b50\u5206\u7c7b\u3002",
+ "lang": "zh"
+ },
+ {
+ "name": "zh category-subcat-count 3,30",
+ "key": "zh_category-subcat-count",
+ "args": [
+ 3,
+ 30
+ ],
+ "result": "\u672c\u5206\u7c7b\u6709\u4ee5\u4e0b3\u4e2a\u5b50\u5206\u7c7b\uff0c\u5171\u670930\u4e2a\u5b50\u5206\u7c7b\u3002",
+ "lang": "zh"
+ }
+ ]
+};
diff --git a/tests/qunit/data/qunitOkCall.js b/tests/qunit/data/qunitOkCall.js
new file mode 100644
index 00000000..3ed5514e
--- /dev/null
+++ b/tests/qunit/data/qunitOkCall.js
@@ -0,0 +1,2 @@
+QUnit.start();
+QUnit.assert.ok( true, 'Successfully loaded!' );
diff --git a/tests/qunit/data/styleTest.css.php b/tests/qunit/data/styleTest.css.php
new file mode 100644
index 00000000..0e845811
--- /dev/null
+++ b/tests/qunit/data/styleTest.css.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Dynamically create a simple stylesheet for unit tests in MediaWiki.
+ *
+ * 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
+ * @package MediaWiki
+ * @author Timo Tijhof
+ * @since 1.20
+ */
+header( 'Content-Type: text/css; charset=utf-8' );
+
+/**
+ * Allows characters in ranges [a-z], [A-Z] and [0-9],
+ * in addition to a dot ("."), dash ("-"), space (" ") and hash ("#").
+ * @since 1.20
+ *
+ * @param string $val
+ * @return string Value with any illegal characters removed.
+ */
+function cssfilter( $val ) {
+ return preg_replace( '/[^A-Za-z0-9\.\- #]/', '', $val );
+}
+
+// Do basic sanitization
+$params = array_map( 'cssfilter', $_GET );
+
+// Defaults
+$selector = isset( $params['selector'] ) ? $params['selector'] : '.mw-test-example';
+$property = isset( $params['prop'] ) ? $params['prop'] : 'float';
+$value = isset( $params['val'] ) ? $params['val'] : 'right';
+$wait = isset( $params['wait'] ) ? (int)$params['wait'] : 0; // seconds
+
+sleep( $wait );
+
+$css = "
+/**
+ * Generated " . gmdate( 'r' ) . ".
+ * Waited {$wait}s.
+ */
+
+$selector {
+ $property: $value;
+}
+";
+
+echo trim( $css ) . "\n";
diff --git a/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js
new file mode 100644
index 00000000..db312b21
--- /dev/null
+++ b/tests/qunit/data/testrunner.js
@@ -0,0 +1,547 @@
+/*global CompletenessTest, sinon */
+/*jshint evil: true */
+( function ( $, mw, QUnit ) {
+ 'use strict';
+
+ var mwTestIgnore, mwTester,
+ addons,
+ ELEMENT_NODE = 1,
+ TEXT_NODE = 3;
+
+ /**
+ * Add bogus to url to prevent IE crazy caching
+ *
+ * @param value {String} a relative path (eg. 'data/foo.js'
+ * or 'data/test.php?foo=bar').
+ * @return {String} Such as 'data/foo.js?131031765087663960'
+ */
+ QUnit.fixurl = function ( value ) {
+ return value + (/\?/.test( value ) ? '&' : '?')
+ + String( new Date().getTime() )
+ + String( parseInt( Math.random() * 100000, 10 ) );
+ };
+
+ /**
+ * Configuration
+ */
+
+ // When a test() indicates asynchronicity with stop(),
+ // allow 10 seconds to pass before killing the test(),
+ // and assuming failure.
+ QUnit.config.testTimeout = 10 * 1000;
+
+ // Add a checkbox to QUnit header to toggle MediaWiki ResourceLoader debug mode.
+ QUnit.config.urlConfig.push( {
+ id: 'debug',
+ label: 'Enable ResourceLoaderDebug',
+ tooltip: 'Enable debug mode in ResourceLoader'
+ } );
+
+ QUnit.config.requireExpects = true;
+
+ /**
+ * Load TestSwarm agent
+ */
+ // Only if the current url indicates that there is a TestSwarm instance watching us
+ // (TestSwarm appends swarmURL to the test suites url it loads in iframes).
+ // Otherwise this is just a simple view of Special:JavaScriptTest/qunit directly,
+ // no point in loading inject.js in that case. Also, make sure that this instance
+ // of MediaWiki has actually been configured with the required url to that inject.js
+ // script. By default it is false.
+ if ( QUnit.urlParams.swarmURL && mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) {
+ jQuery.getScript( QUnit.fixurl( mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) );
+ }
+
+ /**
+ * CompletenessTest
+ *
+ * Adds toggle checkbox to header
+ */
+ QUnit.config.urlConfig.push( {
+ id: 'completenesstest',
+ label: 'Run CompletenessTest',
+ tooltip: 'Run the completeness test'
+ } );
+
+ /**
+ * SinonJS
+ *
+ * Glue code for nicer integration with QUnit setup/teardown
+ * Inspired by http://sinonjs.org/releases/sinon-qunit-1.0.0.js
+ * Fixes:
+ * - Work properly with asynchronous QUnit by using module setup/teardown
+ * instead of synchronously wrapping QUnit.test.
+ */
+ sinon.assert.fail = function ( msg ) {
+ QUnit.assert.ok( false, msg );
+ };
+ sinon.assert.pass = function ( msg ) {
+ QUnit.assert.ok( true, msg );
+ };
+ sinon.config = {
+ injectIntoThis: true,
+ injectInto: null,
+ properties: ['spy', 'stub', 'mock', 'sandbox'],
+ // Don't fake timers by default
+ useFakeTimers: false,
+ useFakeServer: false
+ };
+ ( function () {
+ var orgModule = QUnit.module;
+
+ QUnit.module = function ( name, localEnv ) {
+ localEnv = localEnv || {};
+ orgModule( name, {
+ setup: function () {
+ var config = sinon.getConfig( sinon.config );
+ config.injectInto = this;
+ sinon.sandbox.create( config );
+
+ if ( localEnv.setup ) {
+ localEnv.setup.call( this );
+ }
+ },
+ teardown: function () {
+ this.sandbox.verifyAndRestore();
+
+ if ( localEnv.teardown ) {
+ localEnv.teardown.call( this );
+ }
+ }
+ } );
+ };
+ }() );
+
+ // Extend QUnit.module to provide a fixture element.
+ ( function () {
+ var orgModule = QUnit.module;
+
+ QUnit.module = function ( name, localEnv ) {
+ var fixture;
+ localEnv = localEnv || {};
+ orgModule( name, {
+ setup: function () {
+ fixture = document.createElement( 'div' );
+ fixture.id = 'qunit-fixture';
+ document.body.appendChild( fixture );
+
+ if ( localEnv.setup ) {
+ localEnv.setup.call( this );
+ }
+ },
+ teardown: function () {
+ if ( localEnv.teardown ) {
+ localEnv.teardown.call( this );
+ }
+
+ fixture.parentNode.removeChild( fixture );
+ }
+ } );
+ };
+ }() );
+
+ // Initiate when enabled
+ if ( QUnit.urlParams.completenesstest ) {
+
+ // Return true to ignore
+ mwTestIgnore = function ( val, tester ) {
+
+ // Don't record methods of the properties of constructors,
+ // to avoid getting into a loop (prototype.constructor.prototype..).
+ // Since we're therefor skipping any injection for
+ // "new mw.Foo()", manually set it to true here.
+ if ( val instanceof mw.Map ) {
+ tester.methodCallTracker.Map = true;
+ return true;
+ }
+ if ( val instanceof mw.Title ) {
+ tester.methodCallTracker.Title = true;
+ return true;
+ }
+
+ // Don't record methods of the properties of a jQuery object
+ if ( val instanceof $ ) {
+ return true;
+ }
+
+ // Don't iterate over the module registry (the 'script' references would
+ // be listed as untested methods otherwise)
+ if ( val === mw.loader.moduleRegistry ) {
+ return true;
+ }
+
+ return false;
+ };
+
+ mwTester = new CompletenessTest( mw, mwTestIgnore );
+ }
+
+ /**
+ * Test environment recommended for all QUnit test modules
+ *
+ * Whether to log environment changes to the console
+ */
+ QUnit.config.urlConfig.push( 'mwlogenv' );
+
+ /**
+ * Reset mw.config and others to a fresh copy of the live config for each test(),
+ * and restore it back to the live one afterwards.
+ * @param localEnv {Object} [optional]
+ * @example (see test suite at the bottom of this file)
+ * </code>
+ */
+ QUnit.newMwEnvironment = ( function () {
+ var warn, log, liveConfig, liveMessages;
+
+ liveConfig = mw.config.values;
+ liveMessages = mw.messages.values;
+
+ function suppressWarnings() {
+ warn = mw.log.warn;
+ mw.log.warn = $.noop;
+ }
+
+ function restoreWarnings() {
+ if ( warn !== undefined ) {
+ mw.log.warn = warn;
+ warn = undefined;
+ }
+ }
+
+ function freshConfigCopy( custom ) {
+ var copy;
+ // Tests should mock all factors that directly influence the tested code.
+ // For backwards compatibility though we set mw.config to a fresh copy of the live
+ // config. This way any modifications made to mw.config during the test will not
+ // affect other tests, nor the global scope outside the test runner.
+ // This is a shallow copy, since overriding an array or object value via "custom"
+ // should replace it. Setting a config property means you override it, not extend it.
+ // NOTE: It is important that we suppress warnings because extend() will also access
+ // deprecated properties and trigger deprecation warnings from mw.log#deprecate.
+ suppressWarnings();
+ copy = $.extend( {}, liveConfig, custom );
+ restoreWarnings();
+
+ return copy;
+ }
+
+ function freshMessagesCopy( custom ) {
+ return $.extend( /*deep=*/true, {}, liveMessages, custom );
+ }
+
+ log = QUnit.urlParams.mwlogenv ? mw.log : function () {};
+
+ return function ( localEnv ) {
+ localEnv = $.extend( {
+ // QUnit
+ setup: $.noop,
+ teardown: $.noop,
+ // MediaWiki
+ config: {},
+ messages: {}
+ }, localEnv );
+
+ return {
+ setup: function () {
+ log( 'MwEnvironment> SETUP for "' + QUnit.config.current.module
+ + ': ' + QUnit.config.current.testName + '"' );
+
+ // Greetings, mock environment!
+ mw.config.values = freshConfigCopy( localEnv.config );
+ mw.messages.values = freshMessagesCopy( localEnv.messages );
+ this.suppressWarnings = suppressWarnings;
+ this.restoreWarnings = restoreWarnings;
+
+ localEnv.setup.call( this );
+ },
+
+ teardown: function () {
+ log( 'MwEnvironment> TEARDOWN for "' + QUnit.config.current.module
+ + ': ' + QUnit.config.current.testName + '"' );
+
+ localEnv.teardown.call( this );
+
+ // Farewell, mock environment!
+ mw.config.values = liveConfig;
+ mw.messages.values = liveMessages;
+
+ // As a convenience feature, automatically restore warnings if they're
+ // still suppressed by the end of the test.
+ restoreWarnings();
+
+ // Check for incomplete animations/requests/etc and throw
+ // error if there are any.
+ if ( $.timers && $.timers.length !== 0 ) {
+ // Test may need to use fake timers, wait for animations or
+ // call $.fx.stop().
+ throw new Error( 'Unfinished animations: ' + $.timers.length );
+ }
+ if ( $.active !== undefined && $.active !== 0 ) {
+ // Test may need to use fake XHR, wait for requests or
+ // call abort().
+ throw new Error( 'Unfinished AJAX requests: ' + $.active );
+ }
+ }
+ };
+ };
+ }() );
+
+ // $.when stops as soon as one fails, which makes sense in most
+ // practical scenarios, but not in a unit test where we really do
+ // need to wait until all of them are finished.
+ QUnit.whenPromisesComplete = function () {
+ var altPromises = [];
+
+ $.each( arguments, function ( i, arg ) {
+ var alt = $.Deferred();
+ altPromises.push( alt );
+
+ // Whether this one fails or not, forwards it to
+ // the 'done' (resolve) callback of the alternative promise.
+ arg.always( alt.resolve );
+ } );
+
+ return $.when.apply( $, altPromises );
+ };
+
+ /**
+ * Recursively convert a node to a plain object representing its structure.
+ * Only considers attributes and contents (elements and text nodes).
+ * Attribute values are compared strictly and not normalised.
+ *
+ * @param {Node} node
+ * @return {Object|string} Plain JavaScript value representing the node.
+ */
+ function getDomStructure( node ) {
+ var $node, children, processedChildren, i, len, el;
+ $node = $( node );
+ if ( node.nodeType === ELEMENT_NODE ) {
+ children = $node.contents();
+ processedChildren = [];
+ for ( i = 0, len = children.length; i < len; i++ ) {
+ el = children[i];
+ if ( el.nodeType === ELEMENT_NODE || el.nodeType === TEXT_NODE ) {
+ processedChildren.push( getDomStructure( el ) );
+ }
+ }
+
+ return {
+ tagName: node.tagName,
+ attributes: $node.getAttrs(),
+ contents: processedChildren
+ };
+ } else {
+ // Should be text node
+ return $node.text();
+ }
+ }
+
+ /**
+ * Gets structure of node for this HTML.
+ *
+ * @param {string} html HTML markup for one or more nodes.
+ */
+ function getHtmlStructure( html ) {
+ var el = $( '<div>' ).append( html )[0];
+ return getDomStructure( el );
+ }
+
+ /**
+ * Add-on assertion helpers
+ */
+ // Define the add-ons
+ addons = {
+
+ // Expect boolean true
+ assertTrue: function ( actual, message ) {
+ QUnit.push( actual === true, actual, true, message );
+ },
+
+ // Expect boolean false
+ assertFalse: function ( actual, message ) {
+ QUnit.push( actual === false, actual, false, message );
+ },
+
+ // Expect numerical value less than X
+ lt: function ( actual, expected, message ) {
+ QUnit.push( actual < expected, actual, 'less than ' + expected, message );
+ },
+
+ // Expect numerical value less than or equal to X
+ ltOrEq: function ( actual, expected, message ) {
+ QUnit.push( actual <= expected, actual, 'less than or equal to ' + expected, message );
+ },
+
+ // Expect numerical value greater than X
+ gt: function ( actual, expected, message ) {
+ QUnit.push( actual > expected, actual, 'greater than ' + expected, message );
+ },
+
+ // Expect numerical value greater than or equal to X
+ gtOrEq: function ( actual, expected, message ) {
+ QUnit.push( actual >= expected, actual, 'greater than or equal to ' + expected, message );
+ },
+
+ /**
+ * Asserts that two HTML strings are structurally equivalent.
+ *
+ * @param {string} actualHtml Actual HTML markup.
+ * @param {string} expectedHtml Expected HTML markup
+ * @param {string} message Assertion message.
+ */
+ htmlEqual: function ( actualHtml, expectedHtml, message ) {
+ var actual = getHtmlStructure( actualHtml ),
+ expected = getHtmlStructure( expectedHtml );
+
+ QUnit.push(
+ QUnit.equiv(
+ actual,
+ expected
+ ),
+ actual,
+ expected,
+ message
+ );
+ },
+
+ /**
+ * Asserts that two HTML strings are not structurally equivalent.
+ *
+ * @param {string} actualHtml Actual HTML markup.
+ * @param {string} expectedHtml Expected HTML markup.
+ * @param {string} message Assertion message.
+ */
+ notHtmlEqual: function ( actualHtml, expectedHtml, message ) {
+ var actual = getHtmlStructure( actualHtml ),
+ expected = getHtmlStructure( expectedHtml );
+
+ QUnit.push(
+ !QUnit.equiv(
+ actual,
+ expected
+ ),
+ actual,
+ expected,
+ message
+ );
+ }
+ };
+
+ $.extend( QUnit.assert, addons );
+
+ /**
+ * Small test suite to confirm proper functionality of the utilities and
+ * initializations defined above in this file.
+ */
+ QUnit.module( 'test.mediawiki.qunit.testrunner', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.mwHtmlLive = mw.html;
+ mw.html = {
+ escape: function () {
+ return 'mocked';
+ }
+ };
+ },
+ teardown: function () {
+ mw.html = this.mwHtmlLive;
+ },
+ config: {
+ testVar: 'foo'
+ },
+ messages: {
+ testMsg: 'Foo.'
+ }
+ } ) );
+
+ QUnit.test( 'Setup', 3, function ( assert ) {
+ assert.equal( mw.html.escape( 'foo' ), 'mocked', 'setup() callback was ran.' );
+ assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object applied' );
+ assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object applied' );
+
+ mw.config.set( 'testVar', 'bar' );
+ mw.messages.set( 'testMsg', 'Bar.' );
+ } );
+
+ QUnit.test( 'Teardown', 2, function ( assert ) {
+ assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object restored and re-applied after test()' );
+ assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object restored and re-applied after test()' );
+ } );
+
+ QUnit.test( 'Loader status', 2, function ( assert ) {
+ var i, len, state,
+ modules = mw.loader.getModuleNames(),
+ error = [],
+ missing = [];
+
+ for ( i = 0, len = modules.length; i < len; i++ ) {
+ state = mw.loader.getState( modules[i] );
+ if ( state === 'error' ) {
+ error.push( modules[i] );
+ } else if ( state === 'missing' ) {
+ missing.push( modules[i] );
+ }
+ }
+
+ assert.deepEqual( error, [], 'Modules in error state' );
+ assert.deepEqual( missing, [], 'Modules in missing state' );
+ } );
+
+ QUnit.test( 'htmlEqual', 8, function ( assert ) {
+ assert.htmlEqual(
+ '<div><p class="some classes" data-length="10">Child paragraph with <a href="http://example.com">A link</a></p>Regular text<span>A span</span></div>',
+ '<div><p data-length=\'10\' class=\'some classes\'>Child paragraph with <a href=\'http://example.com\' >A link</a></p>Regular text<span>A span</span></div>',
+ 'Attribute order, spacing and quotation marks (equal)'
+ );
+
+ assert.notHtmlEqual(
+ '<div><p class="some classes" data-length="10">Child paragraph with <a href="http://example.com">A link</a></p>Regular text<span>A span</span></div>',
+ '<div><p data-length=\'10\' class=\'some more classes\'>Child paragraph with <a href=\'http://example.com\' >A link</a></p>Regular text<span>A span</span></div>',
+ 'Attribute order, spacing and quotation marks (not equal)'
+ );
+
+ assert.htmlEqual(
+ '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
+ '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
+ 'Multiple root nodes (equal)'
+ );
+
+ assert.notHtmlEqual(
+ '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
+ '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="important" >Last</label><input id="lastname" />',
+ 'Multiple root nodes (not equal, last label node is different)'
+ );
+
+ assert.htmlEqual(
+ 'fo&quot;o<br/>b&gt;ar',
+ 'fo"o<br/>b>ar',
+ 'Extra escaping is equal'
+ );
+ assert.notHtmlEqual(
+ 'foo&lt;br/&gt;bar',
+ 'foo<br/>bar',
+ 'Text escaping (not equal)'
+ );
+
+ assert.htmlEqual(
+ 'foo<a href="http://example.com">example</a>bar',
+ 'foo<a href="http://example.com">example</a>bar',
+ 'Outer text nodes are compared (equal)'
+ );
+
+ assert.notHtmlEqual(
+ 'foo<a href="http://example.com">example</a>bar',
+ 'foo<a href="http://example.com">example</a>quux',
+ 'Outer text nodes are compared (last text node different)'
+ );
+
+ } );
+
+ QUnit.module( 'test.mediawiki.qunit.testrunner-after', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Teardown', 3, function ( assert ) {
+ assert.equal( mw.html.escape( '<' ), '&lt;', 'teardown() callback was ran.' );
+ assert.equal( mw.config.get( 'testVar' ), null, 'config object restored to live in next module()' );
+ assert.equal( mw.messages.get( 'testMsg' ), null, 'messages object restored to live in next module()' );
+ } );
+
+}( jQuery, mediaWiki, QUnit ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js b/tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js
new file mode 100644
index 00000000..f6ea1b48
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js
@@ -0,0 +1,108 @@
+( function ( $ ) {
+ QUnit.module( 'jquery.accessKeyLabel', QUnit.newMwEnvironment( {
+ messages: {
+ 'brackets': '[$1]',
+ 'word-separator': ' '
+ }
+ } ) );
+
+ var getAccessKeyPrefixTestData = [
+ //ua string, platform string, expected prefix
+ // Internet Explorer
+ ['Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)', 'Win32', 'alt-'],
+ ['Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', 'Win32', 'alt-'],
+ ['Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; rv:11.0) like Gecko', 'Win64', 'alt-'],
+ // Firefox
+ ['Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.19) Gecko/20110420 Firefox/3.5.19', 'MacIntel', 'ctrl-'],
+ ['Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.17) Gecko/20110422 Ubuntu/10.10 (maverick) Firefox/3.6.17', 'Linux i686', 'alt-shift-'],
+ ['Mozilla/5.0 (Windows NT 6.0; rv:2.0.1) Gecko/20100101 Firefox/4.0.1', 'Win32', 'alt-shift-'],
+ // Safari / Konqueror
+ ['Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; nl-nl) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7', 'MacIntel', 'ctrl-alt-'],
+ ['Mozilla/5.0 (Windows; U; Windows NT 6.0; cs-CZ) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7', 'Win32', 'alt-'],
+ ['Mozilla/5.0 (X11; Linux i686) KHTML/4.9.1 (like Gecko) Konqueror/4.9', 'Linux i686', 'ctrl-'],
+ // Opera
+ ['Opera/9.80 (Windows NT 5.1)', 'Win32', 'shift-esc-'],
+ ['Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 OPR/15.0.1147.130', 'Win32', 'shift-esc-'],
+ // Chrome
+ ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30', 'MacIntel', 'ctrl-option-'],
+ ['Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.68 Safari/534.30', 'Linux i686', 'alt-shift-']
+ ],
+ //strings appended to title to make sure updateTooltipAccessKeys handles them correctly
+ updateTooltipAccessKeysTestData = [ '', ' [a]', ' [test-a]', ' [alt-b]' ];
+
+ function makeInput( title, accessKey ) {
+ //The properties aren't escaped, so make sure you don't call this function with values that need to be escaped!
+ return '<input title="' + title + '" ' + ( accessKey ? 'accessKey="' + accessKey + '" ' : '' ) + ' />';
+ }
+
+ QUnit.test( 'getAccessKeyPrefix', getAccessKeyPrefixTestData.length, function ( assert ) {
+ var i;
+ for ( i = 0; i < getAccessKeyPrefixTestData.length; i++ ) {
+ assert.equal( $.fn.updateTooltipAccessKeys.getAccessKeyPrefix( {
+ userAgent: getAccessKeyPrefixTestData[i][0],
+ platform: getAccessKeyPrefixTestData[i][1]
+ } ), getAccessKeyPrefixTestData[i][2], 'Correct prefix for ' + getAccessKeyPrefixTestData[i][0] );
+ }
+ } );
+
+ QUnit.test( 'updateTooltipAccessKeys - current browser', 2, function ( assert ) {
+ var title = $( makeInput ( 'Title', 'a' ) ).updateTooltipAccessKeys().prop( 'title' ),
+ //The new title should be something like "Title [alt-a]", but the exact label will depend on the browser.
+ //The "a" could be capitalized, and the prefix could be anything, e.g. a simple "^" for ctrl-
+ //(no browser is known using such a short prefix, though) or "Alt+Umschalt+" in German Firefox.
+ result = /^Title \[(.+)[aA]\]$/.exec( title );
+ assert.ok( result, 'title should match expected structure.' );
+ assert.notEqual( result[1], 'test-', 'Prefix used for testing shouldn\'t be used in production.' );
+ } );
+
+ QUnit.test( 'updateTooltipAccessKeys - no access key', updateTooltipAccessKeysTestData.length, function ( assert ) {
+ var i, oldTitle, $input, newTitle;
+ for ( i = 0; i < updateTooltipAccessKeysTestData.length; i++ ) {
+ oldTitle = 'Title' + updateTooltipAccessKeysTestData[i];
+ $input = $( makeInput( oldTitle ) );
+ $( '#qunit-fixture' ).append( $input );
+ newTitle = $input.updateTooltipAccessKeys().prop( 'title' );
+ assert.equal( newTitle, 'Title', 'title="' + oldTitle + '"' );
+ }
+ } );
+
+ QUnit.test( 'updateTooltipAccessKeys - with access key', updateTooltipAccessKeysTestData.length, function ( assert ) {
+ $.fn.updateTooltipAccessKeys.setTestMode( true );
+ var i, oldTitle, $input, newTitle;
+ for ( i = 0; i < updateTooltipAccessKeysTestData.length; i++ ) {
+ oldTitle = 'Title' + updateTooltipAccessKeysTestData[i];
+ $input = $( makeInput( oldTitle, 'a' ) );
+ $( '#qunit-fixture' ).append( $input );
+ newTitle = $input.updateTooltipAccessKeys().prop( 'title' );
+ assert.equal( newTitle, 'Title [test-a]', 'title="' + oldTitle + '"' );
+ }
+ $.fn.updateTooltipAccessKeys.setTestMode( false );
+ } );
+
+ QUnit.test( 'updateTooltipAccessKeys with label element', 2, function ( assert ) {
+ $.fn.updateTooltipAccessKeys.setTestMode( true );
+ var html = '<label for="testInput" title="Title">Label</label><input id="testInput" accessKey="a" />',
+ $label, $input;
+ $( '#qunit-fixture' ).html( html );
+ $label = $( '#qunit-fixture label' );
+ $input = $( '#qunit-fixture input' );
+ $input.updateTooltipAccessKeys();
+ assert.equal( $input.prop( 'title' ), '', 'No title attribute added to input element.' );
+ assert.equal( $label.prop( 'title' ), 'Title [test-a]', 'title updated for associated label element.' );
+ $.fn.updateTooltipAccessKeys.setTestMode( false );
+ } );
+
+ QUnit.test( 'updateTooltipAccessKeys with label element as parent', 2, function ( assert ) {
+ $.fn.updateTooltipAccessKeys.setTestMode( true );
+ var html = '<label title="Title">Label<input id="testInput" accessKey="a" /></label>',
+ $label, $input;
+ $( '#qunit-fixture' ).html( html );
+ $label = $( '#qunit-fixture label' );
+ $input = $( '#qunit-fixture input' );
+ $input.updateTooltipAccessKeys();
+ assert.equal( $input.prop( 'title' ), '', 'No title attribute added to input element.' );
+ assert.equal( $label.prop( 'title' ), 'Title [test-a]', 'title updated for associated label element.' );
+ $.fn.updateTooltipAccessKeys.setTestMode( false );
+ } );
+
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js b/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js
new file mode 100644
index 00000000..e8c51214
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js
@@ -0,0 +1,53 @@
+( function ( $ ) {
+
+ QUnit.module( 'jquery.autoEllipsis', QUnit.newMwEnvironment() );
+
+ function createWrappedDiv( text, width ) {
+ var $wrapper = $( '<div>' ).css( 'width', width ),
+ $div = $( '<div>' ).text( text );
+ $wrapper.append( $div );
+ return $wrapper;
+ }
+
+ function findDivergenceIndex( a, b ) {
+ var i = 0;
+ while ( i < a.length && i < b.length && a[i] === b[i] ) {
+ i++;
+ }
+ return i;
+ }
+
+ QUnit.test( 'Position right', 4, function ( assert ) {
+ // We need this thing to be visible, so append it to the DOM
+ var $span, spanText, d, spanTextNew,
+ origText = 'This is a really long random string and there is no way it fits in 100 pixels.',
+ $wrapper = createWrappedDiv( origText, '100px' );
+
+ $( '#qunit-fixture' ).append( $wrapper );
+ $wrapper.autoEllipsis( { position: 'right' } );
+
+ // Verify that, and only one, span element was created
+ $span = $wrapper.find( '> span' );
+ assert.strictEqual( $span.length, 1, 'autoEllipsis wrapped the contents in a span element' );
+
+ // Check that the text fits by turning on word wrapping
+ $span.css( 'whiteSpace', 'nowrap' );
+ assert.ltOrEq(
+ $span.width(),
+ $span.parent().width(),
+ 'Text fits (making the span "white-space: nowrap" does not make it wider than its parent)'
+ );
+
+ // Add two characters using scary black magic
+ spanText = $span.text();
+ d = findDivergenceIndex( origText, spanText );
+ spanTextNew = spanText.slice( 0, d ) + origText[d] + origText[d] + '...';
+
+ assert.gt( spanTextNew.length, spanText.length, 'Verify that the new span-length is indeed greater' );
+
+ // Put this text in the span and verify it doesn't fit
+ $span.text( spanTextNew );
+ assert.gt( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more)' );
+ } );
+
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js
new file mode 100644
index 00000000..e6aa3aa8
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js
@@ -0,0 +1,37 @@
+( function ( $ ) {
+ QUnit.module( 'jquery.byteLength', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Simple text', 5, function ( assert ) {
+ var azLc = 'abcdefghijklmnopqrstuvwxyz',
+ azUc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ num = '0123456789',
+ x = '*',
+ space = ' ';
+
+ assert.equal( $.byteLength( azLc ), 26, 'Lowercase a-z' );
+ assert.equal( $.byteLength( azUc ), 26, 'Uppercase A-Z' );
+ assert.equal( $.byteLength( num ), 10, 'Numbers 0-9' );
+ assert.equal( $.byteLength( x ), 1, 'An asterisk' );
+ assert.equal( $.byteLength( space ), 3, '3 spaces' );
+
+ } );
+
+ QUnit.test( 'Special text', 4, function ( assert ) {
+ // https://en.wikipedia.org/wiki/UTF-8
+ var u0024 = '$',
+ // Cent symbol
+ u00A2 = '\u00A2',
+ // Euro symbol
+ u20AC = '\u20AC',
+ // Character \U00024B62 (Han script) can't be represented in javascript as a single
+ // code point, instead it is composed as a surrogate pair of two separate code units.
+ // http://codepoints.net/U+24B62
+ // http://www.fileformat.info/info/unicode/char/24B62/index.htm
+ u024B62 = '\uD852\uDF62';
+
+ assert.strictEqual( $.byteLength( u0024 ), 1, 'U+0024' );
+ assert.strictEqual( $.byteLength( u00A2 ), 2, 'U+00A2' );
+ assert.strictEqual( $.byteLength( u20AC ), 3, 'U+20AC' );
+ assert.strictEqual( $.byteLength( u024B62 ), 4, 'U+024B62 (surrogate pair: \\uD852\\uDF62)' );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js b/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js
new file mode 100644
index 00000000..22d2af19
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js
@@ -0,0 +1,252 @@
+( function ( $, mw ) {
+ var simpleSample, U_20AC, mbSample;
+
+ QUnit.module( 'jquery.byteLimit', QUnit.newMwEnvironment() );
+
+ // Simple sample (20 chars, 20 bytes)
+ simpleSample = '12345678901234567890';
+
+ // 3 bytes (euro-symbol)
+ U_20AC = '\u20AC';
+
+ // Multi-byte sample (22 chars, 26 bytes)
+ mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC;
+
+ // Basic sendkey-implementation
+ function addChars( $input, charstr ) {
+ var c, len;
+
+ function x( $input, i ) {
+ // Add character to the value
+ return $input.val() + charstr.charAt( i );
+ }
+
+ for ( c = 0, len = charstr.length; c < len; c += 1 ) {
+ $input
+ .val( x( $input, c ) )
+ .trigger( 'change' );
+ }
+ }
+
+ /**
+ * Test factory for $.fn.byteLimit
+ *
+ * @param {Object} options
+ * @param {string} options.description Test name
+ * @param {jQuery} options.$input jQuery object in an input element
+ * @param {string} options.sample Sequence of characters to simulate being
+ * added one by one
+ * @param {string} options.expected Expected final value of `$input`
+ */
+ function byteLimitTest( options ) {
+ var opt = $.extend( {
+ description: '',
+ $input: null,
+ sample: '',
+ expected: ''
+ }, options );
+
+ QUnit.asyncTest( opt.description, 1, function ( assert ) {
+ setTimeout( function () {
+ opt.$input.appendTo( '#qunit-fixture' );
+
+ // Simulate pressing keys for each of the sample characters
+ addChars( opt.$input, opt.sample );
+
+ assert.equal(
+ opt.$input.val(),
+ opt.expected,
+ 'New value matches the expected string'
+ );
+
+ QUnit.start();
+ }, 10 );
+ } );
+ }
+
+ byteLimitTest( {
+ description: 'Plain text input',
+ $input: $( '<input type="text"/>' ),
+ sample: simpleSample,
+ expected: simpleSample
+ } );
+
+ byteLimitTest( {
+ description: 'Plain text input. Calling byteLimit with no parameters and no maxlength attribute (bug 36310)',
+ $input: $( '<input type="text"/>' )
+ .byteLimit(),
+ sample: simpleSample,
+ expected: simpleSample
+ } );
+
+ byteLimitTest( {
+ description: 'Limit using the maxlength attribute',
+ $input: $( '<input type="text"/>' )
+ .attr( 'maxlength', '10' )
+ .byteLimit(),
+ sample: simpleSample,
+ expected: '1234567890'
+ } );
+
+ byteLimitTest( {
+ description: 'Limit using a custom value',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 10 ),
+ sample: simpleSample,
+ expected: '1234567890'
+ } );
+
+ byteLimitTest( {
+ description: 'Limit using a custom value, overriding maxlength attribute',
+ $input: $( '<input type="text"/>' )
+ .attr( 'maxlength', '10' )
+ .byteLimit( 15 ),
+ sample: simpleSample,
+ expected: '123456789012345'
+ } );
+
+ byteLimitTest( {
+ description: 'Limit using a custom value (multibyte)',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 14 ),
+ sample: mbSample,
+ expected: '1234567890' + U_20AC + '1'
+ } );
+
+ byteLimitTest( {
+ description: 'Limit using a custom value (multibyte) overlapping a byte',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 12 ),
+ sample: mbSample,
+ expected: '1234567890' + '12'
+ } );
+
+ byteLimitTest( {
+ description: 'Pass the limit and a callback as input filter',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 6, function ( val ) {
+ var title = mw.Title.newFromText( String( val ) );
+ // Return without namespace prefix
+ return title ? title.getMain() : '';
+ } ),
+ sample: 'User:Sample',
+ expected: 'User:Sample'
+ } );
+
+ byteLimitTest( {
+ description: 'Limit using the maxlength attribute and pass a callback as input filter',
+ $input: $( '<input type="text"/>' )
+ .attr( 'maxlength', '6' )
+ .byteLimit( function ( val ) {
+ var title = mw.Title.newFromText( String( val ) );
+ // Return without namespace prefix
+ return title ? title.getMain() : '';
+ } ),
+ sample: 'User:Sample',
+ expected: 'User:Sample'
+ } );
+
+ byteLimitTest( {
+ description: 'Pass the limit and a callback as input filter',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 6, function ( val ) {
+ var title = mw.Title.newFromText( String( val ) );
+ // Return without namespace prefix
+ return title ? title.getMain() : '';
+ } ),
+ sample: 'User:Example',
+ // The callback alters the value to be used to calculeate
+ // the length. The altered value is "Exampl" which has
+ // a length of 6, the "e" would exceed the limit.
+ expected: 'User:Exampl'
+ } );
+
+ byteLimitTest( {
+ description: 'Input filter that increases the length',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 10, function ( text ) {
+ return 'prefix' + text;
+ } ),
+ sample: simpleSample,
+ // Prefix adds 6 characters, limit is reached after 4
+ expected: '1234'
+ } );
+
+ // Regression tests for bug 41450
+ byteLimitTest( {
+ description: 'Input filter of which the base exceeds the limit',
+ $input: $( '<input type="text"/>' )
+ .byteLimit( 3, function ( text ) {
+ return 'prefix' + text;
+ } ),
+ sample: simpleSample,
+ hasLimit: true,
+ limit: 6, // 'prefix' length
+ expected: ''
+ } );
+
+ QUnit.test( 'Confirm properties and attributes set', 4, function ( assert ) {
+ var $el, $elA, $elB;
+
+ $el = $( '<input type="text"/>' )
+ .attr( 'maxlength', '7' )
+ .appendTo( '#qunit-fixture' )
+ .byteLimit();
+
+ assert.strictEqual( $el.attr( 'maxlength' ), '7', 'maxlength attribute unchanged for simple limit' );
+
+ $el = $( '<input type="text"/>' )
+ .attr( 'maxlength', '7' )
+ .appendTo( '#qunit-fixture' )
+ .byteLimit( 12 );
+
+ assert.strictEqual( $el.attr( 'maxlength' ), '12', 'maxlength attribute updated for custom limit' );
+
+ $el = $( '<input type="text"/>' )
+ .attr( 'maxlength', '7' )
+ .appendTo( '#qunit-fixture' )
+ .byteLimit( 12, function ( val ) {
+ return val;
+ } );
+
+ assert.strictEqual( $el.attr( 'maxlength' ), undefined, 'maxlength attribute removed for limit with callback' );
+
+ $elA = $( '<input type="text"/>' )
+ .addClass( 'mw-test-byteLimit-foo' )
+ .attr( 'maxlength', '7' )
+ .appendTo( '#qunit-fixture' );
+
+ $elB = $( '<input type="text"/>' )
+ .addClass( 'mw-test-byteLimit-foo' )
+ .attr( 'maxlength', '12' )
+ .appendTo( '#qunit-fixture' );
+
+ $el = $( '.mw-test-byteLimit-foo' );
+
+ assert.strictEqual( $el.length, 2, 'Verify that there are no other elements clashing with this test suite' );
+
+ $el.byteLimit();
+ } );
+
+ QUnit.test( 'Trim from insertion when limit exceeded', 2, function ( assert ) {
+ var $el;
+
+ // Use a new <input /> because the bug only occurs on the first time
+ // the limit it reached (bug 40850)
+ $el = $( '<input type="text"/>' )
+ .appendTo( '#qunit-fixture' )
+ .byteLimit( 3 )
+ .val( 'abc' ).trigger( 'change' )
+ .val( 'zabc' ).trigger( 'change' );
+
+ assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 0), not the end' );
+
+ $el = $( '<input type="text"/>' )
+ .appendTo( '#qunit-fixture' )
+ .byteLimit( 3 )
+ .val( 'abc' ).trigger( 'change' )
+ .val( 'azbc' ).trigger( 'change' );
+
+ assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 1), not the end' );
+ } );
+}( jQuery, mediaWiki ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.client.test.js b/tests/qunit/suites/resources/jquery/jquery.client.test.js
new file mode 100644
index 00000000..c6dd91c4
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.client.test.js
@@ -0,0 +1,638 @@
+( function ( $ ) {
+
+ QUnit.module( 'jquery.client', QUnit.newMwEnvironment() );
+
+ var uacount = 0,
+ // Object keyed by userAgent. Value is an array (human-readable name, client-profile object, navigator.platform value)
+ uas = {
+ // Internet Explorer 6
+ // Internet Explorer 7
+ 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)': {
+ title: 'Internet Explorer 7',
+ platform: 'Win32',
+ profile: {
+ name: 'msie',
+ layout: 'trident',
+ layoutVersion: 'unknown',
+ platform: 'win',
+ version: '7.0',
+ versionBase: '7',
+ versionNumber: 7
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: false
+ }
+ },
+ // Internet Explorer 8
+ // Internet Explorer 9
+ // Internet Explorer 10
+ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)': {
+ title: 'Internet Explorer 10',
+ platform: 'Win32',
+ profile: {
+ name: 'msie',
+ layout: 'trident',
+ layoutVersion: 6,
+ platform: 'win',
+ version: '10.0',
+ versionBase: '10',
+ versionNumber: 10
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Internet Explorer 11
+ 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv 11.0) like Gecko': {
+ title: 'Internet Explorer 11',
+ platform: 'Win32',
+ profile: {
+ name: 'msie',
+ layout: 'trident',
+ layoutVersion: 7,
+ platform: 'win',
+ version: '11.0',
+ versionBase: '11',
+ versionNumber: 11
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Internet Explorer 11 - Windows 8.1 x64 Modern UI
+ 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; rv:11.0) like Gecko': {
+ title: 'Internet Explorer 11',
+ platform: 'Win64',
+ profile: {
+ name: 'msie',
+ layout: 'trident',
+ layoutVersion: 7,
+ platform: 'win',
+ version: '11.0',
+ versionBase: '11',
+ versionNumber: 11
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Internet Explorer 11 - Windows 8.1 x64 desktop UI
+ 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko': {
+ title: 'Internet Explorer 11',
+ platform: 'WOW64',
+ profile: {
+ name: 'msie',
+ layout: 'trident',
+ layoutVersion: 7,
+ platform: 'win',
+ version: '11.0',
+ versionBase: '11',
+ versionNumber: 11
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Firefox 2
+ // Firefox 3.5
+ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.19) Gecko/20110420 Firefox/3.5.19': {
+ title: 'Firefox 3.5',
+ platform: 'MacIntel',
+ profile: {
+ name: 'firefox',
+ layout: 'gecko',
+ layoutVersion: 20110420,
+ platform: 'mac',
+ version: '3.5.19',
+ versionBase: '3',
+ versionNumber: 3.5
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Firefox 3.6
+ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.17) Gecko/20110422 Ubuntu/10.10 (maverick) Firefox/3.6.17': {
+ title: 'Firefox 3.6',
+ platform: 'Linux i686',
+ profile: {
+ name: 'firefox',
+ layout: 'gecko',
+ layoutVersion: 20110422,
+ platform: 'linux',
+ version: '3.6.17',
+ versionBase: '3',
+ versionNumber: 3.6
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Firefox 4
+ 'Mozilla/5.0 (Windows NT 6.0; rv:2.0.1) Gecko/20100101 Firefox/4.0.1': {
+ title: 'Firefox 4',
+ platform: 'Win32',
+ profile: {
+ name: 'firefox',
+ layout: 'gecko',
+ layoutVersion: 20100101,
+ platform: 'win',
+ version: '4.0.1',
+ versionBase: '4',
+ versionNumber: 4
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Firefox 10 nightly build
+ 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0a1) Gecko/20111103 Firefox/10.0a1': {
+ title: 'Firefox 10 nightly',
+ platform: 'Linux',
+ profile: {
+ name: 'firefox',
+ layout: 'gecko',
+ layoutVersion: 20111103,
+ platform: 'linux',
+ version: '10.0a1',
+ versionBase: '10',
+ versionNumber: 10
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Iceweasel 10.0.6
+ 'Mozilla/5.0 (X11; Linux i686; rv:10.0.6) Gecko/20100101 Iceweasel/10.0.6': {
+ title: 'Iceweasel 10.0.6',
+ platform: 'Linux',
+ profile: {
+ name: 'iceweasel',
+ layout: 'gecko',
+ layoutVersion: 20100101,
+ platform: 'linux',
+ version: '10.0.6',
+ versionBase: '10',
+ versionNumber: 10
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Iceweasel 15.0.1
+ 'Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1 Iceweasel/15.0.1': {
+ title: 'Iceweasel 15.0.1',
+ platform: 'Linux',
+ profile: {
+ name: 'iceweasel',
+ layout: 'gecko',
+ layoutVersion: 20100101,
+ platform: 'linux',
+ version: '15.0.1',
+ versionBase: '15',
+ versionNumber: 15
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Firefox 5
+ // Safari 3
+ // Safari 4
+ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; nl-nl) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7': {
+ title: 'Safari 4',
+ platform: 'MacIntel',
+ profile: {
+ name: 'safari',
+ layout: 'webkit',
+ layoutVersion: 531,
+ platform: 'mac',
+ version: '4.0.5',
+ versionBase: '4',
+ versionNumber: 4
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ 'Mozilla/5.0 (Windows; U; Windows NT 6.0; cs-CZ) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7': {
+ title: 'Safari 4',
+ platform: 'Win32',
+ profile: {
+ name: 'safari',
+ layout: 'webkit',
+ layoutVersion: 533,
+ platform: 'win',
+ version: '4.0.5',
+ versionBase: '4',
+ versionNumber: 4
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Safari 5
+ // Safari 6
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.29.13 (KHTML, like Gecko) Version/6.0.4 Safari/536.29.13': {
+ title: 'Safari 6',
+ platform: 'MacIntel',
+ profile: {
+ name: 'safari',
+ layout: 'webkit',
+ layoutVersion: 536,
+ platform: 'mac',
+ version: '6.0.4',
+ versionBase: '6',
+ versionNumber: 6
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Safari 6.0.5+ (doesn't have the comma in "KHTML, like Gecko")
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1': {
+ title: 'Safari 6',
+ platform: 'MacIntel',
+ profile: {
+ name: 'safari',
+ layout: 'webkit',
+ layoutVersion: 536,
+ platform: 'mac',
+ version: '6.0.5',
+ versionBase: '6',
+ versionNumber: 6
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Opera 10+
+ 'Opera/9.80 (Windows NT 5.1)': {
+ title: 'Opera 10+ (exact version unspecified)',
+ platform: 'Win32',
+ profile: {
+ name: 'opera',
+ layout: 'presto',
+ layoutVersion: 'unknown',
+ platform: 'win',
+ version: '10',
+ versionBase: '10',
+ versionNumber: 10
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Opera 12
+ 'Opera/9.80 (Windows NT 5.1) Presto/2.12.388 Version/12.11': {
+ title: 'Opera 12',
+ platform: 'Win32',
+ profile: {
+ name: 'opera',
+ layout: 'presto',
+ layoutVersion: 'unknown',
+ platform: 'win',
+ version: '12.11',
+ versionBase: '12',
+ versionNumber: 12.11
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Opera 15 (WebKit-based)
+ 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 OPR/15.0.1147.130': {
+ title: 'Opera 15',
+ platform: 'Win32',
+ profile: {
+ name: 'opera',
+ layout: 'webkit',
+ layoutVersion: 537,
+ platform: 'win',
+ version: '15.0.1147.130',
+ versionBase: '15',
+ versionNumber: 15
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Chrome 5
+ // Chrome 6
+ // Chrome 7
+ // Chrome 8
+ // Chrome 9
+ // Chrome 10
+ // Chrome 11
+ // Chrome 12
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30': {
+ title: 'Chrome 12',
+ platform: 'MacIntel',
+ profile: {
+ name: 'chrome',
+ layout: 'webkit',
+ layoutVersion: 534,
+ platform: 'mac',
+ version: '12.0.742.112',
+ versionBase: '12',
+ versionNumber: 12
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.68 Safari/534.30': {
+ title: 'Chrome 12',
+ platform: 'Linux i686',
+ profile: {
+ name: 'chrome',
+ layout: 'webkit',
+ layoutVersion: 534,
+ platform: 'linux',
+ version: '12.0.742.68',
+ versionBase: '12',
+ versionNumber: 12
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Android WebKit Browser 2.3
+ 'Mozilla/5.0 (Linux; U; Android 2.3.5; en-us; HTC Vision Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1': {
+ title: 'Android WebKit Browser 2.3',
+ platform: 'Linux armv7l',
+ profile: {
+ name: 'android',
+ layout: 'webkit',
+ layoutVersion: 533,
+ platform: 'linux',
+ version: '2.3.5',
+ versionBase: '2',
+ versionNumber: 2.3
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Rekonq (bug 34924)
+ 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.34 (KHTML, like Gecko) rekonq Safari/534.34': {
+ title: 'Rekonq',
+ platform: 'Linux i686',
+ profile: {
+ name: 'rekonq',
+ layout: 'webkit',
+ layoutVersion: 534,
+ platform: 'linux',
+ version: '534.34',
+ versionBase: '534',
+ versionNumber: 534.34
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Konqueror
+ 'Mozilla/5.0 (X11; Linux i686) KHTML/4.9.1 (like Gecko) Konqueror/4.9': {
+ title: 'Konqueror',
+ platform: 'Linux i686',
+ profile: {
+ name: 'konqueror',
+ layout: 'khtml',
+ layoutVersion: 'unknown',
+ platform: 'linux',
+ version: '4.9.1',
+ versionBase: '4',
+ versionNumber: 4.9
+ },
+ wikiEditor: {
+ // '4.9' is less than '4.11'.
+ ltr: false,
+ rtl: false
+ },
+ wikiEditorLegacy: {
+ // The check is missing in legacyTestMap
+ ltr: true,
+ rtl: true
+ }
+ },
+ // Amazon Silk
+ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.0.13.81_10003810) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true': {
+ title: 'Silk',
+ platform: 'Desktop',
+ profile: {
+ name: 'silk',
+ layout: 'webkit',
+ layoutVersion: 533,
+ platform: 'unknown',
+ version: '1.0.13.81_10003810',
+ versionBase: '1',
+ versionNumber: 1
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ },
+ 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; KFTT Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Silk/2.1 Mobile Safari/535.19 Silk-Accelerated=true': {
+ title: 'Silk',
+ platform: 'Mobile',
+ profile: {
+ name: 'silk',
+ layout: 'webkit',
+ layoutVersion: 535,
+ platform: 'unknown',
+ version: '2.1',
+ versionBase: '2',
+ versionNumber: 2.1
+ },
+ wikiEditor: {
+ ltr: true,
+ rtl: true
+ }
+ }
+ },
+ testMap = {
+ // Example from WikiEditor, modified to provide version identifiers as strings and with
+ // Konqueror 4.11 check added.
+ 'ltr': {
+ 'msie': [['>=', '7.0']],
+ 'firefox': [['>=', '2']],
+ 'opera': [['>=', '9.6']],
+ 'safari': [['>=', '3']],
+ 'chrome': [['>=', '3']],
+ 'netscape': [['>=', '9']],
+ 'konqueror': [['>=', '4.11']],
+ 'blackberry': false,
+ 'ipod': false,
+ 'iphone': false
+ },
+ 'rtl': {
+ 'msie': [['>=', '8']],
+ 'firefox': [['>=', '2']],
+ 'opera': [['>=', '9.6']],
+ 'safari': [['>=', '3']],
+ 'chrome': [['>=', '3']],
+ 'netscape': [['>=', '9']],
+ 'konqueror': [['>=', '4.11']],
+ 'blackberry': false,
+ 'ipod': false,
+ 'iphone': false
+ }
+ },
+ legacyTestMap = {
+ // Original example from WikiEditor.
+ // This is using the old, but still supported way of providing version identifiers as numbers
+ // instead of strings; with this method, 4.9 would be considered larger than 4.11.
+ 'ltr': {
+ 'msie': [['>=', 7.0]],
+ 'firefox': [['>=', 2]],
+ 'opera': [['>=', 9.6]],
+ 'safari': [['>=', 3]],
+ 'chrome': [['>=', 3]],
+ 'netscape': [['>=', 9]],
+ 'blackberry': false,
+ 'ipod': false,
+ 'iphone': false
+ },
+ 'rtl': {
+ 'msie': [['>=', 8]],
+ 'firefox': [['>=', 2]],
+ 'opera': [['>=', 9.6]],
+ 'safari': [['>=', 3]],
+ 'chrome': [['>=', 3]],
+ 'netscape': [['>=', 9]],
+ 'blackberry': false,
+ 'ipod': false,
+ 'iphone': false
+ }
+ }
+ ;
+
+ // Count test cases
+ $.each( uas, function () {
+ uacount++;
+ } );
+
+ QUnit.test( 'profile( navObject )', 7, function ( assert ) {
+ var p = $.client.profile();
+
+ function unknownOrType( val, type, summary ) {
+ assert.ok( typeof val === type || val === 'unknown', summary );
+ }
+
+ assert.equal( typeof p, 'object', 'profile returns an object' );
+ unknownOrType( p.layout, 'string', 'p.layout is a string (or "unknown")' );
+ unknownOrType( p.layoutVersion, 'number', 'p.layoutVersion is a number (or "unknown")' );
+ unknownOrType( p.platform, 'string', 'p.platform is a string (or "unknown")' );
+ unknownOrType( p.version, 'string', 'p.version is a string (or "unknown")' );
+ unknownOrType( p.versionBase, 'string', 'p.versionBase is a string (or "unknown")' );
+ assert.equal( typeof p.versionNumber, 'number', 'p.versionNumber is a number' );
+ } );
+
+ QUnit.test( 'profile( navObject ) - samples', uacount, function ( assert ) {
+ // Loop through and run tests
+ $.each( uas, function ( rawUserAgent, data ) {
+ // Generate a client profile object and compare recursively
+ var ret = $.client.profile( {
+ userAgent: rawUserAgent,
+ platform: data.platform
+ } );
+ assert.deepEqual( ret, data.profile, 'Client profile support check for ' + data.title + ' (' + data.platform + '): ' + rawUserAgent );
+ } );
+ } );
+
+ QUnit.test( 'test( testMap )', 4, function ( assert ) {
+ // .test() uses eval, make sure no exceptions are thrown
+ // then do a basic return value type check
+ var testMatch = $.client.test( testMap ),
+ ie7Profile = $.client.profile( {
+ 'userAgent': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
+ 'platform': ''
+ } );
+
+ assert.equal( typeof testMatch, 'boolean', 'map with ltr/rtl split returns a boolean value' );
+
+ testMatch = $.client.test( testMap.ltr );
+
+ assert.equal( typeof testMatch, 'boolean', 'simple map (without ltr/rtl split) returns a boolean value' );
+
+ assert.equal( $.client.test( {
+ 'msie': null
+ }, ie7Profile ), true, 'returns true if any version of a browser are allowed (null)' );
+
+ assert.equal( $.client.test( {
+ 'msie': false
+ }, ie7Profile ), false, 'returns false if all versions of a browser are not allowed (false)' );
+ } );
+
+ QUnit.test( 'test( testMap, exactMatchOnly )', 2, function ( assert ) {
+ var ie7Profile = $.client.profile( {
+ 'userAgent': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
+ 'platform': ''
+ } );
+
+ assert.equal( $.client.test( {
+ 'firefox': [['>=', 2]]
+ }, ie7Profile, false ), true, 'returns true if browser not found and exactMatchOnly not set' );
+
+ assert.equal( $.client.test( {
+ 'firefox': [['>=', 2]]
+ }, ie7Profile, true ), false, 'returns false if browser not found and exactMatchOnly is set' );
+ } );
+
+ QUnit.test( 'test( testMap ), test( legacyTestMap ) - WikiEditor sample', uacount * 2 * 2, function ( assert ) {
+ var $body = $( 'body' ),
+ bodyClasses = $body.attr( 'class' );
+
+ // Loop through and run tests
+ $.each( uas, function ( agent, data ) {
+ $.each( ['ltr', 'rtl'], function ( i, dir ) {
+ var profile, testMatch, legacyTestMatch;
+ $body.removeClass( 'ltr rtl' ).addClass( dir );
+ profile = $.client.profile( {
+ userAgent: agent,
+ platform: data.platform
+ } );
+ testMatch = $.client.test( testMap, profile );
+ legacyTestMatch = $.client.test( legacyTestMap, profile );
+ $body.removeClass( dir );
+
+ assert.equal(
+ testMatch,
+ data.wikiEditor[dir],
+ 'testing comparison based on ' + dir + ', ' + agent
+ );
+ assert.equal(
+ legacyTestMatch,
+ data.wikiEditorLegacy ? data.wikiEditorLegacy[dir] : data.wikiEditor[dir],
+ 'testing comparison based on ' + dir + ', ' + agent + ' (legacyTestMap)'
+ );
+ } );
+ } );
+
+ // Restore body classes
+ $body.attr( 'class', bodyClasses );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.color.test.js b/tests/qunit/suites/resources/jquery/jquery.color.test.js
new file mode 100644
index 00000000..c8e8ac70
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.color.test.js
@@ -0,0 +1,18 @@
+( function ( $ ) {
+ QUnit.module( 'jquery.color', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.clock = this.sandbox.useFakeTimers();
+ }
+ } ) );
+
+ QUnit.test( 'animate', 1, function ( assert ) {
+ var $canvas = $( '<div>' ).css( 'background-color', '#fff' );
+
+ $canvas.animate( { backgroundColor: '#000' }, 10 ).promise().then( function () {
+ var endColors = $.colorUtil.getRGB( $canvas.css( 'background-color' ) );
+ assert.deepEqual( endColors, [0, 0, 0], 'end state' );
+ } );
+
+ this.clock.tick( 20 );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js b/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js
new file mode 100644
index 00000000..39ae363c
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js
@@ -0,0 +1,63 @@
+( function ( $ ) {
+ QUnit.module( 'jquery.colorUtil', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'getRGB', 18, function ( assert ) {
+ assert.strictEqual( $.colorUtil.getRGB(), undefined, 'No arguments' );
+ assert.strictEqual( $.colorUtil.getRGB( '' ), undefined, 'Empty string' );
+ assert.deepEqual( $.colorUtil.getRGB( [0, 100, 255] ), [0, 100, 255], 'Parse array of rgb values' );
+ assert.deepEqual( $.colorUtil.getRGB( 'rgb(0,100,255)' ), [0, 100, 255], 'Parse simple rgb string' );
+ assert.deepEqual( $.colorUtil.getRGB( 'rgb(0, 100, 255)' ), [0, 100, 255], 'Parse simple rgb string with spaces' );
+ assert.deepEqual( $.colorUtil.getRGB( 'rgb(0%,20%,40%)' ), [0, 51, 102], 'Parse rgb string with percentages' );
+ assert.deepEqual( $.colorUtil.getRGB( 'rgb(0%, 20%, 40%)' ), [0, 51, 102], 'Parse rgb string with percentages and spaces' );
+ assert.deepEqual( $.colorUtil.getRGB( '#f2ddee' ), [242, 221, 238], 'Hex string: 6 char lowercase' );
+ assert.deepEqual( $.colorUtil.getRGB( '#f2DDEE' ), [242, 221, 238], 'Hex string: 6 char uppercase' );
+ assert.deepEqual( $.colorUtil.getRGB( '#f2DdEe' ), [242, 221, 238], 'Hex string: 6 char mixed' );
+ assert.deepEqual( $.colorUtil.getRGB( '#eee' ), [238, 238, 238], 'Hex string: 3 char lowercase' );
+ assert.deepEqual( $.colorUtil.getRGB( '#EEE' ), [238, 238, 238], 'Hex string: 3 char uppercase' );
+ assert.deepEqual( $.colorUtil.getRGB( '#eEe' ), [238, 238, 238], 'Hex string: 3 char mixed' );
+ assert.deepEqual( $.colorUtil.getRGB( 'rgba(0, 0, 0, 0)' ), [255, 255, 255], 'Zero rgba for Safari 3; Transparent (whitespace)' );
+
+ // Perhaps this is a bug in colorUtil, but it is the current behavior so, let's keep
+ // track of it, so we will know in case it would ever change.
+ assert.strictEqual( $.colorUtil.getRGB( 'rgba(0,0,0,0)' ), undefined, 'Zero rgba without whitespace' );
+
+ assert.deepEqual( $.colorUtil.getRGB( 'lightGreen' ), [144, 238, 144], 'Color names (lightGreen)' );
+ assert.deepEqual( $.colorUtil.getRGB( 'transparent' ), [255, 255, 255], 'Color names (transparent)' );
+ assert.strictEqual( $.colorUtil.getRGB( 'mediaWiki' ), undefined, 'Inexisting color name' );
+ } );
+
+ QUnit.test( 'rgbToHsl', 1, function ( assert ) {
+ var hsl, ret;
+
+ // Cross-browser differences in decimals...
+ // Round to two decimals so they can be more reliably checked.
+ function dualDecimals( a ) {
+ return Math.round( a * 100 ) / 100;
+ }
+
+ // Re-create the rgbToHsl return array items, limited to two decimals.
+ hsl = $.colorUtil.rgbToHsl( 144, 238, 144 );
+ ret = [ dualDecimals( hsl[0] ), dualDecimals( hsl[1] ), dualDecimals( hsl[2] ) ];
+
+ assert.deepEqual( ret, [0.33, 0.73, 0.75], 'rgb(144, 238, 144): hsl(0.33, 0.73, 0.75)' );
+ } );
+
+ QUnit.test( 'hslToRgb', 1, function ( assert ) {
+ var rgb, ret;
+ rgb = $.colorUtil.hslToRgb( 0.3, 0.7, 0.8 );
+
+ // Re-create the hslToRgb return array items, rounded to whole numbers.
+ ret = [ Math.round( rgb[0] ), Math.round( rgb[1] ), Math.round( rgb[2] ) ];
+
+ assert.deepEqual( ret, [183, 240, 168], 'hsl(0.3, 0.7, 0.8): rgb(183, 240, 168)' );
+ } );
+
+ QUnit.test( 'getColorBrightness', 2, function ( assert ) {
+ var a, b;
+ a = $.colorUtil.getColorBrightness( 'red', +0.1 );
+ assert.equal( a, 'rgb(255,50,50)', 'Start with named color "red", brighten 10%' );
+
+ b = $.colorUtil.getColorBrightness( 'rgb(200,50,50)', -0.2 );
+ assert.equal( b, 'rgb(118,29,29)', 'Start with rgb string "rgb(200,50,50)", darken 20%' );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js b/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js
new file mode 100644
index 00000000..0b7e87ee
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js
@@ -0,0 +1,13 @@
+( function ( $ ) {
+ QUnit.module( 'jquery.getAttrs', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Check', 1, function ( assert ) {
+ var attrs = {
+ foo: 'bar',
+ 'class': 'lorem'
+ },
+ $el = $( '<div>' ).attr( attrs );
+
+ assert.deepEqual( $el.getAttrs(), attrs, 'getAttrs() return object should match the attributes set, no more, no less' );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js b/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js
new file mode 100644
index 00000000..906369ee
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js
@@ -0,0 +1,22 @@
+( function ( $ ) {
+ QUnit.module( 'jquery.hidpi', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'devicePixelRatio', 1, function ( assert ) {
+ var devicePixelRatio = $.devicePixelRatio();
+ assert.equal( typeof devicePixelRatio, 'number', '$.devicePixelRatio() returns a number' );
+ } );
+
+ QUnit.test( 'matchSrcSet', 6, function ( assert ) {
+ var srcset = 'onefive.png 1.5x, two.png 2x';
+
+ // Nice exact matches
+ assert.equal( $.matchSrcSet( 1, srcset ), null, '1.0 gives no match' );
+ assert.equal( $.matchSrcSet( 1.5, srcset ), 'onefive.png', '1.5 gives match' );
+ assert.equal( $.matchSrcSet( 2, srcset ), 'two.png', '2 gives match' );
+
+ // Non-exact matches; should return the next-biggest specified
+ assert.equal( $.matchSrcSet( 1.25, srcset ), null, '1.25 gives no match' );
+ assert.equal( $.matchSrcSet( 1.75, srcset ), 'onefive.png', '1.75 gives match to 1.5' );
+ assert.equal( $.matchSrcSet( 2.25, srcset ), 'two.png', '2.25 gives match to 2' );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js b/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js
new file mode 100644
index 00000000..e1fb96dc
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js
@@ -0,0 +1,235 @@
+( function ( $ ) {
+ QUnit.module( 'jquery.highlightText', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Check', function ( assert ) {
+ var $fixture, cases = [
+ {
+ desc: 'Test 001',
+ text: 'Blue Öyster Cult',
+ highlight: 'Blue',
+ expected: '<span class="highlight">Blue</span> Öyster Cult'
+ },
+ {
+ desc: 'Test 002',
+ text: 'Blue Öyster Cult',
+ highlight: 'Blue ',
+ expected: '<span class="highlight">Blue</span> Öyster Cult'
+ },
+ {
+ desc: 'Test 003',
+ text: 'Blue Öyster Cult',
+ highlight: 'Blue Ö',
+ expected: '<span class="highlight">Blue</span> <span class="highlight">Ö</span>yster Cult'
+ },
+ {
+ desc: 'Test 004',
+ text: 'Blue Öyster Cult',
+ highlight: 'Blue Öy',
+ expected: '<span class="highlight">Blue</span> <span class="highlight">Öy</span>ster Cult'
+ },
+ {
+ desc: 'Test 005',
+ text: 'Blue Öyster Cult',
+ highlight: ' Blue',
+ expected: '<span class="highlight">Blue</span> Öyster Cult'
+ },
+ {
+ desc: 'Test 006',
+ text: 'Blue Öyster Cult',
+ highlight: ' Blue ',
+ expected: '<span class="highlight">Blue</span> Öyster Cult'
+ },
+ {
+ desc: 'Test 007',
+ text: 'Blue Öyster Cult',
+ highlight: ' Blue Ö',
+ expected: '<span class="highlight">Blue</span> <span class="highlight">Ö</span>yster Cult'
+ },
+ {
+ desc: 'Test 008',
+ text: 'Blue Öyster Cult',
+ highlight: ' Blue Öy',
+ expected: '<span class="highlight">Blue</span> <span class="highlight">Öy</span>ster Cult'
+ },
+ {
+ desc: 'Test 009: Highlighter broken on starting Umlaut?',
+ text: 'Österreich',
+ highlight: 'Österreich',
+ expected: '<span class="highlight">Österreich</span>'
+ },
+ {
+ desc: 'Test 010: Highlighter broken on starting Umlaut?',
+ text: 'Österreich',
+ highlight: 'Ö',
+ expected: '<span class="highlight">Ö</span>sterreich'
+ },
+ {
+ desc: 'Test 011: Highlighter broken on starting Umlaut?',
+ text: 'Österreich',
+ highlight: 'Öst',
+ expected: '<span class="highlight">Öst</span>erreich'
+ },
+ {
+ desc: 'Test 012: Highlighter broken on starting Umlaut?',
+ text: 'Österreich',
+ highlight: 'Oe',
+ expected: 'Österreich'
+ },
+ {
+ desc: 'Test 013: Highlighter broken on punctuation mark?',
+ text: 'So good. To be there',
+ highlight: 'good',
+ expected: 'So <span class="highlight">good</span>. To be there'
+ },
+ {
+ desc: 'Test 014: Highlighter broken on space?',
+ text: 'So good. To be there',
+ highlight: 'be',
+ expected: 'So good. To <span class="highlight">be</span> there'
+ },
+ {
+ desc: 'Test 015: Highlighter broken on space?',
+ text: 'So good. To be there',
+ highlight: ' be',
+ expected: 'So good. To <span class="highlight">be</span> there'
+ },
+ {
+ desc: 'Test 016: Highlighter broken on space?',
+ text: 'So good. To be there',
+ highlight: 'be ',
+ expected: 'So good. To <span class="highlight">be</span> there'
+ },
+ {
+ desc: 'Test 017: Highlighter broken on space?',
+ text: 'So good. To be there',
+ highlight: ' be ',
+ expected: 'So good. To <span class="highlight">be</span> there'
+ },
+ {
+ desc: 'Test 018: en de Highlighter broken on special character at the end?',
+ text: 'So good. xbß',
+ highlight: 'xbß',
+ expected: 'So good. <span class="highlight">xbß</span>'
+ },
+ {
+ desc: 'Test 019: en de Highlighter broken on special character at the end?',
+ text: 'So good. xbß.',
+ highlight: 'xbß.',
+ expected: 'So good. <span class="highlight">xbß.</span>'
+ },
+ {
+ desc: 'Test 020: RTL he Hebrew',
+ text: 'חסיד אומות העולם',
+ highlight: 'חסיד אומות העולם',
+ expected: '<span class="highlight">חסיד</span> <span class="highlight">אומות</span> <span class="highlight">העולם</span>'
+ },
+ {
+ desc: 'Test 021: RTL he Hebrew',
+ text: 'חסיד אומות העולם',
+ highlight: 'חסי',
+ expected: '<span class="highlight">חסי</span>ד אומות העולם'
+ },
+ {
+ desc: 'Test 022: ja Japanese',
+ text: '諸国民の中の正義の人',
+ highlight: '諸国民の中の正義の人',
+ expected: '<span class="highlight">諸国民の中の正義の人</span>'
+ },
+ {
+ desc: 'Test 023: ja Japanese',
+ text: '諸国民の中の正義の人',
+ highlight: '諸国',
+ expected: '<span class="highlight">諸国</span>民の中の正義の人'
+ },
+ {
+ desc: 'Test 024: fr French text and « french quotes » (guillemets)',
+ text: '« L\'oiseau est sur l’île »',
+ highlight: '« L\'oiseau est sur l’île »',
+ expected: '<span class="highlight">«</span> <span class="highlight">L\'oiseau</span> <span class="highlight">est</span> <span class="highlight">sur</span> <span class="highlight">l’île</span> <span class="highlight">»</span>'
+ },
+ {
+ desc: 'Test 025: fr French text and « french quotes » (guillemets)',
+ text: '« L\'oiseau est sur l’île »',
+ highlight: '« L\'oise',
+ expected: '<span class="highlight">«</span> <span class="highlight">L\'oise</span>au est sur l’île »'
+ },
+ {
+ desc: 'Test 025a: fr French text and « french quotes » (guillemets) - does it match the single strings "«" and "L" separately?',
+ text: '« L\'oiseau est sur l’île »',
+ highlight: '« L',
+ expected: '<span class="highlight">«</span> <span class="highlight">L</span>\'oiseau est sur <span class="highlight">l</span>’île »'
+ },
+ {
+ desc: 'Test 026: ru Russian',
+ text: 'Праведники мира',
+ highlight: 'Праведники мира',
+ expected: '<span class="highlight">Праведники</span> <span class="highlight">мира</span>'
+ },
+ {
+ desc: 'Test 027: ru Russian',
+ text: 'Праведники мира',
+ highlight: 'Праве',
+ expected: '<span class="highlight">Праве</span>дники мира'
+ },
+ {
+ desc: 'Test 028 ka Georgian',
+ text: 'მთავარი გვერდი',
+ highlight: 'მთავარი გვერდი',
+ expected: '<span class="highlight">მთავარი</span> <span class="highlight">გვერდი</span>'
+ },
+ {
+ desc: 'Test 029 ka Georgian',
+ text: 'მთავარი გვერდი',
+ highlight: 'მთა',
+ expected: '<span class="highlight">მთა</span>ვარი გვერდი'
+ },
+ {
+ desc: 'Test 030 hy Armenian',
+ text: 'Նոնա Գափրինդաշվիլի',
+ highlight: 'Նոնա Գափրինդաշվիլի',
+ expected: '<span class="highlight">Նոնա</span> <span class="highlight">Գափրինդաշվիլի</span>'
+ },
+ {
+ desc: 'Test 031 hy Armenian',
+ text: 'Նոնա Գափրինդաշվիլի',
+ highlight: 'Նոն',
+ expected: '<span class="highlight">Նոն</span>ա Գափրինդաշվիլի'
+ },
+ {
+ desc: 'Test 032: th Thai',
+ text: 'พอล แอร์ดิช',
+ highlight: 'พอล แอร์ดิช',
+ expected: '<span class="highlight">พอล</span> <span class="highlight">แอร์ดิช</span>'
+ },
+ {
+ desc: 'Test 033: th Thai',
+ text: 'พอล แอร์ดิช',
+ highlight: 'พอ',
+ expected: '<span class="highlight">พอ</span>ล แอร์ดิช'
+ },
+ {
+ desc: 'Test 034: RTL ar Arabic',
+ text: 'بول إيردوس',
+ highlight: 'بول إيردوس',
+ expected: '<span class="highlight">بول</span> <span class="highlight">إيردوس</span>'
+ },
+ {
+ desc: 'Test 035: RTL ar Arabic',
+ text: 'بول إيردوس',
+ highlight: 'بو',
+ expected: '<span class="highlight">بو</span>ل إيردوس'
+ }
+ ];
+ QUnit.expect( cases.length );
+
+ $.each( cases, function ( i, item ) {
+ $fixture = $( '<p>' ).text( item.text ).highlightText( item.highlight );
+ assert.equal(
+ $fixture.html(),
+ // Re-parse to normalize
+ $( '<p>' ).html( item.expected ).html(),
+ item.desc || undefined
+ );
+ } );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.localize.test.js b/tests/qunit/suites/resources/jquery/jquery.localize.test.js
new file mode 100644
index 00000000..3ef27903
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.localize.test.js
@@ -0,0 +1,135 @@
+( function ( $, mw ) {
+ QUnit.module( 'jquery.localize', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Handle basic replacements', 4, function ( assert ) {
+ var html, $lc;
+ mw.messages.set( 'basic', 'Basic stuff' );
+
+ // Tag: html:msg
+ html = '<div><span><html:msg key="basic" /></span></div>';
+ $lc = $( html ).localize().find( 'span' );
+
+ assert.strictEqual( $lc.text(), 'Basic stuff', 'Tag: html:msg' );
+
+ // Attribute: title-msg
+ html = '<div><span title-msg="basic"></span></div>';
+ $lc = $( html ).localize().find( 'span' );
+
+ assert.strictEqual( $lc.attr( 'title' ), 'Basic stuff', 'Attribute: title-msg' );
+
+ // Attribute: alt-msg
+ html = '<div><span alt-msg="basic"></span></div>';
+ $lc = $( html ).localize().find( 'span' );
+
+ assert.strictEqual( $lc.attr( 'alt' ), 'Basic stuff', 'Attribute: alt-msg' );
+
+ // Attribute: placeholder-msg
+ html = '<div><input placeholder-msg="basic" /></div>';
+ $lc = $( html ).localize().find( 'input' );
+
+ assert.strictEqual( $lc.attr( 'placeholder' ), 'Basic stuff', 'Attribute: placeholder-msg' );
+ } );
+
+ QUnit.test( 'Proper escaping', 2, function ( assert ) {
+ var html, $lc;
+ mw.messages.set( 'properfoo', '<proper esc="test">' );
+
+ // This is handled by jQuery inside $.fn.localize, just a simple sanity checked
+ // making sure it is actually using text() and attr() (or something with the same effect)
+
+ // Text escaping
+ html = '<div><span><html:msg key="properfoo" /></span></div>';
+ $lc = $( html ).localize().find( 'span' );
+
+ assert.strictEqual( $lc.text(), mw.msg( 'properfoo' ), 'Content is inserted as text, not as html.' );
+
+ // Attribute escaping
+ html = '<div><span title-msg="properfoo"></span></div>';
+ $lc = $( html ).localize().find( 'span' );
+
+ assert.strictEqual( $lc.attr( 'title' ), mw.msg( 'properfoo' ), 'Attributes are not inserted raw.' );
+ } );
+
+ QUnit.test( 'Options', 7, function ( assert ) {
+ mw.messages.set( {
+ 'foo-lorem': 'Lorem',
+ 'foo-ipsum': 'Ipsum',
+ 'foo-bar-title': 'Read more about bars',
+ 'foo-bar-label': 'The Bars',
+ 'foo-bazz-title': 'Read more about bazz at $1 (last modified: $2)',
+ 'foo-bazz-label': 'The Bazz ($1)',
+ 'foo-welcome': 'Welcome to $1! (last visit: $2)'
+ } );
+ var html, $lc, x, sitename = 'Wikipedia';
+
+ // Message key prefix
+ html = '<div><span title-msg="lorem"><html:msg key="ipsum" /></span></div>';
+ $lc = $( html ).localize( {
+ prefix: 'foo-'
+ } ).find( 'span' );
+
+ assert.strictEqual( $lc.attr( 'title' ), 'Lorem', 'Message key prefix - attr' );
+ assert.strictEqual( $lc.text(), 'Ipsum', 'Message key prefix - text' );
+
+ // Variable keys mapping
+ x = 'bar';
+ html = '<div><span title-msg="title"><html:msg key="label" /></span></div>';
+ $lc = $( html ).localize( {
+ keys: {
+ 'title': 'foo-' + x + '-title',
+ 'label': 'foo-' + x + '-label'
+ }
+ } ).find( 'span' );
+
+ assert.strictEqual( $lc.attr( 'title' ), 'Read more about bars', 'Variable keys mapping - attr' );
+ assert.strictEqual( $lc.text(), 'The Bars', 'Variable keys mapping - text' );
+
+ // Passing parameteters to mw.msg
+ html = '<div><span><html:msg key="foo-welcome" /></span></div>';
+ $lc = $( html ).localize( {
+ params: {
+ 'foo-welcome': [sitename, 'yesterday']
+ }
+ } ).find( 'span' );
+
+ assert.strictEqual( $lc.text(), 'Welcome to Wikipedia! (last visit: yesterday)', 'Passing parameteters to mw.msg' );
+
+ // Combination of options prefix, params and keys
+ x = 'bazz';
+ html = '<div><span title-msg="title"><html:msg key="label" /></span></div>';
+ $lc = $( html ).localize( {
+ prefix: 'foo-',
+ keys: {
+ 'title': x + '-title',
+ 'label': x + '-label'
+ },
+ params: {
+ 'title': [sitename, '3 minutes ago'],
+ 'label': [sitename, '3 minutes ago']
+
+ }
+ } ).find( 'span' );
+
+ assert.strictEqual( $lc.text(), 'The Bazz (Wikipedia)', 'Combination of options prefix, params and keys - text' );
+ assert.strictEqual( $lc.attr( 'title' ), 'Read more about bazz at Wikipedia (last modified: 3 minutes ago)', 'Combination of options prefix, params and keys - attr' );
+ } );
+
+ QUnit.test( 'Handle data text', 2, function ( assert ) {
+ var html, $lc;
+ mw.messages.set( 'option-one', 'Item 1' );
+ mw.messages.set( 'option-two', 'Item 2' );
+ html = '<select><option data-msg-text="option-one"></option><option data-msg-text="option-two"></option></select>';
+ $lc = $( html ).localize().find( 'option' );
+ assert.strictEqual( $lc.eq( 0 ).text(), mw.msg( 'option-one' ), 'data-msg-text becomes text of options' );
+ assert.strictEqual( $lc.eq( 1 ).text(), mw.msg( 'option-two' ), 'data-msg-text becomes text of options' );
+ } );
+
+ QUnit.test( 'Handle data html', 2, function ( assert ) {
+ var html, $lc;
+ mw.messages.set( 'html', 'behold... there is a <a>link</a> here!!' );
+ html = '<div><div data-msg-html="html"></div></div>';
+ $lc = $( html ).localize().find( 'a' );
+ assert.strictEqual( $lc.length, 1, 'link is created' );
+ assert.strictEqual( $lc.text(), 'link', 'the link text got added' );
+ } );
+}( jQuery, mediaWiki ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js b/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js
new file mode 100644
index 00000000..80405819
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js
@@ -0,0 +1,339 @@
+( function ( mw, $ ) {
+ var loremIpsum = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.';
+
+ QUnit.module( 'jquery.makeCollapsible', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.clock = this.sandbox.useFakeTimers();
+ }
+ } ) );
+
+ function prepareCollapsible( html, options ) {
+ return $( $.parseHTML( html ) )
+ .appendTo( '#qunit-fixture' )
+ // options might be undefined here - this is okay
+ .makeCollapsible( options );
+ }
+
+ // This test is first because if it fails, then almost all of the latter tests are meaningless.
+ QUnit.test( 'testing hooks/triggers', 4, function ( assert ) {
+ var test = this,
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>'
+ ),
+ $content = $collapsible.find( '.mw-collapsible-content' ),
+ $toggle = $collapsible.find( '.mw-collapsible-toggle' );
+
+ // In one full collapse-expand cycle, each event will be fired once
+
+ // On collapse...
+ $collapsible.on( 'beforeCollapse.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'first beforeCollapseExpand: content is visible' );
+ } );
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':hidden' ), 'first afterCollapseExpand: content is hidden' );
+
+ // On expand...
+ $collapsible.on( 'beforeExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':hidden' ), 'second beforeCollapseExpand: content is hidden' );
+ } );
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'second afterCollapseExpand: content is visible' );
+ } );
+
+ // ...expanding happens here
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ } );
+
+ // ...collapsing happens here
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ } );
+
+ QUnit.test( 'basic operation (<div>)', 5, function ( assert ) {
+ var test = this,
+ $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>'
+ ),
+ $content = $collapsible.find( '.mw-collapsible-content' ),
+ $toggle = $collapsible.find( '.mw-collapsible-toggle' );
+
+ assert.equal( $content.length, 1, 'content is present' );
+ assert.equal( $content.find( $toggle ).length, 0, 'toggle is not a descendant of content' );
+
+ assert.assertTrue( $content.is( ':visible' ), 'content is visible' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':hidden' ), 'after collapsing: content is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' );
+ } );
+
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ } );
+
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ } );
+
+ QUnit.test( 'basic operation (<table>)', 7, function ( assert ) {
+ var test = this,
+ $collapsible = prepareCollapsible(
+ '<table class="mw-collapsible">' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '</table>'
+ ),
+ $headerRow = $collapsible.find( 'tr:first' ),
+ $contentRow = $collapsible.find( 'tr:last' ),
+ $toggle = $headerRow.find( 'td:last .mw-collapsible-toggle' );
+
+ assert.equal( $toggle.length, 1, 'toggle is added to last cell of first row' );
+
+ assert.assertTrue( $headerRow.is( ':visible' ), 'headerRow is visible' );
+ assert.assertTrue( $contentRow.is( ':visible' ), 'contentRow is visible' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.assertTrue( $headerRow.is( ':visible' ), 'after collapsing: headerRow is still visible' );
+ assert.assertTrue( $contentRow.is( ':hidden' ), 'after collapsing: contentRow is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $headerRow.is( ':visible' ), 'after expanding: headerRow is still visible' );
+ assert.assertTrue( $contentRow.is( ':visible' ), 'after expanding: contentRow is visible' );
+ } );
+
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ } );
+
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ } );
+
+ function tableWithCaptionTest( $collapsible, test, assert ) {
+ var $caption = $collapsible.find( 'caption' ),
+ $headerRow = $collapsible.find( 'tr:first' ),
+ $contentRow = $collapsible.find( 'tr:last' ),
+ $toggle = $caption.find( '.mw-collapsible-toggle' );
+
+ assert.equal( $toggle.length, 1, 'toggle is added to the end of the caption' );
+
+ assert.assertTrue( $caption.is( ':visible' ), 'caption is visible' );
+ assert.assertTrue( $headerRow.is( ':visible' ), 'headerRow is visible' );
+ assert.assertTrue( $contentRow.is( ':visible' ), 'contentRow is visible' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.assertTrue( $caption.is( ':visible' ), 'after collapsing: caption is still visible' );
+ assert.assertTrue( $headerRow.is( ':hidden' ), 'after collapsing: headerRow is hidden' );
+ assert.assertTrue( $contentRow.is( ':hidden' ), 'after collapsing: contentRow is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $caption.is( ':visible' ), 'after expanding: caption is still visible' );
+ assert.assertTrue( $headerRow.is( ':visible' ), 'after expanding: headerRow is visible' );
+ assert.assertTrue( $contentRow.is( ':visible' ), 'after expanding: contentRow is visible' );
+ } );
+
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ } );
+
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ }
+
+ QUnit.test( 'basic operation (<table> with caption)', 10, function ( assert ) {
+ tableWithCaptionTest( prepareCollapsible(
+ '<table class="mw-collapsible">' +
+ '<caption>' + loremIpsum + '</caption>' +
+ '<tr><th>' + loremIpsum + '</th><th>' + loremIpsum + '</th></tr>' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '</table>'
+ ), this, assert );
+ } );
+
+ QUnit.test( 'basic operation (<table> with caption and <thead>)', 10, function ( assert ) {
+ tableWithCaptionTest( prepareCollapsible(
+ '<table class="mw-collapsible">' +
+ '<caption>' + loremIpsum + '</caption>' +
+ '<thead><tr><th>' + loremIpsum + '</th><th>' + loremIpsum + '</th></tr></thead>' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
+ '</table>'
+ ), this, assert );
+ } );
+
+ function listTest( listType, test, assert ) {
+ var $collapsible = prepareCollapsible(
+ '<' + listType + ' class="mw-collapsible">' +
+ '<li>' + loremIpsum + '</li>' +
+ '<li>' + loremIpsum + '</li>' +
+ '</' + listType + '>'
+ ),
+ $toggleItem = $collapsible.find( 'li.mw-collapsible-toggle-li:first-child' ),
+ $contentItem = $collapsible.find( 'li:last' ),
+ $toggle = $toggleItem.find( '.mw-collapsible-toggle' );
+
+ assert.equal( $toggle.length, 1, 'toggle is present, added inside new zeroth list item' );
+
+ assert.assertTrue( $toggleItem.is( ':visible' ), 'toggleItem is visible' );
+ assert.assertTrue( $contentItem.is( ':visible' ), 'contentItem is visible' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.assertTrue( $toggleItem.is( ':visible' ), 'after collapsing: toggleItem is still visible' );
+ assert.assertTrue( $contentItem.is( ':hidden' ), 'after collapsing: contentItem is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $toggleItem.is( ':visible' ), 'after expanding: toggleItem is still visible' );
+ assert.assertTrue( $contentItem.is( ':visible' ), 'after expanding: contentItem is visible' );
+ } );
+
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ } );
+
+ $toggle.trigger( 'click' );
+ test.clock.tick( 500 );
+ }
+
+ QUnit.test( 'basic operation (<ul>)', 7, function ( assert ) {
+ listTest( 'ul', this, assert );
+ } );
+
+ QUnit.test( 'basic operation (<ol>)', 7, function ( assert ) {
+ listTest( 'ol', this, assert );
+ } );
+
+ QUnit.test( 'basic operation when synchronous (options.instantHide)', 2, function ( assert ) {
+ var $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>',
+ { instantHide: true }
+ ),
+ $content = $collapsible.find( '.mw-collapsible-content' );
+
+ assert.assertTrue( $content.is( ':visible' ), 'content is visible' );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+
+ assert.assertTrue( $content.is( ':hidden' ), 'after collapsing: content is hidden' );
+ } );
+
+ QUnit.test( 'mw-made-collapsible data added', 1, function ( assert ) {
+ var $collapsible = prepareCollapsible(
+ '<div>' + loremIpsum + '</div>'
+ );
+
+ assert.equal( $collapsible.data( 'mw-made-collapsible' ), true, 'mw-made-collapsible data present' );
+ } );
+
+ QUnit.test( 'mw-collapsible added when missing', 1, function ( assert ) {
+ var $collapsible = prepareCollapsible(
+ '<div>' + loremIpsum + '</div>'
+ );
+
+ assert.assertTrue( $collapsible.hasClass( 'mw-collapsible' ), 'mw-collapsible class present' );
+ } );
+
+ QUnit.test( 'mw-collapsed added when missing', 1, function ( assert ) {
+ var $collapsible = prepareCollapsible(
+ '<div>' + loremIpsum + '</div>',
+ { collapsed: true }
+ );
+
+ assert.assertTrue( $collapsible.hasClass( 'mw-collapsed' ), 'mw-collapsed class present' );
+ } );
+
+ QUnit.test( 'initial collapse (mw-collapsed class)', 2, function ( assert ) {
+ var $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible mw-collapsed">' + loremIpsum + '</div>'
+ ),
+ $content = $collapsible.find( '.mw-collapsible-content' );
+
+ // Synchronous - mw-collapsed should cause instantHide: true to be used on initial collapsing
+ assert.assertTrue( $content.is( ':hidden' ), 'content is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' );
+ } );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+ this.clock.tick( 500 );
+ } );
+
+ QUnit.test( 'initial collapse (options.collapsed)', 2, function ( assert ) {
+ var $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>',
+ { collapsed: true }
+ ),
+ $content = $collapsible.find( '.mw-collapsible-content' );
+
+ // Synchronous - collapsed: true should cause instantHide: true to be used on initial collapsing
+ assert.assertTrue( $content.is( ':hidden' ), 'content is hidden' );
+
+ $collapsible.on( 'afterExpand.mw-collapsible', function () {
+ assert.assertTrue( $content.is( ':visible' ), 'after expanding: content is visible' );
+ } );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+ this.clock.tick( 500 );
+ } );
+
+ QUnit.test( 'clicks on links inside toggler pass through (options.linksPassthru)', 2, function ( assert ) {
+ var $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' +
+ '<div class="mw-collapsible-toggle">' +
+ 'Toggle <a href="#top">toggle</a> toggle <b>toggle</b>' +
+ '</div>' +
+ '<div class="mw-collapsible-content">' + loremIpsum + '</div>' +
+ '</div>',
+ // Can't do asynchronous because we're testing that the event *doesn't* happen
+ { instantHide: true }
+ ),
+ $content = $collapsible.find( '.mw-collapsible-content' );
+
+ $collapsible.find( '.mw-collapsible-toggle a' ).trigger( 'click' );
+ assert.assertTrue( $content.is( ':visible' ), 'click event on link inside toggle passes through (content not toggled)' );
+
+ $collapsible.find( '.mw-collapsible-toggle b' ).trigger( 'click' );
+ assert.assertTrue( $content.is( ':hidden' ), 'click event on non-link inside toggle toggles content' );
+ } );
+
+ QUnit.test( 'collapse/expand text (data-collapsetext, data-expandtext)', 2, function ( assert ) {
+ var $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible" data-collapsetext="Collapse me!" data-expandtext="Expand me!">' +
+ loremIpsum +
+ '</div>'
+ ),
+ $toggleLink = $collapsible.find( '.mw-collapsible-toggle a' );
+
+ assert.equal( $toggleLink.text(), 'Collapse me!', 'data-collapsetext is respected' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.equal( $toggleLink.text(), 'Expand me!', 'data-expandtext is respected' );
+ } );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+ this.clock.tick( 500 );
+ } );
+
+ QUnit.test( 'collapse/expand text (options.collapseText, options.expandText)', 2, function ( assert ) {
+ var $collapsible = prepareCollapsible(
+ '<div class="mw-collapsible">' + loremIpsum + '</div>',
+ { collapseText: 'Collapse me!', expandText: 'Expand me!' }
+ ),
+ $toggleLink = $collapsible.find( '.mw-collapsible-toggle a' );
+
+ assert.equal( $toggleLink.text(), 'Collapse me!', 'options.collapseText is respected' );
+
+ $collapsible.on( 'afterCollapse.mw-collapsible', function () {
+ assert.equal( $toggleLink.text(), 'Expand me!', 'options.expandText is respected' );
+ } );
+
+ $collapsible.find( '.mw-collapsible-toggle' ).trigger( 'click' );
+ this.clock.tick( 500 );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js b/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
new file mode 100644
index 00000000..795c2bbb
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js
@@ -0,0 +1,55 @@
+( function ( $ ) {
+ QUnit.module( 'jquery.mwExtension', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'String functions', 7, function ( assert ) {
+ assert.equal( $.trimLeft( ' foo bar ' ), 'foo bar ', 'trimLeft' );
+ assert.equal( $.trimRight( ' foo bar ' ), ' foo bar', 'trimRight' );
+ assert.equal( $.ucFirst( 'foo' ), 'Foo', 'ucFirst' );
+
+ assert.equal( $.escapeRE( '<!-- ([{+mW+}]) $^|?>' ),
+ '<!\\-\\- \\(\\[\\{\\+mW\\+\\}\\]\\) \\$\\^\\|\\?>', 'escapeRE - Escape specials' );
+ assert.equal( $.escapeRE( 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ),
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'escapeRE - Leave uppercase alone' );
+ assert.equal( $.escapeRE( 'abcdefghijklmnopqrstuvwxyz' ),
+ 'abcdefghijklmnopqrstuvwxyz', 'escapeRE - Leave lowercase alone' );
+ assert.equal( $.escapeRE( '0123456789' ), '0123456789', 'escapeRE - Leave numbers alone' );
+ } );
+
+ QUnit.test( 'isDomElement', 6, function ( assert ) {
+ assert.strictEqual( $.isDomElement( document.createElement( 'div' ) ), true,
+ 'isDomElement: HTMLElement' );
+ assert.strictEqual( $.isDomElement( document.createTextNode( '' ) ), true,
+ 'isDomElement: TextNode' );
+ assert.strictEqual( $.isDomElement( null ), false,
+ 'isDomElement: null' );
+ assert.strictEqual( $.isDomElement( document.getElementsByTagName( 'div' ) ), false,
+ 'isDomElement: NodeList' );
+ assert.strictEqual( $.isDomElement( $( 'div' ) ), false,
+ 'isDomElement: jQuery' );
+ assert.strictEqual( $.isDomElement( { foo: 1 } ), false,
+ 'isDomElement: Plain Object' );
+ } );
+
+ QUnit.test( 'isEmpty', 7, function ( assert ) {
+ assert.strictEqual( $.isEmpty( 'string' ), false, 'isEmpty: "string"' );
+ assert.strictEqual( $.isEmpty( '0' ), true, 'isEmpty: "0"' );
+ assert.strictEqual( $.isEmpty( '' ), true, 'isEmpty: ""' );
+ assert.strictEqual( $.isEmpty( 1 ), false, 'isEmpty: 1' );
+ assert.strictEqual( $.isEmpty( [] ), true, 'isEmpty: []' );
+ assert.strictEqual( $.isEmpty( {} ), true, 'isEmpty: {}' );
+
+ // Documented behavior
+ assert.strictEqual( $.isEmpty( { length: 0 } ), true, 'isEmpty: { length: 0 }' );
+ } );
+
+ QUnit.test( 'Comparison functions', 5, function ( assert ) {
+ assert.ok( $.compareArray( [0, 'a', [], [2, 'b'] ], [0, 'a', [], [2, 'b'] ] ),
+ 'compareArray: Two deep arrays that are excactly the same' );
+ assert.ok( !$.compareArray( [1], [2] ), 'compareArray: Two different arrays (false)' );
+
+ assert.ok( $.compareObject( {}, {} ), 'compareObject: Two empty objects' );
+ assert.ok( $.compareObject( { foo: 1 }, { foo: 1 } ), 'compareObject: Two the same objects' );
+ assert.ok( !$.compareObject( { bar: true }, { baz: false } ),
+ 'compareObject: Two different objects (false)' );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.placeholder.test.js b/tests/qunit/suites/resources/jquery/jquery.placeholder.test.js
new file mode 100644
index 00000000..bbea8297
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.placeholder.test.js
@@ -0,0 +1,145 @@
+(function ($) {
+
+ QUnit.module('jquery.placeholder', QUnit.newMwEnvironment());
+
+ QUnit.test('caches results of feature tests', 2, function (assert) {
+ assert.strictEqual(typeof $.fn.placeholder.input, 'boolean', '$.fn.placeholder.input');
+ assert.strictEqual(typeof $.fn.placeholder.textarea, 'boolean', '$.fn.placeholder.textarea');
+ });
+
+ if ($.fn.placeholder.input && $.fn.placeholder.textarea) {
+ return;
+ }
+
+ var html = '<form>' +
+ '<input id="input-type-search" type="search" placeholder="Search this site...">' +
+ '<input id="input-type-text" type="text" placeholder="e.g. John Doe">' +
+ '<input id="input-type-email" type="email" placeholder="e.g. address@example.ext">' +
+ '<input id="input-type-url" type="url" placeholder="e.g. http://mathiasbynens.be/">' +
+ '<input id="input-type-tel" type="tel" placeholder="e.g. +32 472 77 69 88">' +
+ '<input id="input-type-password" type="password" placeholder="e.g. hunter2">' +
+ '<textarea id="textarea" name="message" placeholder="Your message goes here"></textarea>' +
+ '</form>',
+ testElement = function ($el, assert) {
+
+ var el = $el[0],
+ placeholder = el.getAttribute('placeholder');
+
+ assert.strictEqual($el.placeholder(), $el, 'should be chainable');
+
+ assert.strictEqual(el.value, placeholder, 'should set `placeholder` text as `value`');
+ assert.strictEqual($el.prop('value'), '', 'propHooks works properly');
+ assert.strictEqual($el.val(), '', 'valHooks works properly');
+ assert.ok($el.hasClass('placeholder'), 'should have `placeholder` class');
+
+ // test on focus
+ $el.focus();
+ assert.strictEqual(el.value, '', '`value` should be the empty string on focus');
+ assert.strictEqual($el.prop('value'), '', 'propHooks works properly');
+ assert.strictEqual($el.val(), '', 'valHooks works properly');
+ assert.ok(!$el.hasClass('placeholder'), 'should not have `placeholder` class on focus');
+
+ // and unfocus (blur) again
+ $el.blur();
+
+ assert.strictEqual(el.value, placeholder, 'should set `placeholder` text as `value`');
+ assert.strictEqual($el.prop('value'), '', 'propHooks works properly');
+ assert.strictEqual($el.val(), '', 'valHooks works properly');
+ assert.ok($el.hasClass('placeholder'), 'should have `placeholder` class');
+
+ // change the value
+ $el.val('lorem ipsum');
+ assert.strictEqual($el.prop('value'), 'lorem ipsum', '`$el.val(string)` should change the `value` property');
+ assert.strictEqual(el.value, 'lorem ipsum', '`$el.val(string)` should change the `value` attribute');
+ assert.ok(!$el.hasClass('placeholder'), '`$el.val(string)` should remove `placeholder` class');
+
+ // and clear it again
+ $el.val('');
+ assert.strictEqual($el.prop('value'), '', '`$el.val("")` should change the `value` property');
+ assert.strictEqual(el.value, placeholder, '`$el.val("")` should change the `value` attribute');
+ assert.ok($el.hasClass('placeholder'), '`$el.val("")` should re-enable `placeholder` class');
+
+ // make sure the placeholder property works as expected.
+ assert.strictEqual($el.prop('placeholder'), placeholder, '$el.prop(`placeholder`) should return the placeholder value');
+ $el.placeholder('new placeholder');
+ assert.strictEqual(el.getAttribute('placeholder'), 'new placeholder', '$el.placeholder(<string>) should set the placeholder value');
+ assert.strictEqual(el.value, 'new placeholder', '$el.placeholder(<string>) should update the displayed placeholder value');
+ $el.placeholder(placeholder);
+ };
+
+ QUnit.test('emulates placeholder for <input type=text>', 22, function (assert) {
+ $('<div>').html(html).appendTo($('#qunit-fixture'));
+ testElement($('#input-type-text'), assert);
+ });
+
+ QUnit.test('emulates placeholder for <input type=search>', 22, function (assert) {
+ $('<div>').html(html).appendTo($('#qunit-fixture'));
+ testElement($('#input-type-search'), assert);
+ });
+
+ QUnit.test('emulates placeholder for <input type=email>', 22, function (assert) {
+ $('<div>').html(html).appendTo($('#qunit-fixture'));
+ testElement($('#input-type-email'), assert);
+ });
+
+ QUnit.test('emulates placeholder for <input type=url>', 22, function (assert) {
+ $('<div>').html(html).appendTo($('#qunit-fixture'));
+ testElement($('#input-type-url'), assert);
+ });
+
+ QUnit.test('emulates placeholder for <input type=tel>', 22, function (assert) {
+ $('<div>').html(html).appendTo($('#qunit-fixture'));
+ testElement($('#input-type-tel'), assert);
+ });
+
+ QUnit.test('emulates placeholder for <input type=password>', 13, function (assert) {
+ $('<div>').html(html).appendTo($('#qunit-fixture'));
+
+ var selector = '#input-type-password',
+ $el = $(selector),
+ el = $el[0],
+ placeholder = el.getAttribute('placeholder');
+
+ assert.strictEqual($el.placeholder(), $el, 'should be chainable');
+
+ // Re-select the element, as it gets replaced by another one in some browsers
+ $el = $(selector);
+ el = $el[0];
+
+ assert.strictEqual(el.value, placeholder, 'should set `placeholder` text as `value`');
+ assert.strictEqual($el.prop('value'), '', 'propHooks works properly');
+ assert.strictEqual($el.val(), '', 'valHooks works properly');
+ assert.ok($el.hasClass('placeholder'), 'should have `placeholder` class');
+
+ // test on focus
+ $el.focus();
+
+ // Re-select the element, as it gets replaced by another one in some browsers
+ $el = $(selector);
+ el = $el[0];
+
+ assert.strictEqual(el.value, '', '`value` should be the empty string on focus');
+ assert.strictEqual($el.prop('value'), '', 'propHooks works properly');
+ assert.strictEqual($el.val(), '', 'valHooks works properly');
+ assert.ok(!$el.hasClass('placeholder'), 'should not have `placeholder` class on focus');
+
+ // and unfocus (blur) again
+ $el.blur();
+
+ // Re-select the element, as it gets replaced by another one in some browsers
+ $el = $(selector);
+ el = $el[0];
+
+ assert.strictEqual(el.value, placeholder, 'should set `placeholder` text as `value`');
+ assert.strictEqual($el.prop('value'), '', 'propHooks works properly');
+ assert.strictEqual($el.val(), '', 'valHooks works properly');
+ assert.ok($el.hasClass('placeholder'), 'should have `placeholder` class');
+
+ });
+
+ QUnit.test('emulates placeholder for <textarea></textarea>', 22, function (assert) {
+ $('<div>').html(html).appendTo($('#qunit-fixture'));
+ testElement($('#textarea'), assert);
+ });
+
+}(jQuery));
diff --git a/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js b/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js
new file mode 100644
index 00000000..12137931
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js
@@ -0,0 +1,35 @@
+( function ( $ ) {
+ QUnit.module( 'jquery.tabIndex', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'firstTabIndex', 2, function ( assert ) {
+ var html, $testA, $testB;
+ html = '<form>' +
+ '<input tabindex="7" />' +
+ '<input tabindex="9" />' +
+ '<textarea tabindex="2">Foobar</textarea>' +
+ '<textarea tabindex="5">Foobar</textarea>' +
+ '</form>';
+
+ $testA = $( '<div>' ).html( html ).appendTo( '#qunit-fixture' );
+ assert.strictEqual( $testA.firstTabIndex(), 2, 'First tabindex should be 2 within this context.' );
+
+ $testB = $( '<div>' );
+ assert.strictEqual( $testB.firstTabIndex(), null, 'Return null if none available.' );
+ } );
+
+ QUnit.test( 'lastTabIndex', 2, function ( assert ) {
+ var html, $testA, $testB;
+ html = '<form>' +
+ '<input tabindex="7" />' +
+ '<input tabindex="9" />' +
+ '<textarea tabindex="2">Foobar</textarea>' +
+ '<textarea tabindex="5">Foobar</textarea>' +
+ '</form>';
+
+ $testA = $( '<div>' ).html( html ).appendTo( '#qunit-fixture' );
+ assert.strictEqual( $testA.lastTabIndex(), 9, 'Last tabindex should be 9 within this context.' );
+
+ $testB = $( '<div>' );
+ assert.strictEqual( $testB.lastTabIndex(), null, 'Return null if none available.' );
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js
new file mode 100644
index 00000000..92dad9ff
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js
@@ -0,0 +1,1327 @@
+( function ( $, mw ) {
+ var header,
+
+ // Data set "simple"
+ a1 = [ 'A', '1' ],
+ a2 = [ 'A', '2' ],
+ a3 = [ 'A', '3' ],
+ b1 = [ 'B', '1' ],
+ b2 = [ 'B', '2' ],
+ b3 = [ 'B', '3' ],
+ simple = [a2, b3, a1, a3, b2, b1],
+ simpleAsc = [a1, a2, a3, b1, b2, b3],
+ simpleDescasc = [b1, b2, b3, a1, a2, a3],
+
+ // Data set "colspan"
+ aaa1 = [ 'A', 'A', 'A', '1' ],
+ aab5 = [ 'A', 'A', 'B', '5' ],
+ abc3 = [ 'A', 'B', 'C', '3' ],
+ bbc2 = [ 'B', 'B', 'C', '2' ],
+ caa4 = [ 'C', 'A', 'A', '4' ],
+ colspanInitial = [ aab5, aaa1, abc3, bbc2, caa4 ],
+
+ // Data set "planets"
+ mercury = [ 'Mercury', '2439.7' ],
+ venus = [ 'Venus', '6051.8' ],
+ earth = [ 'Earth', '6371.0' ],
+ mars = [ 'Mars', '3390.0' ],
+ jupiter = [ 'Jupiter', '69911' ],
+ saturn = [ 'Saturn', '58232' ],
+ planets = [mercury, venus, earth, mars, jupiter, saturn],
+ planetsAscName = [earth, jupiter, mars, mercury, saturn, venus],
+ planetsAscRadius = [mercury, mars, venus, earth, saturn, jupiter],
+ planetsRowspan,
+ planetsRowspanII,
+ planetsAscNameLegacy,
+
+ // Data set "ipv4"
+ ipv4 = [
+ // Some randomly generated fake IPs
+ ['45.238.27.109'],
+ ['44.172.9.22'],
+ ['247.240.82.209'],
+ ['204.204.132.158'],
+ ['170.38.91.162'],
+ ['197.219.164.9'],
+ ['45.68.154.72'],
+ ['182.195.149.80']
+ ],
+ ipv4Sorted = [
+ // Sort order should go octet by octet
+ ['44.172.9.22'],
+ ['45.68.154.72'],
+ ['45.238.27.109'],
+ ['170.38.91.162'],
+ ['182.195.149.80'],
+ ['197.219.164.9'],
+ ['204.204.132.158'],
+ ['247.240.82.209']
+ ],
+
+ // Data set "umlaut"
+ umlautWords = [
+ ['Günther'],
+ ['Peter'],
+ ['Björn'],
+ ['Bjorn'],
+ ['Apfel'],
+ ['Äpfel'],
+ ['Strasse'],
+ ['Sträßschen']
+ ],
+ umlautWordsSorted = [
+ ['Äpfel'],
+ ['Apfel'],
+ ['Björn'],
+ ['Bjorn'],
+ ['Günther'],
+ ['Peter'],
+ ['Sträßschen'],
+ ['Strasse']
+ ],
+
+ complexMDYDates = [
+ ['January, 19 2010'],
+ ['April 21 1991'],
+ ['04 22 1991'],
+ ['5.12.1990'],
+ ['December 12 \'10']
+ ],
+ complexMDYSorted = [
+ ['5.12.1990'],
+ ['April 21 1991'],
+ ['04 22 1991'],
+ ['January, 19 2010'],
+ ['December 12 \'10']
+ ],
+
+ currencyUnsorted = [
+ ['1.02 $'],
+ ['$ 3.00'],
+ ['€ 2,99'],
+ ['$ 1.00'],
+ ['$3.50'],
+ ['$ 1.50'],
+ ['€ 0.99']
+ ],
+ currencySorted = [
+ ['€ 0.99'],
+ ['$ 1.00'],
+ ['1.02 $'],
+ ['$ 1.50'],
+ ['$ 3.00'],
+ ['$3.50'],
+ // Comma's sort after dots
+ // Not intentional but test to detect changes
+ ['€ 2,99']
+ ],
+
+ numbers = [
+ [ '12' ],
+ [ '7' ],
+ [ '13,000'],
+ [ '9' ],
+ [ '14' ],
+ [ '8.0' ]
+ ],
+ numbersAsc = [
+ [ '7' ],
+ [ '8.0' ],
+ [ '9' ],
+ [ '12' ],
+ [ '14' ],
+ [ '13,000']
+ ],
+
+ correctDateSorting1 = [
+ ['01 January 2010'],
+ ['05 February 2010'],
+ ['16 January 2010']
+ ],
+ correctDateSortingSorted1 = [
+ ['01 January 2010'],
+ ['16 January 2010'],
+ ['05 February 2010']
+ ],
+
+ correctDateSorting2 = [
+ ['January 01 2010'],
+ ['February 05 2010'],
+ ['January 16 2010']
+ ],
+ correctDateSortingSorted2 = [
+ ['January 01 2010'],
+ ['January 16 2010'],
+ ['February 05 2010']
+ ];
+
+ QUnit.module( 'jquery.tablesorter', QUnit.newMwEnvironment( {
+ config: {
+ wgMonthNames: ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ wgMonthNamesShort: ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ wgDefaultDateFormat: 'dmy',
+ wgSeparatorTransformTable: ['', ''],
+ wgDigitTransformTable: ['', ''],
+ wgContentLanguage: 'en'
+ }
+ } ) );
+
+ /**
+ * Create an HTML table from an array of row arrays containing text strings.
+ * First row will be header row. No fancy rowspan/colspan stuff.
+ *
+ * @param {String[]} header
+ * @param {String[][]} data
+ * @return jQuery
+ */
+ function tableCreate( header, data ) {
+ var i,
+ $table = $( '<table class="sortable"><thead></thead><tbody></tbody></table>' ),
+ $thead = $table.find( 'thead' ),
+ $tbody = $table.find( 'tbody' ),
+ $tr = $( '<tr>' );
+
+ $.each( header, function ( i, str ) {
+ var $th = $( '<th>' );
+ $th.text( str ).appendTo( $tr );
+ } );
+ $tr.appendTo( $thead );
+
+ for ( i = 0; i < data.length; i++ ) {
+ /*jshint loopfunc: true */
+ $tr = $( '<tr>' );
+ $.each( data[i], function ( j, str ) {
+ var $td = $( '<td>' );
+ $td.text( str ).appendTo( $tr );
+ } );
+ $tr.appendTo( $tbody );
+ }
+ return $table;
+ }
+
+ /**
+ * Extract text from table.
+ *
+ * @param {jQuery} $table
+ * @return String[][]
+ */
+ function tableExtract( $table ) {
+ var data = [];
+
+ $table.find( 'tbody' ).find( 'tr' ).each( function ( i, tr ) {
+ var row = [];
+ $( tr ).find( 'td,th' ).each( function ( i, td ) {
+ row.push( $( td ).text() );
+ } );
+ data.push( row );
+ } );
+ return data;
+ }
+
+ /**
+ * Run a table test by building a table with the given data,
+ * running some callback on it, then checking the results.
+ *
+ * @param {String} msg text to pass on to qunit for the comparison
+ * @param {String[]} header cols to make the table
+ * @param {String[][]} data rows/cols to make the table
+ * @param {String[][]} expected rows/cols to compare against at end
+ * @param {function($table)} callback something to do with the table before we compare
+ */
+ function tableTest( msg, header, data, expected, callback ) {
+ QUnit.test( msg, 1, function ( assert ) {
+ var extracted,
+ $table = tableCreate( header, data );
+
+ // Give caller a chance to set up sorting and manipulate the table.
+ callback( $table );
+
+ // Table sorting is done synchronously; if it ever needs to change back
+ // to asynchronous, we'll need a timeout or a callback here.
+ extracted = tableExtract( $table );
+ assert.deepEqual( extracted, expected, msg );
+ } );
+ }
+
+ /**
+ * Run a table test by building a table with the given HTML,
+ * running some callback on it, then checking the results.
+ *
+ * @param {String} msg text to pass on to qunit for the comparison
+ * @param {String} HTML to make the table
+ * @param {String[][]} expected rows/cols to compare against at end
+ * @param {function($table)} callback something to do with the table before we compare
+ */
+ function tableTestHTML( msg, html, expected, callback ) {
+ QUnit.test( msg, 1, function ( assert ) {
+ var extracted,
+ $table = $( html );
+
+ // Give caller a chance to set up sorting and manipulate the table.
+ if ( callback ) {
+ callback( $table );
+ } else {
+ $table.tablesorter();
+ $table.find( '#sortme' ).click();
+ }
+
+ // Table sorting is done synchronously; if it ever needs to change back
+ // to asynchronous, we'll need a timeout or a callback here.
+ extracted = tableExtract( $table );
+ assert.deepEqual( extracted, expected, msg );
+ } );
+ }
+
+ function reversed( arr ) {
+ // Clone array
+ var arr2 = arr.slice( 0 );
+
+ arr2.reverse();
+
+ return arr2;
+ }
+
+ // Sample data set using planets named and their radius
+ header = [ 'Planet', 'Radius (km)'];
+
+ tableTest(
+ 'Basic planet table: sorting initially - ascending by name',
+ header,
+ planets,
+ planetsAscName,
+ function ( $table ) {
+ $table.tablesorter( { sortList: [
+ { 0: 'asc' }
+ ] } );
+ }
+ );
+ tableTest(
+ 'Basic planet table: sorting initially - descending by radius',
+ header,
+ planets,
+ reversed( planetsAscRadius ),
+ function ( $table ) {
+ $table.tablesorter( { sortList: [
+ { 1: 'desc' }
+ ] } );
+ }
+ );
+ tableTest(
+ 'Basic planet table: ascending by name',
+ header,
+ planets,
+ planetsAscName,
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest(
+ 'Basic planet table: ascending by name a second time',
+ header,
+ planets,
+ planetsAscName,
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest(
+ 'Basic planet table: ascending by name (multiple clicks)',
+ header,
+ planets,
+ planetsAscName,
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ $table.find( '.headerSort:eq(1)' ).click();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest(
+ 'Basic planet table: descending by name',
+ header,
+ planets,
+ reversed( planetsAscName ),
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click().click();
+ }
+ );
+ tableTest(
+ 'Basic planet table: ascending radius',
+ header,
+ planets,
+ planetsAscRadius,
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(1)' ).click();
+ }
+ );
+ tableTest(
+ 'Basic planet table: descending radius',
+ header,
+ planets,
+ reversed( planetsAscRadius ),
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(1)' ).click().click();
+ }
+ );
+
+ header = [ 'column1', 'column2' ];
+
+ tableTest(
+ 'Sorting multiple columns by passing sort list',
+ header,
+ simple,
+ simpleAsc,
+ function ( $table ) {
+ $table.tablesorter(
+ { sortList: [
+ { 0: 'asc' },
+ { 1: 'asc' }
+ ] }
+ );
+ }
+ );
+ tableTest(
+ 'Sorting multiple columns by programmatically triggering sort()',
+ header,
+ simple,
+ simpleDescasc,
+ function ( $table ) {
+ $table.tablesorter();
+ $table.data( 'tablesorter' ).sort(
+ [
+ { 0: 'desc' },
+ { 1: 'asc' }
+ ]
+ );
+ }
+ );
+ tableTest(
+ 'Reset to initial sorting by triggering sort() without any parameters',
+ header,
+ simple,
+ simpleAsc,
+ function ( $table ) {
+ $table.tablesorter(
+ { sortList: [
+ { 0: 'asc' },
+ { 1: 'asc' }
+ ] }
+ );
+ $table.data( 'tablesorter' ).sort(
+ [
+ { 0: 'desc' },
+ { 1: 'asc' }
+ ]
+ );
+ $table.data( 'tablesorter' ).sort();
+ }
+ );
+ tableTest(
+ 'Sort via click event after having initialized the tablesorter with initial sorting',
+ header,
+ simple,
+ simpleDescasc,
+ function ( $table ) {
+ $table.tablesorter(
+ { sortList: [ { 0: 'asc' }, { 1: 'asc' } ] }
+ );
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest(
+ 'Multi-sort via click event after having initialized the tablesorter with initial sorting',
+ header,
+ simple,
+ simpleAsc,
+ function ( $table ) {
+ $table.tablesorter(
+ { sortList: [ { 0: 'desc' }, { 1: 'desc' } ] }
+ );
+ $table.find( '.headerSort:eq(0)' ).click();
+
+ // Pretend to click while pressing the multi-sort key
+ var event = $.Event( 'click' );
+ event[$table.data( 'tablesorter' ).config.sortMultiSortKey] = true;
+ $table.find( '.headerSort:eq(1)' ).trigger( event );
+ }
+ );
+ QUnit.test( 'Reset sorting making table appear unsorted', 3, function ( assert ) {
+ var $table = tableCreate( header, simple );
+ $table.tablesorter(
+ { sortList: [
+ { 0: 'desc' },
+ { 1: 'asc' }
+ ] }
+ );
+ $table.data( 'tablesorter' ).sort( [] );
+
+ assert.equal(
+ $table.find( 'th.headerSortUp' ).length + $table.find( 'th.headerSortDown' ).length,
+ 0,
+ 'No sort specific sort classes addign to header cells'
+ );
+
+ assert.equal(
+ $table.find( 'th' ).first().attr( 'title' ),
+ mw.msg( 'sort-ascending' ),
+ 'First header cell has default title'
+ );
+
+ assert.equal(
+ $table.find( 'th' ).first().attr( 'title' ),
+ $table.find( 'th' ).last().attr( 'title' ),
+ 'Both header cells\' titles match'
+ );
+ } );
+
+ // Sorting with colspans
+ header = [ 'column1a', 'column1b', 'column1c', 'column2' ];
+
+ tableTest( 'Sorting with colspanned headers: spanned column',
+ header,
+ colspanInitial,
+ [ aaa1, aab5, abc3, bbc2, caa4 ],
+ function ( $table ) {
+ // Make colspanned header for test
+ $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove();
+ $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest( 'Sorting with colspanned headers: sort spanned column twice',
+ header,
+ colspanInitial,
+ [ caa4, bbc2, abc3, aab5, aaa1 ],
+ function ( $table ) {
+ // Make colspanned header for test
+ $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove();
+ $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest( 'Sorting with colspanned headers: subsequent column',
+ header,
+ colspanInitial,
+ [ aaa1, bbc2, abc3, caa4, aab5 ],
+ function ( $table ) {
+ // Make colspanned header for test
+ $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove();
+ $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(1)' ).click();
+ }
+ );
+ tableTest( 'Sorting with colspanned headers: sort subsequent column twice',
+ header,
+ colspanInitial,
+ [ aab5, caa4, abc3, bbc2, aaa1 ],
+ function ( $table ) {
+ // Make colspanned header for test
+ $table.find( 'tr:eq(0) th:eq(1), tr:eq(0) th:eq(2)' ).remove();
+ $table.find( 'tr:eq(0) th:eq(0)' ).attr( 'colspan', '3' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(1)' ).click();
+ $table.find( '.headerSort:eq(1)' ).click();
+ }
+ );
+
+ tableTest(
+ 'Basic planet table: one unsortable column',
+ header,
+ planets,
+ planets,
+ function ( $table ) {
+ $table.find( 'tr:eq(0) > th:eq(0)' ).addClass( 'unsortable' );
+
+ $table.tablesorter();
+ $table.find( 'tr:eq(0) > th:eq(0)' ).click();
+ }
+ );
+
+ // Regression tests!
+ tableTest(
+ 'Bug 28775: German-style (dmy) short numeric dates',
+ ['Date'],
+ [
+ // German-style dates are day-month-year
+ ['11.11.2011'],
+ ['01.11.2011'],
+ ['02.10.2011'],
+ ['03.08.2011'],
+ ['09.11.2011']
+ ],
+ [
+ // Sorted by ascending date
+ ['03.08.2011'],
+ ['02.10.2011'],
+ ['01.11.2011'],
+ ['09.11.2011'],
+ ['11.11.2011']
+ ],
+ function ( $table ) {
+ mw.config.set( 'wgDefaultDateFormat', 'dmy' );
+ mw.config.set( 'wgContentLanguage', 'de' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ tableTest(
+ 'Bug 28775: American-style (mdy) short numeric dates',
+ ['Date'],
+ [
+ // American-style dates are month-day-year
+ ['11.11.2011'],
+ ['01.11.2011'],
+ ['02.10.2011'],
+ ['03.08.2011'],
+ ['09.11.2011']
+ ],
+ [
+ // Sorted by ascending date
+ ['01.11.2011'],
+ ['02.10.2011'],
+ ['03.08.2011'],
+ ['09.11.2011'],
+ ['11.11.2011']
+ ],
+ function ( $table ) {
+ mw.config.set( 'wgDefaultDateFormat', 'mdy' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ tableTest(
+ 'Bug 17141: IPv4 address sorting',
+ ['IP'],
+ ipv4,
+ ipv4Sorted,
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest(
+ 'Bug 17141: IPv4 address sorting (reverse)',
+ ['IP'],
+ ipv4,
+ reversed( ipv4Sorted ),
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click().click();
+ }
+ );
+
+ tableTest(
+ 'Accented Characters with custom collation',
+ ['Name'],
+ umlautWords,
+ umlautWordsSorted,
+ function ( $table ) {
+ mw.config.set( 'tableSorterCollation', {
+ 'ä': 'ae',
+ 'ö': 'oe',
+ 'ß': 'ss',
+ 'ü': 'ue'
+ } );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ QUnit.test( 'Rowspan not exploded on init', 1, function ( assert ) {
+ var $table = tableCreate( header, planets );
+
+ // Modify the table to have a multiple-row-spanning cell:
+ // - Remove 2nd cell of 4th row, and, 2nd cell or 5th row.
+ $table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove();
+ // - Set rowspan for 2nd cell of 3rd row to 3.
+ // This covers the removed cell in the 4th and 5th row.
+ $table.find( 'tr:eq(2) td:eq(1)' ).attr( 'rowspan', '3' );
+
+ $table.tablesorter();
+
+ assert.equal(
+ $table.find( 'tr:eq(2) td:eq(1)' ).prop( 'rowSpan' ),
+ 3,
+ 'Rowspan not exploded'
+ );
+ } );
+
+ planetsRowspan = [
+ [ 'Earth', '6051.8' ],
+ jupiter,
+ [ 'Mars', '6051.8' ],
+ mercury,
+ saturn,
+ venus
+ ];
+ planetsRowspanII = [ jupiter, mercury, saturn, venus, [ 'Venus', '6371.0' ], [ 'Venus', '3390.0' ] ];
+
+ tableTest(
+ 'Basic planet table: same value for multiple rows via rowspan',
+ header,
+ planets,
+ planetsRowspan,
+ function ( $table ) {
+ // Modify the table to have a multiple-row-spanning cell:
+ // - Remove 2nd cell of 4th row, and, 2nd cell or 5th row.
+ $table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove();
+ // - Set rowspan for 2nd cell of 3rd row to 3.
+ // This covers the removed cell in the 4th and 5th row.
+ $table.find( 'tr:eq(2) td:eq(1)' ).attr( 'rowspan', '3' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+ tableTest(
+ 'Basic planet table: same value for multiple rows via rowspan (sorting initially)',
+ header,
+ planets,
+ planetsRowspan,
+ function ( $table ) {
+ // Modify the table to have a multiple-row-spanning cell:
+ // - Remove 2nd cell of 4th row, and, 2nd cell or 5th row.
+ $table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove();
+ // - Set rowspan for 2nd cell of 3rd row to 3.
+ // This covers the removed cell in the 4th and 5th row.
+ $table.find( 'tr:eq(2) td:eq(1)' ).attr( 'rowspan', '3' );
+
+ $table.tablesorter( { sortList: [
+ { 0: 'asc' }
+ ] } );
+ }
+ );
+ tableTest(
+ 'Basic planet table: Same value for multiple rows via rowspan II',
+ header,
+ planets,
+ planetsRowspanII,
+ function ( $table ) {
+ // Modify the table to have a multiple-row-spanning cell:
+ // - Remove 1st cell of 4th row, and, 1st cell or 5th row.
+ $table.find( 'tr:eq(3) td:eq(0), tr:eq(4) td:eq(0)' ).remove();
+ // - Set rowspan for 1st cell of 3rd row to 3.
+ // This covers the removed cell in the 4th and 5th row.
+ $table.find( 'tr:eq(2) td:eq(0)' ).attr( 'rowspan', '3' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ tableTest(
+ 'Complex date parsing I',
+ ['date'],
+ complexMDYDates,
+ complexMDYSorted,
+ function ( $table ) {
+ mw.config.set( 'wgDefaultDateFormat', 'mdy' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ tableTest(
+ 'Currency parsing I',
+ ['currency'],
+ currencyUnsorted,
+ currencySorted,
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ planetsAscNameLegacy = planetsAscName.slice( 0 );
+ planetsAscNameLegacy[4] = planetsAscNameLegacy[5];
+ planetsAscNameLegacy.pop();
+
+ tableTest(
+ 'Legacy compat with .sortbottom',
+ header,
+ planets,
+ planetsAscNameLegacy,
+ function ( $table ) {
+ $table.find( 'tr:last' ).addClass( 'sortbottom' );
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ QUnit.test( 'Test detection routine', 1, function ( assert ) {
+ var $table;
+ $table = $(
+ '<table class="sortable">' +
+ '<caption>CAPTION</caption>' +
+ '<tr><th>THEAD</th></tr>' +
+ '<tr><td>1</td></tr>' +
+ '<tr class="sortbottom"><td>text</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+
+ assert.equal(
+ $table.data( 'tablesorter' ).config.parsers[0].id,
+ 'number',
+ 'Correctly detected column content skipping sortbottom'
+ );
+ } );
+
+ /** FIXME: the diff output is not very readeable. */
+ QUnit.test( 'bug 32047 - caption must be before thead', 1, function ( assert ) {
+ var $table;
+ $table = $(
+ '<table class="sortable">' +
+ '<caption>CAPTION</caption>' +
+ '<tr><th>THEAD</th></tr>' +
+ '<tr><td>A</td></tr>' +
+ '<tr><td>B</td></tr>' +
+ '<tr class="sortbottom"><td>TFOOT</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter();
+
+ assert.equal(
+ $table.children().get( 0 ).nodeName,
+ 'CAPTION',
+ 'First element after <thead> must be <caption> (bug 32047)'
+ );
+ } );
+
+ QUnit.test( 'data-sort-value attribute, when available, should override sorting position', 3, function ( assert ) {
+ var $table, data;
+
+ // Example 1: All cells except one cell without data-sort-value,
+ // which should be sorted at it's text content value.
+ $table = $(
+ '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>Cheetah</td></tr>' +
+ '<tr><td data-sort-value="Apple">Bird</td></tr>' +
+ '<tr><td data-sort-value="Bananna">Ferret</td></tr>' +
+ '<tr><td data-sort-value="Drupe">Elephant</td></tr>' +
+ '<tr><td data-sort-value="Cherry">Dolphin</td></tr>' +
+ '</tbody></table>'
+ );
+ $table.tablesorter().find( '.headerSort:eq(0)' ).click();
+
+ data = [];
+ $table.find( 'tbody > tr' ).each( function ( i, tr ) {
+ $( tr ).find( 'td' ).each( function ( i, td ) {
+ data.push( {
+ data: $( td ).data( 'sortValue' ),
+ text: $( td ).text()
+ } );
+ } );
+ } );
+
+ assert.deepEqual( data, [
+ {
+ data: 'Apple',
+ text: 'Bird'
+ },
+ {
+ data: 'Bananna',
+ text: 'Ferret'
+ },
+ {
+ data: undefined,
+ text: 'Cheetah'
+ },
+ {
+ data: 'Cherry',
+ text: 'Dolphin'
+ },
+ {
+ data: 'Drupe',
+ text: 'Elephant'
+ }
+ ], 'Order matches expected order (based on data-sort-value attribute values)' );
+
+ // Example 2
+ $table = $(
+ '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>D</td></tr>' +
+ '<tr><td data-sort-value="E">A</td></tr>' +
+ '<tr><td>B</td></tr>' +
+ '<tr><td>G</td></tr>' +
+ '<tr><td data-sort-value="F">C</td></tr>' +
+ '</tbody></table>'
+ );
+ $table.tablesorter().find( '.headerSort:eq(0)' ).click();
+
+ data = [];
+ $table.find( 'tbody > tr' ).each( function ( i, tr ) {
+ $( tr ).find( 'td' ).each( function ( i, td ) {
+ data.push( {
+ data: $( td ).data( 'sortValue' ),
+ text: $( td ).text()
+ } );
+ } );
+ } );
+
+ assert.deepEqual( data, [
+ {
+ data: undefined,
+ text: 'B'
+ },
+ {
+ data: undefined,
+ text: 'D'
+ },
+ {
+ data: 'E',
+ text: 'A'
+ },
+ {
+ data: 'F',
+ text: 'C'
+ },
+ {
+ data: undefined,
+ text: 'G'
+ }
+ ], 'Order matches expected order (based on data-sort-value attribute values)' );
+
+ // Example 3: Test that live changes are used from data-sort-value,
+ // even if they change after the tablesorter is constructed (bug 38152).
+ $table = $(
+ '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>D</td></tr>' +
+ '<tr><td data-sort-value="1">A</td></tr>' +
+ '<tr><td>B</td></tr>' +
+ '<tr><td data-sort-value="2">G</td></tr>' +
+ '<tr><td>C</td></tr>' +
+ '</tbody></table>'
+ );
+ // initialize table sorter and sort once
+ $table
+ .tablesorter()
+ .find( '.headerSort:eq(0)' ).click();
+
+ // Change the sortValue data properties (bug 38152)
+ // - change data
+ $table.find( 'td:contains(A)' ).data( 'sortValue', 3 );
+ // - add data
+ $table.find( 'td:contains(B)' ).data( 'sortValue', 1 );
+ // - remove data, bring back attribute: 2
+ $table.find( 'td:contains(G)' ).removeData( 'sortValue' );
+
+ // Now sort again (twice, so it is back at Ascending)
+ $table.find( '.headerSort:eq(0)' ).click();
+ $table.find( '.headerSort:eq(0)' ).click();
+
+ data = [];
+ $table.find( 'tbody > tr' ).each( function ( i, tr ) {
+ $( tr ).find( 'td' ).each( function ( i, td ) {
+ data.push( {
+ data: $( td ).data( 'sortValue' ),
+ text: $( td ).text()
+ } );
+ } );
+ } );
+
+ assert.deepEqual( data, [
+ {
+ data: 1,
+ text: 'B'
+ },
+ {
+ data: 2,
+ text: 'G'
+ },
+ {
+ data: 3,
+ text: 'A'
+ },
+ {
+ data: undefined,
+ text: 'C'
+ },
+ {
+ data: undefined,
+ text: 'D'
+ }
+ ], 'Order matches expected order, using the current sortValue in $.data()' );
+
+ } );
+
+ tableTest( 'bug 8115: sort numbers with commas (ascending)',
+ ['Numbers'], numbers, numbersAsc,
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ tableTest( 'bug 8115: sort numbers with commas (descending)',
+ ['Numbers'], numbers, reversed( numbersAsc ),
+ function ( $table ) {
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click().click();
+ }
+ );
+ // TODO add numbers sorting tests for bug 8115 with a different language
+
+ QUnit.test( 'bug 32888 - Tables inside a tableheader cell', 2, function ( assert ) {
+ var $table;
+ $table = $(
+ '<table class="sortable" id="mw-bug-32888">' +
+ '<tr><th>header<table id="mw-bug-32888-2">' +
+ '<tr><th>1</th><th>2</th></tr>' +
+ '</table></th></tr>' +
+ '<tr><td>A</td></tr>' +
+ '<tr><td>B</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter();
+
+ assert.equal(
+ $table.find( '> thead:eq(0) > tr > th.headerSort' ).length,
+ 1,
+ 'Child tables inside a headercell should not interfere with sortable headers (bug 32888)'
+ );
+ assert.equal(
+ $( '#mw-bug-32888-2' ).find( 'th.headerSort' ).length,
+ 0,
+ 'The headers of child tables inside a headercell should not be sortable themselves (bug 32888)'
+ );
+ } );
+
+ tableTest(
+ 'Correct date sorting I',
+ ['date'],
+ correctDateSorting1,
+ correctDateSortingSorted1,
+ function ( $table ) {
+ mw.config.set( 'wgDefaultDateFormat', 'mdy' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ tableTest(
+ 'Correct date sorting II',
+ ['date'],
+ correctDateSorting2,
+ correctDateSortingSorted2,
+ function ( $table ) {
+ mw.config.set( 'wgDefaultDateFormat', 'dmy' );
+
+ $table.tablesorter();
+ $table.find( '.headerSort:eq(0)' ).click();
+ }
+ );
+
+ QUnit.test( 'Sorting images using alt text', 1, function ( assert ) {
+ var $table = $(
+ '<table class="sortable">' +
+ '<tr><th>THEAD</th></tr>' +
+ '<tr><td><img alt="2"/></td></tr>' +
+ '<tr><td>1</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter().find( '.headerSort:eq(0)' ).click();
+
+ assert.equal(
+ $table.find( 'td' ).first().text(),
+ '1',
+ 'Applied correct sorting order'
+ );
+ } );
+
+ QUnit.test( 'Sorting images using alt text (complex)', 1, function ( assert ) {
+ var $table = $(
+ '<table class="sortable">' +
+ '<tr><th>THEAD</th></tr>' +
+ '<tr><td><img alt="D" />A</td></tr>' +
+ '<tr><td>CC</td></tr>' +
+ '<tr><td><a><img alt="A" /></a>F</tr>' +
+ '<tr><td><img alt="A" /><strong>E</strong></tr>' +
+ '<tr><td><strong><img alt="A" />D</strong></tr>' +
+ '<tr><td><img alt="A" />C</tr>' +
+ '</table>'
+ );
+ $table.tablesorter().find( '.headerSort:eq(0)' ).click();
+
+ assert.equal(
+ $table.find( 'td' ).text(),
+ 'CDEFCCA',
+ 'Applied correct sorting order'
+ );
+ } );
+
+ QUnit.test( 'Sorting images using alt text (with format autodetection)', 1, function ( assert ) {
+ var $table = $(
+ '<table class="sortable">' +
+ '<tr><th>THEAD</th></tr>' +
+ '<tr><td><img alt="1" />7</td></tr>' +
+ '<tr><td>1<img alt="6" /></td></tr>' +
+ '<tr><td>5</td></tr>' +
+ '<tr><td>4</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter().find( '.headerSort:eq(0)' ).click();
+
+ assert.equal(
+ $table.find( 'td' ).text(),
+ '4517',
+ 'Applied correct sorting order'
+ );
+ } );
+
+ QUnit.test( 'bug 38911 - The row with the largest amount of columns should receive the sort indicators', 3, function ( assert ) {
+ var $table = $(
+ '<table class="sortable">' +
+ '<thead>' +
+ '<tr><th rowspan="2" id="A1">A1</th><th colspan="2">B2a</th></tr>' +
+ '<tr><th id="B2b">B2b</th><th id="C2b">C2b</th></tr>' +
+ '</thead>' +
+ '<tr><td>A</td><td>Aa</td><td>Ab</td></tr>' +
+ '<tr><td>B</td><td>Ba</td><td>Bb</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter();
+
+ assert.equal(
+ $table.find( '#A1' ).attr( 'class' ),
+ 'headerSort',
+ 'The first column of the first row should be sortable'
+ );
+ assert.equal(
+ $table.find( '#B2b' ).attr( 'class' ),
+ 'headerSort',
+ 'The th element of the 2nd row of the 2nd column should be sortable'
+ );
+ assert.equal(
+ $table.find( '#C2b' ).attr( 'class' ),
+ 'headerSort',
+ 'The th element of the 2nd row of the 3rd column should be sortable'
+ );
+ } );
+
+ QUnit.test( 'rowspans in table headers should prefer the last row when rows are equal in length', 2, function ( assert ) {
+ var $table = $(
+ '<table class="sortable">' +
+ '<thead>' +
+ '<tr><th rowspan="2" id="A1">A1</th><th>B2a</th></tr>' +
+ '<tr><th id="B2b">B2b</th></tr>' +
+ '</thead>' +
+ '<tr><td>A</td><td>Aa</td></tr>' +
+ '<tr><td>B</td><td>Ba</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter();
+
+ assert.equal(
+ $table.find( '#A1' ).attr( 'class' ),
+ 'headerSort',
+ 'The first column of the first row should be sortable'
+ );
+ assert.equal(
+ $table.find( '#B2b' ).attr( 'class' ),
+ 'headerSort',
+ 'The th element of the 2nd row of the 2nd column should be sortable'
+ );
+ } );
+
+ QUnit.test( 'holes in the table headers should not throw JS errors', 2, function ( assert ) {
+ var $table = $(
+ '<table class="sortable">' +
+ '<thead>' +
+ '<tr><th id="A1">A1</th><th>B1</th><th id="C1" rowspan="2">C1</th></tr>' +
+ '<tr><th id="A2">A2</th></tr>' +
+ '</thead>' +
+ '<tr><td>A</td><td>Aa</td><td>Aaa</td></tr>' +
+ '<tr><td>B</td><td>Ba</td><td>Bbb</td></tr>' +
+ '</table>'
+ );
+ $table.tablesorter();
+ assert.equal( $table.find( '#A2' ).prop( 'headerIndex' ),
+ undefined,
+ 'A2 should not be a sort header'
+ );
+ assert.equal( $table.find( '#C1' ).prop( 'headerIndex' ),
+ 2,
+ 'C1 should be a sort header'
+ );
+ } );
+
+ // bug 53527
+ QUnit.test( 'td cells in thead should not be taken into account for longest row calculation', 2, function ( assert ) {
+ var $table = $(
+ '<table class="sortable">' +
+ '<thead>' +
+ '<tr><th id="A1">A1</th><th>B1</th><td id="C1">C1</td></tr>' +
+ '<tr><th id="A2">A2</th><th>B2</th><th id="C2">C2</th></tr>' +
+ '</thead>' +
+ '</table>'
+ );
+ $table.tablesorter();
+ assert.equal( $table.find( '#C2' ).prop( 'headerIndex' ),
+ 2,
+ 'C2 should be a sort header'
+ );
+ assert.equal( $table.find( '#C1' ).prop( 'headerIndex' ),
+ undefined,
+ 'C1 should not be a sort header'
+ );
+ } );
+
+ // bug 41889 - exploding rowspans in more complex cases
+ tableTestHTML(
+ 'Rowspan exploding with row headers',
+ '<table class="sortable">' +
+ '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>1</td><th rowspan="2">foo</th><td rowspan="2">bar</td><td>baz</td></tr>' +
+ '<tr><td>2</td><td>baz</td></tr>' +
+ '</tbody></table>',
+ [
+ [ '1', 'foo', 'bar', 'baz' ],
+ [ '2', 'foo', 'bar', 'baz' ]
+ ]
+ );
+
+ // bug 53211 - exploding rowspans in more complex cases
+ QUnit.test(
+ 'Rowspan exploding with row headers and colspans', 1, function ( assert ) {
+ var $table = $( '<table class="sortable">' +
+ '<thead><tr><th rowspan="2">n</th><th colspan="2">foo</th><th rowspan="2">baz</th></tr>' +
+ '<tr><th>foo</th><th>bar</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>1</td><td>foo</td><td>bar</td><td>baz</td></tr>' +
+ '<tr><td>2</td><td>foo</td><td>bar</td><td>baz</td></tr>' +
+ '</tbody></table>' );
+
+ $table.tablesorter();
+ assert.equal( $table.find( 'tr:eq(1) th:eq(1)').prop('headerIndex'),
+ 2,
+ 'Incorrect index of sort header' );
+ }
+ );
+
+ tableTestHTML(
+ 'Rowspan exploding with colspanned cells',
+ '<table class="sortable">' +
+ '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>1</td><td>foo</td><td>bar</td><td rowspan="2">baz</td></tr>' +
+ '<tr><td>2</td><td colspan="2">foobar</td></tr>' +
+ '</tbody></table>',
+ [
+ [ '1', 'foo', 'bar', 'baz' ],
+ [ '2', 'foobar', 'baz' ]
+ ]
+ );
+
+ tableTestHTML(
+ 'Rowspan exploding with colspanned cells (2)',
+ '<table class="sortable">' +
+ '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th><th>quux</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>1</td><td>foo</td><td>bar</td><td rowspan="2">baz</td><td>quux</td></tr>' +
+ '<tr><td>2</td><td colspan="2">foobar</td><td>quux</td></tr>' +
+ '</tbody></table>',
+ [
+ [ '1', 'foo', 'bar', 'baz', 'quux' ],
+ [ '2', 'foobar', 'baz', 'quux' ]
+ ]
+ );
+
+ tableTestHTML(
+ 'Rowspan exploding with rightmost rows spanning most',
+ '<table class="sortable">' +
+ '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>1</td><td rowspan="2">foo</td><td rowspan="4">bar</td></tr>' +
+ '<tr><td>2</td></tr>' +
+ '<tr><td>3</td><td rowspan="2">foo</td></tr>' +
+ '<tr><td>4</td></tr>' +
+ '</tbody></table>',
+ [
+ [ '1', 'foo', 'bar' ],
+ [ '2', 'foo', 'bar' ],
+ [ '3', 'foo', 'bar' ],
+ [ '4', 'foo', 'bar' ]
+ ]
+ );
+
+ tableTestHTML(
+ 'Rowspan exploding with rightmost rows spanning most (2)',
+ '<table class="sortable">' +
+ '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>1</td><td rowspan="2">foo</td><td rowspan="4">bar</td><td>baz</td></tr>' +
+ '<tr><td>2</td><td>baz</td></tr>' +
+ '<tr><td>3</td><td rowspan="2">foo</td><td>baz</td></tr>' +
+ '<tr><td>4</td><td>baz</td></tr>' +
+ '</tbody></table>',
+ [
+ [ '1', 'foo', 'bar', 'baz' ],
+ [ '2', 'foo', 'bar', 'baz' ],
+ [ '3', 'foo', 'bar', 'baz' ],
+ [ '4', 'foo', 'bar', 'baz' ]
+ ]
+ );
+
+ tableTestHTML(
+ 'Rowspan exploding with row-and-colspanned cells',
+ '<table class="sortable">' +
+ '<thead><tr><th id="sortme">n</th><th>foo1</th><th>foo2</th><th>bar</th><th>baz</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>1</td><td rowspan="2">foo1</td><td rowspan="2">foo2</td><td rowspan="4">bar</td><td>baz</td></tr>' +
+ '<tr><td>2</td><td>baz</td></tr>' +
+ '<tr><td>3</td><td colspan="2" rowspan="2">foo</td><td>baz</td></tr>' +
+ '<tr><td>4</td><td>baz</td></tr>' +
+ '</tbody></table>',
+ [
+ [ '1', 'foo1', 'foo2', 'bar', 'baz' ],
+ [ '2', 'foo1', 'foo2', 'bar', 'baz' ],
+ [ '3', 'foo', 'bar', 'baz' ],
+ [ '4', 'foo', 'bar', 'baz' ]
+ ]
+ );
+
+ tableTestHTML(
+ 'Rowspan exploding with uneven rowspan layout',
+ '<table class="sortable">' +
+ '<thead><tr><th id="sortme">n</th><th>foo1</th><th>foo2</th><th>foo3</th><th>bar</th><th>baz</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>1</td><td rowspan="2">foo1</td><td rowspan="2">foo2</td><td rowspan="2">foo3</td><td>bar</td><td>baz</td></tr>' +
+ '<tr><td>2</td><td rowspan="3">bar</td><td>baz</td></tr>' +
+ '<tr><td>3</td><td rowspan="2">foo1</td><td rowspan="2">foo2</td><td rowspan="2">foo3</td><td>baz</td></tr>' +
+ '<tr><td>4</td><td>baz</td></tr>' +
+ '</tbody></table>',
+ [
+ [ '1', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ],
+ [ '2', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ],
+ [ '3', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ],
+ [ '4', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ]
+ ]
+ );
+
+}( jQuery, mediaWiki ) );
diff --git a/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js b/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js
new file mode 100644
index 00000000..56b0fa92
--- /dev/null
+++ b/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js
@@ -0,0 +1,273 @@
+( function ( $ ) {
+
+ QUnit.module( 'jquery.textSelection', QUnit.newMwEnvironment() );
+
+ /**
+ * Test factory for $.fn.textSelection( 'encapsulateText' )
+ *
+ * @param options {object} associative array containing:
+ * description {string}
+ * input {string}
+ * output {string}
+ * start {int} starting char for selection
+ * end {int} ending char for selection
+ * params {object} add'l parameters for $().textSelection( 'encapsulateText' )
+ */
+ function encapsulateTest( options ) {
+ var opt = $.extend( {
+ description: '',
+ before: {},
+ after: {},
+ replace: {}
+ }, options );
+
+ opt.before = $.extend( {
+ text: '',
+ start: 0,
+ end: 0
+ }, opt.before );
+ opt.after = $.extend( {
+ text: '',
+ selected: null
+ }, opt.after );
+
+ QUnit.test( opt.description, function ( assert ) {
+ var $textarea, start, end, options, text, selected,
+ tests = 1;
+ if ( opt.after.selected !== null ) {
+ tests++;
+ }
+ QUnit.expect( tests );
+
+ $textarea = $( '<textarea>' );
+
+ $( '#qunit-fixture' ).append( $textarea );
+
+ $textarea.textSelection( 'setContents', opt.before.text );
+
+ start = opt.before.start;
+ end = opt.before.end;
+
+ // Clone opt.replace
+ options = $.extend( {}, opt.replace );
+ options.selectionStart = start;
+ options.selectionEnd = end;
+ $textarea.textSelection( 'encapsulateSelection', options );
+
+ text = $textarea.textSelection( 'getContents' ).replace( /\r\n/g, '\n' );
+
+ assert.equal( text, opt.after.text, 'Checking full text after encapsulation' );
+
+ if ( opt.after.selected !== null ) {
+ selected = $textarea.textSelection( 'getSelection' );
+ assert.equal( selected, opt.after.selected, 'Checking selected text after encapsulation.' );
+ }
+
+ } );
+ }
+
+ var caretSample,
+ sig = {
+ pre: '--~~~~'
+ },
+ bold = {
+ pre: '\'\'\'',
+ peri: 'Bold text',
+ post: '\'\'\''
+ },
+ h2 = {
+ pre: '== ',
+ peri: 'Heading 2',
+ post: ' ==',
+ regex: /^(\s*)(={1,6})(.*?)\2(\s*)$/,
+ regexReplace: '$1==$3==$4',
+ ownline: true
+ },
+ ulist = {
+ pre: '* ',
+ peri: 'Bulleted list item',
+ post: '',
+ ownline: true,
+ splitlines: true
+ };
+
+ encapsulateTest( {
+ description: 'Adding sig to end of text',
+ before: {
+ text: 'Wikilove dude! ',
+ start: 15,
+ end: 15
+ },
+ after: {
+ text: 'Wikilove dude! --~~~~',
+ selected: ''
+ },
+ replace: sig
+ } );
+
+ encapsulateTest( {
+ description: 'Adding bold to empty',
+ before: {
+ text: '',
+ start: 0,
+ end: 0
+ },
+ after: {
+ text: '\'\'\'Bold text\'\'\'',
+ selected: 'Bold text' // selected because it's the default
+ },
+ replace: bold
+ } );
+
+ encapsulateTest( {
+ description: 'Adding bold to existing text',
+ before: {
+ text: 'Now is the time for all good men to come to the aid of their country',
+ start: 20,
+ end: 32
+ },
+ after: {
+ text: 'Now is the time for \'\'\'all good men\'\'\' to come to the aid of their country',
+ selected: '' // empty because it's not the default'
+ },
+ replace: bold
+ } );
+
+ encapsulateTest( {
+ description: 'ownline option: adding new h2',
+ before: {
+ text: 'Before\nAfter',
+ start: 7,
+ end: 7
+ },
+ after: {
+ text: 'Before\n== Heading 2 ==\nAfter',
+ selected: 'Heading 2'
+ },
+ replace: h2
+ } );
+
+ encapsulateTest( {
+ description: 'ownline option: turn a whole line into new h2',
+ before: {
+ text: 'Before\nMy heading\nAfter',
+ start: 7,
+ end: 17
+ },
+ after: {
+ text: 'Before\n== My heading ==\nAfter',
+ selected: ''
+ },
+ replace: h2
+ } );
+
+ encapsulateTest( {
+ description: 'ownline option: turn a partial line into new h2',
+ before: {
+ text: 'BeforeMy headingAfter',
+ start: 6,
+ end: 16
+ },
+ after: {
+ text: 'Before\n== My heading ==\nAfter',
+ selected: ''
+ },
+ replace: h2
+ } );
+
+ encapsulateTest( {
+ description: 'splitlines option: no selection, insert new list item',
+ before: {
+ text: 'Before\nAfter',
+ start: 7,
+ end: 7
+ },
+ after: {
+ text: 'Before\n* Bulleted list item\nAfter'
+ },
+ replace: ulist
+ } );
+
+ encapsulateTest( {
+ description: 'splitlines option: single partial line selection, insert new list item',
+ before: {
+ text: 'BeforeMy List ItemAfter',
+ start: 6,
+ end: 18
+ },
+ after: {
+ text: 'Before\n* My List Item\nAfter'
+ },
+ replace: ulist
+ } );
+
+ encapsulateTest( {
+ description: 'splitlines option: multiple lines',
+ before: {
+ text: 'Before\nFirst\nSecond\nThird\nAfter',
+ start: 7,
+ end: 25
+ },
+ after: {
+ text: 'Before\n* First\n* Second\n* Third\nAfter'
+ },
+ replace: ulist
+ } );
+
+ function caretTest( options ) {
+ QUnit.test( options.description, 2, function ( assert ) {
+ var pos,
+ $textarea = $( '<textarea>' ).text( options.text );
+
+ $( '#qunit-fixture' ).append( $textarea );
+
+ if ( options.mode === 'set' ) {
+ $textarea.textSelection( 'setSelection', {
+ start: options.start,
+ end: options.end
+ } );
+ }
+
+ function among( actual, expected, message ) {
+ if ( $.isArray( expected ) ) {
+ assert.ok( $.inArray( actual, expected ) !== -1, message + ' (got ' + actual + '; expected one of ' + expected.join( ', ' ) + ')' );
+ } else {
+ assert.equal( actual, expected, message );
+ }
+ }
+
+ pos = $textarea.textSelection( 'getCaretPosition', { startAndEnd: true } );
+ among( pos[0], options.start, 'Caret start should be where we set it.' );
+ among( pos[1], options.end, 'Caret end should be where we set it.' );
+ } );
+ }
+
+ caretSample = 'Some big text that we like to work with. Nothing fancy... you know what I mean?';
+
+/*
+ // @broken: Disabled per bug 34820
+ caretTest({
+ description: 'getCaretPosition with original/empty selection - bug 31847 with IE 6/7/8',
+ text: caretSample,
+ start: [0, caretSample.length], // Opera and Firefox (prior to FF 6.0) default caret to the end of the box (caretSample.length)
+ end: [0, caretSample.length], // Other browsers default it to the beginning (0), so check both.
+ mode: 'get'
+ });
+*/
+
+ caretTest( {
+ description: 'set/getCaretPosition with forced empty selection',
+ text: caretSample,
+ start: 7,
+ end: 7,
+ mode: 'set'
+ } );
+
+ caretTest( {
+ description: 'set/getCaretPosition with small selection',
+ text: caretSample,
+ start: 6,
+ end: 11,
+ mode: 'set'
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js
new file mode 100644
index 00000000..a0c7daf1
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js
@@ -0,0 +1,30 @@
+( function ( mw ) {
+ QUnit.module( 'mediawiki.api.category', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.server = this.sandbox.useFakeServer();
+ }
+ } ) );
+
+ QUnit.test( '.getCategoriesByPrefix()', function ( assert ) {
+ QUnit.expect( 1 );
+
+ var api = new mw.Api();
+
+ api.getCategoriesByPrefix( 'Foo' ).done( function ( matches ) {
+ assert.deepEqual(
+ matches,
+ [ 'Food', 'Fool Supermarine S.6', 'Fools' ]
+ );
+ } );
+
+ this.server.respond( function ( req ) {
+ req.respond( 200, { 'Content-Type': 'application/json' },
+ '{ "query": { "allpages": [ ' +
+ '{ "title": "Category:Food" },' +
+ '{ "title": "Category:Fool Supermarine S.6" },' +
+ '{ "title": "Category:Fools" }' +
+ '] } }'
+ );
+ } );
+ } );
+}( mediaWiki ) );
diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js
new file mode 100644
index 00000000..cd0db7c9
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js
@@ -0,0 +1,25 @@
+( function ( mw ) {
+ QUnit.module( 'mediawiki.api.parse', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.server = this.sandbox.useFakeServer();
+ }
+ } ) );
+
+ QUnit.test( 'Hello world', function ( assert ) {
+ QUnit.expect( 1 );
+
+ var api = new mw.Api();
+
+ api.parse( '\'\'\'Hello world\'\'\'' ).done( function ( html ) {
+ assert.equal( html, '<p><b>Hello world</b></p>' );
+ } );
+
+ this.server.respondWith( /action=parse.*&text='''Hello\+world'''/, function ( request ) {
+ request.respond( 200, { 'Content-Type': 'application/json' },
+ '{ "parse": { "text": { "*": "<p><b>Hello world</b></p>" } } }'
+ );
+ } );
+
+ this.server.respond();
+ } );
+}( mediaWiki ) );
diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js
new file mode 100644
index 00000000..f156c728
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js
@@ -0,0 +1,312 @@
+( function ( mw ) {
+ QUnit.module( 'mediawiki.api', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.server = this.sandbox.useFakeServer();
+ }
+ } ) );
+
+ QUnit.test( 'Basic functionality', function ( assert ) {
+ QUnit.expect( 2 );
+
+ var api = new mw.Api();
+
+ api.get( {} )
+ .done( function ( data ) {
+ assert.deepEqual( data, [], 'If request succeeds without errors, resolve deferred' );
+ } );
+
+ api.post( {} )
+ .done( function ( data ) {
+ assert.deepEqual( data, [], 'Simple POST request' );
+ } );
+
+ this.server.respond( function ( request ) {
+ request.respond( 200, { 'Content-Type': 'application/json' }, '[]' );
+ } );
+ } );
+
+ QUnit.test( 'API error', function ( assert ) {
+ QUnit.expect( 1 );
+
+ var api = new mw.Api();
+
+ api.get( { action: 'doesntexist' } )
+ .fail( function ( errorCode ) {
+ assert.equal( errorCode, 'unknown_action', 'API error should reject the deferred' );
+ } );
+
+ this.server.respond( function ( request ) {
+ request.respond( 200, { 'Content-Type': 'application/json' },
+ '{ "error": { "code": "unknown_action" } }'
+ );
+ } );
+ } );
+
+ QUnit.test( 'FormData support', function ( assert ) {
+ QUnit.expect( 2 );
+
+ var api = new mw.Api();
+
+ api.post( { action: 'test' }, { contentType: 'multipart/form-data' } );
+
+ this.server.respond( function ( request ) {
+ if ( window.FormData ) {
+ assert.ok( !request.url.match( /action=/), 'Request has no query string' );
+ assert.ok( request.requestBody instanceof FormData, 'Request uses FormData body' );
+ } else {
+ assert.ok( !request.url.match( /action=test/), 'Request has no query string' );
+ assert.equal( request.requestBody, 'action=test&format=json', 'Request uses query string body' );
+ }
+ request.respond( 200, { 'Content-Type': 'application/json' }, '[]' );
+ } );
+ } );
+
+ QUnit.test( 'Deprecated callback methods', function ( assert ) {
+ QUnit.expect( 3 );
+
+ var api = new mw.Api();
+
+ this.suppressWarnings();
+
+ api.get( {}, function () {
+ assert.ok( true, 'Function argument treated as success callback.' );
+ } );
+
+ api.get( {}, {
+ ok: function () {
+ assert.ok( true, '"ok" property treated as success callback.' );
+ }
+ } );
+
+ api.get( { action: 'doesntexist' }, {
+ err: function () {
+ assert.ok( true, '"err" property treated as error callback.' );
+ }
+ } );
+
+ this.restoreWarnings();
+
+ this.server.respondWith( /action=query/, function ( request ) {
+ request.respond( 200, { 'Content-Type': 'application/json' }, '[]' );
+ } );
+
+ this.server.respondWith( /action=doesntexist/, function ( request ) {
+ request.respond( 200, { 'Content-Type': 'application/json' },
+ '{ "error": { "code": "unknown_action" } }'
+ );
+ } );
+
+ this.server.respond();
+ } );
+
+ QUnit.test( 'getToken( pre-populated )', function ( assert ) {
+ QUnit.expect( 2 );
+
+ var api = new mw.Api();
+
+ // Get editToken for local wiki, this should not make
+ // a request as it should be retrieved from user.tokens.
+ api.getToken( 'edit' )
+ .done( function ( token ) {
+ assert.ok( token.length, 'Got a token' );
+ } )
+ .fail( function ( err ) {
+ assert.equal( '', err, 'API error' );
+ } );
+
+ assert.equal( this.server.requests.length, 0, 'Requests made' );
+ } );
+
+ QUnit.test( 'getToken()', function ( assert ) {
+ QUnit.expect( 5 );
+
+ var test = this,
+ api = new mw.Api();
+
+ // Get a token of a type that isn't prepopulated by user.tokens.
+ // Could use "block" or "delete" here, but those could in theory
+ // be added to user.tokens, use a fake one instead.
+ api.getToken( 'testaction' )
+ .done( function ( token ) {
+ assert.ok( token.length, 'Got testaction token' );
+ } )
+ .fail( function ( err ) {
+ assert.equal( err, '', 'API error' );
+ } );
+ api.getToken( 'testaction' )
+ .done( function ( token ) {
+ assert.ok( token.length, 'Got testaction token (cached)' );
+ } )
+ .fail( function ( err ) {
+ assert.equal( err, '', 'API error' );
+ } );
+
+ // Don't cache error (bug 65268)
+ api.getToken( 'testaction2' )
+ .fail( function ( err ) {
+ assert.equal( err, 'bite-me', 'Expected error' );
+ } )
+ .always( function () {
+ // Make this request after the first one has finished.
+ // If we make it simultaneously we still want it to share
+ // the cache, but as soon as it is fulfilled as error we
+ // reject it so that the next one tries fresh.
+ api.getToken( 'testaction2' )
+ .done( function ( token ) {
+ assert.ok( token.length, 'Got testaction2 token (error was not be cached)' );
+ } )
+ .fail( function ( err ) {
+ assert.equal( err, '', 'API error' );
+ } );
+
+ assert.equal( test.server.requests.length, 3, 'Requests made' );
+
+ test.server.requests[2].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "tokens": { "testaction2token": "0123abc" } }'
+ );
+ } );
+
+ this.server.requests[0].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "tokens": { "testactiontoken": "0123abc" } }'
+ );
+
+ this.server.requests[1].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "error": { "code": "bite-me", "info": "Smite me, O Mighty Smiter" } }'
+ );
+ } );
+
+ QUnit.test( 'postWithToken( tokenType, params )', function ( assert ) {
+ QUnit.expect( 1 );
+
+ var api = new mw.Api( { ajax: { url: '/postWithToken/api.php' } } );
+
+ // - Requests token
+ // - Performs action=example
+ api.postWithToken( 'testsimpletoken', { action: 'example', key: 'foo' } )
+ .done( function ( data ) {
+ assert.deepEqual( data, { example: { foo: 'quux' } } );
+ } );
+
+ this.server.requests[0].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "tokens": { "testsimpletokentoken": "a-bad-token" } }'
+ );
+
+ this.server.requests[1].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "example": { "foo": "quux" } }'
+ );
+ } );
+
+ QUnit.test( 'postWithToken( tokenType, params, ajaxOptions )', function ( assert ) {
+ QUnit.expect( 3 );
+
+ var api = new mw.Api();
+
+ api.postWithToken(
+ 'edit',
+ {
+ action: 'example'
+ },
+ {
+ headers: {
+ 'X-Foo': 'Bar'
+ }
+ }
+ );
+
+ api.postWithToken(
+ 'edit',
+ {
+ action: 'example'
+ },
+ function () {
+ assert.ok( false, 'This parameter cannot be a callback' );
+ }
+ )
+ .always( function ( data ) {
+ assert.equal( data.example, 'quux' );
+ } );
+
+ assert.equal( this.server.requests.length, 2, 'Request made' );
+ assert.equal( this.server.requests[0].requestHeaders['X-Foo'], 'Bar', 'Header sent' );
+
+ this.server.respond( function ( request ) {
+ request.respond( 200, { 'Content-Type': 'application/json' }, '{ "example": "quux" }' );
+ } );
+ } );
+
+ QUnit.test( 'postWithToken() - badtoken', function ( assert ) {
+ QUnit.expect( 1 );
+
+ var api = new mw.Api();
+
+ // - Request: token
+ // - Request: action=example -> badtoken error
+ // - Request: new token
+ // - Request: action=example
+ api.postWithToken( 'testbadtoken', { action: 'example', key: 'foo' } )
+ .done( function ( data ) {
+ assert.deepEqual( data, { example: { foo: 'quux' } } );
+ } );
+
+ this.server.requests[0].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "tokens": { "testbadtokentoken": "a-bad-token" } }'
+ );
+
+ this.server.requests[1].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "error": { "code": "badtoken" } }'
+ );
+
+ this.server.requests[2].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "tokens": { "testbadtokentoken": "a-good-token" } }'
+ );
+
+ this.server.requests[3].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "example": { "foo": "quux" } }'
+ );
+
+ } );
+
+ QUnit.test( 'postWithToken() - badtoken-cached', function ( assert ) {
+ QUnit.expect( 2 );
+
+ var api = new mw.Api();
+
+ // - Request: token
+ // - Request: action=example
+ api.postWithToken( 'testbadtokencache', { action: 'example', key: 'foo' } )
+ .done( function ( data ) {
+ assert.deepEqual( data, { example: { foo: 'quux' } } );
+ } );
+
+ // - Cache: Try previously cached token
+ // - Request: action=example -> badtoken error
+ // - Request: new token
+ // - Request: action=example
+ api.postWithToken( 'testbadtokencache', { action: 'example', key: 'bar' } )
+ .done( function ( data ) {
+ assert.deepEqual( data, { example: { bar: 'quux' } } );
+ } );
+
+ this.server.requests[0].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "tokens": { "testbadtokencachetoken": "a-good-token-once" } }'
+ );
+
+ this.server.requests[1].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "example": { "foo": "quux" } }'
+ );
+
+ this.server.requests[2].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "error": { "code": "badtoken" } }'
+ );
+
+ this.server.requests[3].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "tokens": { "testbadtokencachetoken": "a-good-new-token" } }'
+ );
+
+ this.server.requests[4].respond( 200, { 'Content-Type': 'application/json' },
+ '{ "example": { "bar": "quux" } }'
+ );
+
+ } );
+
+}( mediaWiki ) );
diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js
new file mode 100644
index 00000000..5965ab7b
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js
@@ -0,0 +1,46 @@
+( function ( mw ) {
+ QUnit.module( 'mediawiki.api.watch', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.server = this.sandbox.useFakeServer();
+ }
+ } ) );
+
+ QUnit.test( '.watch()', function ( assert ) {
+ QUnit.expect( 4 );
+
+ var api = new mw.Api();
+
+ // Ensure we don't mistake a single item array for a single item and vice versa.
+ // The query parameter in request is the same either way (separated by pipe).
+ api.watch( 'Foo' ).done( function ( item ) {
+ assert.equal( item.title, 'Foo' );
+ } );
+
+ api.watch( [ 'Foo' ] ).done( function ( items ) {
+ assert.equal( items[0].title, 'Foo' );
+ } );
+
+ api.watch( [ 'Foo', 'Bar' ] ).done( function ( items ) {
+ assert.equal( items[0].title, 'Foo' );
+ assert.equal( items[1].title, 'Bar' );
+ } );
+
+ // Requests are POST, match requestBody instead of url
+ this.server.respond( function ( req ) {
+ if ( /action=watch.*&titles=Foo(&|$)/.test( req.requestBody ) ) {
+ req.respond( 200, { 'Content-Type': 'application/json' },
+ '{ "watch": [ { "title": "Foo", "watched": true, "message": "<b>Added</b>" } ] }'
+ );
+ }
+
+ if ( /action=watch.*&titles=Foo%7CBar/.test( req.requestBody ) ) {
+ req.respond( 200, { 'Content-Type': 'application/json' },
+ '{ "watch": [ ' +
+ '{ "title": "Foo", "watched": true, "message": "<b>Added</b>" },' +
+ '{ "title": "Bar", "watched": true, "message": "<b>Added</b>" }' +
+ '] }'
+ );
+ }
+ } );
+ } );
+}( mediaWiki ) );
diff --git a/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js
new file mode 100644
index 00000000..ee854aef
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js
@@ -0,0 +1,63 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mediawiki.special.recentchanges', QUnit.newMwEnvironment() );
+
+ // TODO: verify checkboxes == [ 'nsassociated', 'nsinvert' ]
+
+ QUnit.test( '"all" namespace disable checkboxes', 8, function ( assert ) {
+ var selectHtml, $env, $options;
+
+ // from Special:Recentchanges
+ selectHtml = '<select id="namespace" name="namespace" class="namespaceselector">'
+ + '<option value="" selected="selected">all</option>'
+ + '<option value="0">(Main)</option>'
+ + '<option value="1">Talk</option>'
+ + '<option value="2">User</option>'
+ + '<option value="3">User talk</option>'
+ + '<option value="4">ProjectName</option>'
+ + '<option value="5">ProjectName talk</option>'
+ + '</select>'
+ + '<input name="invert" type="checkbox" value="1" id="nsinvert" title="no title" />'
+ + '<label for="nsinvert" title="no title">Invert selection</label>'
+ + '<input name="associated" type="checkbox" value="1" id="nsassociated" title="no title" />'
+ + '<label for="nsassociated" title="no title">Associated namespace</label>'
+ + '<input type="submit" value="Go" />'
+ + '<input type="hidden" value="Special:RecentChanges" name="title" />';
+
+ $env = $( '<div>' ).html( selectHtml ).appendTo( 'body' );
+
+ // TODO abstract the double strictEquals
+
+ // At first checkboxes are enabled
+ assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), false );
+ assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), false );
+
+ // Initiate the recentchanges module
+ mw.special.recentchanges.init();
+
+ // By default
+ assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), true );
+ assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), true );
+
+ // select second option...
+ $options = $( '#namespace' ).find( 'option' );
+ $options.eq( 0 ).removeProp( 'selected' );
+ $options.eq( 1 ).prop( 'selected', true );
+ $( '#namespace' ).change();
+
+ // ... and checkboxes should be enabled again
+ assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), false );
+ assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), false );
+
+ // select first option ( 'all' namespace)...
+ $options.eq( 1 ).removeProp( 'selected' );
+ $options.eq( 0 ).prop( 'selected', true );
+ $( '#namespace' ).change();
+
+ // ... and checkboxes should now be disabled
+ assert.strictEqual( $( '#nsinvert' ).prop( 'disabled' ), true );
+ assert.strictEqual( $( '#nsassociated' ).prop( 'disabled' ), true );
+
+ // DOM cleanup
+ $env.remove();
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js
new file mode 100644
index 00000000..7ab309aa
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js
@@ -0,0 +1,650 @@
+/*jshint -W024 */
+( function ( mw, $ ) {
+ var repeat = function ( input, multiplier ) {
+ return new Array( multiplier + 1 ).join( input );
+ },
+ cases = {
+ // See also TitleTest.php#testSecureAndSplit
+ valid: [
+ 'Sandbox',
+ 'A "B"',
+ 'A \'B\'',
+ '.com',
+ '~',
+ '"',
+ '\'',
+ 'Talk:Sandbox',
+ 'Talk:Foo:Sandbox',
+ 'File:Example.svg',
+ 'File_talk:Example.svg',
+ 'Foo/.../Sandbox',
+ 'Sandbox/...',
+ 'A~~',
+ ':A',
+ // Length is 256 total, but only title part matters
+ 'Category:' + repeat( 'x', 248 ),
+ repeat( 'x', 252 )
+ ],
+ invalid: [
+ '',
+ ':',
+ '__ __',
+ ' __ ',
+ // Bad characters forbidden regardless of wgLegalTitleChars
+ 'A [ B',
+ 'A ] B',
+ 'A { B',
+ 'A } B',
+ 'A < B',
+ 'A > B',
+ 'A | B',
+ // URL encoding
+ 'A%20B',
+ 'A%23B',
+ 'A%2523B',
+ // XML/HTML character entity references
+ // Note: The ones with # are commented out as those are interpreted as fragment and
+ // as such end up being valid.
+ 'A &eacute; B',
+ //'A &#233; B',
+ //'A &#x00E9; B',
+ // Subject of NS_TALK does not roundtrip to NS_MAIN
+ 'Talk:File:Example.svg',
+ // Directory navigation
+ '.',
+ '..',
+ './Sandbox',
+ '../Sandbox',
+ 'Foo/./Sandbox',
+ 'Foo/../Sandbox',
+ 'Sandbox/.',
+ 'Sandbox/..',
+ // Tilde
+ 'A ~~~ Name',
+ 'A ~~~~ Signature',
+ 'A ~~~~~ Timestamp',
+ repeat( 'x', 256 ),
+ // Extension separation is a js invention, for length
+ // purposes it is part of the title
+ repeat( 'x', 252 ) + '.json',
+ // Namespace prefix without actual title
+ 'Talk:',
+ 'Category: ',
+ 'Category: #bar'
+ ]
+ };
+
+ QUnit.module( 'mediawiki.Title', QUnit.newMwEnvironment( {
+ // mw.Title relies on these three config vars
+ // Restore them after each test run
+ config: {
+ wgFormattedNamespaces: {
+ '-2': 'Media',
+ '-1': 'Special',
+ 0: '',
+ 1: 'Talk',
+ 2: 'User',
+ 3: 'User talk',
+ 4: 'Wikipedia',
+ 5: 'Wikipedia talk',
+ 6: 'File',
+ 7: 'File talk',
+ 8: 'MediaWiki',
+ 9: 'MediaWiki talk',
+ 10: 'Template',
+ 11: 'Template talk',
+ 12: 'Help',
+ 13: 'Help talk',
+ 14: 'Category',
+ 15: 'Category talk',
+ // testing custom / localized namespace
+ 100: 'Penguins'
+ },
+ wgNamespaceIds: {
+ 'media': -2,
+ 'special': -1,
+ '': 0,
+ 'talk': 1,
+ 'user': 2,
+ 'user_talk': 3,
+ 'wikipedia': 4,
+ 'wikipedia_talk': 5,
+ 'file': 6,
+ 'file_talk': 7,
+ 'mediawiki': 8,
+ 'mediawiki_talk': 9,
+ 'template': 10,
+ 'template_talk': 11,
+ 'help': 12,
+ 'help_talk': 13,
+ 'category': 14,
+ 'category_talk': 15,
+ 'image': 6,
+ 'image_talk': 7,
+ 'project': 4,
+ 'project_talk': 5,
+ // Testing custom namespaces and aliases
+ 'penguins': 100,
+ 'antarctic_waterfowl': 100
+ },
+ wgCaseSensitiveNamespaces: []
+ }
+ } ) );
+
+ QUnit.test( 'constructor', cases.invalid.length, function ( assert ) {
+ var i, title;
+ for ( i = 0; i < cases.valid.length; i++ ) {
+ title = new mw.Title( cases.valid[i] );
+ }
+ for ( i = 0; i < cases.invalid.length; i++ ) {
+ /*jshint loopfunc:true */
+ title = cases.invalid[i];
+ assert.throws( function () {
+ return new mw.Title( title );
+ }, cases.invalid[i] );
+ }
+ } );
+
+ QUnit.test( 'newFromText', cases.valid.length + cases.invalid.length, function ( assert ) {
+ var i;
+ for ( i = 0; i < cases.valid.length; i++ ) {
+ assert.equal(
+ $.type( mw.Title.newFromText( cases.valid[i] ) ),
+ 'object',
+ cases.valid[i]
+ );
+ }
+ for ( i = 0; i < cases.invalid.length; i++ ) {
+ assert.equal(
+ $.type( mw.Title.newFromText( cases.invalid[i] ) ),
+ 'null',
+ cases.invalid[i]
+ );
+ }
+ } );
+
+ QUnit.test( 'Basic parsing', 12, function ( assert ) {
+ var title;
+ title = new mw.Title( 'File:Foo_bar.JPG' );
+
+ assert.equal( title.getNamespaceId(), 6 );
+ assert.equal( title.getNamespacePrefix(), 'File:' );
+ assert.equal( title.getName(), 'Foo_bar' );
+ assert.equal( title.getNameText(), 'Foo bar' );
+ assert.equal( title.getExtension(), 'JPG' );
+ assert.equal( title.getDotExtension(), '.JPG' );
+ assert.equal( title.getMain(), 'Foo_bar.JPG' );
+ assert.equal( title.getMainText(), 'Foo bar.JPG' );
+ assert.equal( title.getPrefixedDb(), 'File:Foo_bar.JPG' );
+ assert.equal( title.getPrefixedText(), 'File:Foo bar.JPG' );
+
+ title = new mw.Title( 'Foo#bar' );
+ assert.equal( title.getPrefixedText(), 'Foo' );
+ assert.equal( title.getFragment(), 'bar' );
+ } );
+
+ QUnit.test( 'Transformation', 11, function ( assert ) {
+ var title;
+
+ title = new mw.Title( 'File:quux pif.jpg' );
+ assert.equal( title.getNameText(), 'Quux pif', 'First character of title' );
+
+ title = new mw.Title( 'File:Glarg_foo_glang.jpg' );
+ assert.equal( title.getNameText(), 'Glarg foo glang', 'Underscores' );
+
+ title = new mw.Title( 'User:ABC.DEF' );
+ assert.equal( title.toText(), 'User:ABC.DEF', 'Round trip text' );
+ assert.equal( title.getNamespaceId(), 2, 'Parse canonical namespace prefix' );
+
+ title = new mw.Title( 'Image:quux pix.jpg' );
+ assert.equal( title.getNamespacePrefix(), 'File:', 'Transform alias to canonical namespace' );
+
+ title = new mw.Title( 'uSEr:hAshAr' );
+ assert.equal( title.toText(), 'User:HAshAr' );
+ assert.equal( title.getNamespaceId(), 2, 'Case-insensitive namespace prefix' );
+
+ // Don't ask why, it's the way the backend works. One space is kept of each set.
+ title = new mw.Title( 'Foo __ \t __ bar' );
+ assert.equal( title.getMain(), 'Foo_bar', 'Merge multiple types of whitespace/underscores into a single underscore' );
+
+ // Regression test: Previously it would only detect an extension if there is no space after it
+ title = new mw.Title( 'Example.js ' );
+ assert.equal( title.getExtension(), 'js', 'Space after an extension is stripped' );
+
+ title = new mw.Title( 'Example#foo' );
+ assert.equal( title.getFragment(), 'foo', 'Fragment' );
+
+ title = new mw.Title( 'Example#_foo_bar baz_' );
+ assert.equal( title.getFragment(), ' foo bar baz', 'Fragment' );
+ } );
+
+ QUnit.test( 'Namespace detection and conversion', 10, function ( assert ) {
+ var title;
+
+ title = new mw.Title( 'File:User:Example' );
+ assert.equal( title.getNamespaceId(), 6, 'Titles can contain namespace prefixes, which are otherwise ignored' );
+
+ title = new mw.Title( 'Example', 6 );
+ assert.equal( title.getNamespaceId(), 6, 'Default namespace passed is used' );
+
+ title = new mw.Title( 'User:Example', 6 );
+ assert.equal( title.getNamespaceId(), 2, 'Included namespace prefix overrides the given default' );
+
+ title = new mw.Title( ':Example', 6 );
+ assert.equal( title.getNamespaceId(), 0, 'Colon forces main namespace' );
+
+ title = new mw.Title( 'something.PDF', 6 );
+ assert.equal( title.toString(), 'File:Something.PDF' );
+
+ title = new mw.Title( 'NeilK', 3 );
+ assert.equal( title.toString(), 'User_talk:NeilK' );
+ assert.equal( title.toText(), 'User talk:NeilK' );
+
+ title = new mw.Title( 'Frobisher', 100 );
+ assert.equal( title.toString(), 'Penguins:Frobisher' );
+
+ title = new mw.Title( 'antarctic_waterfowl:flightless_yet_cute.jpg' );
+ assert.equal( title.toString(), 'Penguins:Flightless_yet_cute.jpg' );
+
+ title = new mw.Title( 'Penguins:flightless_yet_cute.jpg' );
+ assert.equal( title.toString(), 'Penguins:Flightless_yet_cute.jpg' );
+ } );
+
+ QUnit.test( 'Throw error on invalid title', 1, function ( assert ) {
+ assert.throws( function () {
+ return new mw.Title( '' );
+ }, 'Throw error on empty string' );
+ } );
+
+ QUnit.test( 'Case-sensivity', 3, function ( assert ) {
+ var title;
+
+ // Default config
+ mw.config.set( 'wgCaseSensitiveNamespaces', [] );
+
+ title = new mw.Title( 'article' );
+ assert.equal( title.toString(), 'Article', 'Default config: No sensitive namespaces by default. First-letter becomes uppercase' );
+
+ // $wgCapitalLinks = false;
+ mw.config.set( 'wgCaseSensitiveNamespaces', [0, -2, 1, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15] );
+
+ title = new mw.Title( 'article' );
+ assert.equal( title.toString(), 'article', '$wgCapitalLinks=false: Article namespace is sensitive, first-letter case stays lowercase' );
+
+ title = new mw.Title( 'john', 2 );
+ assert.equal( title.toString(), 'User:John', '$wgCapitalLinks=false: User namespace is insensitive, first-letter becomes uppercase' );
+ } );
+
+ QUnit.test( 'toString / toText', 2, function ( assert ) {
+ var title = new mw.Title( 'Some random page' );
+
+ assert.equal( title.toString(), title.getPrefixedDb() );
+ assert.equal( title.toText(), title.getPrefixedText() );
+ } );
+
+ QUnit.test( 'getExtension', 7, function ( assert ) {
+ function extTest( pagename, ext, description ) {
+ var title = new mw.Title( pagename );
+ assert.equal( title.getExtension(), ext, description || pagename );
+ }
+
+ extTest( 'MediaWiki:Vector.js', 'js' );
+ extTest( 'User:Example/common.css', 'css' );
+ extTest( 'File:Example.longextension', 'longextension', 'Extension parsing not limited (bug 36151)' );
+ extTest( 'Example/information.json', 'json', 'Extension parsing not restricted from any namespace' );
+ extTest( 'Foo.', null, 'Trailing dot is not an extension' );
+ extTest( 'Foo..', null, 'Trailing dots are not an extension' );
+ extTest( 'Foo.a.', null, 'Page name with dots and ending in a dot does not have an extension' );
+
+ // @broken: Throws an exception
+ // extTest( '.NET', null, 'Leading dot is (or is not?) an extension' );
+ } );
+
+ QUnit.test( 'exists', 3, function ( assert ) {
+ var title;
+
+ // Empty registry, checks default to null
+
+ title = new mw.Title( 'Some random page', 4 );
+ assert.strictEqual( title.exists(), null, 'Return null with empty existance registry' );
+
+ // Basic registry, checks default to boolean
+ mw.Title.exist.set( ['Does_exist', 'User_talk:NeilK', 'Wikipedia:Sandbox_rules'], true );
+ mw.Title.exist.set( ['Does_not_exist', 'User:John', 'Foobar'], false );
+
+ title = new mw.Title( 'Project:Sandbox rules' );
+ assert.assertTrue( title.exists(), 'Return true for page titles marked as existing' );
+ title = new mw.Title( 'Foobar' );
+ assert.assertFalse( title.exists(), 'Return false for page titles marked as nonexistent' );
+
+ } );
+
+ QUnit.test( 'getUrl', 3, function ( assert ) {
+ var title;
+
+ // Config
+ mw.config.set( 'wgArticlePath', '/wiki/$1' );
+
+ title = new mw.Title( 'Foobar' );
+ assert.equal( title.getUrl(), '/wiki/Foobar', 'Basic functionality, getUrl uses mw.util.getUrl' );
+ assert.equal( title.getUrl({ action: 'edit' }), '/wiki/Foobar?action=edit', 'Basic functionality, \'params\' parameter' );
+
+ title = new mw.Title( 'John Doe', 3 );
+ assert.equal( title.getUrl(), '/wiki/User_talk:John_Doe', 'Escaping in title and namespace for urls' );
+ } );
+
+ QUnit.test( 'newFromImg', 40, function ( assert ) {
+ var title, i, thisCase, prefix,
+ cases = [
+ {
+ url: '//upload.wikimedia.org/wikipedia/commons/thumb/b/bf/Princess_Alexandra_of_Denmark_%28later_Queen_Alexandra%2C_wife_of_Edward_VII%29_with_her_two_eldest_sons%2C_Prince_Albert_Victor_%28Eddy%29_and_George_Frederick_Ernest_Albert_%28later_George_V%29.jpg/939px-thumbnail.jpg',
+ typeOfUrl: 'Hashed thumb with shortened path',
+ nameText: 'Princess Alexandra of Denmark (later Queen Alexandra, wife of Edward VII) with her two eldest sons, Prince Albert Victor (Eddy) and George Frederick Ernest Albert (later George V)',
+ prefixedText: 'File:Princess Alexandra of Denmark (later Queen Alexandra, wife of Edward VII) with her two eldest sons, Prince Albert Victor (Eddy) and George Frederick Ernest Albert (later George V).jpg'
+ },
+ {
+ url: '/wiki/images/thumb/9/91/Anticlockwise_heliotrope%27s.jpg/99px-Anticlockwise_heliotrope%27s.jpg',
+ typeOfUrl: 'Normal hashed directory thumbnail',
+ nameText: 'Anticlockwise heliotrope\'s',
+ prefixedText: 'File:Anticlockwise heliotrope\'s.jpg'
+ },
+
+ {
+ url: '/wiki/images/thumb/8/80/Wikipedia-logo-v2.svg/langde-150px-Wikipedia-logo-v2.svg.png',
+ typeOfUrl: 'Normal hashed directory thumbnail with complex thumbnail parameters',
+ nameText: 'Wikipedia-logo-v2',
+ prefixedText: 'File:Wikipedia-logo-v2.svg'
+ },
+
+ {
+ url: '//upload.wikimedia.org/wikipedia/commons/thumb/8/80/Wikipedia-logo-v2.svg/150px-Wikipedia-logo-v2.svg.png',
+ typeOfUrl: 'Commons thumbnail',
+ nameText: 'Wikipedia-logo-v2',
+ prefixedText: 'File:Wikipedia-logo-v2.svg'
+ },
+
+ {
+ url: '/wiki/images/9/91/Anticlockwise_heliotrope%27s.jpg',
+ typeOfUrl: 'Full image',
+ nameText: 'Anticlockwise heliotrope\'s',
+ prefixedText: 'File:Anticlockwise heliotrope\'s.jpg'
+ },
+
+ {
+ url: 'http://localhost/thumb.php?f=Stuffless_Figaro%27s.jpg&width=180',
+ typeOfUrl: 'thumb.php-based thumbnail',
+ nameText: 'Stuffless Figaro\'s',
+ prefixedText: 'File:Stuffless Figaro\'s.jpg'
+ },
+
+ {
+ url: '/wikipedia/commons/thumb/Wikipedia-logo-v2.svg/150px-Wikipedia-logo-v2.svg.png',
+ typeOfUrl: 'Commons unhashed thumbnail',
+ nameText: 'Wikipedia-logo-v2',
+ prefixedText: 'File:Wikipedia-logo-v2.svg'
+ },
+
+ {
+ url: '/wikipedia/commons/thumb/Wikipedia-logo-v2.svg/langde-150px-Wikipedia-logo-v2.svg.png',
+ typeOfUrl: 'Commons unhashed thumbnail with complex thumbnail parameters',
+ nameText: 'Wikipedia-logo-v2',
+ prefixedText: 'File:Wikipedia-logo-v2.svg'
+ },
+
+ {
+ url: '/wiki/images/Anticlockwise_heliotrope%27s.jpg',
+ typeOfUrl: 'Unhashed local file',
+ nameText: 'Anticlockwise heliotrope\'s',
+ prefixedText: 'File:Anticlockwise heliotrope\'s.jpg'
+ },
+
+ {
+ url: '',
+ typeOfUrl: 'Empty string'
+ },
+
+ {
+ url: 'foo',
+ typeOfUrl: 'String with only alphabet characters'
+ },
+
+ {
+ url: 'foobar.foobar',
+ typeOfUrl: 'Not a file path'
+ },
+
+ {
+ url: '/a/a0/blah blah blah',
+ typeOfUrl: 'Space characters'
+ }
+ ];
+
+ for ( i = 0; i < cases.length; i++ ) {
+ thisCase = cases[i];
+ title = mw.Title.newFromImg( { src: thisCase.url } );
+
+ if ( thisCase.nameText !== undefined ) {
+ prefix = '[' + thisCase.typeOfUrl + ' URL' + '] ';
+
+ assert.notStrictEqual( title, null, prefix + 'Parses successfully' );
+ assert.equal( title.getNameText(), thisCase.nameText, prefix + 'Filename matches original' );
+ assert.equal( title.getPrefixedText(), thisCase.prefixedText, prefix + 'File page title matches original' );
+ assert.equal( title.getNamespaceId(), 6, prefix + 'Namespace ID matches File namespace' );
+ } else {
+ assert.strictEqual( title, null, thisCase.typeOfUrl + ', should not produce an mw.Title object' );
+ }
+ }
+ } );
+
+ QUnit.test( 'getRelativeText', 5, function ( assert ) {
+ var cases = [
+ {
+ text: 'asd',
+ relativeTo: 123,
+ expectedResult: ':Asd'
+ },
+ {
+ text: 'dfg',
+ relativeTo: 0,
+ expectedResult: 'Dfg'
+ },
+ {
+ text: 'Template:Ghj',
+ relativeTo: 0,
+ expectedResult: 'Template:Ghj'
+ },
+ {
+ text: 'Template:1',
+ relativeTo: 10,
+ expectedResult: '1'
+ },
+ {
+ text: 'User:Hi',
+ relativeTo: 10,
+ expectedResult: 'User:Hi'
+ }
+ ], i, thisCase, title;
+
+ for ( i = 0; i < cases.length; i++ ) {
+ thisCase = cases[i];
+
+ title = mw.Title.newFromText( thisCase.text );
+ assert.equal( title.getRelativeText( thisCase.relativeTo ), thisCase.expectedResult );
+ }
+ } );
+
+ QUnit.test( 'newFromUserInput', 8, function ( assert ) {
+ var title, i, thisCase, prefix,
+ cases = [
+ {
+ title: 'DCS0001557854455.JPG',
+ defaultNamespace: 0,
+ options: {
+ fileExtension: 'PNG'
+ },
+ expected: 'DCS0001557854455.JPG',
+ description: 'Title in normal namespace without anything invalid but with "file extension"'
+ },
+ {
+ title: 'MediaWiki:Msg-awesome',
+ defaultNamespace: undefined,
+ expected: 'MediaWiki:Msg-awesome',
+ description: 'Full title (page in MediaWiki namespace) supplied as string'
+ },
+ {
+ title: 'The/Mw/Sound.flac',
+ defaultNamespace: -2,
+ expected: 'Media:The-Mw-Sound.flac',
+ description: 'Page in Media-namespace without explicit options'
+ },
+ {
+ title: 'File:The/Mw/Sound.kml',
+ defaultNamespace: 6,
+ options: {
+ forUploading: false
+ },
+ expected: 'File:The/Mw/Sound.kml',
+ description: 'Page in File-namespace without explicit options'
+ }
+ ];
+
+ for ( i = 0; i < cases.length; i++ ) {
+ thisCase = cases[i];
+ title = mw.Title.newFromUserInput( thisCase.title, thisCase.defaultNamespace, thisCase.options );
+
+ if ( thisCase.expected !== undefined ) {
+ prefix = '[' + thisCase.description + '] ';
+
+ assert.notStrictEqual( title, null, prefix + 'Parses successfully' );
+ assert.equal( title.toText(), thisCase.expected, prefix + 'Title as expected' );
+ } else {
+ assert.strictEqual( title, null, thisCase.description + ', should not produce an mw.Title object' );
+ }
+ }
+ } );
+
+ QUnit.test( 'newFromFileName', 62, function ( assert ) {
+ var title, i, thisCase, prefix,
+ cases = [
+ {
+ fileName: 'DCS0001557854455.JPG',
+ typeOfName: 'Standard camera output',
+ nameText: 'DCS0001557854455',
+ prefixedText: 'File:DCS0001557854455.JPG',
+ extensionDesired: 'jpg'
+ },
+ {
+ fileName: 'File:Sample.png',
+ typeOfName: 'Carrying namespace',
+ nameText: 'File-Sample',
+ prefixedText: 'File:File-Sample.png'
+ },
+ {
+ fileName: 'Treppe 2222 Test upload.jpg',
+ typeOfName: 'File name with spaces in it and lower case file extension',
+ nameText: 'Treppe 2222 Test upload',
+ prefixedText: 'File:Treppe 2222 Test upload.jpg',
+ extensionDesired: 'JPG'
+ },
+ {
+ fileName: 'I contain a \ttab.jpg',
+ typeOfName: 'Name containing a tab character',
+ nameText: 'I contain a tab',
+ prefixedText: 'File:I contain a tab.jpg'
+ },
+ {
+ fileName: 'I_contain multiple__ ___ _underscores.jpg',
+ typeOfName: 'Name containing multiple underscores',
+ nameText: 'I contain multiple underscores',
+ prefixedText: 'File:I contain multiple underscores.jpg'
+ },
+ {
+ fileName: 'I like ~~~~~~~~es.jpg',
+ typeOfName: 'Name containing more than three consecutive tilde characters',
+ nameText: 'I like ~~es',
+ prefixedText: 'File:I like ~~es.jpg'
+ },
+ {
+ fileName: 'BI\u200EDI.jpg',
+ typeOfName: 'Name containing BIDI overrides',
+ nameText: 'BIDI',
+ prefixedText: 'File:BIDI.jpg'
+ },
+ {
+ fileName: '100%ab progress.jpg',
+ typeOfName: 'File name with URL encoding',
+ nameText: '100% ab progress',
+ prefixedText: 'File:100% ab progress.jpg'
+ },
+ {
+ fileName: '<([>]):/#.jpg',
+ typeOfName: 'File name with characters not permitted in titles that are replaced',
+ nameText: '((()))---',
+ prefixedText: 'File:((()))---.jpg'
+ },
+ {
+ fileName: 'spaces\u0009\u2000\u200A\u200Bx.djvu',
+ typeOfName: 'File name with different kind of spaces',
+ nameText: 'Spaces \u200Bx',
+ prefixedText: 'File:Spaces \u200Bx.djvu'
+ },
+ {
+ fileName: 'dot.dot.dot.dot.dotdot',
+ typeOfName: 'File name with a lot of dots',
+ nameText: 'Dot.dot.dot.dot',
+ prefixedText: 'File:Dot.dot.dot.dot.dotdot'
+ },
+ {
+ fileName: 'dot. dot ._dot',
+ typeOfName: 'File name with multiple dots and spaces',
+ nameText: 'Dot. dot',
+ prefixedText: 'File:Dot. dot. dot'
+ },
+ {
+ fileName: 'dot. dot ._dot',
+ typeOfName: 'File name with different file extension desired',
+ nameText: 'Dot. dot . dot',
+ prefixedText: 'File:Dot. dot . dot.png',
+ extensionDesired: 'png'
+ },
+ {
+ fileName: 'fileWOExt',
+ typeOfName: 'File W/O extension with extension desired',
+ nameText: 'FileWOExt',
+ prefixedText: 'File:FileWOExt.png',
+ extensionDesired: 'png'
+ },
+ {
+ fileName: '𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎𠸏𠹷𠺝𠺢𠻗𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜𡁯𡁵𡁶𡁻𡃁𡃉𡇙𢃇𢞵𢫕𢭃𢯊𢱑𢱕𢳂𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜𡁯𡁵𡁶𡁻𡃁𡃉𡇙𢃇𢞵𢫕𢭃𢯊𢱑𢱕𢳂.png',
+ typeOfName: 'File name longer than 240 bytes',
+ nameText: '𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎𠸏𠹷𠺝𠺢𠻗𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜𡁯𡁵𡁶𡁻𡃁𡃉𡇙𢃇𢞵𢫕𢭃𢯊𢱑𢱕𢳂𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜𡁯𡁵𡁶𡁻𡃁𡃉𡇙𢃇𢞵',
+ prefixedText: 'File:𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎𠸏𠹷𠺝𠺢𠻗𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜𡁯𡁵𡁶𡁻𡃁𡃉𡇙𢃇𢞵𢫕𢭃𢯊𢱑𢱕𢳂𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜𡁯𡁵𡁶𡁻𡃁𡃉𡇙𢃇𢞵.png'
+ },
+ {
+ fileName: '',
+ typeOfName: 'Empty string'
+ },
+ {
+ fileName: 'foo',
+ typeOfName: 'String with only alphabet characters'
+ }
+ ];
+
+ for ( i = 0; i < cases.length; i++ ) {
+ thisCase = cases[i];
+ title = mw.Title.newFromFileName( thisCase.fileName, thisCase.extensionDesired );
+
+ if ( thisCase.nameText !== undefined ) {
+ prefix = '[' + thisCase.typeOfName + '] ';
+
+ assert.notStrictEqual( title, null, prefix + 'Parses successfully' );
+ assert.equal( title.getNameText(), thisCase.nameText, prefix + 'Filename matches original' );
+ assert.equal( title.getPrefixedText(), thisCase.prefixedText, prefix + 'File page title matches original' );
+ assert.equal( title.getNamespaceId(), 6, prefix + 'Namespace ID matches File namespace' );
+ } else {
+ assert.strictEqual( title, null, thisCase.typeOfName + ', should not produce an mw.Title object' );
+ }
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js
new file mode 100644
index 00000000..7a58d38d
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js
@@ -0,0 +1,434 @@
+/*jshint -W024 */
+( function ( mw, $ ) {
+ QUnit.module( 'mediawiki.Uri', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.mwUriOrg = mw.Uri;
+ mw.Uri = mw.UriRelative( 'http://example.org/w/index.php' );
+ },
+ teardown: function () {
+ mw.Uri = this.mwUriOrg;
+ delete this.mwUriOrg;
+ }
+ } ) );
+
+ $.each( [true, false], function ( i, strictMode ) {
+ QUnit.test( 'Basic construction and properties (' + ( strictMode ? '' : 'non-' ) + 'strict mode)', 2, function ( assert ) {
+ var uriString, uri;
+ uriString = 'http://www.ietf.org/rfc/rfc2396.txt';
+ uri = new mw.Uri( uriString, {
+ strictMode: strictMode
+ } );
+
+ assert.deepEqual(
+ {
+ protocol: uri.protocol,
+ host: uri.host,
+ port: uri.port,
+ path: uri.path,
+ query: uri.query,
+ fragment: uri.fragment
+ }, {
+ protocol: 'http',
+ host: 'www.ietf.org',
+ port: undefined,
+ path: '/rfc/rfc2396.txt',
+ query: {},
+ fragment: undefined
+ },
+ 'basic object properties'
+ );
+
+ assert.deepEqual(
+ {
+ userInfo: uri.getUserInfo(),
+ authority: uri.getAuthority(),
+ hostPort: uri.getHostPort(),
+ queryString: uri.getQueryString(),
+ relativePath: uri.getRelativePath(),
+ toString: uri.toString()
+ },
+ {
+ userInfo: '',
+ authority: 'www.ietf.org',
+ hostPort: 'www.ietf.org',
+ queryString: '',
+ relativePath: '/rfc/rfc2396.txt',
+ toString: uriString
+ },
+ 'construct composite components of URI on request'
+ );
+ } );
+ } );
+
+ QUnit.test( 'Constructor( String[, Object ] )', 10, function ( assert ) {
+ var uri;
+
+ uri = new mw.Uri( 'http://www.example.com/dir/?m=foo&m=bar&n=1', {
+ overrideKeys: true
+ } );
+
+ // Strict comparison to assert that numerical values stay strings
+ assert.strictEqual( uri.query.n, '1', 'Simple parameter with overrideKeys:true' );
+ assert.strictEqual( uri.query.m, 'bar', 'Last key overrides earlier keys with overrideKeys:true' );
+
+ uri = new mw.Uri( 'http://www.example.com/dir/?m=foo&m=bar&n=1', {
+ overrideKeys: false
+ } );
+
+ assert.strictEqual( uri.query.n, '1', 'Simple parameter with overrideKeys:false' );
+ assert.strictEqual( uri.query.m[0], 'foo', 'Order of multi-value parameters with overrideKeys:true' );
+ assert.strictEqual( uri.query.m[1], 'bar', 'Order of multi-value parameters with overrideKeys:true' );
+ assert.strictEqual( uri.query.m.length, 2, 'Number of mult-value field is correct' );
+
+ uri = new mw.Uri( 'ftp://usr:pwd@192.0.2.16/' );
+
+ assert.deepEqual(
+ {
+ protocol: uri.protocol,
+ user: uri.user,
+ password: uri.password,
+ host: uri.host,
+ port: uri.port,
+ path: uri.path,
+ query: uri.query,
+ fragment: uri.fragment
+ },
+ {
+ protocol: 'ftp',
+ user: 'usr',
+ password: 'pwd',
+ host: '192.0.2.16',
+ port: undefined,
+ path: '/',
+ query: {},
+ fragment: undefined
+ },
+ 'Parse an ftp URI correctly with user and password'
+ );
+
+ assert.throws(
+ function () {
+ return new mw.Uri( 'glaswegian penguins' );
+ },
+ function ( e ) {
+ return e.message === 'Bad constructor arguments';
+ },
+ 'throw error on non-URI as argument to constructor'
+ );
+
+ assert.throws(
+ function () {
+ return new mw.Uri( 'example.com/bar/baz', {
+ strictMode: true
+ } );
+ },
+ function ( e ) {
+ return e.message === 'Bad constructor arguments';
+ },
+ 'throw error on URI without protocol or // or leading / in strict mode'
+ );
+
+ uri = new mw.Uri( 'example.com/bar/baz', {
+ strictMode: false
+ } );
+ assert.equal( uri.toString(), 'http://example.com/bar/baz', 'normalize URI without protocol or // in loose mode' );
+ } );
+
+ QUnit.test( 'Constructor( Object )', 3, function ( assert ) {
+ var uri = new mw.Uri( {
+ protocol: 'http',
+ host: 'www.foo.local',
+ path: '/this'
+ } );
+ assert.equal( uri.toString(), 'http://www.foo.local/this', 'Basic properties' );
+
+ uri = new mw.Uri( {
+ protocol: 'http',
+ host: 'www.foo.local',
+ path: '/this',
+ query: { hi: 'there' },
+ fragment: 'blah'
+ } );
+ assert.equal( uri.toString(), 'http://www.foo.local/this?hi=there#blah', 'More complex properties' );
+
+ assert.throws(
+ function () {
+ return new mw.Uri( {
+ protocol: 'http',
+ host: 'www.foo.local'
+ } );
+ },
+ function ( e ) {
+ return e.message === 'Bad constructor arguments';
+ },
+ 'Construction failed when missing required properties'
+ );
+ } );
+
+ QUnit.test( 'Constructor( empty )', 4, function ( assert ) {
+ var testuri, MyUri, uri;
+
+ testuri = 'http://example.org/w/index.php';
+ MyUri = mw.UriRelative( testuri );
+
+ uri = new MyUri();
+ assert.equal( uri.toString(), testuri, 'no arguments' );
+
+ uri = new MyUri( undefined );
+ assert.equal( uri.toString(), testuri, 'undefined' );
+
+ uri = new MyUri( null );
+ assert.equal( uri.toString(), testuri, 'null' );
+
+ uri = new MyUri( '' );
+ assert.equal( uri.toString(), testuri, 'empty string' );
+ } );
+
+ QUnit.test( 'Properties', 8, function ( assert ) {
+ var uriBase, uri;
+
+ uriBase = new mw.Uri( 'http://en.wiki.local/w/api.php' );
+
+ uri = uriBase.clone();
+ uri.fragment = 'frag';
+ assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php#frag', 'add a fragment' );
+
+ uri = uriBase.clone();
+ uri.host = 'fr.wiki.local';
+ uri.port = '8080';
+ assert.equal( uri.toString(), 'http://fr.wiki.local:8080/w/api.php', 'change host and port' );
+
+ uri = uriBase.clone();
+ uri.query.foo = 'bar';
+ assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php?foo=bar', 'add query arguments' );
+
+ delete uri.query.foo;
+ assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php', 'delete query arguments' );
+
+ uri = uriBase.clone();
+ uri.query.foo = 'bar';
+ assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php?foo=bar', 'extend query arguments' );
+ uri.extend( {
+ foo: 'quux',
+ pif: 'paf'
+ } );
+ assert.ok( uri.toString().indexOf( 'foo=quux' ) >= 0, 'extend query arguments' );
+ assert.ok( uri.toString().indexOf( 'foo=bar' ) === -1, 'extend query arguments' );
+ assert.ok( uri.toString().indexOf( 'pif=paf' ) >= 0, 'extend query arguments' );
+ } );
+
+ QUnit.test( '.getQueryString()', 2, function ( assert ) {
+ var uri = new mw.Uri( 'http://search.example.com/?q=uri' );
+
+ assert.deepEqual(
+ {
+ protocol: uri.protocol,
+ host: uri.host,
+ port: uri.port,
+ path: uri.path,
+ query: uri.query,
+ fragment: uri.fragment,
+ queryString: uri.getQueryString()
+ },
+ {
+ protocol: 'http',
+ host: 'search.example.com',
+ port: undefined,
+ path: '/',
+ query: { q: 'uri' },
+ fragment: undefined,
+ queryString: 'q=uri'
+ },
+ 'basic object properties'
+ );
+
+ uri = new mw.Uri( 'https://example.com/mw/index.php?title=Sandbox/7&other=Sandbox/7&foo' );
+ assert.equal(
+ uri.getQueryString(),
+ 'title=Sandbox/7&other=Sandbox%2F7&foo',
+ 'title parameter is escaped the wiki-way'
+ );
+
+ } );
+
+ QUnit.test( '.clone()', 6, function ( assert ) {
+ var original, clone;
+
+ original = new mw.Uri( 'http://foo.example.org/index.php?one=1&two=2' );
+ clone = original.clone();
+
+ assert.deepEqual( clone, original, 'clone has equivalent properties' );
+ assert.equal( original.toString(), clone.toString(), 'toString matches original' );
+
+ assert.notStrictEqual( clone, original, 'clone is a different object when compared by reference' );
+
+ clone.host = 'bar.example.org';
+ assert.notEqual( original.host, clone.host, 'manipulating clone did not effect original' );
+ assert.notEqual( original.toString(), clone.toString(), 'Stringified url no longer matches original' );
+
+ clone.query.three = 3;
+
+ assert.deepEqual(
+ original.query,
+ { 'one': '1', 'two': '2' },
+ 'Properties is deep cloned (bug 37708)'
+ );
+ } );
+
+ QUnit.test( '.toString() after query manipulation', 8, function ( assert ) {
+ var uri;
+
+ uri = new mw.Uri( 'http://www.example.com/dir/?m=foo&m=bar&n=1', {
+ overrideKeys: true
+ } );
+
+ uri.query.n = [ 'x', 'y', 'z' ];
+
+ // Verify parts and total length instead of entire string because order
+ // of iteration can vary.
+ assert.ok( uri.toString().indexOf( 'm=bar' ), 'toString preserves other values' );
+ assert.ok( uri.toString().indexOf( 'n=x&n=y&n=z' ), 'toString parameter includes all values of an array query parameter' );
+ assert.equal( uri.toString().length, 'http://www.example.com/dir/?m=bar&n=x&n=y&n=z'.length, 'toString matches expected string' );
+
+ uri = new mw.Uri( 'http://www.example.com/dir/?m=foo&m=bar&n=1', {
+ overrideKeys: false
+ } );
+
+ // Change query values
+ uri.query.n = [ 'x', 'y', 'z' ];
+
+ // Verify parts and total length instead of entire string because order
+ // of iteration can vary.
+ assert.ok( uri.toString().indexOf( 'm=foo&m=bar' ) >= 0, 'toString preserves other values' );
+ assert.ok( uri.toString().indexOf( 'n=x&n=y&n=z' ) >= 0, 'toString parameter includes all values of an array query parameter' );
+ assert.equal( uri.toString().length, 'http://www.example.com/dir/?m=foo&m=bar&n=x&n=y&n=z'.length, 'toString matches expected string' );
+
+ // Remove query values
+ uri.query.m.splice( 0, 1 );
+ delete uri.query.n;
+
+ assert.equal( uri.toString(), 'http://www.example.com/dir/?m=bar', 'deletion properties' );
+
+ // Remove more query values, leaving an empty array
+ uri.query.m.splice( 0, 1 );
+ assert.equal( uri.toString(), 'http://www.example.com/dir/', 'empty array value is ommitted' );
+ } );
+
+ QUnit.test( 'Advanced URL', 11, function ( assert ) {
+ var uri, queryString, relativePath;
+
+ uri = new mw.Uri( 'http://auth@www.example.com:81/dir/dir.2/index.htm?q1=0&&test1&test2=value+%28escaped%29#top' );
+
+ assert.deepEqual(
+ {
+ protocol: uri.protocol,
+ user: uri.user,
+ password: uri.password,
+ host: uri.host,
+ port: uri.port,
+ path: uri.path,
+ query: uri.query,
+ fragment: uri.fragment
+ },
+ {
+ protocol: 'http',
+ user: 'auth',
+ password: undefined,
+ host: 'www.example.com',
+ port: '81',
+ path: '/dir/dir.2/index.htm',
+ query: { q1: '0', test1: null, test2: 'value (escaped)' },
+ fragment: 'top'
+ },
+ 'basic object properties'
+ );
+
+ assert.equal( uri.getUserInfo(), 'auth', 'user info' );
+
+ assert.equal( uri.getAuthority(), 'auth@www.example.com:81', 'authority equal to auth@hostport' );
+
+ assert.equal( uri.getHostPort(), 'www.example.com:81', 'hostport equal to host:port' );
+
+ queryString = uri.getQueryString();
+ assert.ok( queryString.indexOf( 'q1=0' ) >= 0, 'query param with numbers' );
+ assert.ok( queryString.indexOf( 'test1' ) >= 0, 'query param with null value is included' );
+ assert.ok( queryString.indexOf( 'test1=' ) === -1, 'query param with null value does not generate equals sign' );
+ assert.ok( queryString.indexOf( 'test2=value+%28escaped%29' ) >= 0, 'query param is url escaped' );
+
+ relativePath = uri.getRelativePath();
+ assert.ok( relativePath.indexOf( uri.path ) >= 0, 'path in relative path' );
+ assert.ok( relativePath.indexOf( uri.getQueryString() ) >= 0, 'query string in relative path' );
+ assert.ok( relativePath.indexOf( uri.fragment ) >= 0, 'fragement in relative path' );
+ } );
+
+ QUnit.test( 'Parse a uri with an @ symbol in the path and query', 1, function ( assert ) {
+ var uri = new mw.Uri( 'http://www.example.com/test@test?x=@uri&y@=uri&z@=@' );
+
+ assert.deepEqual(
+ {
+ protocol: uri.protocol,
+ user: uri.user,
+ password: uri.password,
+ host: uri.host,
+ port: uri.port,
+ path: uri.path,
+ query: uri.query,
+ fragment: uri.fragment,
+ queryString: uri.getQueryString()
+ },
+ {
+ protocol: 'http',
+ user: undefined,
+ password: undefined,
+ host: 'www.example.com',
+ port: undefined,
+ path: '/test@test',
+ query: { x: '@uri', 'y@': 'uri', 'z@': '@' },
+ fragment: undefined,
+ queryString: 'x=%40uri&y%40=uri&z%40=%40'
+ },
+ 'basic object properties'
+ );
+ } );
+
+ QUnit.test( 'Handle protocol-relative URLs', 5, function ( assert ) {
+ var UriRel, uri;
+
+ UriRel = mw.UriRelative( 'glork://en.wiki.local/foo.php' );
+
+ uri = new UriRel( '//en.wiki.local/w/api.php' );
+ assert.equal( uri.protocol, 'glork', 'create protocol-relative URLs with same protocol as document' );
+
+ uri = new UriRel( '/foo.com' );
+ assert.equal( uri.toString(), 'glork://en.wiki.local/foo.com', 'handle absolute paths by supplying protocol and host from document in loose mode' );
+
+ uri = new UriRel( 'http:/foo.com' );
+ assert.equal( uri.toString(), 'http://en.wiki.local/foo.com', 'handle absolute paths by supplying host from document in loose mode' );
+
+ uri = new UriRel( '/foo.com', true );
+ assert.equal( uri.toString(), 'glork://en.wiki.local/foo.com', 'handle absolute paths by supplying protocol and host from document in strict mode' );
+
+ uri = new UriRel( 'http:/foo.com', true );
+ assert.equal( uri.toString(), 'http://en.wiki.local/foo.com', 'handle absolute paths by supplying host from document in strict mode' );
+ } );
+
+ QUnit.test( 'bug 35658', 2, function ( assert ) {
+ var testProtocol, testServer, testPort, testPath, UriClass, uri, href;
+
+ testProtocol = 'https://';
+ testServer = 'foo.example.org';
+ testPort = '3004';
+ testPath = '/!1qy';
+
+ UriClass = mw.UriRelative( testProtocol + testServer + '/some/path/index.html' );
+ uri = new UriClass( testPath );
+ href = uri.toString();
+ assert.equal( href, testProtocol + testServer + testPath, 'Root-relative URL gets host & protocol supplied' );
+
+ UriClass = mw.UriRelative( testProtocol + testServer + ':' + testPort + '/some/path.php' );
+ uri = new UriClass( testPath );
+ href = uri.toString();
+ assert.equal( href, testProtocol + testServer + ':' + testPort + testPath, 'Root-relative URL gets host, protocol, and port supplied' );
+
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js
new file mode 100644
index 00000000..779a0ed4
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js
@@ -0,0 +1,81 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mediawiki.cldr', QUnit.newMwEnvironment() );
+
+ var pluralTestcases = {
+ /*
+ * Sample:
+ * languagecode : [
+ * [ number, [ 'form1', 'form2', ... ], 'expected', 'description' ]
+ * ];
+ */
+ en: [
+ [ 0, [ 'one', 'other' ], 'other', 'English plural test- 0 is other' ],
+ [ 1, [ 'one', 'other' ], 'one', 'English plural test- 1 is one' ]
+ ],
+ fa: [
+ [ 0, [ 'one', 'other' ], 'other', 'Persian plural test- 0 is other' ],
+ [ 1, [ 'one', 'other' ], 'one', 'Persian plural test- 1 is one' ],
+ [ 2, [ 'one', 'other' ], 'other', 'Persian plural test- 2 is other' ]
+ ],
+ fr: [
+ [ 0, [ 'one', 'other' ], 'other', 'French plural test- 0 is other' ],
+ [ 1, [ 'one', 'other' ], 'one', 'French plural test- 1 is one' ]
+ ],
+ hi: [
+ [ 0, [ 'one', 'other' ], 'one', 'Hindi plural test- 0 is one' ],
+ [ 1, [ 'one', 'other' ], 'one', 'Hindi plural test- 1 is one' ],
+ [ 2, [ 'one', 'other' ], 'other', 'Hindi plural test- 2 is other' ]
+ ],
+ he: [
+ [ 0, [ 'one', 'other' ], 'other', 'Hebrew plural test- 0 is other' ],
+ [ 1, [ 'one', 'other' ], 'one', 'Hebrew plural test- 1 is one' ],
+ [ 2, [ 'one', 'other' ], 'other', 'Hebrew plural test- 2 is other with 2 forms' ],
+ [ 2, [ 'one', 'dual', 'other' ], 'dual', 'Hebrew plural test- 2 is dual with 3 forms' ]
+ ],
+ hu: [
+ [ 0, [ 'one', 'other' ], 'other', 'Hungarian plural test- 0 is other' ],
+ [ 1, [ 'one', 'other' ], 'one', 'Hungarian plural test- 1 is one' ],
+ [ 2, [ 'one', 'other' ], 'other', 'Hungarian plural test- 2 is other' ]
+ ],
+ hy: [
+ [ 0, [ 'one', 'other' ], 'other', 'Armenian plural test- 0 is other' ],
+ [ 1, [ 'one', 'other' ], 'one', 'Armenian plural test- 1 is one' ],
+ [ 2, [ 'one', 'other' ], 'other', 'Armenian plural test- 2 is other' ]
+ ],
+ ar: [
+ [ 0, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'zero', 'Arabic plural test - 0 is zero' ],
+ [ 1, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'one', 'Arabic plural test - 1 is one' ],
+ [ 2, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'two', 'Arabic plural test - 2 is two' ],
+ [ 3, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'few', 'Arabic plural test - 3 is few' ],
+ [ 9, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'few', 'Arabic plural test - 9 is few' ],
+ [ '9', [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'few', 'Arabic plural test - 9 is few' ],
+ [ 110, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'few', 'Arabic plural test - 110 is few' ],
+ [ 11, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'many', 'Arabic plural test - 11 is many' ],
+ [ 15, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'many', 'Arabic plural test - 15 is many' ],
+ [ 99, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'many', 'Arabic plural test - 99 is many' ],
+ [ 9999, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'many', 'Arabic plural test - 9999 is many' ],
+ [ 100, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'other', 'Arabic plural test - 100 is other' ],
+ [ 102, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'other', 'Arabic plural test - 102 is other' ],
+ [ 1000, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'other', 'Arabic plural test - 1000 is other' ],
+ [ 1.7, [ 'zero', 'one', 'two', 'few', 'many', 'other' ], 'other', 'Arabic plural test - 1.7 is other' ]
+ ]
+ };
+
+ function pluralTest( langCode, tests ) {
+ QUnit.test( 'Plural Test for ' + langCode, tests.length, function ( assert ) {
+ for ( var i = 0; i < tests.length; i++ ) {
+ assert.equal(
+ mw.language.convertPlural( tests[i][0], tests[i][1] ),
+ tests[i][2],
+ tests[i][3]
+ );
+ }
+ } );
+ }
+
+ $.each( pluralTestcases, function ( langCode, tests ) {
+ if ( langCode === mw.config.get( 'wgUserLanguage' ) ) {
+ pluralTest( langCode, tests );
+ }
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js
new file mode 100644
index 00000000..c9653dab
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js
@@ -0,0 +1,172 @@
+( function ( mw, $ ) {
+
+ var NOW = 9012, // miliseconds
+ DEFAULT_DURATION = 5678, // seconds
+ expiryDate = new Date();
+
+ expiryDate.setTime( NOW + ( DEFAULT_DURATION * 1000 ) );
+
+ QUnit.module( 'mediawiki.cookie', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.stub( $, 'cookie' ).returns( null );
+
+ this.sandbox.useFakeTimers( NOW );
+ },
+ config: {
+ wgCookiePrefix: 'mywiki',
+ wgCookieDomain: 'example.org',
+ wgCookiePath: '/path',
+ wgCookieExpiration: DEFAULT_DURATION
+ }
+ } ) );
+
+ QUnit.test( 'set( key, value )', 7, function ( assert ) {
+ var call;
+
+ // Simple case
+ mw.cookie.set( 'foo', 'bar' );
+
+ call = $.cookie.lastCall.args;
+ assert.strictEqual( call[ 0 ], 'mywikifoo' );
+ assert.strictEqual( call[ 1 ], 'bar' );
+ assert.deepEqual( call[ 2 ], {
+ expires: expiryDate,
+ domain: 'example.org',
+ path: '/path',
+ secure: false
+ } );
+
+ mw.cookie.set( 'foo', null );
+ call = $.cookie.lastCall.args;
+ assert.strictEqual( call[ 1 ], null, 'null removes cookie' );
+
+ mw.cookie.set( 'foo', undefined );
+ call = $.cookie.lastCall.args;
+ assert.strictEqual( call[ 1 ], 'undefined', 'undefined is value' );
+
+ mw.cookie.set( 'foo', false );
+ call = $.cookie.lastCall.args;
+ assert.strictEqual( call[ 1 ], 'false', 'false is a value' );
+
+ mw.cookie.set( 'foo', 0 );
+ call = $.cookie.lastCall.args;
+ assert.strictEqual( call[ 1 ], '0', '0 is value' );
+ } );
+
+ QUnit.test( 'set( key, value, expires )', 5, function ( assert ) {
+ var date, options;
+
+ date = new Date();
+ date.setTime( 1234 );
+
+ mw.cookie.set( 'foo', 'bar' );
+ options = $.cookie.lastCall.args[ 2 ];
+ assert.deepEqual( options.expires, expiryDate, 'Default cookie expiration is used' );
+
+ mw.cookie.set( 'foo', 'bar', date );
+ options = $.cookie.lastCall.args[ 2 ];
+ assert.strictEqual( options.expires, date, 'Custom expiration date' );
+
+ mw.cookie.set( 'foo', 'bar', null );
+ options = $.cookie.lastCall.args[ 2 ];
+ assert.strictEqual( options.expires, undefined, 'Expiry null forces session cookie' );
+
+ // Per DefaultSettings.php, when wgCookieExpiration is 0, the default should
+ // be session cookies
+ mw.config.set( 'wgCookieExpiration', 0 );
+
+ mw.cookie.set( 'foo', 'bar' );
+ options = $.cookie.lastCall.args[ 2 ];
+ assert.strictEqual( options.expires, undefined, 'wgCookieExpiration=0 results in session cookies by default' );
+
+ mw.cookie.set( 'foo', 'bar', date );
+ options = $.cookie.lastCall.args[ 2 ];
+ assert.strictEqual( options.expires, date, 'Custom expiration when default is session cookies' );
+ } );
+
+ QUnit.test( 'set( key, value, options )', 4, function ( assert ) {
+ var date, call;
+
+ mw.cookie.set( 'foo', 'bar', {
+ prefix: 'myPrefix',
+ domain: 'myDomain',
+ path: 'myPath',
+ secure: true
+ } );
+
+ call = $.cookie.lastCall.args;
+ assert.strictEqual( call[0], 'myPrefixfoo' );
+ assert.deepEqual( call[ 2 ], {
+ expires: expiryDate,
+ domain: 'myDomain',
+ path: 'myPath',
+ secure: true
+ }, 'Options (without expires)' );
+
+ date = new Date();
+ date.setTime( 1234 );
+
+ mw.cookie.set( 'foo', 'bar', {
+ expires: date,
+ prefix: 'myPrefix',
+ domain: 'myDomain',
+ path: 'myPath',
+ secure: true
+ } );
+
+ call = $.cookie.lastCall.args;
+ assert.strictEqual( call[0], 'myPrefixfoo' );
+ assert.deepEqual( call[ 2 ], {
+ expires: date,
+ domain: 'myDomain',
+ path: 'myPath',
+ secure: true
+ }, 'Options (incl. expires)' );
+ } );
+
+ QUnit.test( 'get( key ) - no values', 6, function ( assert ) {
+ var key, value;
+
+ mw.cookie.get( 'foo' );
+
+ key = $.cookie.lastCall.args[ 0 ];
+ assert.strictEqual( key, 'mywikifoo', 'Default prefix' );
+
+ mw.cookie.get( 'foo', undefined );
+ key = $.cookie.lastCall.args[ 0 ];
+ assert.strictEqual( key, 'mywikifoo', 'Use default prefix for undefined' );
+
+ mw.cookie.get( 'foo', null );
+ key = $.cookie.lastCall.args[ 0 ];
+ assert.strictEqual( key, 'mywikifoo', 'Use default prefix for null' );
+
+ mw.cookie.get( 'foo', '' );
+ key = $.cookie.lastCall.args[ 0 ];
+ assert.strictEqual( key, 'foo', 'Don\'t use default prefix for empty string' );
+
+ value = mw.cookie.get( 'foo' );
+ assert.strictEqual( value, null, 'Return null by default' );
+
+ value = mw.cookie.get( 'foo', null, 'bar' );
+ assert.strictEqual( value, 'bar', 'Custom default value' );
+ } );
+
+ QUnit.test( 'get( key ) - with value', 1, function ( assert ) {
+ var value;
+
+ $.cookie.returns( 'bar' );
+
+ value = mw.cookie.get( 'foo' );
+ assert.strictEqual( value, 'bar', 'Return value of cookie' );
+ } );
+
+ QUnit.test( 'get( key, prefix )', 1, function ( assert ) {
+ var key;
+
+ mw.cookie.get( 'foo', 'bar' );
+
+ key = $.cookie.lastCall.args[ 0 ];
+ assert.strictEqual( key, 'barfoo' );
+ } );
+
+} ( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
new file mode 100644
index 00000000..6b3be43b
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
@@ -0,0 +1,798 @@
+( function ( mw, $ ) {
+ var formatText, formatParse, formatnumTests, specialCharactersPageName, expectedListUsers, expectedEntrypoints,
+ mwLanguageCache = {},
+ hasOwn = Object.hasOwnProperty;
+
+ // When the expected result is the same in both modes
+ function assertBothModes( assert, parserArguments, expectedResult, assertMessage ) {
+ assert.equal( formatText.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'text\'' );
+ assert.equal( formatParse.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'parse\'' );
+ }
+
+ QUnit.module( 'mediawiki.jqueryMsg', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.originalMwLanguage = mw.language;
+
+ specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?';
+
+ expectedListUsers = '注册<a title="Special:ListUsers" href="/wiki/Special:ListUsers">用户</a>';
+
+ expectedEntrypoints = '<a href="https://www.mediawiki.org/wiki/Manual:index.php">index.php</a>';
+
+ formatText = mw.jqueryMsg.getMessageFunction( {
+ format: 'text'
+ } );
+
+ formatParse = mw.jqueryMsg.getMessageFunction( {
+ format: 'parse'
+ } );
+ },
+ teardown: function () {
+ mw.language = this.originalMwLanguage;
+ },
+ config: {
+ wgArticlePath: '/wiki/$1'
+ },
+ // Messages that are reused in multiple tests
+ messages: {
+ // The values for gender are not significant,
+ // what matters is which of the values is choosen by the parser
+ 'gender-msg': '$1: {{GENDER:$2|blue|pink|green}}',
+ 'gender-msg-currentuser': '{{GENDER:|blue|pink|green}}',
+
+ 'plural-msg': 'Found $1 {{PLURAL:$1|item|items}}',
+ // See https://bugzilla.wikimedia.org/69993
+ 'plural-msg-explicit-forms-nested': 'Found {{PLURAL:$1|$1 results|0=no results in {{SITENAME}}|1=$1 result}}',
+ // Assume the grammar form grammar_case_foo is not valid in any language
+ 'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}',
+
+ 'formatnum-msg': '{{formatnum:$1}}',
+
+ 'portal-url': 'Project:Community portal',
+ 'see-portal-url': '{{Int:portal-url}} is an important community page.',
+
+ 'jquerymsg-test-statistics-users': '注册[[Special:ListUsers|用户]]',
+
+ 'jquerymsg-test-version-entrypoints-index-php': '[https://www.mediawiki.org/wiki/Manual:index.php index.php]',
+
+ 'external-link-replace': 'Foo [$1 bar]'
+ }
+ } ) );
+
+ /**
+ * Be careful to no run this in parallel as it uses a global identifier (mw.language)
+ * to transport the module back to the test. It musn't be overwritten concurrentely.
+ *
+ * This function caches the mw.language data to avoid having to request the same module
+ * multiple times. There is more than one test case for any given language.
+ */
+ function getMwLanguage( langCode ) {
+ if ( !hasOwn.call( mwLanguageCache, langCode ) ) {
+ mwLanguageCache[langCode] = $.ajax( {
+ url: mw.util.wikiScript( 'load' ),
+ data: {
+ skin: mw.config.get( 'skin' ),
+ lang: langCode,
+ debug: mw.config.get( 'debug' ),
+ modules: [
+ 'mediawiki.language.data',
+ 'mediawiki.language'
+ ].join( '|' ),
+ only: 'scripts'
+ },
+ dataType: 'script',
+ cache: true
+ } ).then( function () {
+ return mw.language;
+ } );
+ }
+ return mwLanguageCache[langCode];
+ }
+
+ /**
+ * @param {Function[]} tasks List of functions that perform tasks
+ * that may be asynchronous. Invoke the callback parameter when done.
+ * @param {Function} done When all tasks are done.
+ * @return
+ */
+ function process( tasks, done ) {
+ function run() {
+ var task = tasks.shift();
+ if ( task ) {
+ task( run );
+ } else {
+ done();
+ }
+ }
+ run();
+ }
+
+ QUnit.test( 'Replace', 9, function ( assert ) {
+ mw.messages.set( 'simple', 'Foo $1 baz $2' );
+
+ assert.equal( formatParse( 'simple' ), 'Foo $1 baz $2', 'Replacements with no substitutes' );
+ assert.equal( formatParse( 'simple', 'bar' ), 'Foo bar baz $2', 'Replacements with less substitutes' );
+ assert.equal( formatParse( 'simple', 'bar', 'quux' ), 'Foo bar baz quux', 'Replacements with all substitutes' );
+
+ mw.messages.set( 'plain-input', '<foo foo="foo">x$1y&lt;</foo>z' );
+
+ assert.equal(
+ formatParse( 'plain-input', 'bar' ),
+ '&lt;foo foo="foo"&gt;xbary&amp;lt;&lt;/foo&gt;z',
+ 'Input is not considered html'
+ );
+
+ mw.messages.set( 'plain-replace', 'Foo $1' );
+
+ assert.equal(
+ formatParse( 'plain-replace', '<bar bar="bar">&gt;</bar>' ),
+ 'Foo &lt;bar bar="bar"&gt;&amp;gt;&lt;/bar&gt;',
+ 'Replacement is not considered html'
+ );
+
+ mw.messages.set( 'object-replace', 'Foo $1' );
+
+ assert.equal(
+ formatParse( 'object-replace', $( '<div class="bar">&gt;</div>' ) ),
+ 'Foo <div class="bar">&gt;</div>',
+ 'jQuery objects are preserved as raw html'
+ );
+
+ assert.equal(
+ formatParse( 'object-replace', $( '<div class="bar">&gt;</div>' ).get( 0 ) ),
+ 'Foo <div class="bar">&gt;</div>',
+ 'HTMLElement objects are preserved as raw html'
+ );
+
+ assert.equal(
+ formatParse( 'object-replace', $( '<div class="bar">&gt;</div>' ).toArray() ),
+ 'Foo <div class="bar">&gt;</div>',
+ 'HTMLElement[] arrays are preserved as raw html'
+ );
+
+ assert.equal(
+ formatParse( 'external-link-replace', 'http://example.org/?x=y&z' ),
+ 'Foo <a href="http://example.org/?x=y&amp;z">bar</a>',
+ 'Href is not double-escaped in wikilink function'
+ );
+ } );
+
+ QUnit.test( 'Plural', 6, function ( assert ) {
+ assert.equal( formatParse( 'plural-msg', 0 ), 'Found 0 items', 'Plural test for english with zero as count' );
+ assert.equal( formatParse( 'plural-msg', 1 ), 'Found 1 item', 'Singular test for english' );
+ assert.equal( formatParse( 'plural-msg', 2 ), 'Found 2 items', 'Plural test for english' );
+ assert.equal( formatParse( 'plural-msg-explicit-forms-nested', 6 ), 'Found 6 results', 'Plural message with explicit plural forms' );
+ assert.equal( formatParse( 'plural-msg-explicit-forms-nested', 0 ), 'Found no results in ' + mw.config.get( 'wgSiteName' ), 'Plural message with explicit plural forms, with nested {{SITENAME}}' );
+ assert.equal( formatParse( 'plural-msg-explicit-forms-nested', 1 ), 'Found 1 result', 'Plural message with explicit plural forms with placeholder nested' );
+ } );
+
+ QUnit.test( 'Gender', 15, function ( assert ) {
+ var originalGender = mw.user.options.get( 'gender' );
+
+ // TODO: These tests should be for mw.msg once mw.msg integrated with mw.jqueryMsg
+ // TODO: English may not be the best language for these tests. Use a language like Arabic or Russian
+ mw.user.options.set( 'gender', 'male' );
+ assert.equal(
+ formatParse( 'gender-msg', 'Bob', 'male' ),
+ 'Bob: blue',
+ 'Masculine from string "male"'
+ );
+ assert.equal(
+ formatParse( 'gender-msg', 'Bob', mw.user ),
+ 'Bob: blue',
+ 'Masculine from mw.user object'
+ );
+ assert.equal(
+ formatParse( 'gender-msg-currentuser' ),
+ 'blue',
+ 'Masculine for current user'
+ );
+
+ mw.user.options.set( 'gender', 'female' );
+ assert.equal(
+ formatParse( 'gender-msg', 'Alice', 'female' ),
+ 'Alice: pink',
+ 'Feminine from string "female"' );
+ assert.equal(
+ formatParse( 'gender-msg', 'Alice', mw.user ),
+ 'Alice: pink',
+ 'Feminine from mw.user object'
+ );
+ assert.equal(
+ formatParse( 'gender-msg-currentuser' ),
+ 'pink',
+ 'Feminine for current user'
+ );
+
+ mw.user.options.set( 'gender', 'unknown' );
+ assert.equal(
+ formatParse( 'gender-msg', 'Foo', mw.user ),
+ 'Foo: green',
+ 'Neutral from mw.user object' );
+ assert.equal(
+ formatParse( 'gender-msg', 'User' ),
+ 'User: green',
+ 'Neutral when no parameter given' );
+ assert.equal(
+ formatParse( 'gender-msg', 'User', 'unknown' ),
+ 'User: green',
+ 'Neutral from string "unknown"'
+ );
+ assert.equal(
+ formatParse( 'gender-msg-currentuser' ),
+ 'green',
+ 'Neutral for current user'
+ );
+
+ mw.messages.set( 'gender-msg-one-form', '{{GENDER:$1|User}}: $2 {{PLURAL:$2|edit|edits}}' );
+
+ assert.equal(
+ formatParse( 'gender-msg-one-form', 'male', 10 ),
+ 'User: 10 edits',
+ 'Gender neutral and plural form'
+ );
+ assert.equal(
+ formatParse( 'gender-msg-one-form', 'female', 1 ),
+ 'User: 1 edit',
+ 'Gender neutral and singular form'
+ );
+
+ mw.messages.set( 'gender-msg-lowercase', '{{gender:$1|he|she}} is awesome' );
+ assert.equal(
+ formatParse( 'gender-msg-lowercase', 'male' ),
+ 'he is awesome',
+ 'Gender masculine'
+ );
+ assert.equal(
+ formatParse( 'gender-msg-lowercase', 'female' ),
+ 'she is awesome',
+ 'Gender feminine'
+ );
+
+ mw.messages.set( 'gender-msg-wrong', '{{gender}} test' );
+ assert.equal(
+ formatParse( 'gender-msg-wrong', 'female' ),
+ ' test',
+ 'Invalid syntax should result in {{gender}} simply being stripped away'
+ );
+
+ mw.user.options.set( 'gender', originalGender );
+ } );
+
+ QUnit.test( 'Grammar', 2, function ( assert ) {
+ assert.equal( formatParse( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar Test with sitename' );
+
+ mw.messages.set( 'grammar-msg-wrong-syntax', 'Przeszukaj {{GRAMMAR:grammar_case_xyz}}' );
+ assert.equal( formatParse( 'grammar-msg-wrong-syntax' ), 'Przeszukaj ', 'Grammar Test with wrong grammar template syntax' );
+ } );
+
+ QUnit.test( 'Match PHP parser', mw.libs.phpParserData.tests.length, function ( assert ) {
+ mw.messages.set( mw.libs.phpParserData.messages );
+ var tasks = $.map( mw.libs.phpParserData.tests, function ( test ) {
+ return function ( next ) {
+ getMwLanguage( test.lang )
+ .done( function ( langClass ) {
+ mw.config.set( 'wgUserLanguage', test.lang );
+ var parser = new mw.jqueryMsg.parser( { language: langClass } );
+ assert.equal(
+ parser.parse( test.key, test.args ).html(),
+ test.result,
+ test.name
+ );
+ } )
+ .fail( function () {
+ assert.ok( false, 'Language "' + test.lang + '" failed to load.' );
+ } )
+ .always( next );
+ };
+ } );
+
+ QUnit.stop();
+ process( tasks, QUnit.start );
+ } );
+
+ QUnit.test( 'Links', 6, function ( assert ) {
+ var expectedDisambiguationsText,
+ expectedMultipleBars,
+ expectedSpecialCharacters;
+
+ // The below three are all identical to or based on real messages. For disambiguations-text,
+ // the bold was removed because it is not yet implemented.
+
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-test-statistics-users' ),
+ expectedListUsers,
+ 'Piped wikilink'
+ );
+
+ expectedDisambiguationsText = 'The following pages contain at least one link to a disambiguation page.\nThey may have to link to a more appropriate page instead.\nA page is treated as a disambiguation page if it uses a template that is linked from ' +
+ '<a title="MediaWiki:Disambiguationspage" href="/wiki/MediaWiki:Disambiguationspage">MediaWiki:Disambiguationspage</a>.';
+
+ mw.messages.set( 'disambiguations-text', 'The following pages contain at least one link to a disambiguation page.\nThey may have to link to a more appropriate page instead.\nA page is treated as a disambiguation page if it uses a template that is linked from [[MediaWiki:Disambiguationspage]].' );
+ assert.htmlEqual(
+ formatParse( 'disambiguations-text' ),
+ expectedDisambiguationsText,
+ 'Wikilink without pipe'
+ );
+
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-test-version-entrypoints-index-php' ),
+ expectedEntrypoints,
+ 'External link'
+ );
+
+ // Pipe trick is not supported currently, but should not parse as text either.
+ mw.messages.set( 'pipe-trick', '[[Tampa, Florida|]]' );
+ this.suppressWarnings();
+ assert.equal(
+ formatParse( 'pipe-trick' ),
+ '[[Tampa, Florida|]]',
+ 'Pipe trick should not be parsed.'
+ );
+ this.restoreWarnings();
+
+ expectedMultipleBars = '<a title="Main Page" href="/wiki/Main_Page">Main|Page</a>';
+ mw.messages.set( 'multiple-bars', '[[Main Page|Main|Page]]' );
+ assert.htmlEqual(
+ formatParse( 'multiple-bars' ),
+ expectedMultipleBars,
+ 'Bar in anchor'
+ );
+
+ expectedSpecialCharacters = '<a title="&quot;Who&quot; wants to be a millionaire &amp; live on &#039;Exotic Island&#039;?" href="/wiki/%22Who%22_wants_to_be_a_millionaire_%26_live_on_%27Exotic_Island%27%3F">&quot;Who&quot; wants to be a millionaire &amp; live on &#039;Exotic Island&#039;?</a>';
+
+ mw.messages.set( 'special-characters', '[[' + specialCharactersPageName + ']]' );
+ assert.htmlEqual(
+ formatParse( 'special-characters' ),
+ expectedSpecialCharacters,
+ 'Special characters'
+ );
+ } );
+
+// Tests that {{-transformation vs. general parsing are done as requested
+ QUnit.test( 'Curly brace transformation', 16, function ( assert ) {
+ var oldUserLang = mw.config.get( 'wgUserLanguage' );
+
+ assertBothModes( assert, ['gender-msg', 'Bob', 'male'], 'Bob: blue', 'gender is resolved' );
+
+ assertBothModes( assert, ['plural-msg', 5], 'Found 5 items', 'plural is resolved' );
+
+ assertBothModes( assert, ['grammar-msg'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'grammar is resolved' );
+
+ mw.config.set( 'wgUserLanguage', 'en' );
+ assertBothModes( assert, ['formatnum-msg', '987654321.654321'], '987,654,321.654', 'formatnum is resolved' );
+
+ // Test non-{{ wikitext, where behavior differs
+
+ // Wikilink
+ assert.equal(
+ formatText( 'jquerymsg-test-statistics-users' ),
+ mw.messages.get( 'jquerymsg-test-statistics-users' ),
+ 'Internal link message unchanged when format is \'text\''
+ );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-test-statistics-users' ),
+ expectedListUsers,
+ 'Internal link message parsed when format is \'parse\''
+ );
+
+ // External link
+ assert.equal(
+ formatText( 'jquerymsg-test-version-entrypoints-index-php' ),
+ mw.messages.get( 'jquerymsg-test-version-entrypoints-index-php' ),
+ 'External link message unchanged when format is \'text\''
+ );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-test-version-entrypoints-index-php' ),
+ expectedEntrypoints,
+ 'External link message processed when format is \'parse\''
+ );
+
+ // External link with parameter
+ assert.equal(
+ formatText( 'external-link-replace', 'http://example.com' ),
+ 'Foo [http://example.com bar]',
+ 'External link message only substitutes parameter when format is \'text\''
+ );
+ assert.htmlEqual(
+ formatParse( 'external-link-replace', 'http://example.com' ),
+ 'Foo <a href="http://example.com">bar</a>',
+ 'External link message processed when format is \'parse\''
+ );
+ assert.htmlEqual(
+ formatParse( 'external-link-replace', $( '<i>' ) ),
+ 'Foo <i>bar</i>',
+ 'External link message processed as jQuery object when format is \'parse\''
+ );
+ assert.htmlEqual(
+ formatParse( 'external-link-replace', function () {} ),
+ 'Foo <a href="#">bar</a>',
+ 'External link message processed as function when format is \'parse\''
+ );
+
+ mw.config.set( 'wgUserLanguage', oldUserLang );
+ } );
+
+ QUnit.test( 'Int', 4, function ( assert ) {
+ var newarticletextSource = 'You have followed a link to a page that does not exist yet. To create the page, start typing in the box below (see the [[{{Int:Foobar}}|foobar]] for more info). If you are here by mistake, click your browser\'s back button.',
+ expectedNewarticletext,
+ helpPageTitle = 'Help:Foobar';
+
+ mw.messages.set( 'foobar', helpPageTitle );
+
+ expectedNewarticletext = 'You have followed a link to a page that does not exist yet. To create the page, start typing in the box below (see the ' +
+ '<a title="Help:Foobar" href="/wiki/Help:Foobar">foobar</a> for more info). If you are here by mistake, click your browser\'s back button.';
+
+ mw.messages.set( 'newarticletext', newarticletextSource );
+
+ assert.htmlEqual(
+ formatParse( 'newarticletext' ),
+ expectedNewarticletext,
+ 'Link with nested message'
+ );
+
+ assert.equal(
+ formatParse( 'see-portal-url' ),
+ 'Project:Community portal is an important community page.',
+ 'Nested message'
+ );
+
+ mw.messages.set( 'newarticletext-lowercase',
+ newarticletextSource.replace( 'Int:Helppage', 'int:helppage' ) );
+
+ assert.htmlEqual(
+ formatParse( 'newarticletext-lowercase' ),
+ expectedNewarticletext,
+ 'Link with nested message, lowercase include'
+ );
+
+ mw.messages.set( 'uses-missing-int', '{{int:doesnt-exist}}' );
+
+ assert.equal(
+ formatParse( 'uses-missing-int' ),
+ '[doesnt-exist]',
+ 'int: where nested message does not exist'
+ );
+ } );
+
+ // Tests that getMessageFunction is used for non-plain messages with curly braces or
+ // square brackets, but not otherwise.
+ QUnit.test( 'mw.Message.prototype.parser monkey-patch', 22, function ( assert ) {
+ var oldGMF, outerCalled, innerCalled;
+
+ mw.messages.set( {
+ 'curly-brace': '{{int:message}}',
+ 'single-square-bracket': '[https://www.mediawiki.org/ MediaWiki]',
+ 'double-square-bracket': '[[Some page]]',
+ 'regular': 'Other message'
+ } );
+
+ oldGMF = mw.jqueryMsg.getMessageFunction;
+
+ mw.jqueryMsg.getMessageFunction = function () {
+ outerCalled = true;
+ return function () {
+ innerCalled = true;
+ };
+ };
+
+ function verifyGetMessageFunction( key, format, shouldCall ) {
+ var message;
+ outerCalled = false;
+ innerCalled = false;
+ message = mw.message( key );
+ message[format]();
+ assert.strictEqual( outerCalled, shouldCall, 'Outer function called for ' + key );
+ assert.strictEqual( innerCalled, shouldCall, 'Inner function called for ' + key );
+ }
+
+ verifyGetMessageFunction( 'curly-brace', 'parse', true );
+ verifyGetMessageFunction( 'curly-brace', 'plain', false );
+
+ verifyGetMessageFunction( 'single-square-bracket', 'parse', true );
+ verifyGetMessageFunction( 'single-square-bracket', 'plain', false );
+
+ verifyGetMessageFunction( 'double-square-bracket', 'parse', true );
+ verifyGetMessageFunction( 'double-square-bracket', 'plain', false );
+
+ verifyGetMessageFunction( 'regular', 'parse', false );
+ verifyGetMessageFunction( 'regular', 'plain', false );
+
+ verifyGetMessageFunction( 'jquerymsg-test-pagetriage-del-talk-page-notify-summary', 'plain', false );
+ verifyGetMessageFunction( 'jquerymsg-test-categorytree-collapse-bullet', 'plain', false );
+ verifyGetMessageFunction( 'jquerymsg-test-wikieditor-toolbar-help-content-signature-result', 'plain', false );
+
+ mw.jqueryMsg.getMessageFunction = oldGMF;
+ } );
+
+formatnumTests = [
+ {
+ lang: 'en',
+ number: 987654321.654321,
+ result: '987,654,321.654',
+ description: 'formatnum test for English, decimal seperator'
+ },
+ {
+ lang: 'ar',
+ number: 987654321.654321,
+ result: '٩٨٧٬٦٥٤٬٣٢١٫٦٥٤',
+ description: 'formatnum test for Arabic, with decimal seperator'
+ },
+ {
+ lang: 'ar',
+ number: '٩٨٧٦٥٤٣٢١٫٦٥٤٣٢١',
+ result: 987654321,
+ integer: true,
+ description: 'formatnum test for Arabic, with decimal seperator, reverse'
+ },
+ {
+ lang: 'ar',
+ number: -12.89,
+ result: '-١٢٫٨٩',
+ description: 'formatnum test for Arabic, negative number'
+ },
+ {
+ lang: 'ar',
+ number: '-١٢٫٨٩',
+ result: -12,
+ integer: true,
+ description: 'formatnum test for Arabic, negative number, reverse'
+ },
+ {
+ lang: 'nl',
+ number: 987654321.654321,
+ result: '987.654.321,654',
+ description: 'formatnum test for Nederlands, decimal seperator'
+ },
+ {
+ lang: 'nl',
+ number: -12.89,
+ result: '-12,89',
+ description: 'formatnum test for Nederlands, negative number'
+ },
+ {
+ lang: 'nl',
+ number: '.89',
+ result: '0,89',
+ description: 'formatnum test for Nederlands'
+ },
+ {
+ lang: 'nl',
+ number: 'invalidnumber',
+ result: 'invalidnumber',
+ description: 'formatnum test for Nederlands, invalid number'
+ },
+ {
+ lang: 'ml',
+ number: '1000000000',
+ result: '1,00,00,00,000',
+ description: 'formatnum test for Malayalam'
+ },
+ {
+ lang: 'ml',
+ number: '-1000000000',
+ result: '-1,00,00,00,000',
+ description: 'formatnum test for Malayalam, negative number'
+ },
+ /*
+ * This will fail because of wrong pattern for ml in MW(different from CLDR)
+ {
+ lang: 'ml',
+ number: '1000000000.000',
+ result: '1,00,00,00,000.000',
+ description: 'formatnum test for Malayalam with decimal place'
+ },
+ */
+ {
+ lang: 'hi',
+ number: '123456789.123456789',
+ result: '१२,३४,५६,७८९',
+ description: 'formatnum test for Hindi'
+ },
+ {
+ lang: 'hi',
+ number: '१२,३४,५६,७८९',
+ result: '१२,३४,५६,७८९',
+ description: 'formatnum test for Hindi, Devanagari digits passed'
+ },
+ {
+ lang: 'hi',
+ number: '१२३४५६,७८९',
+ result: '123456',
+ integer: true,
+ description: 'formatnum test for Hindi, Devanagari digits passed to get integer value'
+ }
+];
+
+QUnit.test( 'formatnum', formatnumTests.length, function ( assert ) {
+ mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' );
+ mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' );
+ var queue = $.map( formatnumTests, function ( test ) {
+ return function ( next ) {
+ getMwLanguage( test.lang )
+ .done( function ( langClass ) {
+ mw.config.set( 'wgUserLanguage', test.lang );
+ var parser = new mw.jqueryMsg.parser( { language: langClass } );
+ assert.equal(
+ parser.parse( test.integer ? 'formatnum-msg-int' : 'formatnum-msg',
+ [ test.number ] ).html(),
+ test.result,
+ test.description
+ );
+ } )
+ .fail( function () {
+ assert.ok( false, 'Language "' + test.lang + '" failed to load' );
+ } )
+ .always( next );
+ };
+ } );
+ QUnit.stop();
+ process( queue, QUnit.start );
+} );
+
+// HTML in wikitext
+QUnit.test( 'HTML', 26, function ( assert ) {
+ mw.messages.set( 'jquerymsg-italics-msg', '<i>Very</i> important' );
+
+ assertBothModes( assert, ['jquerymsg-italics-msg'], mw.messages.get( 'jquerymsg-italics-msg' ), 'Simple italics unchanged' );
+
+ mw.messages.set( 'jquerymsg-bold-msg', '<b>Strong</b> speaker' );
+ assertBothModes( assert, ['jquerymsg-bold-msg'], mw.messages.get( 'jquerymsg-bold-msg' ), 'Simple bold unchanged' );
+
+ mw.messages.set( 'jquerymsg-bold-italics-msg', 'It is <b><i>key</i></b>' );
+ assertBothModes( assert, ['jquerymsg-bold-italics-msg'], mw.messages.get( 'jquerymsg-bold-italics-msg' ), 'Bold and italics nesting order preserved' );
+
+ mw.messages.set( 'jquerymsg-italics-bold-msg', 'It is <i><b>vital</b></i>' );
+ assertBothModes( assert, ['jquerymsg-italics-bold-msg'], mw.messages.get( 'jquerymsg-italics-bold-msg' ), 'Italics and bold nesting order preserved' );
+
+ mw.messages.set( 'jquerymsg-italics-with-link', 'An <i>italicized [[link|wiki-link]]</i>' );
+
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-italics-with-link' ),
+ 'An <i>italicized <a title="link" href="' + mw.html.escape( mw.util.getUrl( 'link' ) ) + '">wiki-link</i>',
+ 'Italics with link inside in parse mode'
+ );
+
+ assert.equal(
+ formatText( 'jquerymsg-italics-with-link' ),
+ mw.messages.get( 'jquerymsg-italics-with-link' ),
+ 'Italics with link unchanged in text mode'
+ );
+
+ mw.messages.set( 'jquerymsg-italics-id-class', '<i id="foo" class="bar">Foo</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-italics-id-class' ),
+ mw.messages.get( 'jquerymsg-italics-id-class' ),
+ 'ID and class are allowed'
+ );
+
+ mw.messages.set( 'jquerymsg-italics-onclick', '<i onclick="alert(\'foo\')">Foo</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-italics-onclick' ),
+ '&lt;i onclick=&quot;alert(\'foo\')&quot;&gt;Foo&lt;/i&gt;',
+ 'element with onclick is escaped because it is not allowed'
+ );
+
+ mw.messages.set( 'jquerymsg-script-msg', '<script >alert( "Who put this tag here?" );</script>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-script-msg' ),
+ '&lt;script &gt;alert( &quot;Who put this tag here?&quot; );&lt;/script&gt;',
+ 'Tag outside whitelist escaped in parse mode'
+ );
+
+ assert.equal(
+ formatText( 'jquerymsg-script-msg' ),
+ mw.messages.get( 'jquerymsg-script-msg' ),
+ 'Tag outside whitelist unchanged in text mode'
+ );
+
+ mw.messages.set( 'jquerymsg-script-link-msg', '<script>[[Foo|bar]]</script>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-script-link-msg' ),
+ '&lt;script&gt;<a title="Foo" href="' + mw.html.escape( mw.util.getUrl( 'Foo' ) ) + '">bar</a>&lt;/script&gt;',
+ 'Script tag text is escaped because that element is not allowed, but link inside is still HTML'
+ );
+
+ mw.messages.set( 'jquerymsg-mismatched-html', '<i class="important">test</b>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-mismatched-html' ),
+ '&lt;i class=&quot;important&quot;&gt;test&lt;/b&gt;',
+ 'Mismatched HTML start and end tag treated as text'
+ );
+
+ // TODO (mattflaschen, 2013-03-18): It's not a security issue, but there's no real
+ // reason the htmlEmitter span needs to be here. It's an artifact of how emitting works.
+ mw.messages.set( 'jquerymsg-script-and-external-link', '<script>alert( "jquerymsg-script-and-external-link test" );</script> [http://example.com <i>Foo</i> bar]' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-script-and-external-link' ),
+ '&lt;script&gt;alert( "jquerymsg-script-and-external-link test" );&lt;/script&gt; <a href="http://example.com"><span class="mediaWiki_htmlEmitter"><i>Foo</i> bar</span></a>',
+ 'HTML tags in external links not interfering with escaping of other tags'
+ );
+
+ mw.messages.set( 'jquerymsg-link-script', '[http://example.com <script>alert( "jquerymsg-link-script test" );</script>]' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-link-script' ),
+ '<a href="http://example.com"><span class="mediaWiki_htmlEmitter">&lt;script&gt;alert( "jquerymsg-link-script test" );&lt;/script&gt;</span></a>',
+ 'Non-whitelisted HTML tag in external link anchor treated as text'
+ );
+
+ // Intentionally not using htmlEqual for the quote tests
+ mw.messages.set( 'jquerymsg-double-quotes-preserved', '<i id="double">Double</i>' );
+ assert.equal(
+ formatParse( 'jquerymsg-double-quotes-preserved' ),
+ mw.messages.get( 'jquerymsg-double-quotes-preserved' ),
+ 'Attributes with double quotes are preserved as such'
+ );
+
+ mw.messages.set( 'jquerymsg-single-quotes-normalized-to-double', '<i id=\'single\'>Single</i>' );
+ assert.equal(
+ formatParse( 'jquerymsg-single-quotes-normalized-to-double' ),
+ '<i id="single">Single</i>',
+ 'Attributes with single quotes are normalized to double'
+ );
+
+ mw.messages.set( 'jquerymsg-escaped-double-quotes-attribute', '<i style="font-family:&quot;Arial&quot;">Styled</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-escaped-double-quotes-attribute' ),
+ mw.messages.get( 'jquerymsg-escaped-double-quotes-attribute' ),
+ 'Escaped attributes are parsed correctly'
+ );
+
+ mw.messages.set( 'jquerymsg-escaped-single-quotes-attribute', '<i style=\'font-family:&#039;Arial&#039;\'>Styled</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-escaped-single-quotes-attribute' ),
+ mw.messages.get( 'jquerymsg-escaped-single-quotes-attribute' ),
+ 'Escaped attributes are parsed correctly'
+ );
+
+ mw.messages.set( 'jquerymsg-wikitext-contents-parsed', '<i>[http://example.com Example]</i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-wikitext-contents-parsed' ),
+ '<i><a href="http://example.com">Example</a></i>',
+ 'Contents of valid tag are treated as wikitext, so external link is parsed'
+ );
+
+ mw.messages.set( 'jquerymsg-wikitext-contents-script', '<i><script>Script inside</script></i>' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-wikitext-contents-script' ),
+ '<i><span class="mediaWiki_htmlEmitter">&lt;script&gt;Script inside&lt;/script&gt;</span></i>',
+ 'Contents of valid tag are treated as wikitext, so invalid HTML element is treated as text'
+ );
+
+ mw.messages.set( 'jquerymsg-unclosed-tag', 'Foo<tag>bar' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-unclosed-tag' ),
+ 'Foo&lt;tag&gt;bar',
+ 'Nonsupported unclosed tags are escaped'
+ );
+
+ mw.messages.set( 'jquerymsg-self-closing-tag', 'Foo<tag/>bar' );
+ assert.htmlEqual(
+ formatParse( 'jquerymsg-self-closing-tag' ),
+ 'Foo&lt;tag/&gt;bar',
+ 'Self-closing tags don\'t cause a parse error'
+ );
+} );
+
+ QUnit.test( 'Behavior in case of invalid wikitext', 3, function ( assert ) {
+ mw.messages.set( 'invalid-wikitext', '<b>{{FAIL}}</b>' );
+
+ this.suppressWarnings();
+ var logSpy = this.sandbox.spy( mw.log, 'warn' );
+
+ assert.equal(
+ formatParse( 'invalid-wikitext' ),
+ '&lt;b&gt;{{FAIL}}&lt;/b&gt;',
+ 'Invalid wikitext: \'parse\' format'
+ );
+
+ assert.equal(
+ formatText( 'invalid-wikitext' ),
+ '<b>{{FAIL}}</b>',
+ 'Invalid wikitext: \'text\' format'
+ );
+
+ assert.equal( logSpy.callCount, 2, 'mw.log.warn calls' );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js
new file mode 100644
index 00000000..3328ce3f
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js
@@ -0,0 +1,70 @@
+/**
+ * Some misc JavaScript compatibility tests,
+ * just to make sure the environments we run in are consistent.
+ */
+( function ( $ ) {
+ QUnit.module( 'mediawiki.jscompat', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Variable with Unicode letter in name', 3, function ( assert ) {
+ var orig, ŝablono;
+
+ orig = 'some token';
+ ŝablono = orig;
+
+ assert.deepEqual( ŝablono, orig, 'ŝablono' );
+ assert.deepEqual( \u015dablono, orig, '\\u015dablono' );
+ assert.deepEqual( \u015Dablono, orig, '\\u015Dablono' );
+ } );
+
+ /*
+ // Not that we need this. ;)
+ // This fails on IE 6-8
+ // Works on IE 9, Firefox 6, Chrome 14
+ QUnit.test( 'Keyword workaround: "if" as variable name using Unicode escapes', function ( assert ) {
+ var orig = "another token";
+ \u0069\u0066 = orig;
+ assert.deepEqual( \u0069\u0066, orig, '\\u0069\\u0066' );
+ });
+ */
+
+ /*
+ // Not that we need this. ;)
+ // This fails on IE 6-9
+ // Works on Firefox 6, Chrome 14
+ QUnit.test( 'Keyword workaround: "if" as member variable name using Unicode escapes', function ( assert ) {
+ var orig = "another token";
+ var foo = {};
+ foo.\u0069\u0066 = orig;
+ assert.deepEqual( foo.\u0069\u0066, orig, 'foo.\\u0069\\u0066' );
+ });
+ */
+
+ QUnit.test( 'Stripping of single initial newline from textarea\'s literal contents (bug 12130)', function ( assert ) {
+ var maxn, n,
+ expected, $textarea;
+
+ maxn = 4;
+ QUnit.expect( maxn * 2 );
+
+ function repeat( str, n ) {
+ var out;
+ if ( n <= 0 ) {
+ return '';
+ } else {
+ out = [];
+ out.length = n + 1;
+ return out.join( str );
+ }
+ }
+
+ for ( n = 0; n < maxn; n++ ) {
+ expected = repeat( '\n', n ) + 'some text';
+
+ $textarea = $( '<textarea>\n' + expected + '</textarea>' );
+ assert.equal( $textarea.val(), expected, 'Expecting ' + n + ' newlines (HTML contained ' + (n + 1) + ')' );
+
+ $textarea = $( '<textarea>' ).val( expected );
+ assert.equal( $textarea.val(), expected, 'Expecting ' + n + ' newlines (from DOM set with ' + n + ')' );
+ }
+ } );
+}( jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js
new file mode 100644
index 00000000..16f90df8
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js
@@ -0,0 +1,475 @@
+( function ( mw, $ ) {
+ 'use strict';
+
+ QUnit.module( 'mediawiki.language', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.liveLangData = mw.language.data.values;
+ mw.language.data.values = $.extend( true, {}, this.liveLangData );
+ },
+ teardown: function () {
+ mw.language.data.values = this.liveLangData;
+ }
+ } ) );
+
+ QUnit.test( 'mw.language getData and setData', 2, function ( assert ) {
+ mw.language.setData( 'en', 'testkey', 'testvalue' );
+ assert.equal( mw.language.getData( 'en', 'testkey' ), 'testvalue', 'Getter setter test for mw.language' );
+ assert.equal( mw.language.getData( 'en', 'invalidkey' ), undefined, 'Getter setter test for mw.language with invalid key' );
+ } );
+
+ QUnit.test( 'mw.language.commafy test', 9, function ( assert ) {
+ // Number grouping patterns are as per http://cldr.unicode.org/translation/number-patterns
+ assert.equal( mw.language.commafy( 1234.567, '###0.#####' ), '1234.567', 'Pattern with no digit grouping separator defined' );
+ assert.equal( mw.language.commafy( 123456789.567, '###0.#####' ), '123456789.567', 'Pattern with no digit grouping seperator defined, bigger decimal part' );
+ assert.equal( mw.language.commafy( 0.567, '###0.#####' ), '0.567', 'Decimal part 0' );
+ assert.equal( mw.language.commafy( '.567', '###0.#####' ), '0.567', 'Decimal part missing. replace with zero' );
+ assert.equal( mw.language.commafy( 1234, '##,#0.#####' ), '12,34', 'Pattern with no fractional part' );
+ assert.equal( mw.language.commafy( -1234.567, '###0.#####' ), '-1234.567', 'Negative number' );
+ assert.equal( mw.language.commafy( -1234.567, '#,###.00' ), '-1,234.56', 'Fractional part bigger than pattern.' );
+ assert.equal( mw.language.commafy( 123456789.567, '###,##0.00' ), '123,456,789.56', 'Decimal part as group of 3' );
+ assert.equal( mw.language.commafy( 123456789.567, '###,###,#0.00' ), '1,234,567,89.56', 'Decimal part as group of 3 and last one 2' );
+ } );
+
+ function grammarTest( langCode, test ) {
+ // The test works only if the content language is opt.language
+ // because it requires [lang].js to be loaded.
+ QUnit.test( 'Grammar test for lang=' + langCode, function ( assert ) {
+ QUnit.expect( test.length );
+
+ for ( var i = 0; i < test.length; i++ ) {
+ assert.equal(
+ mw.language.convertGrammar( test[i].word, test[i].grammarForm ),
+ test[i].expected,
+ test[i].description
+ );
+ }
+ } );
+ }
+
+ // These tests run only for the current UI language.
+ var grammarTests = {
+ bs: [
+ {
+ word: 'word',
+ grammarForm: 'instrumental',
+ expected: 's word',
+ description: 'Grammar test for instrumental case'
+ },
+ {
+ word: 'word',
+ grammarForm: 'lokativ',
+ expected: 'o word',
+ description: 'Grammar test for lokativ case'
+ }
+ ],
+
+ he: [
+ {
+ word: 'ויקיפדיה',
+ grammarForm: 'prefixed',
+ expected: 'וויקיפדיה',
+ description: 'Duplicate the "Waw" if prefixed'
+ },
+ {
+ word: 'וולפגנג',
+ grammarForm: 'prefixed',
+ expected: 'וולפגנג',
+ description: 'Duplicate the "Waw" if prefixed, but not if it is already duplicated.'
+ },
+ {
+ word: 'הקובץ',
+ grammarForm: 'prefixed',
+ expected: 'קובץ',
+ description: 'Remove the "He" if prefixed'
+ },
+ {
+ word: 'Wikipedia',
+ grammarForm: 'תחילית',
+ expected: '־Wikipedia',
+ description: 'GAdd a hyphen (maqaf) before non-Hebrew letters'
+ },
+ {
+ word: '1995',
+ grammarForm: 'תחילית',
+ expected: '־1995',
+ description: 'Add a hyphen (maqaf) before numbers'
+ }
+ ],
+
+ hsb: [
+ {
+ word: 'word',
+ grammarForm: 'instrumental',
+ expected: 'z word',
+ description: 'Grammar test for instrumental case'
+ },
+ {
+ word: 'word',
+ grammarForm: 'lokatiw',
+ expected: 'wo word',
+ description: 'Grammar test for lokatiw case'
+ }
+ ],
+
+ dsb: [
+ {
+ word: 'word',
+ grammarForm: 'instrumental',
+ expected: 'z word',
+ description: 'Grammar test for instrumental case'
+ },
+ {
+ word: 'word',
+ grammarForm: 'lokatiw',
+ expected: 'wo word',
+ description: 'Grammar test for lokatiw case'
+ }
+ ],
+
+ hy: [
+ {
+ word: 'Մաունա',
+ grammarForm: 'genitive',
+ expected: 'Մաունայի',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'հետո',
+ grammarForm: 'genitive',
+ expected: 'հետոյի',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'գիրք',
+ grammarForm: 'genitive',
+ expected: 'գրքի',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'ժամանակի',
+ grammarForm: 'genitive',
+ expected: 'ժամանակիի',
+ description: 'Grammar test for genitive case'
+ }
+ ],
+
+ fi: [
+ {
+ word: 'talo',
+ grammarForm: 'genitive',
+ expected: 'talon',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'linux',
+ grammarForm: 'genitive',
+ expected: 'linuxin',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'talo',
+ grammarForm: 'elative',
+ expected: 'talosta',
+ description: 'Grammar test for elative case'
+ },
+ {
+ word: 'pastöroitu',
+ grammarForm: 'partitive',
+ expected: 'pastöroitua',
+ description: 'Grammar test for partitive case'
+ },
+ {
+ word: 'talo',
+ grammarForm: 'partitive',
+ expected: 'taloa',
+ description: 'Grammar test for partitive case'
+ },
+ {
+ word: 'talo',
+ grammarForm: 'illative',
+ expected: 'taloon',
+ description: 'Grammar test for illative case'
+ },
+ {
+ word: 'linux',
+ grammarForm: 'inessive',
+ expected: 'linuxissa',
+ description: 'Grammar test for inessive case'
+ }
+ ],
+
+ ru: [
+ {
+ word: 'тесть',
+ grammarForm: 'genitive',
+ expected: 'тестя',
+ description: 'Grammar test for genitive case, тесть -> тестя'
+ },
+ {
+ word: 'привилегия',
+ grammarForm: 'genitive',
+ expected: 'привилегии',
+ description: 'Grammar test for genitive case, привилегия -> привилегии'
+ },
+ {
+ word: 'установка',
+ grammarForm: 'genitive',
+ expected: 'установки',
+ description: 'Grammar test for genitive case, установка -> установки'
+ },
+ {
+ word: 'похоти',
+ grammarForm: 'genitive',
+ expected: 'похотей',
+ description: 'Grammar test for genitive case, похоти -> похотей'
+ },
+ {
+ word: 'доводы',
+ grammarForm: 'genitive',
+ expected: 'доводов',
+ description: 'Grammar test for genitive case, доводы -> доводов'
+ },
+ {
+ word: 'песчаник',
+ grammarForm: 'genitive',
+ expected: 'песчаника',
+ description: 'Grammar test for genitive case, песчаник -> песчаника'
+ },
+ {
+ word: 'данные',
+ grammarForm: 'genitive',
+ expected: 'данных',
+ description: 'Grammar test for genitive case, данные -> данных'
+ },
+ {
+ word: 'тесть',
+ grammarForm: 'prepositional',
+ expected: 'тесте',
+ description: 'Grammar test for prepositional case, тесть -> тесте'
+ },
+ {
+ word: 'привилегия',
+ grammarForm: 'prepositional',
+ expected: 'привилегии',
+ description: 'Grammar test for prepositional case, привилегия -> привилегии'
+ },
+ {
+ word: 'установка',
+ grammarForm: 'prepositional',
+ expected: 'установке',
+ description: 'Grammar test for prepositional case, установка -> установке'
+ },
+ {
+ word: 'похоти',
+ grammarForm: 'prepositional',
+ expected: 'похотях',
+ description: 'Grammar test for prepositional case, похоти -> похотях'
+ },
+ {
+ word: 'доводы',
+ grammarForm: 'prepositional',
+ expected: 'доводах',
+ description: 'Grammar test for prepositional case, доводы -> доводах'
+ },
+ {
+ word: 'Викисклад',
+ grammarForm: 'prepositional',
+ expected: 'Викискладе',
+ description: 'Grammar test for prepositional case, Викисклад -> Викискладе'
+ },
+ {
+ word: 'Викисклад',
+ grammarForm: 'genitive',
+ expected: 'Викисклада',
+ description: 'Grammar test for genitive case, Викисклад -> Викисклада'
+ },
+ {
+ word: 'песчаник',
+ grammarForm: 'prepositional',
+ expected: 'песчанике',
+ description: 'Grammar test for prepositional case, песчаник -> песчанике'
+ },
+ {
+ word: 'данные',
+ grammarForm: 'prepositional',
+ expected: 'данных',
+ description: 'Grammar test for prepositional case, данные -> данных'
+ }
+ ],
+
+ hu: [
+ {
+ word: 'Wikipédiá',
+ grammarForm: 'rol',
+ expected: 'Wikipédiáról',
+ description: 'Grammar test for rol case'
+ },
+ {
+ word: 'Wikipédiá',
+ grammarForm: 'ba',
+ expected: 'Wikipédiába',
+ description: 'Grammar test for ba case'
+ },
+ {
+ word: 'Wikipédiá',
+ grammarForm: 'k',
+ expected: 'Wikipédiák',
+ description: 'Grammar test for k case'
+ }
+ ],
+
+ ga: [
+ {
+ word: 'an Domhnach',
+ grammarForm: 'ainmlae',
+ expected: 'Dé Domhnaigh',
+ description: 'Grammar test for ainmlae case'
+ },
+ {
+ word: 'an Luan',
+ grammarForm: 'ainmlae',
+ expected: 'Dé Luain',
+ description: 'Grammar test for ainmlae case'
+ },
+ {
+ word: 'an Satharn',
+ grammarForm: 'ainmlae',
+ expected: 'Dé Sathairn',
+ description: 'Grammar test for ainmlae case'
+ }
+ ],
+
+ uk: [
+ {
+ word: 'тесть',
+ grammarForm: 'genitive',
+ expected: 'тестя',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'Вікіпедія',
+ grammarForm: 'genitive',
+ expected: 'Вікіпедії',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'установка',
+ grammarForm: 'genitive',
+ expected: 'установки',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'похоти',
+ grammarForm: 'genitive',
+ expected: 'похотей',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'доводы',
+ grammarForm: 'genitive',
+ expected: 'доводов',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'песчаник',
+ grammarForm: 'genitive',
+ expected: 'песчаника',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'Вікіпедія',
+ grammarForm: 'accusative',
+ expected: 'Вікіпедію',
+ description: 'Grammar test for accusative case'
+ }
+ ],
+
+ sl: [
+ {
+ word: 'word',
+ grammarForm: 'orodnik',
+ expected: 'z word',
+ description: 'Grammar test for orodnik case'
+ },
+ {
+ word: 'word',
+ grammarForm: 'mestnik',
+ expected: 'o word',
+ description: 'Grammar test for mestnik case'
+ }
+ ],
+
+ os: [
+ {
+ word: 'бæстæ',
+ grammarForm: 'genitive',
+ expected: 'бæсты',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'бæстæ',
+ grammarForm: 'allative',
+ expected: 'бæстæм',
+ description: 'Grammar test for allative case'
+ },
+ {
+ word: 'Тигр',
+ grammarForm: 'dative',
+ expected: 'Тигрæн',
+ description: 'Grammar test for dative case'
+ },
+ {
+ word: 'цъити',
+ grammarForm: 'dative',
+ expected: 'цъитийæн',
+ description: 'Grammar test for dative case'
+ },
+ {
+ word: 'лæппу',
+ grammarForm: 'genitive',
+ expected: 'лæппуйы',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: '2011',
+ grammarForm: 'equative',
+ expected: '2011-ау',
+ description: 'Grammar test for equative case'
+ }
+ ],
+
+ la: [
+ {
+ word: 'Translatio',
+ grammarForm: 'genitive',
+ expected: 'Translationis',
+ description: 'Grammar test for genitive case'
+ },
+ {
+ word: 'Translatio',
+ grammarForm: 'accusative',
+ expected: 'Translationem',
+ description: 'Grammar test for accusative case'
+ },
+ {
+ word: 'Translatio',
+ grammarForm: 'ablative',
+ expected: 'Translatione',
+ description: 'Grammar test for ablative case'
+ }
+ ]
+ };
+
+ $.each( grammarTests, function ( langCode, test ) {
+ if ( langCode === mw.config.get( 'wgUserLanguage' ) ) {
+ grammarTest( langCode, test );
+ }
+ } );
+
+ QUnit.test( 'List to text test', 4, function ( assert ) {
+ assert.equal( mw.language.listToText( [] ), '', 'Blank list' );
+ assert.equal( mw.language.listToText( ['a'] ), 'a', 'Single item' );
+ assert.equal( mw.language.listToText( ['a', 'b'] ), 'a and b', 'Two items' );
+ assert.equal( mw.language.listToText( ['a', 'b', 'c'] ), 'a, b and c', 'More than two items' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js
new file mode 100644
index 00000000..7e0ee917
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js
@@ -0,0 +1,985 @@
+/*jshint -W024 */
+( function ( mw, $ ) {
+ var specialCharactersPageName;
+
+ // Since QUnitTestResources.php loads both mediawiki and mediawiki.jqueryMsg as
+ // dependencies, this only tests the monkey-patched behavior with the two of them combined.
+
+ // See mediawiki.jqueryMsg.test.js for unit tests for jqueryMsg-specific functionality.
+
+ QUnit.module( 'mediawiki', QUnit.newMwEnvironment( {
+ setup: function () {
+ specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?';
+ },
+ config: {
+ wgArticlePath: '/wiki/$1',
+
+ // For formatnum tests
+ wgUserLanguage: 'en'
+ },
+ // Messages used in multiple tests
+ messages: {
+ 'other-message': 'Other Message',
+ 'mediawiki-test-pagetriage-del-talk-page-notify-summary': 'Notifying author of deletion nomination for [[$1]]',
+ 'gender-plural-msg': '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome',
+ 'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}',
+ 'formatnum-msg': '{{formatnum:$1}}',
+ 'int-msg': 'Some {{int:other-message}}',
+ 'mediawiki-test-version-entrypoints-index-php': '[https://www.mediawiki.org/wiki/Manual:index.php index.php]',
+ 'external-link-replace': 'Foo [$1 bar]'
+ }
+ } ) );
+
+ mw.loader.addSource(
+ 'testloader',
+ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' )
+ );
+
+ QUnit.test( 'Initial check', 8, function ( assert ) {
+ assert.ok( window.jQuery, 'jQuery defined' );
+ assert.ok( window.$, '$ defined' );
+ assert.strictEqual( window.$, window.jQuery, '$ alias to jQuery' );
+
+ this.suppressWarnings();
+ assert.ok( window.$j, '$j defined' );
+ assert.strictEqual( window.$j, window.jQuery, '$j alias to jQuery' );
+ this.restoreWarnings();
+
+ // window.mw and window.mediaWiki are not deprecated, but for some reason
+ // PhantomJS is triggerring the accessors on all mw.* properties in this test,
+ // and with that lots of unrelated deprecation notices.
+ this.suppressWarnings();
+ assert.ok( window.mediaWiki, 'mediaWiki defined' );
+ assert.ok( window.mw, 'mw defined' );
+ assert.strictEqual( window.mw, window.mediaWiki, 'mw alias to mediaWiki' );
+ this.restoreWarnings();
+ } );
+
+ QUnit.test( 'mw.Map', 28, function ( assert ) {
+ var arry, conf, funky, globalConf, nummy, someValues;
+
+ conf = new mw.Map();
+ // Dummy variables
+ funky = function () {};
+ arry = [];
+ nummy = 7;
+
+ // Single get and set
+
+ assert.strictEqual( conf.set( 'foo', 'Bar' ), true, 'Map.set returns boolean true if a value was set for a valid key string' );
+ assert.equal( conf.get( 'foo' ), 'Bar', 'Map.get returns a single value value correctly' );
+
+ assert.strictEqual( conf.get( 'example' ), null, 'Map.get returns null if selection was a string and the key was not found' );
+ assert.strictEqual( conf.get( 'example', arry ), arry, 'Map.get returns fallback by reference if the key was not found' );
+ assert.strictEqual( conf.get( 'example', undefined ), undefined, 'Map.get supports `undefined` as fallback instead of `null`' );
+
+ assert.strictEqual( conf.get( 'constructor' ), null, 'Map.get does not look at Object.prototype of internal storage (constructor)' );
+ assert.strictEqual( conf.get( 'hasOwnProperty' ), null, 'Map.get does not look at Object.prototype of internal storage (hasOwnProperty)' );
+
+ conf.set( 'hasOwnProperty', function () { return true; } );
+ assert.strictEqual( conf.get( 'example', 'missing' ), 'missing', 'Map.get uses neutral hasOwnProperty method (positive)' );
+
+ conf.set( 'example', 'Foo' );
+ conf.set( 'hasOwnProperty', function () { return false; } );
+ assert.strictEqual( conf.get( 'example' ), 'Foo', 'Map.get uses neutral hasOwnProperty method (negative)' );
+
+ assert.strictEqual( conf.set( 'constructor', 42 ), true, 'Map.set for key "constructor"' );
+ assert.strictEqual( conf.get( 'constructor' ), 42, 'Map.get for key "constructor"' );
+
+ assert.strictEqual( conf.set( 'ImUndefined', undefined ), true, 'Map.set allows setting value to `undefined`' );
+ assert.equal( conf.get( 'ImUndefined', 'fallback' ), undefined, 'Map.get supports retreiving value of `undefined`' );
+
+ assert.strictEqual( conf.set( funky, 'Funky' ), false, 'Map.set returns boolean false if key was invalid (Function)' );
+ assert.strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' );
+ assert.strictEqual( conf.set( nummy, 'Nummy' ), false, 'Map.set returns boolean false if key was invalid (Number)' );
+
+ assert.strictEqual( conf.get( funky ), null, 'Map.get ruturns null if selection was invalid (Function)' );
+ assert.strictEqual( conf.get( nummy ), null, 'Map.get ruturns null if selection was invalid (Number)' );
+
+ conf.set( String( nummy ), 'I used to be a number' );
+
+ assert.strictEqual( conf.exists( 'doesNotExist' ), false, 'Map.exists where property does not exist' );
+ assert.strictEqual( conf.exists( 'ImUndefined' ), true, 'Map.exists where value is `undefined`' );
+ assert.strictEqual( conf.exists( nummy ), false, 'Map.exists where key is invalid but looks like an existing key' );
+
+ // Multiple values at once
+ someValues = {
+ 'foo': 'bar',
+ 'lorem': 'ipsum',
+ 'MediaWiki': true
+ };
+ assert.strictEqual( conf.set( someValues ), true, 'Map.set returns boolean true if multiple values were set by passing an object' );
+ assert.deepEqual( conf.get( ['foo', 'lorem'] ), {
+ 'foo': 'bar',
+ 'lorem': 'ipsum'
+ }, 'Map.get returns multiple values correctly as an object' );
+
+ assert.deepEqual( conf, new mw.Map( conf.values ), 'new mw.Map maps over existing values-bearing object' );
+
+ assert.deepEqual( conf.get( ['foo', 'notExist'] ), {
+ 'foo': 'bar',
+ 'notExist': null
+ }, 'Map.get return includes keys that were not found as null values' );
+
+ // Interacting with globals and accessing the values object
+ assert.strictEqual( conf.get(), conf.values, 'Map.get returns the entire values object by reference (if called without arguments)' );
+
+ conf.set( 'globalMapChecker', 'Hi' );
+
+ assert.ok( 'globalMapChecker' in window === false, 'new mw.Map did not store its values in the global window object by default' );
+
+ globalConf = new mw.Map( true );
+ globalConf.set( 'anotherGlobalMapChecker', 'Hello' );
+
+ assert.ok( 'anotherGlobalMapChecker' in window, 'new mw.Map( true ) did store its values in the global window object' );
+
+ // Whitelist this global variable for QUnit's 'noglobal' mode
+ if ( QUnit.config.noglobals ) {
+ QUnit.config.pollution.push( 'anotherGlobalMapChecker' );
+ }
+ } );
+
+ QUnit.test( 'mw.config', 1, function ( assert ) {
+ assert.ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' );
+ } );
+
+ QUnit.test( 'mw.message & mw.messages', 100, function ( assert ) {
+ var goodbye, hello;
+
+ // Convenience method for asserting the same result for multiple formats
+ function assertMultipleFormats( messageArguments, formats, expectedResult, assertMessage ) {
+ var len = formats.length, format, i;
+ for ( i = 0; i < len; i++ ) {
+ format = formats[i];
+ assert.equal( mw.message.apply( null, messageArguments )[format](), expectedResult, assertMessage + ' when format is ' + format );
+ }
+ }
+
+ assert.ok( mw.messages, 'messages defined' );
+ assert.ok( mw.messages instanceof mw.Map, 'mw.messages instance of mw.Map' );
+ assert.ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' );
+
+ hello = mw.message( 'hello' );
+
+ // https://bugzilla.wikimedia.org/show_bug.cgi?id=44459
+ assert.equal( hello.format, 'text', 'Message property "format" defaults to "text"' );
+
+ assert.strictEqual( hello.map, mw.messages, 'Message property "map" defaults to the global instance in mw.messages' );
+ assert.equal( hello.key, 'hello', 'Message property "key" (currect key)' );
+ assert.deepEqual( hello.parameters, [], 'Message property "parameters" defaults to an empty array' );
+
+ // Todo
+ assert.ok( hello.params, 'Message prototype "params"' );
+
+ hello.format = 'plain';
+ assert.equal( hello.toString(), 'Hello <b>awesome</b> world', 'Message.toString returns the message as a string with the current "format"' );
+
+ assert.equal( hello.escaped(), 'Hello &lt;b&gt;awesome&lt;/b&gt; world', 'Message.escaped returns the escaped message' );
+ assert.equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' );
+
+ assert.ok( mw.messages.set( 'multiple-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['multiple-curly-brace'], ['text', 'parse'], '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message', 'Curly brace format works correctly' );
+ assert.equal( mw.message( 'multiple-curly-brace' ).plain(), mw.messages.get( 'multiple-curly-brace' ), 'Plain format works correctly for curly brace message' );
+ assert.equal( mw.message( 'multiple-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
+
+ assert.ok( mw.messages.set( 'multiple-square-brackets-and-ampersand', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['multiple-square-brackets-and-ampersand'], ['plain', 'text'], mw.messages.get( 'multiple-square-brackets-and-ampersand' ), 'Square bracket message is not processed' );
+ assert.equal( mw.message( 'multiple-square-brackets-and-ampersand' ).escaped(), 'Visit the [[Project:Community portal|community portal]] &amp; [[Project:Help desk|help desk]]', 'Escaped format works correctly for square bracket message' );
+ assert.htmlEqual( mw.message( 'multiple-square-brackets-and-ampersand' ).parse(), 'Visit the ' +
+ '<a title="Project:Community portal" href="/wiki/Project:Community_portal">community portal</a>' +
+ ' &amp; <a title="Project:Help desk" href="/wiki/Project:Help_desk">help desk</a>', 'Internal links work with parse' );
+
+ assertMultipleFormats( ['mediawiki-test-version-entrypoints-index-php'], ['plain', 'text', 'escaped'], mw.messages.get( 'mediawiki-test-version-entrypoints-index-php' ), 'External link markup is unprocessed' );
+ assert.htmlEqual( mw.message( 'mediawiki-test-version-entrypoints-index-php' ).parse(), '<a href="https://www.mediawiki.org/wiki/Manual:index.php">index.php</a>', 'External link works correctly in parse mode' );
+
+ assertMultipleFormats( ['external-link-replace', 'http://example.org/?x=y&z'], ['plain', 'text'], 'Foo [http://example.org/?x=y&z bar]', 'Parameters are substituted but external link is not processed' );
+ assert.equal( mw.message( 'external-link-replace', 'http://example.org/?x=y&z' ).escaped(), 'Foo [http://example.org/?x=y&amp;z bar]', 'In escaped mode, parameters are substituted and ampersand is escaped, but external link is not processed' );
+ assert.htmlEqual( mw.message( 'external-link-replace', 'http://example.org/?x=y&z' ).parse(), 'Foo <a href="http://example.org/?x=y&amp;z">bar</a>', 'External link with replacement works in parse mode without double-escaping' );
+
+ hello.parse();
+ assert.equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' );
+
+ hello.plain();
+ assert.equal( hello.format, 'plain', 'Message.plain correctly updated the "format" property' );
+
+ hello.text();
+ assert.equal( hello.format, 'text', 'Message.text correctly updated the "format" property' );
+
+ assert.strictEqual( hello.exists(), true, 'Message.exists returns true for existing messages' );
+
+ goodbye = mw.message( 'goodbye' );
+ assert.strictEqual( goodbye.exists(), false, 'Message.exists returns false for nonexistent messages' );
+
+ assertMultipleFormats( ['goodbye'], ['plain', 'text'], '<goodbye>', 'Message.toString returns <key> if key does not exist' );
+ // bug 30684
+ assertMultipleFormats( ['goodbye'], ['parse', 'escaped'], '&lt;goodbye&gt;', 'Message.toString returns properly escaped &lt;key&gt; if key does not exist' );
+
+ assert.ok( mw.messages.set( 'plural-test-msg', 'There {{PLURAL:$1|is|are}} $1 {{PLURAL:$1|result|results}}' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['plural-test-msg', 6], ['text', 'parse', 'escaped'], 'There are 6 results', 'plural get resolved' );
+ assert.equal( mw.message( 'plural-test-msg', 6 ).plain(), 'There {{PLURAL:6|is|are}} 6 {{PLURAL:6|result|results}}', 'Parameter is substituted but plural is not resolved in plain' );
+
+ assert.ok( mw.messages.set( 'plural-test-msg-explicit', 'There {{plural:$1|is one car|are $1 cars|0=are no cars|12=are a dozen cars}}' ), 'mw.messages.set: Register message with explicit plural forms' );
+ assertMultipleFormats( ['plural-test-msg-explicit', 12], ['text', 'parse', 'escaped'], 'There are a dozen cars', 'explicit plural get resolved' );
+
+ assert.ok( mw.messages.set( 'plural-test-msg-explicit-beginning', 'Basket has {{plural:$1|0=no eggs|12=a dozen eggs|6=half a dozen eggs|one egg|$1 eggs}}' ), 'mw.messages.set: Register message with explicit plural forms' );
+ assertMultipleFormats( ['plural-test-msg-explicit-beginning', 1], ['text', 'parse', 'escaped'], 'Basket has one egg', 'explicit plural given at beginning get resolved for singular' );
+ assertMultipleFormats( ['plural-test-msg-explicit-beginning', 4], ['text', 'parse', 'escaped'], 'Basket has 4 eggs', 'explicit plural given at beginning get resolved for plural' );
+ assertMultipleFormats( ['plural-test-msg-explicit-beginning', 6], ['text', 'parse', 'escaped'], 'Basket has half a dozen eggs', 'explicit plural given at beginning get resolved for 6' );
+ assertMultipleFormats( ['plural-test-msg-explicit-beginning', 0], ['text', 'parse', 'escaped'], 'Basket has no eggs', 'explicit plural given at beginning get resolved for 0' );
+
+ assertMultipleFormats( ['mediawiki-test-pagetriage-del-talk-page-notify-summary'], ['plain', 'text'], mw.messages.get( 'mediawiki-test-pagetriage-del-talk-page-notify-summary' ), 'Double square brackets with no parameters unchanged' );
+
+ assertMultipleFormats( ['mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName], ['plain', 'text'], 'Notifying author of deletion nomination for [[' + specialCharactersPageName + ']]', 'Double square brackets with one parameter' );
+
+ assert.equal( mw.message( 'mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName ).escaped(), 'Notifying author of deletion nomination for [[' + mw.html.escape( specialCharactersPageName ) + ']]', 'Double square brackets with one parameter, when escaped' );
+
+ assert.ok( mw.messages.set( 'mediawiki-test-categorytree-collapse-bullet', '[<b>−</b>]' ), 'mw.messages.set: Register' );
+ assert.equal( mw.message( 'mediawiki-test-categorytree-collapse-bullet' ).plain(), mw.messages.get( 'mediawiki-test-categorytree-collapse-bullet' ), 'Single square brackets unchanged in plain mode' );
+
+ assert.ok( mw.messages.set( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result', '<a href=\'#\' title=\'{{#special:mypage}}\'>Username</a> (<a href=\'#\' title=\'{{#special:mytalk}}\'>talk</a>)' ), 'mw.messages.set: Register' );
+ assert.equal( mw.message( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ).plain(), mw.messages.get( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ), 'HTML message with curly braces is not changed in plain mode' );
+
+ assertMultipleFormats( ['gender-plural-msg', 'male', 1], ['text', 'parse', 'escaped'], 'he is awesome', 'Gender and plural are resolved' );
+ assert.equal( mw.message( 'gender-plural-msg', 'male', 1 ).plain(), '{{GENDER:male|he|she|they}} {{PLURAL:1|is|are}} awesome', 'Parameters are substituted, but gender and plural are not resolved in plain mode' );
+
+ assert.equal( mw.message( 'grammar-msg' ).plain(), mw.messages.get( 'grammar-msg' ), 'Grammar is not resolved in plain mode' );
+ assertMultipleFormats( ['grammar-msg'], ['text', 'parse'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' );
+ assert.equal( mw.message( 'grammar-msg' ).escaped(), 'Przeszukaj ' + mw.html.escape( mw.config.get( 'wgSiteName' ) ), 'Grammar is resolved in escaped mode' );
+
+ assertMultipleFormats( ['formatnum-msg', '987654321.654321'], ['text', 'parse', 'escaped'], '987,654,321.654', 'formatnum is resolved' );
+ assert.equal( mw.message( 'formatnum-msg' ).plain(), mw.messages.get( 'formatnum-msg' ), 'formatnum is not resolved in plain mode' );
+
+ assertMultipleFormats( ['int-msg'], ['text', 'parse', 'escaped'], 'Some Other Message', 'int is resolved' );
+ assert.equal( mw.message( 'int-msg' ).plain(), mw.messages.get( 'int-msg' ), 'int is not resolved in plain mode' );
+
+ assert.ok( mw.messages.set( 'mediawiki-italics-msg', '<i>Very</i> important' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['mediawiki-italics-msg'], ['plain', 'text', 'parse'], mw.messages.get( 'mediawiki-italics-msg' ), 'Simple italics unchanged' );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-italics-msg' ).escaped(),
+ '&lt;i&gt;Very&lt;/i&gt; important',
+ 'Italics are escaped in escaped mode'
+ );
+
+ assert.ok( mw.messages.set( 'mediawiki-italics-with-link', 'An <i>italicized [[link|wiki-link]]</i>' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['mediawiki-italics-with-link'], ['plain', 'text'], mw.messages.get( 'mediawiki-italics-with-link' ), 'Italics with link unchanged' );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-italics-with-link' ).escaped(),
+ 'An &lt;i&gt;italicized [[link|wiki-link]]&lt;/i&gt;',
+ 'Italics and link unchanged except for escaping in escaped mode'
+ );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-italics-with-link' ).parse(),
+ 'An <i>italicized <a title="link" href="' + mw.util.getUrl( 'link' ) + '">wiki-link</i>',
+ 'Italics with link inside in parse mode'
+ );
+
+ assert.ok( mw.messages.set( 'mediawiki-script-msg', '<script >alert( "Who put this script here?" );</script>' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['mediawiki-script-msg'], ['plain', 'text'], mw.messages.get( 'mediawiki-script-msg' ), 'Script unchanged' );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-script-msg' ).escaped(),
+ '&lt;script &gt;alert( "Who put this script here?" );&lt;/script&gt;',
+ 'Script escaped when using escaped format'
+ );
+ assert.htmlEqual(
+ mw.message( 'mediawiki-script-msg' ).parse(),
+ '&lt;script &gt;alert( "Who put this script here?" );&lt;/script&gt;',
+ 'Script escaped when using parse format'
+ );
+
+ } );
+
+ QUnit.test( 'mw.msg', 14, function ( assert ) {
+ assert.ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' );
+ assert.equal( mw.msg( 'hello' ), 'Hello <b>awesome</b> world', 'Gets message with default options (existing message)' );
+ assert.equal( mw.msg( 'goodbye' ), '<goodbye>', 'Gets message with default options (nonexistent message)' );
+
+ assert.ok( mw.messages.set( 'plural-item', 'Found $1 {{PLURAL:$1|item|items}}' ), 'mw.messages.set: Register' );
+ assert.equal( mw.msg( 'plural-item', 5 ), 'Found 5 items', 'Apply plural for count 5' );
+ assert.equal( mw.msg( 'plural-item', 0 ), 'Found 0 items', 'Apply plural for count 0' );
+ assert.equal( mw.msg( 'plural-item', 1 ), 'Found 1 item', 'Apply plural for count 1' );
+
+ assert.equal( mw.msg( 'mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName ), 'Notifying author of deletion nomination for [[' + specialCharactersPageName + ']]', 'Double square brackets in mw.msg one parameter' );
+
+ assert.equal( mw.msg( 'gender-plural-msg', 'male', 1 ), 'he is awesome', 'Gender test for male, plural count 1' );
+ assert.equal( mw.msg( 'gender-plural-msg', 'female', '1' ), 'she is awesome', 'Gender test for female, plural count 1' );
+ assert.equal( mw.msg( 'gender-plural-msg', 'unknown', 10 ), 'they are awesome', 'Gender test for neutral, plural count 10' );
+
+ assert.equal( mw.msg( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' );
+
+ assert.equal( mw.msg( 'formatnum-msg', '987654321.654321' ), '987,654,321.654', 'formatnum is resolved' );
+
+ assert.equal( mw.msg( 'int-msg' ), 'Some Other Message', 'int is resolved' );
+ } );
+
+ /**
+ * The sync style load test (for @import). This is, in a way, also an open bug for
+ * ResourceLoader ("execute js after styles are loaded"), but browsers don't offer a
+ * way to get a callback from when a stylesheet is loaded (that is, including any
+ * @import rules inside). To work around this, we'll have a little time loop to check
+ * if the styles apply.
+ * Note: This test originally used new Image() and onerror to get a callback
+ * when the url is loaded, but that is fragile since it doesn't monitor the
+ * same request as the css @import, and Safari 4 has issues with
+ * onerror/onload not being fired at all in weird cases like this.
+ */
+ function assertStyleAsync( assert, $element, prop, val, fn ) {
+ var styleTestStart,
+ el = $element.get( 0 ),
+ styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) - 200;
+
+ function isCssImportApplied() {
+ // Trigger reflow, repaint, redraw, whatever (cross-browser)
+ var x = $element.css( 'height' );
+ x = el.innerHTML;
+ el.className = el.className;
+ x = document.documentElement.clientHeight;
+
+ return $element.css( prop ) === val;
+ }
+
+ function styleTestLoop() {
+ var styleTestSince = new Date().getTime() - styleTestStart;
+ // If it is passing or if we timed out, run the real test and stop the loop
+ if ( isCssImportApplied() || styleTestSince > styleTestTimeout ) {
+ assert.equal( $element.css( prop ), val,
+ 'style "' + prop + ': ' + val + '" from url is applied (after ' + styleTestSince + 'ms)'
+ );
+
+ if ( fn ) {
+ fn();
+ }
+
+ return;
+ }
+ // Otherwise, keep polling
+ setTimeout( styleTestLoop, 150 );
+ }
+
+ // Start the loop
+ styleTestStart = new Date().getTime();
+ styleTestLoop();
+ }
+
+ function urlStyleTest( selector, prop, val ) {
+ return QUnit.fixurl(
+ mw.config.get( 'wgScriptPath' ) +
+ '/tests/qunit/data/styleTest.css.php?' +
+ $.param( {
+ selector: selector,
+ prop: prop,
+ val: val
+ } )
+ );
+ }
+
+ QUnit.asyncTest( 'mw.loader', 2, function ( assert ) {
+ var isAwesomeDone;
+
+ mw.loader.testCallback = function () {
+ QUnit.start();
+ assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
+ isAwesomeDone = true;
+ };
+
+ mw.loader.implement( 'test.callback', [QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' )], {}, {} );
+
+ mw.loader.using( 'test.callback', function () {
+
+ // /sample/awesome.js declares the "mw.loader.testCallback" function
+ // which contains a call to start() and ok()
+ assert.strictEqual( isAwesomeDone, true, 'test.callback module should\'ve caused isAwesomeDone to be true' );
+ delete mw.loader.testCallback;
+
+ }, function () {
+ QUnit.start();
+ assert.ok( false, 'Error callback fired while loader.using "test.callback" module' );
+ } );
+ } );
+
+ QUnit.asyncTest( 'mw.loader.using( .. ).promise', 2, function ( assert ) {
+ var isAwesomeDone;
+
+ mw.loader.testCallback = function () {
+ QUnit.start();
+ assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
+ isAwesomeDone = true;
+ };
+
+ mw.loader.implement( 'test.promise', [QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' )], {}, {} );
+
+ mw.loader.using( 'test.promise' )
+ .done( function () {
+
+ // /sample/awesome.js declares the "mw.loader.testCallback" function
+ // which contains a call to start() and ok()
+ assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
+ delete mw.loader.testCallback;
+
+ } )
+ .fail( function () {
+ QUnit.start();
+ assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
+ } );
+ } );
+
+ QUnit.asyncTest( 'mw.loader.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
+ var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' );
+
+ assert.notEqual(
+ $element.css( 'float' ),
+ 'right',
+ 'style is clear'
+ );
+
+ mw.loader.implement(
+ 'test.implement.a',
+ function () {
+ assert.equal(
+ $element.css( 'float' ),
+ 'right',
+ 'style is applied'
+ );
+ QUnit.start();
+ },
+ {
+ 'all': '.mw-test-implement-a { float: right; }'
+ },
+ {}
+ );
+
+ mw.loader.load( [
+ 'test.implement.a'
+ ] );
+ } );
+
+ QUnit.asyncTest( 'mw.loader.implement( styles={ "url": { <media>: [url, ..] } } )', 7, function ( assert ) {
+ var $element1 = $( '<div class="mw-test-implement-b1"></div>' ).appendTo( '#qunit-fixture' ),
+ $element2 = $( '<div class="mw-test-implement-b2"></div>' ).appendTo( '#qunit-fixture' ),
+ $element3 = $( '<div class="mw-test-implement-b3"></div>' ).appendTo( '#qunit-fixture' );
+
+ assert.notEqual(
+ $element1.css( 'text-align' ),
+ 'center',
+ 'style is clear'
+ );
+ assert.notEqual(
+ $element2.css( 'float' ),
+ 'left',
+ 'style is clear'
+ );
+ assert.notEqual(
+ $element3.css( 'text-align' ),
+ 'right',
+ 'style is clear'
+ );
+
+ mw.loader.implement(
+ 'test.implement.b',
+ function () {
+ // Note: QUnit.start() must only be called when the entire test is
+ // complete. So, make sure that we don't start until *both*
+ // assertStyleAsync calls have completed.
+ var pending = 2;
+ assertStyleAsync( assert, $element2, 'float', 'left', function () {
+ assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
+
+ pending--;
+ if ( pending === 0 ) {
+ QUnit.start();
+ }
+ } );
+ assertStyleAsync( assert, $element3, 'float', 'right', function () {
+ assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
+
+ pending--;
+ if ( pending === 0 ) {
+ QUnit.start();
+ }
+ } );
+ },
+ {
+ 'url': {
+ 'print': [urlStyleTest( '.mw-test-implement-b1', 'text-align', 'center' )],
+ 'screen': [
+ // bug 40834: Make sure it actually works with more than 1 stylesheet reference
+ urlStyleTest( '.mw-test-implement-b2', 'float', 'left' ),
+ urlStyleTest( '.mw-test-implement-b3', 'float', 'right' )
+ ]
+ }
+ },
+ {}
+ );
+
+ mw.loader.load( [
+ 'test.implement.b'
+ ] );
+ } );
+
+ // Backwards compatibility
+ QUnit.asyncTest( 'mw.loader.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
+ var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' );
+
+ assert.notEqual(
+ $element.css( 'float' ),
+ 'right',
+ 'style is clear'
+ );
+
+ mw.loader.implement(
+ 'test.implement.c',
+ function () {
+ assert.equal(
+ $element.css( 'float' ),
+ 'right',
+ 'style is applied'
+ );
+ QUnit.start();
+ },
+ {
+ 'all': '.mw-test-implement-c { float: right; }'
+ },
+ {}
+ );
+
+ mw.loader.load( [
+ 'test.implement.c'
+ ] );
+ } );
+
+ // Backwards compatibility
+ QUnit.asyncTest( 'mw.loader.implement( styles={ <media>: [url, ..] } ) (back-compat)', 4, function ( assert ) {
+ var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ),
+ $element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' );
+
+ assert.notEqual(
+ $element.css( 'float' ),
+ 'right',
+ 'style is clear'
+ );
+ assert.notEqual(
+ $element2.css( 'text-align' ),
+ 'center',
+ 'style is clear'
+ );
+
+ mw.loader.implement(
+ 'test.implement.d',
+ function () {
+ assertStyleAsync( assert, $element, 'float', 'right', function () {
+
+ assert.notEqual( $element2.css( 'text-align' ), 'center', 'print style is not applied (bug 40500)' );
+
+ QUnit.start();
+ } );
+ },
+ {
+ 'all': [urlStyleTest( '.mw-test-implement-d', 'float', 'right' )],
+ 'print': [urlStyleTest( '.mw-test-implement-d2', 'text-align', 'center' )]
+ },
+ {}
+ );
+
+ mw.loader.load( [
+ 'test.implement.d'
+ ] );
+ } );
+
+ // @import (bug 31676)
+ QUnit.asyncTest( 'mw.loader.implement( styles has @import)', 5, function ( assert ) {
+ var isJsExecuted, $element;
+
+ mw.loader.implement(
+ 'test.implement.import',
+ function () {
+ assert.strictEqual( isJsExecuted, undefined, 'javascript not executed multiple times' );
+ isJsExecuted = true;
+
+ assert.equal( mw.loader.getState( 'test.implement.import' ), 'ready', 'module state is "ready" while implement() is executing javascript' );
+
+ $element = $( '<div class="mw-test-implement-import">Foo bar</div>' ).appendTo( '#qunit-fixture' );
+
+ assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'Messages are loaded before javascript execution' );
+
+ assertStyleAsync( assert, $element, 'float', 'right', function () {
+ assert.equal( $element.css( 'text-align' ), 'center',
+ 'CSS styles after the @import rule are working'
+ );
+
+ QUnit.start();
+ } );
+ },
+ {
+ 'css': [
+ '@import url(\''
+ + urlStyleTest( '.mw-test-implement-import', 'float', 'right' )
+ + '\');\n'
+ + '.mw-test-implement-import { text-align: center; }'
+ ]
+ },
+ {
+ 'test-foobar': 'Hello Foobar, $1!'
+ }
+ );
+
+ mw.loader.load( 'test.implement' );
+
+ } );
+
+ QUnit.asyncTest( 'mw.loader.implement( only messages )', 2, function ( assert ) {
+ assert.assertFalse( mw.messages.exists( 'bug_29107' ), 'Verify that the test message doesn\'t exist yet' );
+
+ mw.loader.implement( 'test.implement.msgs', [], {}, { 'bug_29107': 'loaded' } );
+ mw.loader.using( 'test.implement.msgs', function () {
+ QUnit.start();
+ assert.ok( mw.messages.exists( 'bug_29107' ), 'Bug 29107: messages-only module should implement ok' );
+ }, function () {
+ QUnit.start();
+ assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' );
+ } );
+ } );
+
+ QUnit.test( 'mw.loader erroneous indirect dependency', 3, function ( assert ) {
+ mw.loader.register( [
+ ['test.module1', '0'],
+ ['test.module2', '0', ['test.module1']],
+ ['test.module3', '0', ['test.module2']]
+ ] );
+ mw.loader.implement( 'test.module1', function () {
+ throw new Error( 'expected' );
+ }, {}, {} );
+ assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' );
+ assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' );
+ assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' );
+ } );
+
+ QUnit.test( 'mw.loader out-of-order implementation', 9, function ( assert ) {
+ mw.loader.register( [
+ ['test.module4', '0'],
+ ['test.module5', '0', ['test.module4']],
+ ['test.module6', '0', ['test.module5']]
+ ] );
+ mw.loader.implement( 'test.module4', function () {
+ }, {}, {} );
+ assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
+ assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
+ assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' );
+ mw.loader.implement( 'test.module6', function () {
+ }, {}, {} );
+ assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
+ assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
+ assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' );
+ mw.loader.implement( 'test.module5', function () {
+ }, {}, {} );
+ assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
+ assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' );
+ assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' );
+ } );
+
+ QUnit.test( 'mw.loader missing dependency', 13, function ( assert ) {
+ mw.loader.register( [
+ ['test.module7', '0'],
+ ['test.module8', '0', ['test.module7']],
+ ['test.module9', '0', ['test.module8']]
+ ] );
+ mw.loader.implement( 'test.module8', function () {
+ }, {}, {} );
+ assert.strictEqual( mw.loader.getState( 'test.module7' ), 'registered', 'Expected "registered" state for test.module7' );
+ assert.strictEqual( mw.loader.getState( 'test.module8' ), 'loaded', 'Expected "loaded" state for test.module8' );
+ assert.strictEqual( mw.loader.getState( 'test.module9' ), 'registered', 'Expected "registered" state for test.module9' );
+ mw.loader.state( 'test.module7', 'missing' );
+ assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
+ assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
+ assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
+ mw.loader.implement( 'test.module9', function () {
+ }, {}, {} );
+ assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
+ assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
+ assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
+ mw.loader.using(
+ ['test.module7'],
+ function () {
+ assert.ok( false, 'Success fired despite missing dependency' );
+ assert.ok( true, 'QUnit expected() count dummy' );
+ },
+ function ( e, dependencies ) {
+ assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
+ assert.deepEqual( dependencies, ['test.module7'], 'Error callback called with module test.module7' );
+ }
+ );
+ mw.loader.using(
+ ['test.module9'],
+ function () {
+ assert.ok( false, 'Success fired despite missing dependency' );
+ assert.ok( true, 'QUnit expected() count dummy' );
+ },
+ function ( e, dependencies ) {
+ assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
+ dependencies.sort();
+ assert.deepEqual(
+ dependencies,
+ ['test.module7', 'test.module8', 'test.module9'],
+ 'Error callback called with all three modules as dependencies'
+ );
+ }
+ );
+ } );
+
+ QUnit.asyncTest( 'mw.loader dependency handling', 5, function ( assert ) {
+ mw.loader.register( [
+ // [module, version, dependencies, group, source]
+ ['testMissing', '1', [], null, 'testloader'],
+ ['testUsesMissing', '1', ['testMissing'], null, 'testloader'],
+ ['testUsesNestedMissing', '1', ['testUsesMissing'], null, 'testloader']
+ ] );
+
+ function verifyModuleStates() {
+ assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module not known to server must have state "missing"' );
+ assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module with missing dependency must have state "error"' );
+ assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module with indirect missing dependency must have state "error"' );
+ }
+
+ mw.loader.using( ['testUsesNestedMissing'],
+ function () {
+ assert.ok( false, 'Error handler should be invoked.' );
+ assert.ok( true ); // Dummy to reach QUnit expect()
+
+ verifyModuleStates();
+
+ QUnit.start();
+ },
+ function ( e, badmodules ) {
+ assert.ok( true, 'Error handler should be invoked.' );
+ // As soon as server spits out state('testMissing', 'missing');
+ // it will bubble up and trigger the error callback.
+ // Therefor the badmodules array is not testUsesMissing or testUsesNestedMissing.
+ assert.deepEqual( badmodules, ['testMissing'], 'Bad modules as expected.' );
+
+ verifyModuleStates();
+
+ QUnit.start();
+ }
+ );
+ } );
+
+ QUnit.asyncTest( 'mw.loader skin-function handling', 5, function ( assert ) {
+ mw.loader.register( [
+ // [module, version, dependencies, group, source, skip]
+ ['testSkipped', '1', [], null, 'testloader', 'return true;'],
+ ['testNotSkipped', '1', [], null, 'testloader', 'return false;'],
+ ['testUsesSkippable', '1', ['testSkipped', 'testNotSkipped'], null, 'testloader']
+ ] );
+
+ function verifyModuleStates() {
+ assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Module is ready when skipped' );
+ assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Module is ready when not skipped but loaded' );
+ assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Module is ready when skippable dependencies are ready' );
+ }
+
+ mw.loader.using( ['testUsesSkippable'],
+ function () {
+ assert.ok( true, 'Success handler should be invoked.' );
+ assert.ok( true ); // Dummy to match error handler and reach QUnit expect()
+
+ verifyModuleStates();
+
+ QUnit.start();
+ },
+ function ( e, badmodules ) {
+ assert.ok( false, 'Error handler should not be invoked.' );
+ assert.deepEqual( badmodules, [], 'Bad modules as expected.' );
+
+ verifyModuleStates();
+
+ QUnit.start();
+ }
+ );
+ } );
+
+ QUnit.asyncTest( 'mw.loader( "//protocol-relative" ) (bug 30825)', 2, function ( assert ) {
+ // This bug was actually already fixed in 1.18 and later when discovered in 1.17.
+ // Test is for regressions!
+
+ // Forge an URL to the test callback script
+ var target = QUnit.fixurl(
+ mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
+ );
+
+ // Confirm that mw.loader.load() works with protocol-relative URLs
+ target = target.replace( /https?:/, '' );
+
+ assert.equal( target.slice( 0, 2 ), '//',
+ 'URL must be relative to test relative URLs!'
+ );
+
+ // Async!
+ // The target calls QUnit.start
+ mw.loader.load( target );
+ } );
+
+ QUnit.test( 'mw.html', 13, function ( assert ) {
+ assert.throws( function () {
+ mw.html.escape();
+ }, TypeError, 'html.escape throws a TypeError if argument given is not a string' );
+
+ assert.equal( mw.html.escape( '<mw awesome="awesome" value=\'test\' />' ),
+ '&lt;mw awesome=&quot;awesome&quot; value=&#039;test&#039; /&gt;', 'escape() escapes special characters to html entities' );
+
+ assert.equal( mw.html.element(),
+ '<undefined/>', 'element() always returns a valid html string (even without arguments)' );
+
+ assert.equal( mw.html.element( 'div' ), '<div/>', 'element() Plain DIV (simple)' );
+
+ assert.equal( mw.html.element( 'div', {}, '' ), '<div></div>', 'element() Basic DIV (simple)' );
+
+ assert.equal(
+ mw.html.element(
+ 'div', {
+ id: 'foobar'
+ }
+ ),
+ '<div id="foobar"/>',
+ 'html.element DIV (attribs)' );
+
+ assert.equal( mw.html.element( 'p', null, 12 ), '<p>12</p>', 'Numbers are valid content and should be casted to a string' );
+
+ assert.equal( mw.html.element( 'p', { title: 12 }, '' ), '<p title="12"></p>', 'Numbers are valid attribute values' );
+
+ // Example from https://www.mediawiki.org/wiki/ResourceLoader/Default_modules#mediaWiki.html
+ assert.equal(
+ mw.html.element(
+ 'div',
+ {},
+ new mw.html.Raw(
+ mw.html.element( 'img', { src: '<' } )
+ )
+ ),
+ '<div><img src="&lt;"/></div>',
+ 'Raw inclusion of another element'
+ );
+
+ assert.equal(
+ mw.html.element(
+ 'option', {
+ selected: true
+ }, 'Foo'
+ ),
+ '<option selected="selected">Foo</option>',
+ 'Attributes may have boolean values. True copies the attribute name to the value.'
+ );
+
+ assert.equal(
+ mw.html.element(
+ 'option', {
+ value: 'foo',
+ selected: false
+ }, 'Foo'
+ ),
+ '<option value="foo">Foo</option>',
+ 'Attributes may have boolean values. False keeps the attribute from output.'
+ );
+
+ assert.equal( mw.html.element( 'div',
+ null, 'a' ),
+ '<div>a</div>',
+ 'html.element DIV (content)' );
+
+ assert.equal( mw.html.element( 'a',
+ { href: 'http://mediawiki.org/w/index.php?title=RL&action=history' }, 'a' ),
+ '<a href="http://mediawiki.org/w/index.php?title=RL&amp;action=history">a</a>',
+ 'html.element DIV (attribs + content)' );
+
+ } );
+
+ QUnit.test( 'mw.hook', 13, function ( assert ) {
+ var hook, add, fire, chars, callback;
+
+ mw.hook( 'test.hook.unfired' ).add( function () {
+ assert.ok( false, 'Unfired hook' );
+ } );
+
+ mw.hook( 'test.hook.basic' ).add( function () {
+ assert.ok( true, 'Basic callback' );
+ } );
+ mw.hook( 'test.hook.basic' ).fire();
+
+ mw.hook( 'hasOwnProperty' ).add( function () {
+ assert.ok( true, 'hook with name of predefined method' );
+ } );
+ mw.hook( 'hasOwnProperty' ).fire();
+
+ mw.hook( 'test.hook.data' ).add( function ( data1, data2 ) {
+ assert.equal( data1, 'example', 'Fire with data (string param)' );
+ assert.deepEqual( data2, ['two'], 'Fire with data (array param)' );
+ } );
+ mw.hook( 'test.hook.data' ).fire( 'example', ['two'] );
+
+ hook = mw.hook( 'test.hook.chainable' );
+ assert.strictEqual( hook.add(), hook, 'hook.add is chainable' );
+ assert.strictEqual( hook.remove(), hook, 'hook.remove is chainable' );
+ assert.strictEqual( hook.fire(), hook, 'hook.fire is chainable' );
+
+ hook = mw.hook( 'test.hook.detach' );
+ add = hook.add;
+ fire = hook.fire;
+ add( function ( x, y ) {
+ assert.deepEqual( [x, y], ['x', 'y'], 'Detached (contextless) with data' );
+ } );
+ fire( 'x', 'y' );
+
+ mw.hook( 'test.hook.fireBefore' ).fire().add( function () {
+ assert.ok( true, 'Invoke handler right away if it was fired before' );
+ } );
+
+ mw.hook( 'test.hook.fireTwiceBefore' ).fire().fire().add( function () {
+ assert.ok( true, 'Invoke handler right away if it was fired before (only last one)' );
+ } );
+
+ chars = [];
+
+ mw.hook( 'test.hook.many' )
+ .add( function ( chr ) {
+ chars.push( chr );
+ } )
+ .fire( 'x' ).fire( 'y' ).fire( 'z' )
+ .add( function ( chr ) {
+ assert.equal( chr, 'z', 'Adding callback later invokes right away with last data' );
+ } );
+
+ assert.deepEqual( chars, ['x', 'y', 'z'], 'Multiple callbacks with multiple fires' );
+
+ chars = [];
+ callback = function ( chr ) {
+ chars.push( chr );
+ };
+
+ mw.hook( 'test.hook.variadic' )
+ .add(
+ callback,
+ callback,
+ function ( chr ) {
+ chars.push( chr );
+ },
+ callback
+ )
+ .fire( 'x' )
+ .remove(
+ function () {
+ 'not-added';
+ },
+ callback
+ )
+ .fire( 'y' )
+ .remove( callback )
+ .fire( 'z' );
+
+ assert.deepEqual(
+ chars,
+ ['x', 'x', 'x', 'x', 'y', 'z'],
+ '"add" and "remove" support variadic arguments. ' +
+ '"add" does not filter unique. ' +
+ '"remove" removes all equal by reference. ' +
+ '"remove" is silent if the function is not found'
+ );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js
new file mode 100644
index 00000000..e43516b0
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js
@@ -0,0 +1,40 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mediawiki.toc', QUnit.newMwEnvironment( {
+ setup: function () {
+ // Prevent live cookies like mw_hidetoc=1 from interferring with the test
+ this.stub( $, 'cookie' ).returns( null );
+ }
+ } ) );
+
+ QUnit.asyncTest( 'toggleToc', 4, function ( assert ) {
+ var tocHtml, $toggleLink, $tocList;
+
+ assert.strictEqual( $( '#toc' ).length, 0, 'There is no table of contents on the page at the beginning' );
+
+ tocHtml = '<div id="toc" class="toc">' +
+ '<div id="toctitle">' +
+ '<h2>Contents</h2>' +
+ '</div>' +
+ '<ul><li></li></ul>' +
+ '</div>';
+ $( tocHtml ).appendTo( '#qunit-fixture' );
+ mw.hook( 'wikipage.content' ).fire( $( '#qunit-fixture' ) );
+
+ $tocList = $( '#toc ul:first' );
+ $toggleLink = $( '#togglelink' );
+
+ assert.strictEqual( $toggleLink.length, 1, 'Toggle link is added to the table of contents' );
+
+ assert.strictEqual( $tocList.is( ':hidden' ), false, 'The table of contents is now visible' );
+
+ $toggleLink.click();
+ $tocList.promise().done( function () {
+ assert.strictEqual( $tocList.is( ':hidden' ), true, 'The table of contents is now hidden' );
+
+ $toggleLink.click();
+ $tocList.promise().done( function () {
+ QUnit.start();
+ } );
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js
new file mode 100644
index 00000000..91321a2f
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js
@@ -0,0 +1,54 @@
+( function ( mw ) {
+ QUnit.module( 'mediawiki.user', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.server = this.sandbox.useFakeServer();
+ }
+ } ) );
+
+ QUnit.test( 'options', 1, function ( assert ) {
+ assert.ok( mw.user.options instanceof mw.Map, 'options instance of mw.Map' );
+ } );
+
+ QUnit.test( 'user status', 7, function ( assert ) {
+
+ // Forge an anonymous user
+ mw.config.set( 'wgUserName', null );
+ delete mw.config.values.wgUserId;
+
+ assert.strictEqual( mw.user.getName(), null, 'user.getName() returns null when anonymous' );
+ assert.assertTrue( mw.user.isAnon(), 'user.isAnon() returns true when anonymous' );
+ assert.strictEqual( mw.user.getId(), 0, 'user.getId() returns 0 when anonymous' );
+
+ // Not part of startUp module
+ mw.config.set( 'wgUserName', 'John' );
+ mw.config.set( 'wgUserId', 123 );
+
+ assert.equal( mw.user.getName(), 'John', 'user.getName() returns username when logged-in' );
+ assert.assertFalse( mw.user.isAnon(), 'user.isAnon() returns false when logged-in' );
+ assert.strictEqual( mw.user.getId(), 123, 'user.getId() returns correct ID when logged-in' );
+
+ assert.equal( mw.user.id(), 'John', 'user.id Returns username when logged-in' );
+ } );
+
+ QUnit.test( 'getUserInfos', 3, function ( assert ) {
+ mw.user.getGroups( function ( groups ) {
+ assert.deepEqual( groups, [ '*', 'user' ], 'Result' );
+ } );
+
+ mw.user.getRights( function ( rights ) {
+ assert.deepEqual( rights, [ 'read', 'edit', 'createtalk' ], 'Result (callback)' );
+ } );
+
+ mw.user.getRights().done( function ( rights ) {
+ assert.deepEqual( rights, [ 'read', 'edit', 'createtalk' ], 'Result (promise)' );
+ } );
+
+ this.server.respondWith( /meta=userinfo/, function ( request ) {
+ request.respond( 200, { 'Content-Type': 'application/json' },
+ '{ "query": { "userinfo": { "groups": [ "*", "user" ], "rights": [ "read", "edit", "createtalk" ] } } }'
+ );
+ } );
+
+ this.server.respond();
+ } );
+}( mediaWiki ) );
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js
new file mode 100644
index 00000000..4401eadb
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js
@@ -0,0 +1,343 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mediawiki.util', QUnit.newMwEnvironment( {
+ setup: function () {
+ $.fn.updateTooltipAccessKeys.setTestMode( true );
+ },
+ teardown: function () {
+ $.fn.updateTooltipAccessKeys.setTestMode( false );
+ },
+ messages: {
+ // Used by accessKeyLabel in test for addPortletLink
+ 'brackets': '[$1]',
+ 'word-separator': ' '
+ }
+ } ) );
+
+ QUnit.test( 'rawurlencode', 1, function ( assert ) {
+ assert.equal( mw.util.rawurlencode( 'Test:A & B/Here' ), 'Test%3AA%20%26%20B%2FHere' );
+ } );
+
+ QUnit.test( 'wikiUrlencode', 10, function ( assert ) {
+ assert.equal( mw.util.wikiUrlencode( 'Test:A & B/Here' ), 'Test:A_%26_B/Here' );
+ // See also wfUrlencodeTest.php#provideURLS
+ $.each( {
+ '+': '%2B',
+ '&': '%26',
+ '=': '%3D',
+ ':': ':',
+ ';@$-_.!*': ';@$-_.!*',
+ '/': '/',
+ '[]': '%5B%5D',
+ '<>': '%3C%3E',
+ '\'': '%27'
+ }, function ( input, output ) {
+ assert.equal( mw.util.wikiUrlencode( input ), output );
+ } );
+ } );
+
+ QUnit.test( 'getUrl', 4, function ( assert ) {
+ // Not part of startUp module
+ mw.config.set( 'wgArticlePath', '/wiki/$1' );
+ mw.config.set( 'wgPageName', 'Foobar' );
+
+ var href = mw.util.getUrl( 'Sandbox' );
+ assert.equal( href, '/wiki/Sandbox', 'Simple title; Get link for "Sandbox"' );
+
+ href = mw.util.getUrl( 'Foo:Sandbox ? 5+5=10 ! (test)/subpage' );
+ assert.equal( href, '/wiki/Foo:Sandbox_%3F_5%2B5%3D10_!_(test)/subpage',
+ 'Advanced title; Get link for "Foo:Sandbox ? 5+5=10 ! (test)/subpage"' );
+
+ href = mw.util.getUrl();
+ assert.equal( href, '/wiki/Foobar', 'Default title; Get link for current page ("Foobar")' );
+
+ href = mw.util.getUrl( 'Sandbox', { action: 'edit' } );
+ assert.equal( href, '/wiki/Sandbox?action=edit',
+ 'Simple title with query string; Get link for "Sandbox" with action=edit' );
+ } );
+
+ QUnit.test( 'wikiScript', 4, function ( assert ) {
+ mw.config.set( {
+ 'wgScript': '/w/i.php', // customized wgScript for bug 39103
+ 'wgLoadScript': '/w/l.php', // customized wgLoadScript for bug 39103
+ 'wgScriptPath': '/w',
+ 'wgScriptExtension': '.php'
+ } );
+
+ assert.equal( mw.util.wikiScript(), mw.config.get( 'wgScript' ),
+ 'wikiScript() returns wgScript'
+ );
+ assert.equal( mw.util.wikiScript( 'index' ), mw.config.get( 'wgScript' ),
+ 'wikiScript( index ) returns wgScript'
+ );
+ assert.equal( mw.util.wikiScript( 'load' ), mw.config.get( 'wgLoadScript' ),
+ 'wikiScript( load ) returns wgLoadScript'
+ );
+ assert.equal( mw.util.wikiScript( 'api' ), '/w/api.php', 'API path' );
+ } );
+
+ QUnit.test( 'addCSS', 3, function ( assert ) {
+ var $el, style;
+ $el = $( '<div>' ).attr( 'id', 'mw-addcsstest' ).appendTo( '#qunit-fixture' );
+
+ style = mw.util.addCSS( '#mw-addcsstest { visibility: hidden; }' );
+ assert.equal( typeof style, 'object', 'addCSS returned an object' );
+ assert.strictEqual( style.disabled, false, 'property "disabled" is available and set to false' );
+
+ assert.equal( $el.css( 'visibility' ), 'hidden', 'Added style properties are in effect' );
+
+ // Clean up
+ $( style.ownerNode ).remove();
+ } );
+
+ QUnit.test( 'getParamValue', 5, function ( assert ) {
+ var url;
+
+ url = 'http://example.org/?foo=wrong&foo=right#&foo=bad';
+ assert.equal( mw.util.getParamValue( 'foo', url ), 'right', 'Use latest one, ignore hash' );
+ assert.strictEqual( mw.util.getParamValue( 'bar', url ), null, 'Return null when not found' );
+
+ url = 'http://example.org/#&foo=bad';
+ assert.strictEqual( mw.util.getParamValue( 'foo', url ), null, 'Ignore hash if param is not in querystring but in hash (bug 27427)' );
+
+ url = 'example.org?' + $.param( { 'TEST': 'a b+c' } );
+ assert.strictEqual( mw.util.getParamValue( 'TEST', url ), 'a b+c', 'Bug 30441: getParamValue must understand "+" encoding of space' );
+
+ url = 'example.org?' + $.param( { 'TEST': 'a b+c d' } ); // check for sloppy code from r95332 :)
+ assert.strictEqual( mw.util.getParamValue( 'TEST', url ), 'a b+c d', 'Bug 30441: getParamValue must understand "+" encoding of space (multiple spaces)' );
+ } );
+
+ QUnit.test( 'tooltipAccessKey', 4, function ( assert ) {
+ this.suppressWarnings();
+
+ assert.equal( typeof mw.util.tooltipAccessKeyPrefix, 'string', 'tooltipAccessKeyPrefix must be a string' );
+ assert.equal( $.type( mw.util.tooltipAccessKeyRegexp ), 'regexp', 'tooltipAccessKeyRegexp is a regexp' );
+ assert.ok( mw.util.updateTooltipAccessKeys, 'updateTooltipAccessKeys is non-empty' );
+
+ 'Example [a]'.replace( mw.util.tooltipAccessKeyRegexp, function ( sub, m1, m2, m3, m4, m5, m6 ) {
+ assert.equal( m6, 'a', 'tooltipAccessKeyRegexp finds the accesskey hint' );
+ } );
+
+ this.restoreWarnings();
+ } );
+
+ QUnit.test( '$content', 2, function ( assert ) {
+ assert.ok( mw.util.$content instanceof jQuery, 'mw.util.$content instance of jQuery' );
+ assert.strictEqual( mw.util.$content.length, 1, 'mw.util.$content must have length of 1' );
+ } );
+
+ /**
+ * Portlet names are prefixed with 'p-test' to avoid conflict with core
+ * when running the test suite under a wiki page.
+ * Previously, test elements where invisible to the selector since only
+ * one element can have a given id.
+ */
+ QUnit.test( 'addPortletLink', 13, function ( assert ) {
+ var pTestTb, pCustom, vectorTabs, tbRL, cuQuux, $cuQuux, tbMW, $tbMW, tbRLDM, caFoo,
+ addedAfter, tbRLDMnonexistentid, tbRLDMemptyjquery;
+
+ pTestTb = '\
+ <div class="portlet" id="p-test-tb">\
+ <h3>Toolbox</h3>\
+ <ul class="body"></ul>\
+ </div>';
+ pCustom = '\
+ <div class="portlet" id="p-test-custom">\
+ <h3>Views</h3>\
+ <ul class="body">\
+ <li id="c-foo"><a href="#">Foo</a></li>\
+ <li id="c-barmenu">\
+ <ul>\
+ <li id="c-bar-baz"><a href="#">Baz</a></a>\
+ </ul>\
+ </li>\
+ </ul>\
+ </div>';
+ vectorTabs = '\
+ <div id="p-test-views" class="vectorTabs">\
+ <h3>Views</h3>\
+ <ul></ul>\
+ </div>';
+
+ $( '#qunit-fixture' ).append( pTestTb, pCustom, vectorTabs );
+
+ tbRL = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/ResourceLoader',
+ 'ResourceLoader', 't-rl', 'More info about ResourceLoader on MediaWiki.org ', 'l'
+ );
+
+ assert.ok( $.isDomElement( tbRL ), 'addPortletLink returns a valid DOM Element according to $.isDomElement' );
+
+ tbMW = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/',
+ 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org', 'm', tbRL );
+ $tbMW = $( tbMW );
+
+ assert.propEqual(
+ $tbMW.getAttrs(),
+ {
+ id: 't-mworg'
+ },
+ 'Validate attributes of created element'
+ );
+
+ assert.propEqual(
+ $tbMW.find( 'a' ).getAttrs(),
+ {
+ href: '//mediawiki.org/',
+ title: 'Go to MediaWiki.org [test-m]',
+ accesskey: 'm'
+ },
+ 'Validate attributes of anchor tag in created element'
+ );
+
+ assert.equal( $tbMW.closest( '.portlet' ).attr( 'id' ), 'p-test-tb', 'Link was inserted within correct portlet' );
+ assert.strictEqual( $tbMW.next()[0], tbRL, 'Link is in the correct position (by passing nextnode)' );
+
+ cuQuux = mw.util.addPortletLink( 'p-test-custom', '#', 'Quux', null, 'Example [shift-x]', 'q' );
+ $cuQuux = $( cuQuux );
+
+ assert.equal( $cuQuux.find( 'a' ).attr( 'title' ), 'Example [test-q]', 'Existing accesskey is stripped and updated' );
+
+ assert.equal(
+ $( '#p-test-custom #c-barmenu ul li' ).length,
+ 1,
+ 'addPortletLink did not add the item to all <ul> elements in the portlet (bug 35082)'
+ );
+
+ tbRLDM = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/RL/DM',
+ 'Default modules', 't-rldm', 'List of all default modules ', 'd', '#t-rl' );
+
+ assert.equal( $( tbRLDM ).next().attr( 'id' ), 't-rl', 'Link is in the correct position (by passing CSS selector)' );
+
+ caFoo = mw.util.addPortletLink( 'p-test-views', '#', 'Foo' );
+
+ assert.strictEqual( $tbMW.find( 'span' ).length, 0, 'No <span> element should be added for porlets without vectorTabs class.' );
+ assert.strictEqual( $( caFoo ).find( 'span' ).length, 1, 'A <span> element should be added for porlets with vectorTabs class.' );
+
+ addedAfter = mw.util.addPortletLink( 'p-test-tb', '#', 'After foo', 'post-foo', 'After foo', null, $( tbRL ) );
+ assert.strictEqual( $( addedAfter ).next()[0], tbRL, 'Link is in the correct position (by passing a jQuery object as nextnode)' );
+
+ // test case - nonexistent id as next node
+ tbRLDMnonexistentid = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/RL/DM',
+ 'Default modules', 't-rldm-nonexistent', 'List of all default modules ', 'd', '#t-rl-nonexistent' );
+
+ assert.equal( tbRLDMnonexistentid, $( '#p-test-tb li:last' )[0], 'Nonexistent id as nextnode adds the portlet at end' );
+
+ // test case - empty jquery object as next node
+ tbRLDMemptyjquery = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/wiki/RL/DM',
+ 'Default modules', 't-rldm-empty-jquery', 'List of all default modules ', 'd', $( '#t-rl-nonexistent' ) );
+
+ assert.equal( tbRLDMemptyjquery, $( '#p-test-tb li:last' )[0], 'Empty jquery as nextnode adds the portlet at end' );
+ } );
+
+ QUnit.test( 'jsMessage', 1, function ( assert ) {
+ this.suppressWarnings();
+ var a = mw.util.jsMessage( 'MediaWiki is <b>Awesome</b>.' );
+ this.restoreWarnings();
+ assert.ok( a, 'Basic checking of return value' );
+ } );
+
+ QUnit.test( 'validateEmail', 6, function ( assert ) {
+ assert.strictEqual( mw.util.validateEmail( '' ), null, 'Should return null for empty string ' );
+ assert.strictEqual( mw.util.validateEmail( 'user@localhost' ), true, 'Return true for a valid e-mail address' );
+
+ // testEmailWithCommasAreInvalids
+ assert.strictEqual( mw.util.validateEmail( 'user,foo@example.org' ), false, 'Emails with commas are invalid' );
+ assert.strictEqual( mw.util.validateEmail( 'userfoo@ex,ample.org' ), false, 'Emails with commas are invalid' );
+
+ // testEmailWithHyphens
+ assert.strictEqual( mw.util.validateEmail( 'user-foo@example.org' ), true, 'Emails may contain a hyphen' );
+ assert.strictEqual( mw.util.validateEmail( 'userfoo@ex-ample.org' ), true, 'Emails may contain a hyphen' );
+ } );
+
+ QUnit.test( 'isIPv6Address', 40, function ( assert ) {
+ // Shortcuts
+ function assertFalseIPv6( addy, summary ) {
+ return assert.strictEqual( mw.util.isIPv6Address( addy ), false, summary );
+ }
+
+ function assertTrueIPv6( addy, summary ) {
+ return assert.strictEqual( mw.util.isIPv6Address( addy ), true, summary );
+ }
+
+ // Based on IPTest.php > testisIPv6
+ assertFalseIPv6( ':fc:100::', 'IPv6 starting with lone ":"' );
+ assertFalseIPv6( 'fc:100:::', 'IPv6 ending with a ":::"' );
+ assertFalseIPv6( 'fc:300', 'IPv6 with only 2 words' );
+ assertFalseIPv6( 'fc:100:300', 'IPv6 with only 3 words' );
+
+ $.each(
+ ['fc:100::',
+ 'fc:100:a::',
+ 'fc:100:a:d::',
+ 'fc:100:a:d:1::',
+ 'fc:100:a:d:1:e::',
+ 'fc:100:a:d:1:e:ac::'], function ( i, addy ) {
+ assertTrueIPv6( addy, addy + ' is a valid IP' );
+ } );
+
+ assertFalseIPv6( 'fc:100:a:d:1:e:ac:0::', 'IPv6 with 8 words ending with "::"' );
+ assertFalseIPv6( 'fc:100:a:d:1:e:ac:0:1::', 'IPv6 with 9 words ending with "::"' );
+
+ assertFalseIPv6( ':::' );
+ assertFalseIPv6( '::0:', 'IPv6 ending in a lone ":"' );
+
+ assertTrueIPv6( '::', 'IPv6 zero address' );
+ $.each(
+ ['::0',
+ '::fc',
+ '::fc:100',
+ '::fc:100:a',
+ '::fc:100:a:d',
+ '::fc:100:a:d:1',
+ '::fc:100:a:d:1:e',
+ '::fc:100:a:d:1:e:ac',
+
+ 'fc:100:a:d:1:e:ac:0'], function ( i, addy ) {
+ assertTrueIPv6( addy, addy + ' is a valid IP' );
+ } );
+
+ assertFalseIPv6( '::fc:100:a:d:1:e:ac:0', 'IPv6 with "::" and 8 words' );
+ assertFalseIPv6( '::fc:100:a:d:1:e:ac:0:1', 'IPv6 with 9 words' );
+
+ assertFalseIPv6( ':fc::100', 'IPv6 starting with lone ":"' );
+ assertFalseIPv6( 'fc::100:', 'IPv6 ending with lone ":"' );
+ assertFalseIPv6( 'fc:::100', 'IPv6 with ":::" in the middle' );
+
+ assertTrueIPv6( 'fc::100', 'IPv6 with "::" and 2 words' );
+ assertTrueIPv6( 'fc::100:a', 'IPv6 with "::" and 3 words' );
+ assertTrueIPv6( 'fc::100:a:d', 'IPv6 with "::" and 4 words' );
+ assertTrueIPv6( 'fc::100:a:d:1', 'IPv6 with "::" and 5 words' );
+ assertTrueIPv6( 'fc::100:a:d:1:e', 'IPv6 with "::" and 6 words' );
+ assertTrueIPv6( 'fc::100:a:d:1:e:ac', 'IPv6 with "::" and 7 words' );
+ assertTrueIPv6( '2001::df', 'IPv6 with "::" and 2 words' );
+ assertTrueIPv6( '2001:5c0:1400:a::df', 'IPv6 with "::" and 5 words' );
+ assertTrueIPv6( '2001:5c0:1400:a::df:2', 'IPv6 with "::" and 6 words' );
+
+ assertFalseIPv6( 'fc::100:a:d:1:e:ac:0', 'IPv6 with "::" and 8 words' );
+ assertFalseIPv6( 'fc::100:a:d:1:e:ac:0:1', 'IPv6 with 9 words' );
+ } );
+
+ QUnit.test( 'isIPv4Address', 11, function ( assert ) {
+ // Shortcuts
+ function assertFalseIPv4( addy, summary ) {
+ assert.strictEqual( mw.util.isIPv4Address( addy ), false, summary );
+ }
+
+ function assertTrueIPv4( addy, summary ) {
+ assert.strictEqual( mw.util.isIPv4Address( addy ), true, summary );
+ }
+
+ // Based on IPTest.php > testisIPv4
+ assertFalseIPv4( false, 'Boolean false is not an IP' );
+ assertFalseIPv4( true, 'Boolean true is not an IP' );
+ assertFalseIPv4( '', 'Empty string is not an IP' );
+ assertFalseIPv4( 'abc', '"abc" is not an IP' );
+ assertFalseIPv4( ':', 'Colon is not an IP' );
+ assertFalseIPv4( '124.24.52', 'IPv4 not enough quads' );
+ assertFalseIPv4( '24.324.52.13', 'IPv4 out of range' );
+ assertFalseIPv4( '.24.52.13', 'IPv4 starts with period' );
+
+ assertTrueIPv4( '124.24.52.13', '124.24.52.134 is a valid IP' );
+ assertTrueIPv4( '1.24.52.13', '1.24.52.13 is a valid IP' );
+ assertFalseIPv4( '74.24.52.13/20', 'IPv4 ranges are not recogzized as valid IPs' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/suites/resources/startup.test.js b/tests/qunit/suites/resources/startup.test.js
new file mode 100644
index 00000000..ed03418a
--- /dev/null
+++ b/tests/qunit/suites/resources/startup.test.js
@@ -0,0 +1,143 @@
+/*global isCompatible: true */
+( function ( $ ) {
+ var testcases = {
+ gradeA: [
+ // Chrome
+ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.205 Safari/534.16',
+ // Firefox 4+
+ 'Mozilla/5.0 (Windows NT 6.1.1; rv:5.0) Gecko/20100101 Firefox/5.0',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0) Gecko/20100101 Firefox/9.0',
+ 'Mozilla/5.0 (Macintosh; I; Intel Mac OS X 11_7_9; de-LI; rv:1.9b4) Gecko/2012010317 Firefox/10.0a4',
+ 'Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/12.0',
+ 'Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1',
+ // Kindle Fire
+ 'Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Kindle Fire Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Safari/533.1',
+ // Safari 5.0+
+ 'Mozilla/5.0 (Macintosh; I; Intel Mac OS X 10_6_7; ru-ru) AppleWebKit/534.31+ (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1',
+ // Opera 12+ (Presto-based)
+ 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
+ 'Opera/9.80 (Windows NT 5.1) Presto/2.12.388 Version/12.17',
+ // Opera 15+ (Chromium-based)
+ 'Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36 OPR/15.0.1147.153',
+ 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36 OPR/16.0.1196.62',
+ 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36 OPR/23.0.1522.75',
+ // Internet Explorer 8+
+ 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)',
+ 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)',
+ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
+ // IE Mobile
+ 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; NOKIA; Lumia 800)',
+ // BlackBerry 6+
+ 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9300; en) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.570 Mobile Safari/534.8+',
+ 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+',
+ 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.3+ (KHTML, like Gecko) Version/10.0.9.386 Mobile Safari/537.3+',
+ // Open WebOS 1.4+ (HP Veer 4G)
+ 'Mozilla/5.0 (webOS/2.1.2; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 P160UNA/1.0',
+ // Firefox Mobile
+ 'Mozilla/5.0 (Mobile; rv:14.0) Gecko/14.0 Firefox/14.0',
+ // iOS
+ 'Mozilla/5.0 (ipod: U;CPU iPhone OS 2_2 like Mac OS X: es_es) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3',
+ 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3',
+ // Android
+ 'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17'
+ ],
+ gradeC: [
+ // Internet Explorer < 8
+ 'Mozilla/2.0 (compatible; MSIE 3.03; Windows 3.1)',
+ 'Mozilla/4.0 (compatible; MSIE 4.01; Windows 95)',
+ 'Mozilla/4.0 (compatible; MSIE 5.0; Windows 98;)',
+ 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',
+ 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)',
+ 'Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; en-US)',
+ // Firefox < 3
+ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2',
+ 'Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.8.1.1) Gecko/20070311 Firefox/2.0.0.1',
+ // Opera < 12
+ 'Mozilla/5.0 (Windows NT 5.0; U) Opera 7.54 [en]',
+ 'Opera/7.54 (Windows NT 5.0; U) [en]',
+ 'Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.0',
+ 'Opera/8.0 (X11; Linux i686; U; cs)',
+ 'Opera/9.00 (X11; Linux i686; U; de)',
+ 'Opera/9.62 (X11; Linux i686; U; en) Presto/2.1.1',
+ 'Opera/9.80 (Windows NT 6.1; U; en) Presto/2.2.15 Version/10.00',
+ 'Opera/9.80 (Windows NT 6.1; U; ru) Presto/2.8.131 Version/11.10',
+ 'Opera/9.80 (Windows NT 6.1; WOW64; U; pt) Presto/2.10.229 Version/11.62',
+ // BlackBerry < 6
+ 'BlackBerry9300/5.0.0.716 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/133',
+ 'BlackBerry7250/4.0.0 Profile/MIDP-2.0 Configuration/CLDC-1.1',
+ // Open WebOS < 1.5 (Palm Pre, Palm Pixi)
+ 'Mozilla/5.0 (webOS/1.0; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0',
+ 'Mozilla/5.0 (webOS/1.4.0; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pixi/1.1 ',
+ // SymbianOS
+ 'NokiaN95_8GB-3;Mozilla/5.0 SymbianOS/9.2;U;Series60/3.1 NokiaN95_8GB-3/11.2.011 Profile/MIDP-2.0 Configuration/CLDC-1.1 AppleWebKit/413 (KHTML, like Gecko)',
+ 'Nokia7610/2.0 (5.0509.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0 ',
+ 'Mozilla/5.0 (SymbianOS/9.1; U; [en]; SymbianOS/91 Series60/3.0) AppleWebKit/413 (KHTML, like Gecko) Safari/413',
+ 'Mozilla/5.0 (SymbianOS/9.3; Series60/3.2 NokiaE52-2/091.003; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.1.34 Mobile Safari/533.4',
+ // NetFront
+ 'Mozilla/4.0 (compatible; Linux 2.6.10) NetFront/3.3 Kindle/1.0 (screen 600x800)',
+ 'Mozilla/4.0 (compatible; Linux 2.6.22) NetFront/3.4 Kindle/2.0 (screen 824x1200; rotate)',
+ 'Mozilla/4.08 (Windows; Mobile Content Viewer/1.0) NetFront/3.2',
+ // Opera Mini
+ 'Opera/9.80 (J2ME/MIDP; Opera Mini/3.1.10423/22.387; U; en) Presto/2.5.25 Version/10.54',
+ 'Opera/9.50 (J2ME/MIDP; Opera Mini/4.0.10031/298; U; en)',
+ 'Opera/9.80 (J2ME/MIDP; Opera Mini/6.24093/26.1305; U; en) Presto/2.8.119 Version/10.54',
+ 'Opera/9.80 (Android; Opera Mini/7.29530/27.1407; U; en) Presto/2.8.119 Version/11.10',
+ // Ovi Browser
+ 'Mozilla/5.0 (Series40; NokiaX3-02/05.60; Profile/MIDP-2.1 Configuration/CLDC-1.1) Gecko/20100401 S40OviBrowser/3.2.0.0.6',
+ 'Mozilla/5.0 (Series40; Nokia305/05.92; Profile/MIDP-2.1 Configuration/CLDC-1.1) Gecko/20100401 S40OviBrowser/3.7.0.0.11',
+ // Google Glass
+ 'Mozilla/5.0 (Linux; U; Android 4.0.4; en-us; Glass 1 Build/IMM76L; XE11) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'
+ ],
+ // No explicit support for or against these browsers, they're given a shot at Grade A.
+ gradeX: [
+ // Firefox 3.6
+ 'Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3',
+ // Gecko
+ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20060928 (Debian|Debian-1.8.0.7-1) Epiphany/2.14',
+ 'Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.6) Gecko/20070817 IceWeasel/2.0.0.6-g2',
+ // KHTML
+ 'Mozilla/5.0 (compatible; Konqueror/4.3; Linux) KHTML/4.3.5 (like Gecko)',
+ // Text browsers
+ 'Links (2.1pre33; Darwin 8.11.0 Power Macintosh; x)',
+ 'Links (6.9; Unix 6.9-astral sparc; 80x25)',
+ 'Lynx/2.8.6rel.4 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/0.9.8g',
+ 'w3m/0.5.1',
+ // Bots
+ 'Googlebot/2.1 (+http://www.google.com/bot.html)',
+ 'Mozilla/5.0 (compatible; googlebot/2.1; +http://www.google.com/bot.html)',
+ 'Mozilla/5.0 (compatible; YandexBot/3.0)',
+ // Scripts
+ 'curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5',
+ 'Wget/1.9',
+ 'Wget/1.10.1 (Red Hat modified)',
+ // Unknown
+ 'I\'m an unknown browser',
+ // Empty
+ ''
+ ]
+ };
+
+ QUnit.module( 'startup', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'isCompatible( Grade A )', testcases.gradeA.length, function ( assert ) {
+ $.each( testcases.gradeA, function ( i, ua ) {
+ assert.strictEqual( isCompatible( ua ), true, ua );
+ }
+ );
+ } );
+
+ QUnit.test( 'isCompatible( Grade C )', testcases.gradeC.length, function ( assert ) {
+ $.each( testcases.gradeC, function ( i, ua ) {
+ assert.strictEqual( isCompatible( ua ), false, ua );
+ }
+ );
+ } );
+
+ QUnit.test( 'isCompatible( Grade X )', testcases.gradeX.length, function ( assert ) {
+ $.each( testcases.gradeX, function ( i, ua ) {
+ assert.strictEqual( isCompatible( ua ), true, ua );
+ }
+ );
+ } );
+
+}( jQuery ) );
diff --git a/tests/testHelpers.inc b/tests/testHelpers.inc
new file mode 100644
index 00000000..62dccbf0
--- /dev/null
+++ b/tests/testHelpers.inc
@@ -0,0 +1,832 @@
+<?php
+/**
+ * Recording for passing/failing tests.
+ *
+ * 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
+ * @ingroup Testing
+ */
+
+/**
+ * Interface to record parser test results.
+ *
+ * The ITestRecorder is a very simple interface to record the result of
+ * MediaWiki parser tests. One should call start() before running the
+ * full parser tests and end() once all the tests have been finished.
+ * After each test, you should use record() to keep track of your tests
+ * results. Finally, report() is used to generate a summary of your
+ * test run, one could dump it to the console for human consumption or
+ * register the result in a database for tracking purposes.
+ *
+ * @since 1.22
+ */
+interface ITestRecorder {
+
+ /**
+ * Called at beginning of the parser test run
+ */
+ public function start();
+
+ /**
+ * Called after each test
+ * @param string $test
+ * @param bool $result
+ */
+ public function record( $test, $result );
+
+ /**
+ * Called before finishing the test run
+ */
+ public function report();
+
+ /**
+ * Called at the end of the parser test run
+ */
+ public function end();
+
+}
+
+class TestRecorder implements ITestRecorder {
+ public $parent;
+ public $term;
+
+ function __construct( $parent ) {
+ $this->parent = $parent;
+ $this->term = $parent->term;
+ }
+
+ function start() {
+ $this->total = 0;
+ $this->success = 0;
+ }
+
+ function record( $test, $result ) {
+ $this->total++;
+ $this->success += ( $result ? 1 : 0 );
+ }
+
+ function end() {
+ // dummy
+ }
+
+ function report() {
+ if ( $this->total > 0 ) {
+ $this->reportPercentage( $this->success, $this->total );
+ } else {
+ throw new MWException( "No tests found.\n" );
+ }
+ }
+
+ function reportPercentage( $success, $total ) {
+ $ratio = wfPercent( 100 * $success / $total );
+ print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)... ";
+
+ if ( $success == $total ) {
+ print $this->term->color( 32 ) . "ALL TESTS PASSED!";
+ } else {
+ $failed = $total - $success;
+ print $this->term->color( 31 ) . "$failed tests failed!";
+ }
+
+ print $this->term->reset() . "\n";
+
+ return ( $success == $total );
+ }
+}
+
+class DbTestPreviewer extends TestRecorder {
+ protected $lb; // /< Database load balancer
+ protected $db; // /< Database connection to the main DB
+ protected $curRun; // /< run ID number for the current run
+ protected $prevRun; // /< run ID number for the previous run, if any
+ protected $results; // /< Result array
+
+ /**
+ * This should be called before the table prefix is changed
+ * @param TestRecorder $parent
+ */
+ function __construct( $parent ) {
+ parent::__construct( $parent );
+
+ $this->lb = wfGetLBFactory()->newMainLB();
+ // This connection will have the wiki's table prefix, not parsertest_
+ $this->db = $this->lb->getConnection( DB_MASTER );
+ }
+
+ /**
+ * Set up result recording; insert a record for the run with the date
+ * and all that fun stuff
+ */
+ function start() {
+ parent::start();
+
+ if ( !$this->db->tableExists( 'testrun', __METHOD__ )
+ || !$this->db->tableExists( 'testitem', __METHOD__ )
+ ) {
+ print "WARNING> `testrun` table not found in database.\n";
+ $this->prevRun = false;
+ } else {
+ // We'll make comparisons against the previous run later...
+ $this->prevRun = $this->db->selectField( 'testrun', 'MAX(tr_id)' );
+ }
+
+ $this->results = array();
+ }
+
+ function record( $test, $result ) {
+ parent::record( $test, $result );
+ $this->results[$test] = $result;
+ }
+
+ function report() {
+ if ( $this->prevRun ) {
+ // f = fail, p = pass, n = nonexistent
+ // codes show before then after
+ $table = array(
+ 'fp' => 'previously failing test(s) now PASSING! :)',
+ 'pn' => 'previously PASSING test(s) removed o_O',
+ 'np' => 'new PASSING test(s) :)',
+
+ 'pf' => 'previously passing test(s) now FAILING! :(',
+ 'fn' => 'previously FAILING test(s) removed O_o',
+ 'nf' => 'new FAILING test(s) :(',
+ 'ff' => 'still FAILING test(s) :(',
+ );
+
+ $prevResults = array();
+
+ $res = $this->db->select( 'testitem', array( 'ti_name', 'ti_success' ),
+ array( 'ti_run' => $this->prevRun ), __METHOD__ );
+
+ foreach ( $res as $row ) {
+ if ( !$this->parent->regex
+ || preg_match( "/{$this->parent->regex}/i", $row->ti_name )
+ ) {
+ $prevResults[$row->ti_name] = $row->ti_success;
+ }
+ }
+
+ $combined = array_keys( $this->results + $prevResults );
+
+ # Determine breakdown by change type
+ $breakdown = array();
+ foreach ( $combined as $test ) {
+ if ( !isset( $prevResults[$test] ) ) {
+ $before = 'n';
+ } elseif ( $prevResults[$test] == 1 ) {
+ $before = 'p';
+ } else /* if ( $prevResults[$test] == 0 )*/ {
+ $before = 'f';
+ }
+
+ if ( !isset( $this->results[$test] ) ) {
+ $after = 'n';
+ } elseif ( $this->results[$test] == 1 ) {
+ $after = 'p';
+ } else /*if ( $this->results[$test] == 0 ) */ {
+ $after = 'f';
+ }
+
+ $code = $before . $after;
+
+ if ( isset( $table[$code] ) ) {
+ $breakdown[$code][$test] = $this->getTestStatusInfo( $test, $after );
+ }
+ }
+
+ # Write out results
+ foreach ( $table as $code => $label ) {
+ if ( !empty( $breakdown[$code] ) ) {
+ $count = count( $breakdown[$code] );
+ printf( "\n%4d %s\n", $count, $label );
+
+ foreach ( $breakdown[$code] as $differing_test_name => $statusInfo ) {
+ print " * $differing_test_name [$statusInfo]\n";
+ }
+ }
+ }
+ } else {
+ print "No previous test runs to compare against.\n";
+ }
+
+ print "\n";
+ parent::report();
+ }
+
+ /**
+ * Returns a string giving information about when a test last had a status change.
+ * Could help to track down when regressions were introduced, as distinct from tests
+ * which have never passed (which are more change requests than regressions).
+ * @param string $testname
+ * @param string $after
+ * @return string
+ */
+ private function getTestStatusInfo( $testname, $after ) {
+ // If we're looking at a test that has just been removed, then say when it first appeared.
+ if ( $after == 'n' ) {
+ $changedRun = $this->db->selectField( 'testitem',
+ 'MIN(ti_run)',
+ array( 'ti_name' => $testname ),
+ __METHOD__ );
+ $appear = $this->db->selectRow( 'testrun',
+ array( 'tr_date', 'tr_mw_version' ),
+ array( 'tr_id' => $changedRun ),
+ __METHOD__ );
+
+ return "First recorded appearance: "
+ . date( "d-M-Y H:i:s", strtotime( $appear->tr_date ) )
+ . ", " . $appear->tr_mw_version;
+ }
+
+ // Otherwise, this test has previous recorded results.
+ // See when this test last had a different result to what we're seeing now.
+ $conds = array(
+ 'ti_name' => $testname,
+ 'ti_success' => ( $after == 'f' ? "1" : "0" ) );
+
+ if ( $this->curRun ) {
+ $conds[] = "ti_run != " . $this->db->addQuotes( $this->curRun );
+ }
+
+ $changedRun = $this->db->selectField( 'testitem', 'MAX(ti_run)', $conds, __METHOD__ );
+
+ // If no record of ever having had a different result.
+ if ( is_null( $changedRun ) ) {
+ if ( $after == "f" ) {
+ return "Has never passed";
+ } else {
+ return "Has never failed";
+ }
+ }
+
+ // Otherwise, we're looking at a test whose status has changed.
+ // (i.e. it used to work, but now doesn't; or used to fail, but is now fixed.)
+ // In this situation, give as much info as we can as to when it changed status.
+ $pre = $this->db->selectRow( 'testrun',
+ array( 'tr_date', 'tr_mw_version' ),
+ array( 'tr_id' => $changedRun ),
+ __METHOD__ );
+ $post = $this->db->selectRow( 'testrun',
+ array( 'tr_date', 'tr_mw_version' ),
+ array( "tr_id > " . $this->db->addQuotes( $changedRun ) ),
+ __METHOD__,
+ array( "LIMIT" => 1, "ORDER BY" => 'tr_id' )
+ );
+
+ if ( $post ) {
+ $postDate = date( "d-M-Y H:i:s", strtotime( $post->tr_date ) ) . ", {$post->tr_mw_version}";
+ } else {
+ $postDate = 'now';
+ }
+
+ return ( $after == "f" ? "Introduced" : "Fixed" ) . " between "
+ . date( "d-M-Y H:i:s", strtotime( $pre->tr_date ) ) . ", " . $pre->tr_mw_version
+ . " and $postDate";
+ }
+
+ /**
+ * Commit transaction and clean up for result recording
+ */
+ function end() {
+ $this->lb->commitMasterChanges();
+ $this->lb->closeAll();
+ parent::end();
+ }
+}
+
+class DbTestRecorder extends DbTestPreviewer {
+ public $version;
+
+ /**
+ * Set up result recording; insert a record for the run with the date
+ * and all that fun stuff
+ */
+ function start() {
+ $this->db->begin( __METHOD__ );
+
+ if ( !$this->db->tableExists( 'testrun' )
+ || !$this->db->tableExists( 'testitem' )
+ ) {
+ print "WARNING> `testrun` table not found in database. Trying to create table.\n";
+ $this->db->sourceFile( $this->db->patchPath( 'patch-testrun.sql' ) );
+ echo "OK, resuming.\n";
+ }
+
+ parent::start();
+
+ $this->db->insert( 'testrun',
+ array(
+ 'tr_date' => $this->db->timestamp(),
+ 'tr_mw_version' => $this->version,
+ 'tr_php_version' => PHP_VERSION,
+ 'tr_db_version' => $this->db->getServerVersion(),
+ 'tr_uname' => php_uname()
+ ),
+ __METHOD__ );
+ if ( $this->db->getType() === 'postgres' ) {
+ $this->curRun = $this->db->currentSequenceValue( 'testrun_id_seq' );
+ } else {
+ $this->curRun = $this->db->insertId();
+ }
+ }
+
+ /**
+ * Record an individual test item's success or failure to the db
+ *
+ * @param string $test
+ * @param bool $result
+ */
+ function record( $test, $result ) {
+ parent::record( $test, $result );
+
+ $this->db->insert( 'testitem',
+ array(
+ 'ti_run' => $this->curRun,
+ 'ti_name' => $test,
+ 'ti_success' => $result ? 1 : 0,
+ ),
+ __METHOD__ );
+ }
+}
+
+class TestFileIterator implements Iterator {
+ private $file;
+ private $fh;
+ /**
+ * @var ParserTest|MediaWikiParserTest An instance of ParserTest (parserTests.php)
+ * or MediaWikiParserTest (phpunit)
+ */
+ private $parserTest;
+ private $index = 0;
+ private $test;
+ private $section = null;
+ /** String|null: current test section being analyzed */
+ private $sectionData = array();
+ private $lineNum;
+ private $eof;
+ # Create a fake parser tests which never run anything unless
+ # asked to do so. This will avoid running hooks for a disabled test
+ private $delayedParserTest;
+ private $nextSubTest = 0;
+
+ function __construct( $file, $parserTest ) {
+ $this->file = $file;
+ $this->fh = fopen( $this->file, "rt" );
+
+ if ( !$this->fh ) {
+ throw new MWException( "Couldn't open file '$file'\n" );
+ }
+
+ $this->parserTest = $parserTest;
+ $this->delayedParserTest = new DelayedParserTest();
+
+ $this->lineNum = $this->index = 0;
+ }
+
+ function rewind() {
+ if ( fseek( $this->fh, 0 ) ) {
+ throw new MWException( "Couldn't fseek to the start of '$this->file'\n" );
+ }
+
+ $this->index = -1;
+ $this->lineNum = 0;
+ $this->eof = false;
+ $this->next();
+
+ return true;
+ }
+
+ function current() {
+ return $this->test;
+ }
+
+ function key() {
+ return $this->index;
+ }
+
+ function next() {
+ if ( $this->readNextTest() ) {
+ $this->index++;
+ return true;
+ } else {
+ $this->eof = true;
+ }
+ }
+
+ function valid() {
+ return $this->eof != true;
+ }
+
+ function setupCurrentTest() {
+ // "input" and "result" are old section names allowed
+ // for backwards-compatibility.
+ $input = $this->checkSection( array( 'wikitext', 'input' ), false );
+ $result = $this->checkSection( array( 'html/php', 'html/*', 'html', 'result' ), false );
+ // some tests have "with tidy" and "without tidy" variants
+ $tidy = $this->checkSection( array( 'html/php+tidy', 'html+tidy' ), false );
+ if ( $tidy != false ) {
+ if ( $this->nextSubTest == 0 ) {
+ if ( $result != false ) {
+ $this->nextSubTest = 1; // rerun non-tidy variant later
+ }
+ $result = $tidy;
+ } else {
+ $this->nextSubTest = 0; // go on to next test after this
+ $tidy = false;
+ }
+ }
+
+ if ( !isset( $this->sectionData['options'] ) ) {
+ $this->sectionData['options'] = '';
+ }
+
+ if ( !isset( $this->sectionData['config'] ) ) {
+ $this->sectionData['config'] = '';
+ }
+
+ $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) && !$this->parserTest->runDisabled;
+ $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) && $result == 'html' && !$this->parserTest->runParsoid;
+ $isFiltered = !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] );
+ if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) {
+ # disabled test
+ return false;
+ }
+
+ # We are really going to run the test, run pending hooks and hooks function
+ wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" );
+ $hooksResult = $this->delayedParserTest->unleash( $this->parserTest );
+ if ( !$hooksResult ) {
+ # Some hook reported an issue. Abort.
+ throw new MWException( "Problem running hook" );
+ }
+
+ $this->test = array(
+ 'test' => ParserTest::chomp( $this->sectionData['test'] ),
+ 'input' => ParserTest::chomp( $this->sectionData[$input] ),
+ 'result' => ParserTest::chomp( $this->sectionData[$result] ),
+ 'options' => ParserTest::chomp( $this->sectionData['options'] ),
+ 'config' => ParserTest::chomp( $this->sectionData['config'] ),
+ );
+ if ( $tidy != false ) {
+ $this->test['options'] .= " tidy";
+ }
+ return true;
+ }
+
+ function readNextTest() {
+ # Run additional subtests of previous test
+ while ( $this->nextSubTest > 0 ) {
+ if ( $this->setupCurrentTest() ) {
+ return true;
+ }
+ }
+
+ $this->clearSection();
+ # Reset hooks for the delayed test object
+ $this->delayedParserTest->reset();
+
+ while ( false !== ( $line = fgets( $this->fh ) ) ) {
+ $this->lineNum++;
+ $matches = array();
+
+ if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
+ $this->section = strtolower( $matches[1] );
+
+ if ( $this->section == 'endarticle' ) {
+ $this->checkSection( 'text' );
+ $this->checkSection( 'article' );
+
+ $this->parserTest->addArticle(
+ ParserTest::chomp( $this->sectionData['article'] ),
+ $this->sectionData['text'], $this->lineNum );
+
+ $this->clearSection();
+
+ continue;
+ }
+
+ if ( $this->section == 'endhooks' ) {
+ $this->checkSection( 'hooks' );
+
+ foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
+ $line = trim( $line );
+
+ if ( $line ) {
+ $this->delayedParserTest->requireHook( $line );
+ }
+ }
+
+ $this->clearSection();
+
+ continue;
+ }
+
+ if ( $this->section == 'endfunctionhooks' ) {
+ $this->checkSection( 'functionhooks' );
+
+ foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
+ $line = trim( $line );
+
+ if ( $line ) {
+ $this->delayedParserTest->requireFunctionHook( $line );
+ }
+ }
+
+ $this->clearSection();
+
+ continue;
+ }
+
+ if ( $this->section == 'endtransparenthooks' ) {
+ $this->checkSection( 'transparenthooks' );
+
+ foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) {
+ $line = trim( $line );
+
+ if ( $line ) {
+ $delayedParserTest->requireTransparentHook( $line );
+ }
+ }
+
+ $this->clearSection();
+
+ continue;
+ }
+
+ if ( $this->section == 'end' ) {
+ $this->checkSection( 'test' );
+ do {
+ if ( $this->setupCurrentTest() ) {
+ return true;
+ }
+ } while ( $this->nextSubTest > 0 );
+ # go on to next test (since this was disabled)
+ $this->clearSection();
+ $this->delayedParserTest->reset();
+ continue;
+ }
+
+ if ( isset( $this->sectionData[$this->section] ) ) {
+ throw new MWException( "duplicate section '$this->section' "
+ . "at line {$this->lineNum} of $this->file\n" );
+ }
+
+ $this->sectionData[$this->section] = '';
+
+ continue;
+ }
+
+ if ( $this->section ) {
+ $this->sectionData[$this->section] .= $line;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Clear section name and its data
+ */
+ private function clearSection() {
+ $this->sectionData = array();
+ $this->section = null;
+
+ }
+
+ /**
+ * Verify the current section data has some value for the given token
+ * name(s) (first parameter).
+ * Throw an exception if it is not set, referencing current section
+ * and adding the current file name and line number
+ *
+ * @param string|array $tokens Expected token(s) that should have been
+ * mentioned before closing this section
+ * @param bool $fatal True iff an exception should be thrown if
+ * the section is not found.
+ * @return bool|string
+ */
+ private function checkSection( $tokens, $fatal = true ) {
+ if ( is_null( $this->section ) ) {
+ throw new MWException( __METHOD__ . " can not verify a null section!\n" );
+ }
+ if ( !is_array( $tokens ) ) {
+ $tokens = array( $tokens );
+ }
+ if ( count( $tokens ) == 0 ) {
+ throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
+ }
+
+ $data = $this->sectionData;
+ $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
+ return isset( $data[$token] );
+ } );
+
+ if ( count( $tokens ) == 0 ) {
+ if ( !$fatal ) {
+ return false;
+ }
+ throw new MWException( sprintf(
+ "'%s' without '%s' at line %s of %s\n",
+ $this->section,
+ implode( ',', $tokens ),
+ $this->lineNum,
+ $this->file
+ ) );
+ }
+ if ( count( $tokens ) > 1 ) {
+ throw new MWException( sprintf(
+ "'%s' with unexpected tokens '%s' at line %s of %s\n",
+ $this->section,
+ implode( ',', $tokens ),
+ $this->lineNum,
+ $this->file
+ ) );
+ }
+
+ $tokens = array_values( $tokens );
+ return $tokens[0];
+ }
+}
+
+/**
+ * A class to delay execution of a parser test hooks.
+ */
+class DelayedParserTest {
+
+ /** Initialized on construction */
+ private $hooks;
+ private $fnHooks;
+ private $transparentHooks;
+
+ public function __construct() {
+ $this->reset();
+ }
+
+ /**
+ * Init/reset or forgot about the current delayed test.
+ * Call to this will erase any hooks function that were pending.
+ */
+ public function reset() {
+ $this->hooks = array();
+ $this->fnHooks = array();
+ $this->transparentHooks = array();
+ }
+
+ /**
+ * Called whenever we actually want to run the hook.
+ * Should be the case if we found the parserTest is not disabled
+ * @param ParserTest|NewParserTest $parserTest
+ * @return bool
+ */
+ public function unleash( &$parserTest ) {
+ if ( !( $parserTest instanceof ParserTest || $parserTest instanceof NewParserTest ) ) {
+ throw new MWException( __METHOD__ . " must be passed an instance of ParserTest or "
+ . "NewParserTest classes\n" );
+ }
+
+ # Trigger delayed hooks. Any failure will make us abort
+ foreach ( $this->hooks as $hook ) {
+ $ret = $parserTest->requireHook( $hook );
+ if ( !$ret ) {
+ return false;
+ }
+ }
+
+ # Trigger delayed function hooks. Any failure will make us abort
+ foreach ( $this->fnHooks as $fnHook ) {
+ $ret = $parserTest->requireFunctionHook( $fnHook );
+ if ( !$ret ) {
+ return false;
+ }
+ }
+
+ # Trigger delayed transparent hooks. Any failure will make us abort
+ foreach ( $this->transparentHooks as $hook ) {
+ $ret = $parserTest->requireTransparentHook( $hook );
+ if ( !$ret ) {
+ return false;
+ }
+ }
+
+ # Delayed execution was successful.
+ return true;
+ }
+
+ /**
+ * Similar to ParserTest object but does not run anything
+ * Use unleash() to really execute the hook
+ * @param string $hook
+ */
+ public function requireHook( $hook ) {
+ $this->hooks[] = $hook;
+ }
+
+ /**
+ * Similar to ParserTest object but does not run anything
+ * Use unleash() to really execute the hook function
+ * @param string $fnHook
+ */
+ public function requireFunctionHook( $fnHook ) {
+ $this->fnHooks[] = $fnHook;
+ }
+
+ /**
+ * Similar to ParserTest object but does not run anything
+ * Use unleash() to really execute the hook function
+ * @param string $hook
+ */
+ public function requireTransparentHook( $hook ) {
+ $this->transparentHooks[] = $hook;
+ }
+
+}
+
+/**
+ * Initialize and detect the DjVu files support
+ */
+class DjVuSupport {
+
+ /**
+ * Initialises DjVu tools global with default values
+ */
+ public function __construct() {
+ global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt;
+
+ $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu';
+ $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump';
+ $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml';
+ $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt';
+
+ if ( !in_array( 'djvu', $wgFileExtensions ) ) {
+ $wgFileExtensions[] = 'djvu';
+ }
+ }
+
+ /**
+ * Returns true if the DjVu tools are usable
+ *
+ * @return bool
+ */
+ public function isEnabled() {
+ global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgDjvuTxt;
+
+ return is_executable( $wgDjvuRenderer )
+ && is_executable( $wgDjvuDump )
+ && is_executable( $wgDjvuToXML )
+ && is_executable( $wgDjvuTxt );
+ }
+}
+
+/**
+ * Initialize and detect the tidy support
+ */
+class TidySupport {
+ private $internalTidy;
+ private $externalTidy;
+
+ /**
+ * Determine if there is a usable tidy.
+ */
+ public function __construct() {
+ global $wgTidyBin;
+
+ $this->internalTidy = extension_loaded( 'tidy' ) &&
+ class_exists( 'tidy' );
+
+ $this->externalTidy = is_executable( $wgTidyBin ) ||
+ Installer::locateExecutableInDefaultPaths( array( $wgTidyBin ) )
+ !== false;
+ }
+
+ /**
+ * Returns true if we should use internal tidy.
+ *
+ * @return bool
+ */
+ public function isInternal() {
+ return $this->internalTidy;
+ }
+
+ /**
+ * Returns true if tidy is usable
+ *
+ * @return bool
+ */
+ public function isEnabled() {
+ return $this->internalTidy || $this->externalTidy;
+ }
+}