Change notes from older releases. For current info see RELEASE-NOTES-1.34.
+= MediaWiki 1.33 =
+
+=== Upgrading notes for 1.33 ===
+1.33 has several database changes since 1.32, and will not work without schema
+updates. Note that due to changes to some very large tables like the revision
+table, the schema update may take quite long (minutes on a medium sized site,
+many hours on a large site).
+
+Don't forget to always back up your database before upgrading!
+
+See the file UPGRADE for more detailed upgrade instructions, including
+important information when upgrading from versions prior to 1.11.
+
+Some specific notes for MediaWiki 1.33 upgrades are below:
+
+* Some external link searches will not work correctly until update.php (or
+ refreshExternallinksIndex.php) is run. These include searches for links using
+ IP addresses, internationalized domain names, and possibly mailto links.
+* If you ran migrateActors.php using an older version of MediaWiki and want to
+ run your wiki with $wgActorTableSchemaMigrationStage SCHEMA_COMPAT_READ_OLD,
+ note that log_search rows needed to find revision deletions by target user
+ were incorrectly deleted. See T215464 for details.
+* If revision deletions were performed when the wiki was configured with
+ $wgActorTableSchemaMigrationStage SCHEMA_COMPAT_WRITE_BOTH and without
+ migrateActors.php having been run, the log_search table may contain rows with
+ empty values for "target_author_actor" which will prevent log searches for
+ revision deletions by target user from finding those log entries. These rows
+ may be corrected by (re-)running migrateActors.php.
+
+For notes on 1.32.x and older releases, see HISTORY.
+
+== MediaWiki 1.33.0 ==
+
+=== Changes since MediaWiki 1.33.0-rc.0 ===
+* (T225558) Update installer link to PHP intl.
+* (T225901) Only attempt to deduplicate if there is data in archive and revision
+ tables.
+* (T225564) Fetch tag ID before calling undefineTag().
+* (T225496) Detect APC for MainCacheType in CLI installer.
+* Call unpack() with correct parameters in MimeAnalyzer.php for PHP 7.0 support.
+* (T212613) Style change tags correctly on Special:Newpages.
+* (T202211) Fix SQLite patch-(page|template)links-fix-pk.sql column order.
+
+== MediaWiki 1.33.0-rc.0 ==
+
+=== Configuration changes for system administrators in 1.33 ===
+==== New configuration ====
+* $wgEnablePartialBlocks – This enables the Partial Blocks feature, which gives
+ accounts with block permissions the ability to block users, IPs, and IP ranges
+ from editing specific pages, while allowing them to edit the rest of the wiki.
+ It is a temporary setting for gradual enablement, current default to `false`,
+ and will be set to `true` and then removed once initial development completes.
+
+==== Changed configuration ====
+* $wgChangeTagsSchemaMigrationStage (T193868) — This temporary setting, added in
+ MediaWiki 1.32, now defaults to MIGRATION_NEW instead of MIGRATION_WRITE_BOTH.
+* $wgPasswordPolicy – There is a new password policy to check that the account's
+ password is not in the large blacklist. This is enabled by default for the
+ built-in user groups bureaucrat, sysop, interface-admin, and bot. To configure
+ this for other user groups, set the `PasswordNotInLargeBlacklist` flag `true`.
+* $wgPasswordDefault – There is a new password type configuration using Argon2
+ password hashing (which requires PHP 7.2 and above). It's designed to resist
+ timing attacks, and (on systems with PHP 7.3+) GPU hacking; if you configure
+ argon2 to be used, by default, it will automatically choose the best available
+ algorithm depending on which version of PHP you have available. To use this,
+ you can set `$wgPasswordDefault = 'argon2';`.
+* $wgActorTableSchemaMigrationStage now defaults to reading the new schema.
+ update.php will back-populate the new database fields due to the changed
+ setting, which may take some time on large wikis. You can avoid downtime by
+ following a process like that described in T188327.
+
+==== Removed configuration ====
+* $wgTagStatisticsNewTable (T199334) — This temporary setting, added in
+ MediaWiki 1.32, has now been removed. When loading Special:Tags, MediaWiki
+ will now always use the `change_tag_def` instead of the `change_tag` table.
+* $wgUseTidy, $wgTidyBin, $wgTidyConf, $wgTidyOpts, $wgTidyInternal, and
+ $wgDebugTidy – These options, all deprecated since 1.26, have now all been
+ removed, as MediaWiki now always tidies user output. The $wgTidyConfig setting
+ remains only for experimental features and debugging, and should not be used.
+* $wgEnableParserCache – This setting has been deprecated since 1.26, has now
+ been removed. If you still desire to disable the parser cache, instead you can
+ set `$wgParserCacheType = CACHE_NONE;`.
+* $wgCommentTableSchemaMigrationStage – This temporary migration setting has now
+ been removed. Code finding it unset should treat it as being MIGRATION_NEW.
+* $wgAuth – This old setting, deprecated in 1.27, has been removed as part of
+ the removal of AuthPlugin.
+* $wgSitesCacheFile – This configuration was introduced in 1.25 with the intent
+ to allow sites to configure a file in which to cache the SiteStore database
+ table, but it was never used. SiteStore already caches its information by
+ default using BagOStuff (e.g. Memcached or APC).
+* $wgClockSkewFudge – This setting was used by User.php to let sites adjust by
+ how much MediaWiki would fudge when trying to minimize the chances of a
+ user.user_touched database update to the "current" timestamp being before the
+ value already there (e.g. due to clock skew between different servers). This
+ is no longer a problem, because the code now ensures the timestamp is always
+ higher than the previous one. The writes are guarded with CAS logic (check
+ and set), which prevents updates that would overlap.
+* $wgDBmysql5 (T196185) - This experimental setting, deprecated in 1.31, has
+ been removed.
+
+=== New user-facing features in 1.33 ===
+* (T96041) __EXPECTUNUSEDCATEGORY__ on a category page causes the category
+ to be hidden on Special:UnusedCategories.
+* (T210814) SVGs are now by default displayed in wiki language on image
+ pages.
+* Special:CreateAccount now warns the user if their chosen username has to be
+ normalized.
+* (T205040) Multilingual images are now be displayed in the current parse
+ language where available.
+* Special:ActiveUsers will no longer filter out users who became inactive since
+ the last time the active users query cache was updated.
+* (T215675) RecentChange and ManualLogEntry implement new Taggable interface.
+* (T215675) Added a hook, ManualLogEntryBeforePublish, to allow extensions
+ to modify (example: add tags) log entries.
+
+=== New developer features in 1.33 ===
+* The AuthManagerLoginAuthenticateAudit hook has a new parameter for
+ additional information about the authentication event.
+* TextContent::getText() was introduced as a replacement for
+ Content::getNativeData() for text-based content models.
+* (T214706) LinksUpdate::getAddedExternalLinks() and
+ LinksUpdate::getRemovedExternalLinks() were introduced.
+* (T213893) Added 'MaintenanceUpdateAddParams' hook
+* (T219655) The MarkPatrolled hook has a new parameter for the tags
+ associated with this entry in the patrol log.
+* (T212472) Extensions can now specify platform abilities they require to work,
+ limited to shell access for now.
+
+
+=== External library changes in 1.33 ===
+==== New external libraries ====
+* Added wikimedia/password-blacklist 0.1.4.
+* Added guzzlehttp/guzzle 6.3.3.
+
+==== Changed external libraries ====
+* Updated OOUI from v0.29.2 to v0.31.3.
+* Updated OOjs Router from pre-release to v0.2.0.
+* Updated moment from v2.19.3 to v2.24.0.
+* Updated wikimedia/xmp-reader from 0.6.0 to 0.6.2.
+* Updated wikimedia/scoped-callback from 2.0.0 to 3.0.0.
+* Updated jquery-client from 2.0.1 to 2.0.2.
+* Updated pear/net_smtp from 1.8.0 to 1.8.1.
+* Updated cssjanus/cssjanus from 1.2.0 to 1.3.0.
+* Updated wikimedia/php-session-serializer from 1.0.6 to 1.0.7.
+
+==== Removed external libraries ====
+* (T219403) jquery.ui.spinner, deprecated since 1.31, was removed.
+
+
+=== Developer library changes in 1.33 ===
+==== New developer libraries ====
+* Added jakub-onderka/php-console-highlighter 0.3.2 explicitly (dev-only).
+* Added mediawiki/mediawiki-phan-config 0.5.0 (dev-only).
+
+==== Changed developer libraries ====
+* Updated wikimedia/ip-set from 1.3.0 to 2.0.1.
+ * The deprecated IPSet\IPSet alias was removed, Wikimedia\IPSet must be
+ used instead.
+* Updated psy/psysh from 0.9.6 to 0.9.9 (dev-only).
+* Updated nikic/php-parser from 3.1.3 to 3.1.5 (dev-only).
+* Updated mediawiki/mediawiki-codesniffer from 22.0.0 to 25.0.0 (dev-only).
+* Updated qunitjs from 2.6.2 to 2.9.1.
+
+==== Removed developer libraries ====
+* The jetbrains/phpstorm-stubs repository was removed in favour of the minimal
+ stubs we need, which are kept in the new `.phan/internal_stubs` directory
+ (dev-only).
+
+
+=== Bug fixes in 1.33 ===
+* (T164211) Special:UserRights could sometimes fail with a
+ "conflict detected" error when there weren't any conflicts.
+* (T216029) Chrome redirects to Special:BadTitle after editing a section with
+ a non-Latin name on a page with non-Latin characters in title.
+* (T222385) resourceloader: Use AND instead of OR for upsert conds in
+ saveFileDependencies().
+
+=== Action API changes in 1.33 ===
+* (T198913) Added 'ApiOptions' hook.
+* The JSON formatversion=2 is no longer experimental.
+* Internal API errors (those with code beginning "internal_api_error") will
+ include the exception class name in a data field named "errorclass".
+ * Class names are not guaranteed to remain stable, and in particular database
+ exceptions will now include the "Wikimedia\Rdbms\" prefix in the class name.
+ * The code including an exception class name is deprecated. In the future,
+ all internal errors will use code "internal_api_error".
+* (T212356) When using action=delete on pages with many revisions, the module
+ may return a boolean-true 'scheduled' and no 'logid'. This signifies that the
+ deletion will be processed via the job queue.
+* action=setnotificationtimestamp will now update the watchlist asynchronously
+ if entirewatchlist is set, so updates may not be visible immediately
+* Block info will be added to "blocked" errors from more modules.
+* (T216245) Autoblocks will now be spread by action=edit and action=move.
+* action=query&meta=userinfo has a new uiprop, 'latestcontrib', that returns
+ the date of user's latest contribution.
+* (T25227) action=logout now requires to be posted and have a csrf token.
+
+=== Action API internal changes in 1.33 ===
+* A number of deprecated methods for API documentation, intended for overriding
+ by extensions, are no longer called by MediaWiki, and will emit deprecation
+ notices if your extension attempts to use them:
+ * ApiBase::getDescription() (deprecated in 1.25)
+ * ApiBase::getParamDescription() (deprecated in 1.25)
+ * ApiBase::getExamples() (deprecated in 1.25)
+ * ApiBase::getDescriptionMessage() (deprecated in 1.30)
+ Additionally, the 'APIGetDescription' and 'APIGetParamDescription' hooks have
+ been removed, as their only use was to let extensions override values returned
+ by getDescription() and getParamDescription(), respectively.
+* API error codes may only contain ASCII letters, numbers, underscore, and
+ hyphen. Methods such as ApiBase::dieWithError() and
+ ApiMessageTrait::setApiCode() will throw an InvalidArgumentException if
+ passed a bad code.
+* ApiBase::checkTitleUserPermissions() now takes an options array as its third
+ parameter. Passing a User object or null is deprecated.
+* The api-feature-usage log channel now has log context. The text message is
+ deprecated and will be removed in the future.
+
+=== Languages updated in 1.33 ===
+MediaWiki supports over 350 languages. Many localisations are updated regularly.
+Below only new and removed languages are listed, as well as changes to languages
+because of Phabricator reports.
+
+* (T203908) Added language support for Eastern Pwo (kjp).
+* (T213717) Fixed a translation error on Goan Konkani (gom-deva) translations
+ for NS_TEMPLATE.
+* (T212221) Added $digitTransformTable for Santali (sat).
+* (T216479) Added language support for Saisiyat (xsy).
+* (T219728) Added support for new Japanese era name "Reiwa"
+
+=== Breaking changes in 1.33 ===
+* The parameteter $lang in DifferenceEngine::setTextLanguage must be of type
+ Language. Other types are deprecated since 1.32.
+* Skin::doEditSectionLink requires type Language for the parameter $lang.
+ The parameters $tooltip and $lang are mandatory. Omitting the parameters is
+ deprecated since 1.32.
+* Language::truncate(), deprecated in 1.31, has been removed.
+* UtfNormal, deprecated in 1.25, was removed. Use UtfNormal\Validator directly
+ instead.
+* (T197179) In OOUI HTMLForm fields, the parameters 'notice', 'notice-messages',
+ and 'notice-message', which were deprecated in 1.32, were removed. Instead,
+ use 'help', 'help-message', and 'help-messages'.
+* (T197179) HTMLFormField::getNotices(), deprecated in 1.32, was removed.
+* The "Parsoid v1" compatibility mappings in ParsoidVirtualRESTService and
+ RestbaseVirtualRESTService, deprecated since 1.26, have been removed.
+ Use the RESTBase v1 or Parsoid v3 API instead.
+* ParserOptions defaults 'tidy' to true now, since the untidy modes of the
+ parser are being deprecated and ParserOptions::getCanonicalOverrides()
+ has always been true at any rate.
+* Support for disabling tidy and external tidy implementations has been removed.
+ This was deprecated in 1.32. The pure PHP Remex tidy implementation is now
+ used and no configuration is necessary.
+* A number of deprecated methods for API documentation, intended for overriding
+ by extensions, are no longer called by MediaWiki, and will emit deprecation
+ notices if your extension attempts to use them:
+ * ApiBase::getDescription() (deprecated in 1.25)
+ * ApiBase::getParamDescription() (deprecated in 1.25)
+ * ApiBase::getExamples() (deprecated in 1.25)
+ * ApiBase::getDescriptionMessage() (deprecated in 1.30)
+ Additionally, the 'APIGetDescription' and 'APIGetParamDescription' hooks have
+ been removed, as their only use was to let extensions override values returned
+ by getDescription() and getParamDescription(), respectively.
+* The authentication hooks 'AbortAutoAccount' 'AbortNewAccount', 'AbortLogin',
+ 'LoginUserMigrated', 'UserCreateForm', and 'UserLoginForm', all deprecated by
+ the creation of AuthManager in 1.27, have been removed. This also means that
+ the FakeAuthTemplate and LoginForm classes are removed, that FakeAuthTemplate
+ is no longer passed into LoginSignupSpecialPage->getFieldDefinitions(), and
+ that LoginSignupSpecialPage->getBCFieldDefinitions() is removed.
+* The 'jquery.localize' module, deprecated in 1.32, has been removed. Instead,
+ use 'jquery.i18n'.
+* The hooks LanguageGetSpecialPageAliases and LanguageGetMagic, deprecated since
+ 1.16, have now been removed. Instead, use $specialPageAliases or $magicWords
+ respectively in a $wgExtensionMessagesFiles file.
+* The following methods of the Preferences class, deprecated in 1.31, have been
+ removed:
+ * getSaveBlacklist()
+ * loadPreferenceValues()
+ * getOptionFromUser()
+ * profilePreferences()
+ * skinPreferences()
+ * filesPreferences()
+ * datetimePreferences()
+ * renderingPreferences()
+ * editingPreferences()
+ * rcPreferences()
+ * watchlistPreferences()
+ * searchPreferences()
+ * miscPreferences()
+ * generateSkinOptions()
+ * getDateOptions()
+ * getImageSizes()
+ * getThumbSizes()
+ * validateSignature()
+ * cleanSignature()
+ * getTimezoneOptions()
+ * filterIntval()
+ * filterTimezoneInput()
+ * getTimeZoneList()
+* mw.util.jsMessage(), deprecated in 1.20, was removed. Use mw.notify instead.
+* (T61113) User::EDIT_TOKEN_SUFFIX was removed. It was deprecated since 1.27.
+* The 'mediawiki.api' module aliases, deprecated in 1.32, have been removed.
+ Specifically: mediawiki.api.category, mediawiki.api.edit,
+ mediawiki.api.login, mediawiki.api.options, mediawiki.api.parse,
+ mediawiki.api.upload, mediawiki.api.user, mediawiki.api.watch,
+ mediawiki.api.messages, and mediawiki.api.rollback.
+* The 'jquery.byteLimit' module alias for 'jquery.lengthLimit',
+ deprecated in 1.31, was removed.
+* Revision::fetchRevision(), deprecated in 1.28, was removed.
+* Class SquidUpdate, deprecated in 1.27, was removed.
+* Title->getSquidURLs(), deprecated in 1.27, was removed. Instead, use
+ Title->getCdnUrls().
+* Title::escapeFragmentForURL(), deprecated in 1.30, was removed. Use
+ Sanitizer::escapeIdForLink() or escapeIdForExternalInterwiki() instead.
+* Title->canTalk(), deprecated in 1.30, was removed. Instead, use
+ Title->canHaveTalkPage().
+* Title's methods for site and user page related to CSS and JS, deprecated in
+ 1.31, were removed:
+ * Title->isCssOrJsPage() — Use Title->isSiteConfigPage()
+ * Title->isCssJsSubpage() – Use Title->isUserConfigPage()
+ * Title->getSkinFromCssJsSubpage() – Use Title->getSkinFromConfigSubpage()
+ * Title->isCssSubpage() – Use Title->isUserCssConfigPage()
+ * Title->isJsSubpage() – Use Title->isUserJsConfigPage()
+* SiteSQLStore, deprecated in 1.27 and whose only method, ::newInstance(),
+ would return the global SiteStore instance, has been removed. You can get to
+ this via MediaWiki\MediaWikiServices::getInstance()->getSiteStore() directly.
+* Linker::formatSize, deprecated in 1.28, has been removed (with DummyLinker's).
+ Instead, use Language->formatSize() with the relevant Language object.
+* Linker::formatTemplates, deprecated in 1.28, has been removed (along with the
+ version in DummyLinker). You can use TemplatesOnThisPageFormatter directly.
+* EventRelayerGroup::singleton(), deprecated in 1.27, has been removed. You can
+ use MediaWikiServices::getInstance()->getEventRelayerGroup() directly.
+* LinkCache->addLink(), deprecated in 1.27, has been removed. It is thought to
+ be unused, and is distinct from OutputPage->addLink(), which remains.
+* JsonContent->getJsonData(), deprecated in 1.25, has been removed. Instead, use
+ JsonContent->getData().
+* MWExceptionHandler::getLogId(), deprecated in 1.27, has been removed, as the
+ exception ID is the same as the request ID, from WebRequest::getRequestId().
+* SearchEngine::getNearMatchResultSet(), deprecated in 1.27, has been removed.
+ You can use SearchEngine::getNearMatcher() instead.
+* EmailNotification::updateWatchlistTimestamp, deprecated in 1.27, has been
+ removed. Instead, use WatchedItemStore::updateNotificationTimestamp directly.
+* User::getGroupName() and ::getGroupMember(), both deprecated in 1.29, have
+ been removed. Instead, please use UserGroupMembership::getGroupName() and
+ UserGroupMembership::getGroupMemberName().
+* Backwards compatibility for setting wgSessionsInObjectCache to false or using
+ wgSessionHandler, both of which were deprecated in 1.27 with the introduction
+ of SessionManager, has been removed.
+* SessionManager::autoCreateUser, deprecated in 1.27, has been removed. Use
+ MediaWiki\Auth\AuthManager::autoCreateUser instead.
+* The mw.libs.jpegmeta property, deprecated in 1.31, was removed.
+ Use require( 'mediawiki.libs.jpegmeta' ) instead.
+* The mw.user.stickyRandomId() method, deprecated in 1.32, was removed.
+ Use mw.user.getPageviewToken() instead.
+* Removed deprecated class property WikiRevision::$importer.
+* ResourceLoaderFileModule::readStyleFiles() now requires its $context
+ parameter.
+* The ChangeList::insertArticleLink() method, that was deprecated in 1.27, has
+ been removed.
+* MessageBlobStore::__construct() now requires its $rl parameter.
+* Second parameter to Sanitizer::escapeIdReferenceList() (deprecated in 1.31)
+ has been removed.
+* The 'jquery.xmldom' module has been removed.
+* The 'jquery.mockjax' module has been removed.
+* The 'jquery.hidpi' module, deprecated in 1.32, has been removed.
+* AuthPlugin and related code, deprecated in 1.27, has been removed. Extensions
+ should instead use AuthManager. The following no longer exist:
+ * The AuthPlugin class itself and the related AuthPluginUser class and i18n
+ * The AuthPluginSetup and AuthPluginAutoCreate hooks
+ * The transitional wrapper classes AuthPluginPrimaryAuthenticationProvider,
+ AuthManagerAuthPlugin, and AuthManagerAuthPluginUser.
+ * The $wgAuth configuration setting and its use in Setup.php and unit tests
+* (T217772) The 'wgAvailableSkins' mw.config key in JavaScript, was removed.
+* Language::markNoConversion, deprecated in 1.32, has been removed. Use
+ LanguageConverter::markNoConversion instead.
+* BagOStuff::modifySimpleRelayEvent() method has been removed.
+* ParserOutput::getLegacyOptions, deprecated in 1.30, has been removed.
+ Use ParserOutput::allCacheVaryingOptions instead.
+* CdnCacheUpdate::newSimplePurge, deprecated in 1.27, has been removed.
+ Use CdnCacheUpdate::newFromTitles() instead.
+* Handling of multiple arguments by the Block constructor, deprecated in 1.26,
+ has been removed.
+* The translation of main page in Sardinian (sc) was changed from "Pàgina Base"
+ to "Pàgina printzipale". Existing wikis using this content language need to
+ move the main page or change the name through MediaWiki:Mainpage page.
+* wfSplitWikiID(), deprecated in 1.32, has been removed.
+* MessageBlobStore::getBlob(), deprecated in 1.27, has been removed.
+ Use ::getBlobs() instead.
+* The .background-size() LESS mixin, deprecated in 1.27, has been removed.
+* ReadOnlyMode::clearCache() and ConfiguredReadOnlyMode::clearCache() have been
+ removed. Use MediaWikiTestCase::overrideMwServices() instead.
+
+=== Deprecations in 1.33 ===
+* The configuration option $wgUseESI has been deprecated, and is expected
+ to be removed in a future release.
+* The configuration option $wgSquidPurgeUseHostHeader has been deprecated,
+ and is expected to be removed in a future release.
+* The configuration options $wgFixArabicUnicode and $wgFixMalayalamUnicode,
+ introduced in MW 1.17, have been deprecated. These fixes will always be
+ applied for Arabic and Malayalam in the future. Please enable these on
+ your local wiki (if you have them explicitly set to false) and run
+ maintenance/cleanupTitles.php to fix any existing page titles.
+* The LegacyHookPreAuthenticationProvider class, deprecated since its creation
+ in 1.27 as part of the AuthManager re-write, now emits deprecation warnings.
+ This will help identify the issue if you added it to $wgAuthManagerConfig.
+* wfSplitWikiId() is now deprecated. Cache key generation should have the wiki
+ domain ID as a key component and use makeGlobalKey().
+* (T202094) Title::getUserCaseDBKey() is deprecated; instead, please use
+ Title::getDBKey(), which doesn't vary case.
+* User::getPasswordValidity() is now deprecated. User::checkPasswordValidity()
+ returns the same information in a more useful format.
+* For Linker::generateTOC() and Linker::tocList(), passing strings or booleans
+ as the $lang parameter was deprecated. The same applies to DummyLinker.
+* The PasswordPolicy 'PasswordCannotBePopular' has been deprecated. To
+ follow best practices, it is reccommended to use 'PasswordNotInLargeBlacklist'
+ instead which blacklists 100,000 commonly used passwords.
+* (T208862) Action::requiresUnblock() is now called from
+ Title::getUserPermissionsErrors() and Title::userCan(). Previously, the method
+ was only called in Action::checkCanExecute(). Actions should ensure that their
+ requiresUnblock() returns the proper result (the default is `true`).
+* (T211608) The MediaWiki\Services namespace has been renamed to
+ Wikimedia\Services. The old name is still supported, but deprecated.
+* (T155582) Content::getNativeData has been deprecated. Please use model-
+ specific getters, such as TextContent::getText().
+* The class WebInstallerOutput is now marked as @private.
+* (T209699) The jquery.async module has been deprecated. JavaScript code that
+ needs asynchronous behaviour should use Promises.
+* Password::equals() is deprecated, use verify().
+* BaseTemplate::msgWiki() and QuickTemplate::msgWiki() will be removed. Use
+ other means to fetch a properly escaped message string or Message object.
+* (T126091) The 'ResourceLoaderTestModules' hook, which lets you declare QUnit
+ testing code for your JavaScript modules, is deprecated. Instead, you can now
+ use the new extension registration key 'QUnitTestModule'.
+* (T213426) The jquery.throttle-debounce module has been deprecated. JavaScript
+ code that needs this behaviour should use OO.ui.debounce/throttle.
+* The mw.language.specialCharacters property from the
+ 'mediawiki.language.specialCharacters' module has been deprecated.
+ Use require( 'mediawiki.language.specialCharacters' ) instead.
+* ChangeTags::purgeTagUsageCache() has been deprecated, and is expected to be
+ removed in a future release.
+* Passing a User object or null as the third parameter to
+ ApiBase::checkTitleUserPermissions() has been deprecated. Pass an array
+ [ 'user' => $user ] instead.
+* (T211578) Block::prevents is deprecated. Use Block::isEmailBlocked,
+ Block::isCreateAccountBlocked and Block::isUsertalkEditAllowed to get and set
+ block properties; use Block::appliesToRight and Block::appliesToUsertalk to
+ check block behaviour.
+* The api-feature-usage log channel now has log context. The text message is
+ deprecated and will be removed in the future.
+* The FileBasedSiteLookup class has been deprecated. For a cacheable SiteLookup
+ implementation, use CachingSiteStore instead.
+* Language::viewPrevNext function is deprecated, use
+ SpecialPage::buildPrevNextNavigation instead
+* ManualLogEntry::setTags() is deprecated, use ManualLogEntry::addTags()
+ instead. The setTags() method was overriding the tags, addTags() doesn't
+ override, only adds new tags.
+* Block::isValid is deprecated, since it is no longer needed in core.
+* Calling Maintenance::hasArg() as well as Maintenance::getArg() with no
+ parameter has been deprecated. Please pass the argument number 0.
+* ResourceLoaderContext::expandModuleNames has been deprecated.
+ Use ResourceLoader::expandModuleNames instead.
+
+=== Other changes in 1.33 ===
+* (T201747) Html::openElement() warns if given an element name with a space
+ in it.
+* The implementation of buildStringCast() in Wikimedia\Rdbms\Database has
+ changed to explicitly cast. Subclasses relying on the base-class
+ implementation should check whether they need to override it now.
+* BagOStuff::add is now abstract and must explicitly be defined in subclasses.
+* LinksDeletionUpdate is now a subclass of LinksUpdate. As a consequence,
+ the following hooks will now be triggered upon page deletion in addition
+ to page updates: LinksUpdateConstructed, LinksUpdate, LinksUpdateComplete.
+ LinksUpdateAfterInsert is not triggered since deletions do not cause
+ insertions into links tables.
+* Category::newFromID( $id )->getID() will now return $id without any
+ validation, to avoid a mostly unnecessary DB query.
+* On Special:Version, the name for an extension can no longer be arbitrary
+ html when no link is specified.
+
+
= MediaWiki 1.32 =
+== MediaWiki 1.32.3 ==
+
+This is a maintenance release of the MediaWiki 1.32 branch.
+
+=== Changes since MediaWiki 1.32.2 ===
+* (T225558) Update installer link to PHP intl.
+* (T225496) Detect APC for MainCacheType in CLI installer.
+* (T226766) Remove jetbrains/phpstorm-stubs from composer dev dependancies.
+* (T202211) Fix SQLite patch-(image|page|template)links-fix-pk.sql column order.
+
== MediaWiki 1.32.2 ==
This is a security and maintenance release of the MediaWiki 1.32 branch.
= MediaWiki 1.31 =
+== MediaWiki 1.31.3 ==
+
+This is a maintenance release of the MediaWiki 1.31 branch.
+
+=== Changes since MediaWiki 1.31.2 ===
+* (T225558) Update installer link to PHP intl.
+* (T225496) Detect APC for MainCacheType in CLI installer.
+* (T226766) Remove jetbrains/phpstorm-stubs from composer dev dependancies.
+* (T202211) Fix SQLite patch-(image|page|template)links-fix-pk.sql column order.
+
== MediaWiki 1.31.2 ==
This is a security and maintenance release of the MediaWiki 1.31 branch.
'MssqlInstaller' => __DIR__ . '/includes/installer/MssqlInstaller.php',
'MssqlUpdater' => __DIR__ . '/includes/installer/MssqlUpdater.php',
'MultiConfig' => __DIR__ . '/includes/config/MultiConfig.php',
- 'MultiHttpClient' => __DIR__ . '/includes/libs/MultiHttpClient.php',
+ 'MultiHttpClient' => __DIR__ . '/includes/libs/http/MultiHttpClient.php',
'MultiWriteBagOStuff' => __DIR__ . '/includes/libs/objectcache/MultiWriteBagOStuff.php',
'MutableConfig' => __DIR__ . '/includes/config/MutableConfig.php',
'MutableContext' => __DIR__ . '/includes/context/MutableContext.php',
$fld_token = isset( $prop['token'] );
$fld_tags = isset( $prop['tags'] );
- if ( isset( $prop['token'] ) ) {
- $p = $this->getModulePrefix();
- }
-
// If we're in a mode that breaks the same-origin policy, no tokens can
// be obtained
if ( $this->lacksSameOriginSecurity() ) {
<?php
use Psr\Log\LoggerInterface;
-use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\ILoadBalancer;
/**
* @since 1.31
private $doUpdates;
/**
- * @var LoadBalancer
+ * @var ILoadBalancer
*/
private $loadBalancer;
/**
* @param bool $doUpdates
* @param LoggerInterface $logger
- * @param LoadBalancer $loadBalancer
+ * @param ILoadBalancer $loadBalancer
*/
public function __construct(
$doUpdates,
LoggerInterface $logger,
- LoadBalancer $loadBalancer
+ ILoadBalancer $loadBalancer
) {
$this->doUpdates = $doUpdates;
$this->logger = $logger;
+++ /dev/null
-<?php
-/**
- * HTTP service client
- *
- * 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
- */
-
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use MediaWiki\MediaWikiServices;
-
-/**
- * Class to handle multiple HTTP requests
- *
- * If curl is available, requests will be made concurrently.
- * Otherwise, they will be made serially.
- *
- * HTTP request maps are arrays that use the following format:
- * - method : GET/HEAD/PUT/POST/DELETE
- * - url : HTTP/HTTPS URL
- * - query : <query parameter field/value associative array> (uses RFC 3986)
- * - headers : <header name/value associative array>
- * - body : source to get the HTTP request body from;
- * this can simply be a string (always), a resource for
- * PUT requests, and a field/value array for POST request;
- * array bodies are encoded as multipart/form-data and strings
- * use application/x-www-form-urlencoded (headers sent automatically)
- * - stream : resource to stream the HTTP response body to
- * - proxy : HTTP proxy to use
- * - flags : map of boolean flags which supports:
- * - relayResponseHeaders : write out header via header()
- * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'.
- *
- * @since 1.23
- */
-class MultiHttpClient implements LoggerAwareInterface {
- /** @var resource */
- protected $multiHandle = null; // curl_multi handle
- /** @var string|null SSL certificates path */
- protected $caBundlePath;
- /** @var float */
- protected $connTimeout = 10;
- /** @var float */
- protected $reqTimeout = 300;
- /** @var bool */
- protected $usePipelining = false;
- /** @var int */
- protected $maxConnsPerHost = 50;
- /** @var string|null proxy */
- protected $proxy;
- /** @var string */
- protected $userAgent = 'wikimedia/multi-http-client v1.0';
- /** @var LoggerInterface */
- protected $logger;
-
- // In PHP 7 due to https://bugs.php.net/bug.php?id=76480 the request/connect
- // timeouts are periodically polled instead of being accurately respected.
- // The select timeout is set to the minimum timeout multiplied by this factor.
- const TIMEOUT_ACCURACY_FACTOR = 0.1;
-
- /**
- * @param array $options
- * - connTimeout : default connection timeout (seconds)
- * - reqTimeout : default request timeout (seconds)
- * - proxy : HTTP proxy to use
- * - usePipelining : whether to use HTTP pipelining if possible (for all hosts)
- * - maxConnsPerHost : maximum number of concurrent connections (per host)
- * - userAgent : The User-Agent header value to send
- * - logger : a \Psr\Log\LoggerInterface instance for debug logging
- * - caBundlePath : path to specific Certificate Authority bundle (if any)
- * @throws Exception
- */
- public function __construct( array $options ) {
- if ( isset( $options['caBundlePath'] ) ) {
- $this->caBundlePath = $options['caBundlePath'];
- if ( !file_exists( $this->caBundlePath ) ) {
- throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath );
- }
- }
- static $opts = [
- 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost',
- 'proxy', 'userAgent', 'logger'
- ];
- foreach ( $opts as $key ) {
- if ( isset( $options[$key] ) ) {
- $this->$key = $options[$key];
- }
- }
- if ( $this->logger === null ) {
- $this->logger = new NullLogger;
- }
- }
-
- /**
- * Execute an HTTP(S) request
- *
- * This method returns a response map of:
- * - code : HTTP response code or 0 if there was a serious error
- * - reason : HTTP response reason (empty if there was a serious error)
- * - headers : <header name/value associative array>
- * - body : HTTP response body or resource (if "stream" was set)
- * - error : Any error string
- * The map also stores integer-indexed copies of these values. This lets callers do:
- * @code
- * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $http->run( $req );
- * @endcode
- * @param array $req HTTP request array
- * @param array $opts
- * - connTimeout : connection timeout per request (seconds)
- * - reqTimeout : post-connection timeout per request (seconds)
- * @return array Response array for request
- */
- public function run( array $req, array $opts = [] ) {
- return $this->runMulti( [ $req ], $opts )[0]['response'];
- }
-
- /**
- * Execute a set of HTTP(S) requests.
- *
- * If curl is available, requests will be made concurrently.
- * Otherwise, they will be made serially.
- *
- * The maps are returned by this method with the 'response' field set to a map of:
- * - code : HTTP response code or 0 if there was a serious error
- * - reason : HTTP response reason (empty if there was a serious error)
- * - headers : <header name/value associative array>
- * - body : HTTP response body or resource (if "stream" was set)
- * - error : Any error string
- * The map also stores integer-indexed copies of these values. This lets callers do:
- * @code
- * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req['response'];
- * @endcode
- * All headers in the 'headers' field are normalized to use lower case names.
- * This is true for the request headers and the response headers. Integer-indexed
- * method/URL entries will also be changed to use the corresponding string keys.
- *
- * @param array $reqs Map of HTTP request arrays
- * @param array $opts
- * - connTimeout : connection timeout per request (seconds)
- * - reqTimeout : post-connection timeout per request (seconds)
- * - usePipelining : whether to use HTTP pipelining if possible
- * - maxConnsPerHost : maximum number of concurrent connections (per host)
- * @return array $reqs With response array populated for each
- * @throws Exception
- */
- public function runMulti( array $reqs, array $opts = [] ) {
- $this->normalizeRequests( $reqs );
- if ( $this->isCurlEnabled() ) {
- return $this->runMultiCurl( $reqs, $opts );
- } else {
- return $this->runMultiHttp( $reqs, $opts );
- }
- }
-
- /**
- * Determines if the curl extension is available
- *
- * @return bool true if curl is available, false otherwise.
- */
- protected function isCurlEnabled() {
- return extension_loaded( 'curl' );
- }
-
- /**
- * Execute a set of HTTP(S) requests concurrently
- *
- * @see MultiHttpClient::runMulti()
- *
- * @param array $reqs Map of HTTP request arrays
- * @param array $opts
- * - connTimeout : connection timeout per request (seconds)
- * - reqTimeout : post-connection timeout per request (seconds)
- * - usePipelining : whether to use HTTP pipelining if possible
- * - maxConnsPerHost : maximum number of concurrent connections (per host)
- * @return array $reqs With response array populated for each
- * @throws Exception
- */
- private function runMultiCurl( array $reqs, array $opts = [] ) {
- $chm = $this->getCurlMulti();
-
- $selectTimeout = $this->getSelectTimeout( $opts );
-
- // Add all of the required cURL handles...
- $handles = [];
- foreach ( $reqs as $index => &$req ) {
- $handles[$index] = $this->getCurlHandle( $req, $opts );
- if ( count( $reqs ) > 1 ) {
- // https://github.com/guzzle/guzzle/issues/349
- curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true );
- }
- }
- unset( $req ); // don't assign over this by accident
-
- $indexes = array_keys( $reqs );
- if ( isset( $opts['usePipelining'] ) ) {
- curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] );
- }
- if ( isset( $opts['maxConnsPerHost'] ) ) {
- // Keep these sockets around as they may be needed later in the request
- curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] );
- }
-
- // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS)
- $batches = array_chunk( $indexes, $this->maxConnsPerHost );
- $infos = [];
-
- foreach ( $batches as $batch ) {
- // Attach all cURL handles for this batch
- foreach ( $batch as $index ) {
- curl_multi_add_handle( $chm, $handles[$index] );
- }
- // Execute the cURL handles concurrently...
- $active = null; // handles still being processed
- do {
- // Do any available work...
- do {
- $mrc = curl_multi_exec( $chm, $active );
- $info = curl_multi_info_read( $chm );
- if ( $info !== false ) {
- $infos[(int)$info['handle']] = $info;
- }
- } while ( $mrc == CURLM_CALL_MULTI_PERFORM );
- // Wait (if possible) for available work...
- if ( $active > 0 && $mrc == CURLM_OK && curl_multi_select( $chm, $selectTimeout ) == -1 ) {
- // PHP bug 63411; https://curl.haxx.se/libcurl/c/curl_multi_fdset.html
- usleep( 5000 ); // 5ms
- }
- } while ( $active > 0 && $mrc == CURLM_OK );
- }
-
- // Remove all of the added cURL handles and check for errors...
- foreach ( $reqs as $index => &$req ) {
- $ch = $handles[$index];
- curl_multi_remove_handle( $chm, $ch );
-
- if ( isset( $infos[(int)$ch] ) ) {
- $info = $infos[(int)$ch];
- $errno = $info['result'];
- if ( $errno !== 0 ) {
- $req['response']['error'] = "(curl error: $errno)";
- if ( function_exists( 'curl_strerror' ) ) {
- $req['response']['error'] .= " " . curl_strerror( $errno );
- }
- $this->logger->warning( "Error fetching URL \"{$req['url']}\": " .
- $req['response']['error'] );
- }
- } else {
- $req['response']['error'] = "(curl error: no status set)";
- }
-
- // For convenience with the list() operator
- $req['response'][0] = $req['response']['code'];
- $req['response'][1] = $req['response']['reason'];
- $req['response'][2] = $req['response']['headers'];
- $req['response'][3] = $req['response']['body'];
- $req['response'][4] = $req['response']['error'];
- curl_close( $ch );
- // Close any string wrapper file handles
- if ( isset( $req['_closeHandle'] ) ) {
- fclose( $req['_closeHandle'] );
- unset( $req['_closeHandle'] );
- }
- }
- unset( $req ); // don't assign over this by accident
-
- // Restore the default settings
- curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining );
- curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
-
- return $reqs;
- }
-
- /**
- * @param array &$req HTTP request map
- * @param array $opts
- * - connTimeout : default connection timeout
- * - reqTimeout : default request timeout
- * @return resource
- * @throws Exception
- */
- protected function getCurlHandle( array &$req, array $opts = [] ) {
- $ch = curl_init();
-
- curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT_MS,
- ( $opts['connTimeout'] ?? $this->connTimeout ) * 1000 );
- curl_setopt( $ch, CURLOPT_PROXY, $req['proxy'] ?? $this->proxy );
- curl_setopt( $ch, CURLOPT_TIMEOUT_MS,
- ( $opts['reqTimeout'] ?? $this->reqTimeout ) * 1000 );
- curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
- curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
- curl_setopt( $ch, CURLOPT_HEADER, 0 );
- if ( !is_null( $this->caBundlePath ) ) {
- curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
- curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath );
- }
- curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
-
- $url = $req['url'];
- $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
- if ( $query != '' ) {
- $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
- }
- curl_setopt( $ch, CURLOPT_URL, $url );
-
- curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] );
- if ( $req['method'] === 'HEAD' ) {
- curl_setopt( $ch, CURLOPT_NOBODY, 1 );
- }
-
- if ( $req['method'] === 'PUT' ) {
- curl_setopt( $ch, CURLOPT_PUT, 1 );
- if ( is_resource( $req['body'] ) ) {
- curl_setopt( $ch, CURLOPT_INFILE, $req['body'] );
- if ( isset( $req['headers']['content-length'] ) ) {
- curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] );
- } elseif ( isset( $req['headers']['transfer-encoding'] ) &&
- $req['headers']['transfer-encoding'] === 'chunks'
- ) {
- curl_setopt( $ch, CURLOPT_UPLOAD, true );
- } else {
- throw new Exception( "Missing 'Content-Length' or 'Transfer-Encoding' header." );
- }
- } elseif ( $req['body'] !== '' ) {
- $fp = fopen( "php://temp", "wb+" );
- fwrite( $fp, $req['body'], strlen( $req['body'] ) );
- rewind( $fp );
- curl_setopt( $ch, CURLOPT_INFILE, $fp );
- curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) );
- $req['_closeHandle'] = $fp; // remember to close this later
- } else {
- curl_setopt( $ch, CURLOPT_INFILESIZE, 0 );
- }
- curl_setopt( $ch, CURLOPT_READFUNCTION,
- function ( $ch, $fd, $length ) {
- $data = fread( $fd, $length );
- $len = strlen( $data );
- return $data;
- }
- );
- } elseif ( $req['method'] === 'POST' ) {
- curl_setopt( $ch, CURLOPT_POST, 1 );
- // Don't interpret POST parameters starting with '@' as file uploads, because this
- // makes it impossible to POST plain values starting with '@' (and causes security
- // issues potentially exposing the contents of local files).
- curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
- curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
- } else {
- if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
- throw new Exception( "HTTP body specified for a non PUT/POST request." );
- }
- $req['headers']['content-length'] = 0;
- }
-
- if ( !isset( $req['headers']['user-agent'] ) ) {
- $req['headers']['user-agent'] = $this->userAgent;
- }
-
- $headers = [];
- foreach ( $req['headers'] as $name => $value ) {
- if ( strpos( $name, ': ' ) ) {
- throw new Exception( "Headers cannot have ':' in the name." );
- }
- $headers[] = $name . ': ' . trim( $value );
- }
- curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
-
- curl_setopt( $ch, CURLOPT_HEADERFUNCTION,
- function ( $ch, $header ) use ( &$req ) {
- if ( !empty( $req['flags']['relayResponseHeaders'] ) && trim( $header ) !== '' ) {
- header( $header );
- }
- $length = strlen( $header );
- $matches = [];
- if ( preg_match( "/^(HTTP\/1\.[01]) (\d{3}) (.*)/", $header, $matches ) ) {
- $req['response']['code'] = (int)$matches[2];
- $req['response']['reason'] = trim( $matches[3] );
- return $length;
- }
- if ( strpos( $header, ":" ) === false ) {
- return $length;
- }
- list( $name, $value ) = explode( ":", $header, 2 );
- $name = strtolower( $name );
- $value = trim( $value );
- if ( isset( $req['response']['headers'][$name] ) ) {
- $req['response']['headers'][$name] .= ', ' . $value;
- } else {
- $req['response']['headers'][$name] = $value;
- }
- return $length;
- }
- );
-
- if ( isset( $req['stream'] ) ) {
- // Don't just use CURLOPT_FILE as that might give:
- // curl_setopt(): cannot represent a stream of type Output as a STDIO FILE*
- // The callback here handles both normal files and php://temp handles.
- curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
- function ( $ch, $data ) use ( &$req ) {
- return fwrite( $req['stream'], $data );
- }
- );
- } else {
- curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
- function ( $ch, $data ) use ( &$req ) {
- $req['response']['body'] .= $data;
- return strlen( $data );
- }
- );
- }
-
- return $ch;
- }
-
- /**
- * @return resource
- * @throws Exception
- */
- protected function getCurlMulti() {
- if ( !$this->multiHandle ) {
- if ( !function_exists( 'curl_multi_init' ) ) {
- throw new Exception( "PHP cURL function curl_multi_init missing. " .
- "Check https://www.mediawiki.org/wiki/Manual:CURL" );
- }
- $cmh = curl_multi_init();
- curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
- curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
- $this->multiHandle = $cmh;
- }
- return $this->multiHandle;
- }
-
- /**
- * Execute a set of HTTP(S) requests sequentially.
- *
- * @see MultiHttpClient::runMulti()
- * @todo Remove dependency on MediaWikiServices: use a separate HTTP client
- * library or copy code from PhpHttpRequest
- * @param array $reqs Map of HTTP request arrays
- * @param array $opts
- * - connTimeout : connection timeout per request (seconds)
- * - reqTimeout : post-connection timeout per request (seconds)
- * @return array $reqs With response array populated for each
- * @throws Exception
- */
- private function runMultiHttp( array $reqs, array $opts = [] ) {
- $httpOptions = [
- 'timeout' => $opts['reqTimeout'] ?? $this->reqTimeout,
- 'connectTimeout' => $opts['connTimeout'] ?? $this->connTimeout,
- 'logger' => $this->logger,
- 'caInfo' => $this->caBundlePath,
- ];
- foreach ( $reqs as &$req ) {
- $reqOptions = $httpOptions + [
- 'method' => $req['method'],
- 'proxy' => $req['proxy'] ?? $this->proxy,
- 'userAgent' => $req['headers']['user-agent'] ?? $this->userAgent,
- 'postData' => $req['body'],
- ];
-
- $url = $req['url'];
- $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
- if ( $query != '' ) {
- $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
- }
-
- $httpRequest = MediaWikiServices::getInstance()->getHttpRequestFactory()->create(
- $url, $reqOptions );
- $sv = $httpRequest->execute()->getStatusValue();
-
- $respHeaders = array_map(
- function ( $v ) {
- return implode( ', ', $v );
- },
- $httpRequest->getResponseHeaders() );
-
- $req['response'] = [
- 'code' => $httpRequest->getStatus(),
- 'reason' => '',
- 'headers' => $respHeaders,
- 'body' => $httpRequest->getContent(),
- 'error' => '',
- ];
-
- if ( !$sv->isOk() ) {
- $svErrors = $sv->getErrors();
- if ( isset( $svErrors[0] ) ) {
- $req['response']['error'] = $svErrors[0]['message'];
-
- // param values vary per failure type (ex. unknown host vs unknown page)
- if ( isset( $svErrors[0]['params'][0] ) ) {
- if ( is_numeric( $svErrors[0]['params'][0] ) ) {
- if ( isset( $svErrors[0]['params'][1] ) ) {
- $req['response']['reason'] = $svErrors[0]['params'][1];
- }
- } else {
- $req['response']['reason'] = $svErrors[0]['params'][0];
- }
- }
- }
- }
-
- $req['response'][0] = $req['response']['code'];
- $req['response'][1] = $req['response']['reason'];
- $req['response'][2] = $req['response']['headers'];
- $req['response'][3] = $req['response']['body'];
- $req['response'][4] = $req['response']['error'];
- }
-
- return $reqs;
- }
-
- /**
- * Normalize request information
- *
- * @param array $reqs the requests to normalize
- */
- private function normalizeRequests( array &$reqs ) {
- foreach ( $reqs as &$req ) {
- $req['response'] = [
- 'code' => 0,
- 'reason' => '',
- 'headers' => [],
- 'body' => '',
- 'error' => ''
- ];
- if ( isset( $req[0] ) ) {
- $req['method'] = $req[0]; // short-form
- unset( $req[0] );
- }
- if ( isset( $req[1] ) ) {
- $req['url'] = $req[1]; // short-form
- unset( $req[1] );
- }
- if ( !isset( $req['method'] ) ) {
- throw new Exception( "Request has no 'method' field set." );
- } elseif ( !isset( $req['url'] ) ) {
- throw new Exception( "Request has no 'url' field set." );
- }
- $this->logger->debug( "{$req['method']}: {$req['url']}" );
- $req['query'] = $req['query'] ?? [];
- $headers = []; // normalized headers
- if ( isset( $req['headers'] ) ) {
- foreach ( $req['headers'] as $name => $value ) {
- $headers[strtolower( $name )] = $value;
- }
- }
- $req['headers'] = $headers;
- if ( !isset( $req['body'] ) ) {
- $req['body'] = '';
- $req['headers']['content-length'] = 0;
- }
- $req['flags'] = $req['flags'] ?? [];
- }
- }
-
- /**
- * Get a suitable select timeout for the given options.
- *
- * @param array $opts
- * @return float
- */
- private function getSelectTimeout( $opts ) {
- $connTimeout = $opts['connTimeout'] ?? $this->connTimeout;
- $reqTimeout = $opts['reqTimeout'] ?? $this->reqTimeout;
- $timeouts = array_filter( [ $connTimeout, $reqTimeout ] );
- if ( count( $timeouts ) === 0 ) {
- return 1;
- }
-
- $selectTimeout = min( $timeouts ) * self::TIMEOUT_ACCURACY_FACTOR;
- // Minimum 10us for sanity
- if ( $selectTimeout < 10e-6 ) {
- $selectTimeout = 10e-6;
- }
- return $selectTimeout;
- }
-
- /**
- * Register a logger
- *
- * @param LoggerInterface $logger
- */
- public function setLogger( LoggerInterface $logger ) {
- $this->logger = $logger;
- }
-
- function __destruct() {
- if ( $this->multiHandle ) {
- curl_multi_close( $this->multiHandle );
- }
- }
-}
--- /dev/null
+<?php
+/**
+ * HTTP service client
+ *
+ * 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
+ */
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Class to handle multiple HTTP requests
+ *
+ * If curl is available, requests will be made concurrently.
+ * Otherwise, they will be made serially.
+ *
+ * HTTP request maps are arrays that use the following format:
+ * - method : GET/HEAD/PUT/POST/DELETE
+ * - url : HTTP/HTTPS URL
+ * - query : <query parameter field/value associative array> (uses RFC 3986)
+ * - headers : <header name/value associative array>
+ * - body : source to get the HTTP request body from;
+ * this can simply be a string (always), a resource for
+ * PUT requests, and a field/value array for POST request;
+ * array bodies are encoded as multipart/form-data and strings
+ * use application/x-www-form-urlencoded (headers sent automatically)
+ * - stream : resource to stream the HTTP response body to
+ * - proxy : HTTP proxy to use
+ * - flags : map of boolean flags which supports:
+ * - relayResponseHeaders : write out header via header()
+ * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'.
+ *
+ * @since 1.23
+ */
+class MultiHttpClient implements LoggerAwareInterface {
+ /** @var resource */
+ protected $multiHandle = null; // curl_multi handle
+ /** @var string|null SSL certificates path */
+ protected $caBundlePath;
+ /** @var float */
+ protected $connTimeout = 10;
+ /** @var float */
+ protected $reqTimeout = 300;
+ /** @var bool */
+ protected $usePipelining = false;
+ /** @var int */
+ protected $maxConnsPerHost = 50;
+ /** @var string|null proxy */
+ protected $proxy;
+ /** @var string */
+ protected $userAgent = 'wikimedia/multi-http-client v1.0';
+ /** @var LoggerInterface */
+ protected $logger;
+
+ // In PHP 7 due to https://bugs.php.net/bug.php?id=76480 the request/connect
+ // timeouts are periodically polled instead of being accurately respected.
+ // The select timeout is set to the minimum timeout multiplied by this factor.
+ const TIMEOUT_ACCURACY_FACTOR = 0.1;
+
+ /**
+ * @param array $options
+ * - connTimeout : default connection timeout (seconds)
+ * - reqTimeout : default request timeout (seconds)
+ * - proxy : HTTP proxy to use
+ * - usePipelining : whether to use HTTP pipelining if possible (for all hosts)
+ * - maxConnsPerHost : maximum number of concurrent connections (per host)
+ * - userAgent : The User-Agent header value to send
+ * - logger : a \Psr\Log\LoggerInterface instance for debug logging
+ * - caBundlePath : path to specific Certificate Authority bundle (if any)
+ * @throws Exception
+ */
+ public function __construct( array $options ) {
+ if ( isset( $options['caBundlePath'] ) ) {
+ $this->caBundlePath = $options['caBundlePath'];
+ if ( !file_exists( $this->caBundlePath ) ) {
+ throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath );
+ }
+ }
+ static $opts = [
+ 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost',
+ 'proxy', 'userAgent', 'logger'
+ ];
+ foreach ( $opts as $key ) {
+ if ( isset( $options[$key] ) ) {
+ $this->$key = $options[$key];
+ }
+ }
+ if ( $this->logger === null ) {
+ $this->logger = new NullLogger;
+ }
+ }
+
+ /**
+ * Execute an HTTP(S) request
+ *
+ * This method returns a response map of:
+ * - code : HTTP response code or 0 if there was a serious error
+ * - reason : HTTP response reason (empty if there was a serious error)
+ * - headers : <header name/value associative array>
+ * - body : HTTP response body or resource (if "stream" was set)
+ * - error : Any error string
+ * The map also stores integer-indexed copies of these values. This lets callers do:
+ * @code
+ * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $http->run( $req );
+ * @endcode
+ * @param array $req HTTP request array
+ * @param array $opts
+ * - connTimeout : connection timeout per request (seconds)
+ * - reqTimeout : post-connection timeout per request (seconds)
+ * @return array Response array for request
+ */
+ public function run( array $req, array $opts = [] ) {
+ return $this->runMulti( [ $req ], $opts )[0]['response'];
+ }
+
+ /**
+ * Execute a set of HTTP(S) requests.
+ *
+ * If curl is available, requests will be made concurrently.
+ * Otherwise, they will be made serially.
+ *
+ * The maps are returned by this method with the 'response' field set to a map of:
+ * - code : HTTP response code or 0 if there was a serious error
+ * - reason : HTTP response reason (empty if there was a serious error)
+ * - headers : <header name/value associative array>
+ * - body : HTTP response body or resource (if "stream" was set)
+ * - error : Any error string
+ * The map also stores integer-indexed copies of these values. This lets callers do:
+ * @code
+ * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req['response'];
+ * @endcode
+ * All headers in the 'headers' field are normalized to use lower case names.
+ * This is true for the request headers and the response headers. Integer-indexed
+ * method/URL entries will also be changed to use the corresponding string keys.
+ *
+ * @param array $reqs Map of HTTP request arrays
+ * @param array $opts
+ * - connTimeout : connection timeout per request (seconds)
+ * - reqTimeout : post-connection timeout per request (seconds)
+ * - usePipelining : whether to use HTTP pipelining if possible
+ * - maxConnsPerHost : maximum number of concurrent connections (per host)
+ * @return array $reqs With response array populated for each
+ * @throws Exception
+ */
+ public function runMulti( array $reqs, array $opts = [] ) {
+ $this->normalizeRequests( $reqs );
+ if ( $this->isCurlEnabled() ) {
+ return $this->runMultiCurl( $reqs, $opts );
+ } else {
+ return $this->runMultiHttp( $reqs, $opts );
+ }
+ }
+
+ /**
+ * Determines if the curl extension is available
+ *
+ * @return bool true if curl is available, false otherwise.
+ */
+ protected function isCurlEnabled() {
+ return extension_loaded( 'curl' );
+ }
+
+ /**
+ * Execute a set of HTTP(S) requests concurrently
+ *
+ * @see MultiHttpClient::runMulti()
+ *
+ * @param array $reqs Map of HTTP request arrays
+ * @param array $opts
+ * - connTimeout : connection timeout per request (seconds)
+ * - reqTimeout : post-connection timeout per request (seconds)
+ * - usePipelining : whether to use HTTP pipelining if possible
+ * - maxConnsPerHost : maximum number of concurrent connections (per host)
+ * @return array $reqs With response array populated for each
+ * @throws Exception
+ */
+ private function runMultiCurl( array $reqs, array $opts = [] ) {
+ $chm = $this->getCurlMulti();
+
+ $selectTimeout = $this->getSelectTimeout( $opts );
+
+ // Add all of the required cURL handles...
+ $handles = [];
+ foreach ( $reqs as $index => &$req ) {
+ $handles[$index] = $this->getCurlHandle( $req, $opts );
+ if ( count( $reqs ) > 1 ) {
+ // https://github.com/guzzle/guzzle/issues/349
+ curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true );
+ }
+ }
+ unset( $req ); // don't assign over this by accident
+
+ $indexes = array_keys( $reqs );
+ if ( isset( $opts['usePipelining'] ) ) {
+ curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] );
+ }
+ if ( isset( $opts['maxConnsPerHost'] ) ) {
+ // Keep these sockets around as they may be needed later in the request
+ curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] );
+ }
+
+ // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS)
+ $batches = array_chunk( $indexes, $this->maxConnsPerHost );
+ $infos = [];
+
+ foreach ( $batches as $batch ) {
+ // Attach all cURL handles for this batch
+ foreach ( $batch as $index ) {
+ curl_multi_add_handle( $chm, $handles[$index] );
+ }
+ // Execute the cURL handles concurrently...
+ $active = null; // handles still being processed
+ do {
+ // Do any available work...
+ do {
+ $mrc = curl_multi_exec( $chm, $active );
+ $info = curl_multi_info_read( $chm );
+ if ( $info !== false ) {
+ $infos[(int)$info['handle']] = $info;
+ }
+ } while ( $mrc == CURLM_CALL_MULTI_PERFORM );
+ // Wait (if possible) for available work...
+ if ( $active > 0 && $mrc == CURLM_OK && curl_multi_select( $chm, $selectTimeout ) == -1 ) {
+ // PHP bug 63411; https://curl.haxx.se/libcurl/c/curl_multi_fdset.html
+ usleep( 5000 ); // 5ms
+ }
+ } while ( $active > 0 && $mrc == CURLM_OK );
+ }
+
+ // Remove all of the added cURL handles and check for errors...
+ foreach ( $reqs as $index => &$req ) {
+ $ch = $handles[$index];
+ curl_multi_remove_handle( $chm, $ch );
+
+ if ( isset( $infos[(int)$ch] ) ) {
+ $info = $infos[(int)$ch];
+ $errno = $info['result'];
+ if ( $errno !== 0 ) {
+ $req['response']['error'] = "(curl error: $errno)";
+ if ( function_exists( 'curl_strerror' ) ) {
+ $req['response']['error'] .= " " . curl_strerror( $errno );
+ }
+ $this->logger->warning( "Error fetching URL \"{$req['url']}\": " .
+ $req['response']['error'] );
+ }
+ } else {
+ $req['response']['error'] = "(curl error: no status set)";
+ }
+
+ // For convenience with the list() operator
+ $req['response'][0] = $req['response']['code'];
+ $req['response'][1] = $req['response']['reason'];
+ $req['response'][2] = $req['response']['headers'];
+ $req['response'][3] = $req['response']['body'];
+ $req['response'][4] = $req['response']['error'];
+ curl_close( $ch );
+ // Close any string wrapper file handles
+ if ( isset( $req['_closeHandle'] ) ) {
+ fclose( $req['_closeHandle'] );
+ unset( $req['_closeHandle'] );
+ }
+ }
+ unset( $req ); // don't assign over this by accident
+
+ // Restore the default settings
+ curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining );
+ curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
+
+ return $reqs;
+ }
+
+ /**
+ * @param array &$req HTTP request map
+ * @param array $opts
+ * - connTimeout : default connection timeout
+ * - reqTimeout : default request timeout
+ * @return resource
+ * @throws Exception
+ */
+ protected function getCurlHandle( array &$req, array $opts = [] ) {
+ $ch = curl_init();
+
+ curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT_MS,
+ ( $opts['connTimeout'] ?? $this->connTimeout ) * 1000 );
+ curl_setopt( $ch, CURLOPT_PROXY, $req['proxy'] ?? $this->proxy );
+ curl_setopt( $ch, CURLOPT_TIMEOUT_MS,
+ ( $opts['reqTimeout'] ?? $this->reqTimeout ) * 1000 );
+ curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
+ curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
+ curl_setopt( $ch, CURLOPT_HEADER, 0 );
+ if ( !is_null( $this->caBundlePath ) ) {
+ curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
+ curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath );
+ }
+ curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
+
+ $url = $req['url'];
+ $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
+ if ( $query != '' ) {
+ $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
+ }
+ curl_setopt( $ch, CURLOPT_URL, $url );
+
+ curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] );
+ if ( $req['method'] === 'HEAD' ) {
+ curl_setopt( $ch, CURLOPT_NOBODY, 1 );
+ }
+
+ if ( $req['method'] === 'PUT' ) {
+ curl_setopt( $ch, CURLOPT_PUT, 1 );
+ if ( is_resource( $req['body'] ) ) {
+ curl_setopt( $ch, CURLOPT_INFILE, $req['body'] );
+ if ( isset( $req['headers']['content-length'] ) ) {
+ curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] );
+ } elseif ( isset( $req['headers']['transfer-encoding'] ) &&
+ $req['headers']['transfer-encoding'] === 'chunks'
+ ) {
+ curl_setopt( $ch, CURLOPT_UPLOAD, true );
+ } else {
+ throw new Exception( "Missing 'Content-Length' or 'Transfer-Encoding' header." );
+ }
+ } elseif ( $req['body'] !== '' ) {
+ $fp = fopen( "php://temp", "wb+" );
+ fwrite( $fp, $req['body'], strlen( $req['body'] ) );
+ rewind( $fp );
+ curl_setopt( $ch, CURLOPT_INFILE, $fp );
+ curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) );
+ $req['_closeHandle'] = $fp; // remember to close this later
+ } else {
+ curl_setopt( $ch, CURLOPT_INFILESIZE, 0 );
+ }
+ curl_setopt( $ch, CURLOPT_READFUNCTION,
+ function ( $ch, $fd, $length ) {
+ $data = fread( $fd, $length );
+ $len = strlen( $data );
+ return $data;
+ }
+ );
+ } elseif ( $req['method'] === 'POST' ) {
+ curl_setopt( $ch, CURLOPT_POST, 1 );
+ // Don't interpret POST parameters starting with '@' as file uploads, because this
+ // makes it impossible to POST plain values starting with '@' (and causes security
+ // issues potentially exposing the contents of local files).
+ curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
+ curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
+ } else {
+ if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
+ throw new Exception( "HTTP body specified for a non PUT/POST request." );
+ }
+ $req['headers']['content-length'] = 0;
+ }
+
+ if ( !isset( $req['headers']['user-agent'] ) ) {
+ $req['headers']['user-agent'] = $this->userAgent;
+ }
+
+ $headers = [];
+ foreach ( $req['headers'] as $name => $value ) {
+ if ( strpos( $name, ': ' ) ) {
+ throw new Exception( "Headers cannot have ':' in the name." );
+ }
+ $headers[] = $name . ': ' . trim( $value );
+ }
+ curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
+
+ curl_setopt( $ch, CURLOPT_HEADERFUNCTION,
+ function ( $ch, $header ) use ( &$req ) {
+ if ( !empty( $req['flags']['relayResponseHeaders'] ) && trim( $header ) !== '' ) {
+ header( $header );
+ }
+ $length = strlen( $header );
+ $matches = [];
+ if ( preg_match( "/^(HTTP\/1\.[01]) (\d{3}) (.*)/", $header, $matches ) ) {
+ $req['response']['code'] = (int)$matches[2];
+ $req['response']['reason'] = trim( $matches[3] );
+ return $length;
+ }
+ if ( strpos( $header, ":" ) === false ) {
+ return $length;
+ }
+ list( $name, $value ) = explode( ":", $header, 2 );
+ $name = strtolower( $name );
+ $value = trim( $value );
+ if ( isset( $req['response']['headers'][$name] ) ) {
+ $req['response']['headers'][$name] .= ', ' . $value;
+ } else {
+ $req['response']['headers'][$name] = $value;
+ }
+ return $length;
+ }
+ );
+
+ if ( isset( $req['stream'] ) ) {
+ // Don't just use CURLOPT_FILE as that might give:
+ // curl_setopt(): cannot represent a stream of type Output as a STDIO FILE*
+ // The callback here handles both normal files and php://temp handles.
+ curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
+ function ( $ch, $data ) use ( &$req ) {
+ return fwrite( $req['stream'], $data );
+ }
+ );
+ } else {
+ curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
+ function ( $ch, $data ) use ( &$req ) {
+ $req['response']['body'] .= $data;
+ return strlen( $data );
+ }
+ );
+ }
+
+ return $ch;
+ }
+
+ /**
+ * @return resource
+ * @throws Exception
+ */
+ protected function getCurlMulti() {
+ if ( !$this->multiHandle ) {
+ if ( !function_exists( 'curl_multi_init' ) ) {
+ throw new Exception( "PHP cURL function curl_multi_init missing. " .
+ "Check https://www.mediawiki.org/wiki/Manual:CURL" );
+ }
+ $cmh = curl_multi_init();
+ curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
+ curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
+ $this->multiHandle = $cmh;
+ }
+ return $this->multiHandle;
+ }
+
+ /**
+ * Execute a set of HTTP(S) requests sequentially.
+ *
+ * @see MultiHttpClient::runMulti()
+ * @todo Remove dependency on MediaWikiServices: use a separate HTTP client
+ * library or copy code from PhpHttpRequest
+ * @param array $reqs Map of HTTP request arrays
+ * @param array $opts
+ * - connTimeout : connection timeout per request (seconds)
+ * - reqTimeout : post-connection timeout per request (seconds)
+ * @return array $reqs With response array populated for each
+ * @throws Exception
+ */
+ private function runMultiHttp( array $reqs, array $opts = [] ) {
+ $httpOptions = [
+ 'timeout' => $opts['reqTimeout'] ?? $this->reqTimeout,
+ 'connectTimeout' => $opts['connTimeout'] ?? $this->connTimeout,
+ 'logger' => $this->logger,
+ 'caInfo' => $this->caBundlePath,
+ ];
+ foreach ( $reqs as &$req ) {
+ $reqOptions = $httpOptions + [
+ 'method' => $req['method'],
+ 'proxy' => $req['proxy'] ?? $this->proxy,
+ 'userAgent' => $req['headers']['user-agent'] ?? $this->userAgent,
+ 'postData' => $req['body'],
+ ];
+
+ $url = $req['url'];
+ $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
+ if ( $query != '' ) {
+ $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
+ }
+
+ $httpRequest = MediaWikiServices::getInstance()->getHttpRequestFactory()->create(
+ $url, $reqOptions );
+ $sv = $httpRequest->execute()->getStatusValue();
+
+ $respHeaders = array_map(
+ function ( $v ) {
+ return implode( ', ', $v );
+ },
+ $httpRequest->getResponseHeaders() );
+
+ $req['response'] = [
+ 'code' => $httpRequest->getStatus(),
+ 'reason' => '',
+ 'headers' => $respHeaders,
+ 'body' => $httpRequest->getContent(),
+ 'error' => '',
+ ];
+
+ if ( !$sv->isOk() ) {
+ $svErrors = $sv->getErrors();
+ if ( isset( $svErrors[0] ) ) {
+ $req['response']['error'] = $svErrors[0]['message'];
+
+ // param values vary per failure type (ex. unknown host vs unknown page)
+ if ( isset( $svErrors[0]['params'][0] ) ) {
+ if ( is_numeric( $svErrors[0]['params'][0] ) ) {
+ if ( isset( $svErrors[0]['params'][1] ) ) {
+ $req['response']['reason'] = $svErrors[0]['params'][1];
+ }
+ } else {
+ $req['response']['reason'] = $svErrors[0]['params'][0];
+ }
+ }
+ }
+ }
+
+ $req['response'][0] = $req['response']['code'];
+ $req['response'][1] = $req['response']['reason'];
+ $req['response'][2] = $req['response']['headers'];
+ $req['response'][3] = $req['response']['body'];
+ $req['response'][4] = $req['response']['error'];
+ }
+
+ return $reqs;
+ }
+
+ /**
+ * Normalize request information
+ *
+ * @param array $reqs the requests to normalize
+ */
+ private function normalizeRequests( array &$reqs ) {
+ foreach ( $reqs as &$req ) {
+ $req['response'] = [
+ 'code' => 0,
+ 'reason' => '',
+ 'headers' => [],
+ 'body' => '',
+ 'error' => ''
+ ];
+ if ( isset( $req[0] ) ) {
+ $req['method'] = $req[0]; // short-form
+ unset( $req[0] );
+ }
+ if ( isset( $req[1] ) ) {
+ $req['url'] = $req[1]; // short-form
+ unset( $req[1] );
+ }
+ if ( !isset( $req['method'] ) ) {
+ throw new Exception( "Request has no 'method' field set." );
+ } elseif ( !isset( $req['url'] ) ) {
+ throw new Exception( "Request has no 'url' field set." );
+ }
+ $this->logger->debug( "{$req['method']}: {$req['url']}" );
+ $req['query'] = $req['query'] ?? [];
+ $headers = []; // normalized headers
+ if ( isset( $req['headers'] ) ) {
+ foreach ( $req['headers'] as $name => $value ) {
+ $headers[strtolower( $name )] = $value;
+ }
+ }
+ $req['headers'] = $headers;
+ if ( !isset( $req['body'] ) ) {
+ $req['body'] = '';
+ $req['headers']['content-length'] = 0;
+ }
+ $req['flags'] = $req['flags'] ?? [];
+ }
+ }
+
+ /**
+ * Get a suitable select timeout for the given options.
+ *
+ * @param array $opts
+ * @return float
+ */
+ private function getSelectTimeout( $opts ) {
+ $connTimeout = $opts['connTimeout'] ?? $this->connTimeout;
+ $reqTimeout = $opts['reqTimeout'] ?? $this->reqTimeout;
+ $timeouts = array_filter( [ $connTimeout, $reqTimeout ] );
+ if ( count( $timeouts ) === 0 ) {
+ return 1;
+ }
+
+ $selectTimeout = min( $timeouts ) * self::TIMEOUT_ACCURACY_FACTOR;
+ // Minimum 10us for sanity
+ if ( $selectTimeout < 10e-6 ) {
+ $selectTimeout = 10e-6;
+ }
+ return $selectTimeout;
+ }
+
+ /**
+ * Register a logger
+ *
+ * @param LoggerInterface $logger
+ */
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ function __destruct() {
+ if ( $this->multiHandle ) {
+ curl_multi_close( $this->multiHandle );
+ }
+ }
+}
}
if ( $any ) {
- $this->getOutput()->addModules( [
- 'mediawiki.special.changecredentials.js'
- ] );
+ $this->getOutput()->addModules( 'mediawiki.misc-authed-ooui' );
}
return $descriptor;
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'move-page', $this->oldTitle->getPrefixedText() ) );
$out->addModuleStyles( 'mediawiki.special' );
- $out->addModules( 'mediawiki.special.movePage' );
+ $out->addModules( 'mediawiki.misc-authed-ooui' );
$this->addHelpLink( 'Help:Moving a page' );
$out->addWikiMsg( $this->getConfig()->get( 'FixDoubleRedirects' ) ?
}
protected function preText() {
- $this->getOutput()->addModules( 'mediawiki.special.pageLanguage' );
+ $this->getOutput()->addModules( 'mediawiki.misc-authed-ooui' );
return parent::preText();
}
$processed = $added = $ignored = $skipped = $overwritten = $failed = 0;
- $this->output( "Import Images\n\n" );
+ $this->output( "Importing Files\n\n" );
$dir = $this->getArg( 0 );
],
'targets' => [ 'desktop', 'mobile' ],
],
- 'mediawiki.special.changecredentials.js' => [
- 'scripts' => 'resources/src/mediawiki.special.changecredentials.js',
+ // This bundles various small (under 5 KB?) JavaScript files that:
+ // - .. are not loaded on when viewing or editing wiki pages.
+ // - .. are used by logged-in users only.
+ // - .. depend on oojs-ui-core.
+ // - .. contain UI intialisation code (e.g. no public module exports, because
+ // requiring or depending on this bundle is awkard)
+ 'mediawiki.misc-authed-ooui' => [
+ 'localBasePath' => "$IP/resources/src/mediawiki.misc-authed-ooui",
+ 'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.misc-authed-ooui",
+ 'scripts' => [
+ 'special.changecredentials.js',
+ 'special.movePage.js',
+ 'special.mute.js',
+ 'special.pageLanguage.js',
+ ],
'dependencies' => [
- 'mediawiki.api',
- 'mediawiki.htmlform.ooui'
+ 'mediawiki.api', // Used by special.changecredentials.js
+ 'mediawiki.htmlform.ooui', // Used by special.changecredentials.js
+ 'mediawiki.widgets.visibleLengthLimit', // Used by special.movePage.js
+ 'mediawiki.widgets', // Used by special.movePage.js
+ 'oojs-ui-core', // Used by special.pageLanguage.js
],
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.import' => [
'scripts' => 'resources/src/mediawiki.special.import.js',
],
- 'mediawiki.special.movePage' => [
- 'scripts' => 'resources/src/mediawiki.special.movePage.js',
- 'dependencies' => [
- 'mediawiki.widgets.visibleLengthLimit',
- 'mediawiki.widgets',
- ],
- ],
- 'mediawiki.special.pageLanguage' => [
- 'scripts' => [
- 'resources/src/mediawiki.special.mute.js',
- 'resources/src/mediawiki.special.pageLanguage.js'
- ],
- 'dependencies' => [
- 'oojs-ui-core',
- ],
- ],
'mediawiki.special.preferences.ooui' => [
'targets' => [ 'desktop', 'mobile' ],
'scripts' => [
--- /dev/null
+/*!
+ * JavaScript for change credentials form.
+ */
+( function () {
+ mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+ var api = new mw.Api();
+
+ $root.find( '.mw-changecredentials-validate-password.oo-ui-fieldLayout' ).each( function () {
+ var currentApiPromise,
+ self = OO.ui.FieldLayout.static.infuse( $( this ) );
+
+ self.getField().setValidation( function ( password ) {
+ var d;
+
+ if ( currentApiPromise ) {
+ currentApiPromise.abort();
+ currentApiPromise = undefined;
+ }
+
+ password = password.trim();
+
+ if ( password === '' ) {
+ self.setErrors( [] );
+ return true;
+ }
+
+ d = $.Deferred();
+ currentApiPromise = api.post( {
+ action: 'validatepassword',
+ password: password,
+ formatversion: 2,
+ errorformat: 'html',
+ errorsuselocal: true,
+ uselang: mw.config.get( 'wgUserLanguage' )
+ } ).done( function ( resp ) {
+ var pwinfo = resp.validatepassword,
+ good = pwinfo.validity === 'Good',
+ errors = [];
+
+ currentApiPromise = undefined;
+
+ if ( !good ) {
+ pwinfo.validitymessages.map( function ( m ) {
+ errors.push( new OO.ui.HtmlSnippet( m.html ) );
+ } );
+ }
+ self.setErrors( errors );
+ d.resolve( good );
+ } ).fail( d.reject );
+
+ return d.promise( { abort: currentApiPromise.abort } );
+ } );
+ } );
+ } );
+}() );
--- /dev/null
+/*!
+ * JavaScript for Special:MovePage
+ */
+( function () {
+ $( function () {
+ var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+ summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+ wpReason = OO.ui.infuse( $( '#wpReason' ) );
+
+ // Infuse for pretty dropdown
+ OO.ui.infuse( $( '#wpNewTitle' ) );
+ // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+ if ( summaryCodePointLimit ) {
+ mw.widgets.visibleCodePointLimit( wpReason, summaryCodePointLimit );
+ } else if ( summaryByteLimit ) {
+ mw.widgets.visibleByteLimit( wpReason, summaryByteLimit );
+ }
+ } );
+}() );
--- /dev/null
+( function () {
+ 'use strict';
+
+ $( function () {
+ var $inputs = $( '#mw-specialmute-form input[type="checkbox"]' ),
+ saveButton, $saveButton = $( '#save' );
+
+ function isFormChanged() {
+ return $inputs.is( function () {
+ return this.checked !== this.defaultChecked;
+ } );
+ }
+
+ if ( $saveButton.length ) {
+ saveButton = OO.ui.infuse( $saveButton );
+ saveButton.setDisabled( !isFormChanged() );
+
+ $inputs.on( 'change', function () {
+ saveButton.setDisabled( !isFormChanged() );
+ } );
+ }
+ } );
+}() );
--- /dev/null
+/*!
+ * JavaScript module used on Special:PageLanguage
+ */
+( function () {
+ $( function () {
+ // Select the 'Language select' option if user is trying to select language
+ if ( $( '#mw-pl-languageselector' ).length ) {
+ OO.ui.infuse( $( '#mw-pl-languageselector' ) ).on( 'change', function () {
+ OO.ui.infuse( $( '#mw-pl-options' ) ).setValue( '2' );
+ } );
+ }
+ } );
+}() );
+++ /dev/null
-/*!
- * JavaScript for change credentials form.
- */
-( function () {
- mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
- var api = new mw.Api();
-
- $root.find( '.mw-changecredentials-validate-password.oo-ui-fieldLayout' ).each( function () {
- var currentApiPromise,
- self = OO.ui.FieldLayout.static.infuse( $( this ) );
-
- self.getField().setValidation( function ( password ) {
- var d;
-
- if ( currentApiPromise ) {
- currentApiPromise.abort();
- currentApiPromise = undefined;
- }
-
- password = password.trim();
-
- if ( password === '' ) {
- self.setErrors( [] );
- return true;
- }
-
- d = $.Deferred();
- currentApiPromise = api.post( {
- action: 'validatepassword',
- password: password,
- formatversion: 2,
- errorformat: 'html',
- errorsuselocal: true,
- uselang: mw.config.get( 'wgUserLanguage' )
- } ).done( function ( resp ) {
- var pwinfo = resp.validatepassword,
- good = pwinfo.validity === 'Good',
- errors = [];
-
- currentApiPromise = undefined;
-
- if ( !good ) {
- pwinfo.validitymessages.map( function ( m ) {
- errors.push( new OO.ui.HtmlSnippet( m.html ) );
- } );
- }
- self.setErrors( errors );
- d.resolve( good );
- } ).fail( d.reject );
-
- return d.promise( { abort: currentApiPromise.abort } );
- } );
- } );
- } );
-}() );
+++ /dev/null
-/*!
- * JavaScript for Special:MovePage
- */
-( function () {
- $( function () {
- var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
- summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
- wpReason = OO.ui.infuse( $( '#wpReason' ) );
-
- // Infuse for pretty dropdown
- OO.ui.infuse( $( '#wpNewTitle' ) );
- // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
- if ( summaryCodePointLimit ) {
- mw.widgets.visibleCodePointLimit( wpReason, summaryCodePointLimit );
- } else if ( summaryByteLimit ) {
- mw.widgets.visibleByteLimit( wpReason, summaryByteLimit );
- }
- } );
-}() );
+++ /dev/null
-( function () {
- 'use strict';
-
- $( function () {
- var $inputs = $( '#mw-specialmute-form input[type="checkbox"]' ),
- saveButton, $saveButton = $( '#save' );
-
- function isFormChanged() {
- return $inputs.is( function () {
- return this.checked !== this.defaultChecked;
- } );
- }
-
- if ( $saveButton.length ) {
- saveButton = OO.ui.infuse( $saveButton );
- saveButton.setDisabled( !isFormChanged() );
-
- $inputs.on( 'change', function () {
- saveButton.setDisabled( !isFormChanged() );
- } );
- }
- } );
-}() );
+++ /dev/null
-/*!
- * JavaScript module used on Special:PageLanguage
- */
-( function () {
- $( function () {
- // Select the 'Language select' option if user is trying to select language
- if ( $( '#mw-pl-languageselector' ).length ) {
- OO.ui.infuse( $( '#mw-pl-languageselector' ) ).on( 'change', function () {
- OO.ui.infuse( $( '#mw-pl-options' ) ).setValue( '2' );
- } );
- }
- } );
-}() );
foreach ( $notesFiles as $index => $fileName ) {
$this->assertFileLength( "Release Notes", $fileName );
}
+ }
+
+ public static function provideFilesAtRoot() {
+ global $IP;
- // Also test the README and similar files
- $otherFiles = [
- "$IP/COPYING",
- "$IP/FAQ",
- "$IP/HISTORY",
- "$IP/INSTALL",
- "$IP/README",
- "$IP/SECURITY"
+ $rootFiles = [
+ "COPYING",
+ "FAQ",
+ "HISTORY",
+ "INSTALL",
+ "README",
+ "SECURITY",
];
- foreach ( $otherFiles as $index => $fileName ) {
- $this->assertFileLength( "Help", $fileName );
+ $testCases = [];
+ foreach ( $rootFiles as $rootFile ) {
+ $testCases["$rootFile file"] = [ "$IP/$rootFile" ];
}
+ return $testCases;
+ }
+
+ /**
+ * @dataProvider provideFilesAtRoot
+ * @coversNothing
+ */
+ public function testRootFilesHaveProperLineLength( $fileName ) {
+ $this->assertFileLength( "Help", $fileName );
}
private function assertFileLength( $type, $fileName ) {
- $file = file( $fileName, FILE_IGNORE_NEW_LINES );
+ $lines = file( $fileName, FILE_IGNORE_NEW_LINES );
- $this->assertFalse(
- !$file,
+ $this->assertNotFalse(
+ $lines,
"$type file '$fileName' is inaccessible."
);
- foreach ( $file as $i => $line ) {
+ $errors = [];
+ foreach ( $lines as $i => $line ) {
$num = $i + 1;
- $this->assertLessThanOrEqual(
- // FILE_IGNORE_NEW_LINES drops the \n at the EOL, so max length is 80 not 81.
- 80,
- mb_strlen( $line ),
- "$type file '$fileName' line $num, is longer than 80 chars:\n\t'$line'"
- );
+
+ // FILE_IGNORE_NEW_LINES drops the \n at the EOL, so max length is 80 not 81.
+ $max_length = 80;
+
+ $length = mb_strlen( $line );
+ if ( $length <= $max_length ) {
+ continue;
+ }
+ $errors[] = "line $num: length $length > $max_length:\n$line";
}
+ # Using assertSame() to show the full line
+ $this->assertSame(
+ [], $errors,
+ "$type file '$fileName' lines " .
+ "have at most $max_length characters"
+ );
}
}
+++ /dev/null
-<?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
- *
- * @author Jeroen De Dauw < jeroendedauw@gmail.com >
- */
-class MediaWikiVersionFetcherTest extends MediaWikiTestCase {
-
- public function testReturnsResult() {
- global $wgVersion;
- $versionFetcher = new MediaWikiVersionFetcher();
- $this->assertSame( $wgVersion, $versionFetcher->fetchVersion() );
- }
-
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Rest;
-
-use EmptyBagOStuff;
-use GuzzleHttp\Psr7\Uri;
-use GuzzleHttp\Psr7\Stream;
-use MediaWiki\Rest\Handler;
-use MediaWikiTestCase;
-use MediaWiki\Rest\EntryPoint;
-use MediaWiki\Rest\RequestData;
-use MediaWiki\Rest\ResponseFactory;
-use MediaWiki\Rest\Router;
-use WebResponse;
-
-/**
- * @covers \MediaWiki\Rest\EntryPoint
- * @covers \MediaWiki\Rest\Router
- */
-class EntryPointTest extends MediaWikiTestCase {
- private static $mockHandler;
-
- private function createRouter() {
- return new Router(
- [ __DIR__ . '/testRoutes.json' ],
- [],
- '/rest',
- new EmptyBagOStuff(),
- new ResponseFactory() );
- }
-
- private function createWebResponse() {
- return $this->getMockBuilder( WebResponse::class )
- ->setMethods( [ 'header' ] )
- ->getMock();
- }
-
- public static function mockHandlerHeader() {
- return new class extends Handler {
- public function execute() {
- $response = $this->getResponseFactory()->create();
- $response->setHeader( 'Foo', 'Bar' );
- return $response;
- }
- };
- }
-
- public function testHeader() {
- $webResponse = $this->createWebResponse();
- $webResponse->expects( $this->any() )
- ->method( 'header' )
- ->withConsecutive(
- [ 'HTTP/1.1 200 OK', true, null ],
- [ 'Foo: Bar', true, null ]
- );
-
- $entryPoint = new EntryPoint(
- new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] ),
- $webResponse,
- $this->createRouter() );
- $entryPoint->execute();
- $this->assertTrue( true );
- }
-
- public static function mockHandlerBodyRewind() {
- return new class extends Handler {
- public function execute() {
- $response = $this->getResponseFactory()->create();
- $stream = new Stream( fopen( 'php://memory', 'w+' ) );
- $stream->write( 'hello' );
- $response->setBody( $stream );
- return $response;
- }
- };
- }
-
- /**
- * Make sure EntryPoint rewinds a seekable body stream before reading.
- */
- public function testBodyRewind() {
- $entryPoint = new EntryPoint(
- new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] ),
- $this->createWebResponse(),
- $this->createRouter() );
- ob_start();
- $entryPoint->execute();
- $this->assertSame( 'hello', ob_get_clean() );
- }
-
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Rest\Handler;
-
-use EmptyBagOStuff;
-use GuzzleHttp\Psr7\Uri;
-use MediaWiki\Rest\RequestData;
-use MediaWiki\Rest\ResponseFactory;
-use MediaWiki\Rest\Router;
-use MediaWikiTestCase;
-
-/**
- * @covers \MediaWiki\Rest\Handler\HelloHandler
- */
-class HelloHandlerTest extends MediaWikiTestCase {
- public static function provideTestViaRouter() {
- return [
- 'normal' => [
- [
- 'method' => 'GET',
- 'uri' => self::makeUri( '/user/Tim/hello' ),
- ],
- [
- 'statusCode' => 200,
- 'reasonPhrase' => 'OK',
- 'protocolVersion' => '1.1',
- 'body' => '{"message":"Hello, Tim!"}',
- ],
- ],
- 'method not allowed' => [
- [
- 'method' => 'POST',
- 'uri' => self::makeUri( '/user/Tim/hello' ),
- ],
- [
- 'statusCode' => 405,
- 'reasonPhrase' => 'Method Not Allowed',
- 'protocolVersion' => '1.1',
- 'body' => '{"httpCode":405,"httpReason":"Method Not Allowed"}',
- ],
- ],
- ];
- }
-
- private static function makeUri( $path ) {
- return new Uri( "http://www.example.com/rest$path" );
- }
-
- /** @dataProvider provideTestViaRouter */
- public function testViaRouter( $requestInfo, $responseInfo ) {
- $router = new Router(
- [ __DIR__ . '/../testRoutes.json' ],
- [],
- '/rest',
- new EmptyBagOStuff(),
- new ResponseFactory() );
- $request = new RequestData( $requestInfo );
- $response = $router->execute( $request );
- if ( isset( $responseInfo['statusCode'] ) ) {
- $this->assertSame( $responseInfo['statusCode'], $response->getStatusCode() );
- }
- if ( isset( $responseInfo['reasonPhrase'] ) ) {
- $this->assertSame( $responseInfo['reasonPhrase'], $response->getReasonPhrase() );
- }
- if ( isset( $responseInfo['protocolVersion'] ) ) {
- $this->assertSame( $responseInfo['protocolVersion'], $response->getProtocolVersion() );
- }
- if ( isset( $responseInfo['body'] ) ) {
- $this->assertSame( $responseInfo['body'], $response->getBody()->getContents() );
- }
- $this->assertSame(
- [],
- array_diff( array_keys( $responseInfo ), [
- 'statusCode',
- 'reasonPhrase',
- 'protocolVersion',
- 'body'
- ] ),
- '$responseInfo may not contain unknown keys' );
- }
-}
+++ /dev/null
-[
- {
- "path": "/user/{name}/hello",
- "class": "MediaWiki\\Rest\\Handler\\HelloHandler"
- },
- {
- "path": "/mock/EntryPoint/header",
- "factory": "MediaWiki\\Tests\\Rest\\EntryPointTest::mockHandlerHeader"
- },
- {
- "path": "/mock/EntryPoint/bodyRewind",
- "factory": "MediaWiki\\Tests\\Rest\\EntryPointTest::mockHandlerBodyRewind"
- }
-]
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use ActorMigration;
-use CommentStore;
-use MediaWiki\Logger\Spi as LoggerSpi;
-use MediaWiki\Revision\RevisionStore;
-use MediaWiki\Revision\RevisionStoreFactory;
-use MediaWiki\Revision\SlotRoleRegistry;
-use MediaWiki\Storage\BlobStore;
-use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\NameTableStore;
-use MediaWiki\Storage\NameTableStoreFactory;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWikiTestCase;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use WANObjectCache;
-use Wikimedia\Rdbms\ILBFactory;
-use Wikimedia\Rdbms\ILoadBalancer;
-use Wikimedia\TestingAccessWrapper;
-
-class RevisionStoreFactoryTest extends MediaWikiTestCase {
-
- /**
- * @covers \MediaWiki\Revision\RevisionStoreFactory::__construct
- */
- public function testValidConstruction_doesntCauseErrors() {
- new RevisionStoreFactory(
- $this->getMockLoadBalancerFactory(),
- $this->getMockBlobStoreFactory(),
- $this->getNameTableStoreFactory(),
- $this->getMockSlotRoleRegistry(),
- $this->getHashWANObjectCache(),
- $this->getMockCommentStore(),
- ActorMigration::newMigration(),
- MIGRATION_OLD,
- $this->getMockLoggerSpi(),
- true
- );
- $this->assertTrue( true );
- }
-
- public function provideWikiIds() {
- yield [ true ];
- yield [ false ];
- yield [ 'somewiki' ];
- yield [ 'somewiki', MIGRATION_OLD , false ];
- yield [ 'somewiki', MIGRATION_NEW , true ];
- }
-
- /**
- * @dataProvider provideWikiIds
- * @covers \MediaWiki\Revision\RevisionStoreFactory::getRevisionStore
- */
- public function testGetRevisionStore(
- $dbDomain,
- $mcrMigrationStage = MIGRATION_OLD,
- $contentHandlerUseDb = true
- ) {
- $lbFactory = $this->getMockLoadBalancerFactory();
- $blobStoreFactory = $this->getMockBlobStoreFactory();
- $nameTableStoreFactory = $this->getNameTableStoreFactory();
- $slotRoleRegistry = $this->getMockSlotRoleRegistry();
- $cache = $this->getHashWANObjectCache();
- $commentStore = $this->getMockCommentStore();
- $actorMigration = ActorMigration::newMigration();
- $loggerProvider = $this->getMockLoggerSpi();
-
- $factory = new RevisionStoreFactory(
- $lbFactory,
- $blobStoreFactory,
- $nameTableStoreFactory,
- $slotRoleRegistry,
- $cache,
- $commentStore,
- $actorMigration,
- $mcrMigrationStage,
- $loggerProvider,
- $contentHandlerUseDb
- );
-
- $store = $factory->getRevisionStore( $dbDomain );
- $wrapper = TestingAccessWrapper::newFromObject( $store );
-
- // ensure the correct object type is returned
- $this->assertInstanceOf( RevisionStore::class, $store );
-
- // ensure the RevisionStore is for the given wikiId
- $this->assertSame( $dbDomain, $wrapper->dbDomain );
-
- // ensure all other required services are correctly set
- $this->assertSame( $cache, $wrapper->cache );
- $this->assertSame( $commentStore, $wrapper->commentStore );
- $this->assertSame( $mcrMigrationStage, $wrapper->mcrMigrationStage );
- $this->assertSame( $actorMigration, $wrapper->actorMigration );
- $this->assertSame( $contentHandlerUseDb, $store->getContentHandlerUseDB() );
-
- $this->assertInstanceOf( ILoadBalancer::class, $wrapper->loadBalancer );
- $this->assertInstanceOf( BlobStore::class, $wrapper->blobStore );
- $this->assertInstanceOf( NameTableStore::class, $wrapper->contentModelStore );
- $this->assertInstanceOf( NameTableStore::class, $wrapper->slotRoleStore );
- $this->assertInstanceOf( LoggerInterface::class, $wrapper->logger );
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
- */
- private function getMockLoadBalancer() {
- return $this->getMockBuilder( ILoadBalancer::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|ILBFactory
- */
- private function getMockLoadBalancerFactory() {
- $mock = $this->getMockBuilder( ILBFactory::class )
- ->disableOriginalConstructor()->getMock();
-
- $mock->method( 'getMainLB' )
- ->willReturnCallback( function () {
- return $this->getMockLoadBalancer();
- } );
-
- return $mock;
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
- */
- private function getMockSqlBlobStore() {
- return $this->getMockBuilder( SqlBlobStore::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|BlobStoreFactory
- */
- private function getMockBlobStoreFactory() {
- $mock = $this->getMockBuilder( BlobStoreFactory::class )
- ->disableOriginalConstructor()->getMock();
-
- $mock->method( 'newSqlBlobStore' )
- ->willReturnCallback( function () {
- return $this->getMockSqlBlobStore();
- } );
-
- return $mock;
- }
-
- /**
- * @return SlotRoleRegistry
- */
- private function getMockSlotRoleRegistry() {
- return $this->createMock( SlotRoleRegistry::class );
- }
-
- /**
- * @return NameTableStoreFactory
- */
- private function getNameTableStoreFactory() {
- return new NameTableStoreFactory(
- $this->getMockLoadBalancerFactory(),
- $this->getHashWANObjectCache(),
- new NullLogger() );
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
- */
- private function getMockCommentStore() {
- return $this->getMockBuilder( CommentStore::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- private function getHashWANObjectCache() {
- return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|LoggerSpi
- */
- private function getMockLoggerSpi() {
- $mock = $this->getMock( LoggerSpi::class );
-
- $mock->method( 'getLogger' )
- ->willReturn( new NullLogger() );
-
- return $mock;
- }
-
-}
+++ /dev/null
-<?php
-
-use MediaWiki\MediaWikiServices;
-
-class ConfigFactoryTest extends MediaWikiTestCase {
-
- /**
- * @covers ConfigFactory::register
- */
- public function testRegister() {
- $factory = new ConfigFactory();
- $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
- $this->assertInstanceOf( GlobalVarConfig::class, $factory->makeConfig( 'unittest' ) );
- }
-
- /**
- * @covers ConfigFactory::register
- */
- public function testRegisterInvalid() {
- $factory = new ConfigFactory();
- $this->setExpectedException( InvalidArgumentException::class );
- $factory->register( 'invalid', 'Invalid callback' );
- }
-
- /**
- * @covers ConfigFactory::register
- */
- public function testRegisterInvalidInstance() {
- $factory = new ConfigFactory();
- $this->setExpectedException( InvalidArgumentException::class );
- $factory->register( 'invalidInstance', new stdClass );
- }
-
- /**
- * @covers ConfigFactory::register
- */
- public function testRegisterInstance() {
- $config = GlobalVarConfig::newInstance();
- $factory = new ConfigFactory();
- $factory->register( 'unittest', $config );
- $this->assertSame( $config, $factory->makeConfig( 'unittest' ) );
- }
-
- /**
- * @covers ConfigFactory::register
- */
- public function testRegisterAgain() {
- $factory = new ConfigFactory();
- $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
- $config1 = $factory->makeConfig( 'unittest' );
-
- $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
- $config2 = $factory->makeConfig( 'unittest' );
-
- $this->assertNotSame( $config1, $config2 );
- }
-
- /**
- * @covers ConfigFactory::salvage
- */
- public function testSalvage() {
- $oldFactory = new ConfigFactory();
- $oldFactory->register( 'foo', 'GlobalVarConfig::newInstance' );
- $oldFactory->register( 'bar', 'GlobalVarConfig::newInstance' );
- $oldFactory->register( 'quux', 'GlobalVarConfig::newInstance' );
-
- // instantiate two of the three defined configurations
- $foo = $oldFactory->makeConfig( 'foo' );
- $bar = $oldFactory->makeConfig( 'bar' );
- $quux = $oldFactory->makeConfig( 'quux' );
-
- // define new config instance
- $newFactory = new ConfigFactory();
- $newFactory->register( 'foo', 'GlobalVarConfig::newInstance' );
- $newFactory->register( 'bar', function () {
- return new HashConfig();
- } );
-
- // "foo" and "quux" are defined in the old and the new factory.
- // The old factory has instances for "foo" and "bar", but not "quux".
- $newFactory->salvage( $oldFactory );
-
- $newFoo = $newFactory->makeConfig( 'foo' );
- $this->assertSame( $foo, $newFoo, 'existing instance should be salvaged' );
-
- $newBar = $newFactory->makeConfig( 'bar' );
- $this->assertNotSame( $bar, $newBar, 'don\'t salvage if callbacks differ' );
-
- // the new factory doesn't have quux defined, so the quux instance should not be salvaged
- $this->setExpectedException( ConfigException::class );
- $newFactory->makeConfig( 'quux' );
- }
-
- /**
- * @covers ConfigFactory::getConfigNames
- */
- public function testGetConfigNames() {
- $factory = new ConfigFactory();
- $factory->register( 'foo', 'GlobalVarConfig::newInstance' );
- $factory->register( 'bar', new HashConfig() );
-
- $this->assertEquals( [ 'foo', 'bar' ], $factory->getConfigNames() );
- }
-
- /**
- * @covers ConfigFactory::makeConfig
- */
- public function testMakeConfigWithCallback() {
- $factory = new ConfigFactory();
- $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
-
- $conf = $factory->makeConfig( 'unittest' );
- $this->assertInstanceOf( Config::class, $conf );
- $this->assertSame( $conf, $factory->makeConfig( 'unittest' ) );
- }
-
- /**
- * @covers ConfigFactory::makeConfig
- */
- public function testMakeConfigWithObject() {
- $factory = new ConfigFactory();
- $conf = new HashConfig();
- $factory->register( 'test', $conf );
- $this->assertSame( $conf, $factory->makeConfig( 'test' ) );
- }
-
- /**
- * @covers ConfigFactory::makeConfig
- */
- public function testMakeConfigFallback() {
- $factory = new ConfigFactory();
- $factory->register( '*', 'GlobalVarConfig::newInstance' );
- $conf = $factory->makeConfig( 'unittest' );
- $this->assertInstanceOf( Config::class, $conf );
- }
-
- /**
- * @covers ConfigFactory::makeConfig
- */
- public function testMakeConfigWithNoBuilders() {
- $factory = new ConfigFactory();
- $this->setExpectedException( ConfigException::class );
- $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::class );
- $factory->makeConfig( 'unittest' );
- }
-
- /**
- * @covers ConfigFactory::getDefaultInstance
- */
- public function testGetDefaultInstance() {
- // NOTE: the global config factory returned here has been overwritten
- // for operation in test mode. It may not reflect LocalSettings.
- $factory = MediaWikiServices::getInstance()->getConfigFactory();
- $this->assertInstanceOf( Config::class, $factory->makeConfig( 'main' ) );
- }
-
-}
+++ /dev/null
-<?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 [
- [
- 'nonanimated.gif',
- [
- 'comment' => [ 'GIF test file ⁕ Created with GIMP' ],
- 'duration' => 0.1,
- 'frameCount' => 1,
- 'looped' => false,
- 'xmp' => '',
- ]
- ],
- [
- 'animated.gif',
- [
- 'comment' => [ 'GIF test file . Created with GIMP' ],
- 'duration' => 2.4,
- 'frameCount' => 4,
- 'looped' => true,
- 'xmp' => '',
- ]
- ],
-
- [
- 'animated-xmp.gif',
- [
- 'xmp' => $xmpNugget,
- 'duration' => 2.4,
- 'frameCount' => 4,
- 'looped' => true,
- 'comment' => [ 'GIƒ·test·file' ],
- ]
- ],
- ];
- }
-}
+++ /dev/null
-<?php
-
-/**
- * @group Media
- * @covers SVGMetadataExtractor
- */
-class SVGMetadataExtractorTest extends MediaWikiTestCase {
-
- /**
- * @dataProvider provideSvgFiles
- */
- public function testGetMetadata( $infile, $expected ) {
- $this->assertMetadata( $infile, $expected );
- }
-
- /**
- * @dataProvider provideSvgFilesWithXMLMetadata
- */
- public function testGetXMLMetadata( $infile, $expected ) {
- $r = new XMLReader();
- $this->assertMetadata( $infile, $expected );
- }
-
- /**
- * @dataProvider provideSvgUnits
- */
- public function testScaleSVGUnit( $inUnit, $expected ) {
- $this->assertEquals(
- $expected,
- SVGReader::scaleSVGUnit( $inUnit ),
- 'SVG unit conversion and scaling failure'
- );
- }
-
- 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 [
- [
- "$base/Wikimedia-logo.svg",
- [
- 'width' => 1024,
- 'height' => 1024,
- 'originalWidth' => '1024',
- 'originalHeight' => '1024',
- 'translations' => [],
- ]
- ],
- [
- "$base/QA_icon.svg",
- [
- 'width' => 60,
- 'height' => 60,
- 'originalWidth' => '60',
- 'originalHeight' => '60',
- 'translations' => [],
- ]
- ],
- [
- "$base/Gtk-media-play-ltr.svg",
- [
- 'width' => 60,
- 'height' => 60,
- 'originalWidth' => '60.0000000',
- 'originalHeight' => '60.0000000',
- 'translations' => [],
- ]
- ],
- [
- "$base/Toll_Texas_1.svg",
- // This file triggered T33719, needs entity expansion in the xmlns checks
- [
- 'width' => 385,
- 'height' => 385,
- 'originalWidth' => '385',
- 'originalHeight' => '385.0004883',
- 'translations' => [],
- ]
- ],
- [
- "$base/Tux.svg",
- [
- 'width' => 512,
- 'height' => 594,
- 'originalWidth' => '100%',
- 'originalHeight' => '100%',
- 'title' => 'Tux',
- 'translations' => [],
- 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
- ]
- ],
- [
- "$base/Speech_bubbles.svg",
- [
- 'width' => 627,
- 'height' => 461,
- 'originalWidth' => '17.7cm',
- 'originalHeight' => '13cm',
- 'translations' => [
- '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
- ],
- ]
- ],
- [
- "$base/Soccer_ball_animated.svg",
- [
- 'width' => 150,
- 'height' => 150,
- 'originalWidth' => '150',
- 'originalHeight' => '150',
- 'animated' => true,
- 'translations' => []
- ],
- ],
- [
- "$base/comma_separated_viewbox.svg",
- [
- 'width' => 512,
- 'height' => 594,
- 'originalWidth' => '100%',
- 'originalHeight' => '100%',
- 'translations' => []
- ],
- ],
- ];
- }
-
- public static function provideSvgFilesWithXMLMetadata() {
- $base = __DIR__ . '/../../data/media';
- // phpcs:disable Generic.Files.LineLength
- $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>';
- // phpcs:enable
-
- $metadata = str_replace( "\r", '', $metadata ); // Windows compat
- return [
- [
- "$base/US_states_by_total_state_tax_revenue.svg",
- [
- 'height' => 593,
- 'metadata' => $metadata,
- 'width' => 959,
- 'originalWidth' => '958.69',
- 'originalHeight' => '592.78998',
- 'translations' => [],
- ]
- ],
- ];
- }
-
- public static function provideSvgUnits() {
- return [
- [ '1' , 1 ],
- [ '1.1' , 1.1 ],
- [ '0.1' , 0.1 ],
- [ '.1' , 0.1 ],
- [ '1e2' , 100 ],
- [ '1E2' , 100 ],
- [ '+1' , 1 ],
- [ '-1' , -1 ],
- [ '-1.1' , -1.1 ],
- [ '1e+2' , 100 ],
- [ '1e-2' , 0.01 ],
- [ '10px' , 10 ],
- [ '10pt' , 10 * 1.25 ],
- [ '10pc' , 10 * 15 ],
- [ '10mm' , 10 * 3.543307 ],
- [ '10cm' , 10 * 35.43307 ],
- [ '10in' , 10 * 90 ],
- [ '10em' , 10 * 16 ],
- [ '10ex' , 10 * 12 ],
- [ '10%' , 51.2 ],
- [ '10 px' , 10 ],
- // Invalid values
- [ '1e1.1', 10 ],
- [ '10bp', 10 ],
- [ 'p10', null ],
- ];
- }
-}
+++ /dev/null
-<?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
- * @since 1.25
- *
- * @ingroup Site
- * @ingroup Test
- *
- * @group Site
- * @group Database
- *
- * @author Jeroen De Dauw < jeroendedauw@gmail.com >
- */
-class CachingSiteStoreTest extends MediaWikiTestCase {
-
- /**
- * @covers CachingSiteStore::getSites
- */
- public function testGetSites() {
- $testSites = TestSites::getSites();
-
- $store = new CachingSiteStore(
- $this->getHashSiteStore( $testSites ),
- ObjectCache::getLocalClusterInstance()
- );
-
- $sites = $store->getSites();
-
- $this->assertInstanceOf( SiteList::class, $sites );
-
- /**
- * @var Site $site
- */
- foreach ( $sites as $site ) {
- $this->assertInstanceOf( Site::class, $site );
- }
-
- foreach ( $testSites as $site ) {
- if ( $site->getGlobalId() !== null ) {
- $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) );
- }
- }
- }
-
- /**
- * @covers CachingSiteStore::saveSites
- */
- public function testSaveSites() {
- $store = new CachingSiteStore(
- new HashSiteStore(), ObjectCache::getLocalClusterInstance()
- );
-
- $sites = [];
-
- $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::class, $site );
- $this->assertEquals( 'en', $site->getLanguageCode() );
-
- $site = $store->getSite( 'sdfhxujgkfpth' );
- $this->assertInstanceOf( Site::class, $site );
- $this->assertEquals( 'nl', $site->getLanguageCode() );
- }
-
- /**
- * @covers CachingSiteStore::reset
- */
- public function testReset() {
- $dbSiteStore = $this->getMockBuilder( SiteStore::class )
- ->disableOriginalConstructor()
- ->getMock();
-
- $dbSiteStore->expects( $this->any() )
- ->method( 'getSite' )
- ->will( $this->returnValue( $this->getTestSite() ) );
-
- $dbSiteStore->expects( $this->any() )
- ->method( 'getSites' )
- ->will( $this->returnCallback( function () {
- $siteList = new SiteList();
- $siteList->setSite( $this->getTestSite() );
-
- return $siteList;
- } ) );
-
- $store = new CachingSiteStore( $dbSiteStore, ObjectCache::getLocalClusterInstance() );
-
- // initialize internal cache
- $this->assertGreaterThan( 0, $store->getSites()->count(), 'count sites' );
-
- $store->getSite( 'enwiki' )->setLanguageCode( 'en-ca' );
-
- // sanity check: $store should have the new language code for 'enwiki'
- $this->assertEquals( 'en-ca', $store->getSite( 'enwiki' )->getLanguageCode(), 'sanity check' );
-
- // purge cache
- $store->reset();
-
- // the internal cache of $store should be updated, and now pulling
- // the site from the 'fallback' DBSiteStore with the original language code.
- $this->assertEquals( 'en', $store->getSite( 'enwiki' )->getLanguageCode(), 'reset' );
- }
-
- public function getTestSite() {
- $enwiki = new MediaWikiSite();
- $enwiki->setGlobalId( 'enwiki' );
- $enwiki->setLanguageCode( 'en' );
-
- return $enwiki;
- }
-
- /**
- * @covers CachingSiteStore::clear
- */
- public function testClear() {
- $store = new CachingSiteStore(
- new HashSiteStore(), ObjectCache::getLocalClusterInstance()
- );
- $this->assertTrue( $store->clear() );
-
- $site = $store->getSite( 'enwiki' );
- $this->assertNull( $site );
-
- $sites = $store->getSites();
- $this->assertEquals( 0, $sites->count() );
- }
-
- /**
- * @param Site[] $sites
- *
- * @return SiteStore
- */
- private function getHashSiteStore( array $sites ) {
- $siteStore = new HashSiteStore();
- $siteStore->saveSites( $sites );
-
- return $siteStore;
- }
-
-}
+++ /dev/null
-<?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
- * @since 1.25
- *
- * @ingroup Site
- * @group Site
- *
- * @author Katie Filbert < aude.wiki@gmail.com >
- */
-class HashSiteStoreTest extends MediaWikiTestCase {
-
- /**
- * @covers HashSiteStore::getSites
- */
- public function testGetSites() {
- $expectedSites = [];
-
- foreach ( TestSites::getSites() as $testSite ) {
- $siteId = $testSite->getGlobalId();
- $expectedSites[$siteId] = $testSite;
- }
-
- $siteStore = new HashSiteStore( $expectedSites );
-
- $this->assertEquals( new SiteList( $expectedSites ), $siteStore->getSites() );
- }
-
- /**
- * @covers HashSiteStore::saveSite
- * @covers HashSiteStore::getSite
- */
- public function testSaveSite() {
- $store = new HashSiteStore();
-
- $site = new Site();
- $site->setGlobalId( 'dewiki' );
-
- $this->assertCount( 0, $store->getSites(), '0 sites in store' );
-
- $store->saveSite( $site );
-
- $this->assertCount( 1, $store->getSites(), 'Store has 1 sites' );
- $this->assertEquals( $site, $store->getSite( 'dewiki' ), 'Store has dewiki' );
- }
-
- /**
- * @covers HashSiteStore::saveSites
- */
- public function testSaveSites() {
- $store = new HashSiteStore();
-
- $sites = [];
-
- $site = new Site();
- $site->setGlobalId( 'enwiki' );
- $site->setLanguageCode( 'en' );
- $sites[] = $site;
-
- $site = new MediaWikiSite();
- $site->setGlobalId( 'eswiki' );
- $site->setLanguageCode( 'es' );
- $sites[] = $site;
-
- $this->assertCount( 0, $store->getSites(), '0 sites in store' );
-
- $store->saveSites( $sites );
-
- $this->assertCount( 2, $store->getSites(), 'Store has 2 sites' );
- $this->assertTrue( $store->getSites()->hasSite( 'enwiki' ), 'Store has enwiki' );
- $this->assertTrue( $store->getSites()->hasSite( 'eswiki' ), 'Store has eswiki' );
- }
-
- /**
- * @covers HashSiteStore::clear
- */
- public function testClear() {
- $store = new HashSiteStore();
-
- $site = new Site();
- $site->setGlobalId( 'arwiki' );
- $store->saveSite( $site );
-
- $this->assertCount( 1, $store->getSites(), '1 site in store' );
-
- $store->clear();
- $this->assertCount( 0, $store->getSites(), '0 sites in store' );
- }
-}
--- /dev/null
+<?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
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class MediaWikiVersionFetcherTest extends \MediaWikiUnitTestCase {
+
+ public function testReturnsResult() {
+ global $wgVersion;
+ $versionFetcher = new MediaWikiVersionFetcher();
+ $this->assertSame( $wgVersion, $versionFetcher->fetchVersion() );
+ }
+
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Rest;
+
+use EmptyBagOStuff;
+use GuzzleHttp\Psr7\Uri;
+use GuzzleHttp\Psr7\Stream;
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\EntryPoint;
+use MediaWiki\Rest\RequestData;
+use MediaWiki\Rest\ResponseFactory;
+use MediaWiki\Rest\Router;
+use WebResponse;
+
+/**
+ * @covers \MediaWiki\Rest\EntryPoint
+ * @covers \MediaWiki\Rest\Router
+ */
+class EntryPointTest extends \MediaWikiUnitTestCase {
+ private static $mockHandler;
+
+ private function createRouter() {
+ return new Router(
+ [ __DIR__ . '/testRoutes.json' ],
+ [],
+ '/rest',
+ new EmptyBagOStuff(),
+ new ResponseFactory() );
+ }
+
+ private function createWebResponse() {
+ return $this->getMockBuilder( WebResponse::class )
+ ->setMethods( [ 'header' ] )
+ ->getMock();
+ }
+
+ public static function mockHandlerHeader() {
+ return new class extends Handler {
+ public function execute() {
+ $response = $this->getResponseFactory()->create();
+ $response->setHeader( 'Foo', 'Bar' );
+ return $response;
+ }
+ };
+ }
+
+ public function testHeader() {
+ $webResponse = $this->createWebResponse();
+ $webResponse->expects( $this->any() )
+ ->method( 'header' )
+ ->withConsecutive(
+ [ 'HTTP/1.1 200 OK', true, null ],
+ [ 'Foo: Bar', true, null ]
+ );
+
+ $entryPoint = new EntryPoint(
+ new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] ),
+ $webResponse,
+ $this->createRouter() );
+ $entryPoint->execute();
+ $this->assertTrue( true );
+ }
+
+ public static function mockHandlerBodyRewind() {
+ return new class extends Handler {
+ public function execute() {
+ $response = $this->getResponseFactory()->create();
+ $stream = new Stream( fopen( 'php://memory', 'w+' ) );
+ $stream->write( 'hello' );
+ $response->setBody( $stream );
+ return $response;
+ }
+ };
+ }
+
+ /**
+ * Make sure EntryPoint rewinds a seekable body stream before reading.
+ */
+ public function testBodyRewind() {
+ $entryPoint = new EntryPoint(
+ new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] ),
+ $this->createWebResponse(),
+ $this->createRouter() );
+ ob_start();
+ $entryPoint->execute();
+ $this->assertSame( 'hello', ob_get_clean() );
+ }
+
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Rest\Handler;
+
+use EmptyBagOStuff;
+use GuzzleHttp\Psr7\Uri;
+use MediaWiki\Rest\RequestData;
+use MediaWiki\Rest\ResponseFactory;
+use MediaWiki\Rest\Router;
+
+/**
+ * @covers \MediaWiki\Rest\Handler\HelloHandler
+ */
+class HelloHandlerTest extends \MediaWikiUnitTestCase {
+ public static function provideTestViaRouter() {
+ return [
+ 'normal' => [
+ [
+ 'method' => 'GET',
+ 'uri' => self::makeUri( '/user/Tim/hello' ),
+ ],
+ [
+ 'statusCode' => 200,
+ 'reasonPhrase' => 'OK',
+ 'protocolVersion' => '1.1',
+ 'body' => '{"message":"Hello, Tim!"}',
+ ],
+ ],
+ 'method not allowed' => [
+ [
+ 'method' => 'POST',
+ 'uri' => self::makeUri( '/user/Tim/hello' ),
+ ],
+ [
+ 'statusCode' => 405,
+ 'reasonPhrase' => 'Method Not Allowed',
+ 'protocolVersion' => '1.1',
+ 'body' => '{"httpCode":405,"httpReason":"Method Not Allowed"}',
+ ],
+ ],
+ ];
+ }
+
+ private static function makeUri( $path ) {
+ return new Uri( "http://www.example.com/rest$path" );
+ }
+
+ /** @dataProvider provideTestViaRouter */
+ public function testViaRouter( $requestInfo, $responseInfo ) {
+ $router = new Router(
+ [ __DIR__ . '/../testRoutes.json' ],
+ [],
+ '/rest',
+ new EmptyBagOStuff(),
+ new ResponseFactory() );
+ $request = new RequestData( $requestInfo );
+ $response = $router->execute( $request );
+ if ( isset( $responseInfo['statusCode'] ) ) {
+ $this->assertSame( $responseInfo['statusCode'], $response->getStatusCode() );
+ }
+ if ( isset( $responseInfo['reasonPhrase'] ) ) {
+ $this->assertSame( $responseInfo['reasonPhrase'], $response->getReasonPhrase() );
+ }
+ if ( isset( $responseInfo['protocolVersion'] ) ) {
+ $this->assertSame( $responseInfo['protocolVersion'], $response->getProtocolVersion() );
+ }
+ if ( isset( $responseInfo['body'] ) ) {
+ $this->assertSame( $responseInfo['body'], $response->getBody()->getContents() );
+ }
+ $this->assertSame(
+ [],
+ array_diff( array_keys( $responseInfo ), [
+ 'statusCode',
+ 'reasonPhrase',
+ 'protocolVersion',
+ 'body'
+ ] ),
+ '$responseInfo may not contain unknown keys' );
+ }
+}
--- /dev/null
+[
+ {
+ "path": "/user/{name}/hello",
+ "class": "MediaWiki\\Rest\\Handler\\HelloHandler"
+ },
+ {
+ "path": "/mock/EntryPoint/header",
+ "factory": "MediaWiki\\Tests\\Rest\\EntryPointTest::mockHandlerHeader"
+ },
+ {
+ "path": "/mock/EntryPoint/bodyRewind",
+ "factory": "MediaWiki\\Tests\\Rest\\EntryPointTest::mockHandlerBodyRewind"
+ }
+]
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use ActorMigration;
+use CommentStore;
+use MediaWiki\Logger\Spi as LoggerSpi;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreFactory;
+use MediaWiki\Revision\SlotRoleRegistry;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\NameTableStoreFactory;
+use MediaWiki\Storage\SqlBlobStore;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use WANObjectCache;
+use Wikimedia\Rdbms\ILBFactory;
+use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\TestingAccessWrapper;
+
+class RevisionStoreFactoryTest extends \MediaWikiUnitTestCase {
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStoreFactory::__construct
+ */
+ public function testValidConstruction_doesntCauseErrors() {
+ new RevisionStoreFactory(
+ $this->getMockLoadBalancerFactory(),
+ $this->getMockBlobStoreFactory(),
+ $this->getNameTableStoreFactory(),
+ $this->getMockSlotRoleRegistry(),
+ $this->getHashWANObjectCache(),
+ $this->getMockCommentStore(),
+ ActorMigration::newMigration(),
+ MIGRATION_OLD,
+ $this->getMockLoggerSpi(),
+ true
+ );
+ $this->assertTrue( true );
+ }
+
+ public function provideWikiIds() {
+ yield [ true ];
+ yield [ false ];
+ yield [ 'somewiki' ];
+ yield [ 'somewiki', MIGRATION_OLD , false ];
+ yield [ 'somewiki', MIGRATION_NEW , true ];
+ }
+
+ /**
+ * @dataProvider provideWikiIds
+ * @covers \MediaWiki\Revision\RevisionStoreFactory::getRevisionStore
+ */
+ public function testGetRevisionStore(
+ $dbDomain,
+ $mcrMigrationStage = MIGRATION_OLD,
+ $contentHandlerUseDb = true
+ ) {
+ $lbFactory = $this->getMockLoadBalancerFactory();
+ $blobStoreFactory = $this->getMockBlobStoreFactory();
+ $nameTableStoreFactory = $this->getNameTableStoreFactory();
+ $slotRoleRegistry = $this->getMockSlotRoleRegistry();
+ $cache = $this->getHashWANObjectCache();
+ $commentStore = $this->getMockCommentStore();
+ $actorMigration = ActorMigration::newMigration();
+ $loggerProvider = $this->getMockLoggerSpi();
+
+ $factory = new RevisionStoreFactory(
+ $lbFactory,
+ $blobStoreFactory,
+ $nameTableStoreFactory,
+ $slotRoleRegistry,
+ $cache,
+ $commentStore,
+ $actorMigration,
+ $mcrMigrationStage,
+ $loggerProvider,
+ $contentHandlerUseDb
+ );
+
+ $store = $factory->getRevisionStore( $dbDomain );
+ $wrapper = TestingAccessWrapper::newFromObject( $store );
+
+ // ensure the correct object type is returned
+ $this->assertInstanceOf( RevisionStore::class, $store );
+
+ // ensure the RevisionStore is for the given wikiId
+ $this->assertSame( $dbDomain, $wrapper->dbDomain );
+
+ // ensure all other required services are correctly set
+ $this->assertSame( $cache, $wrapper->cache );
+ $this->assertSame( $commentStore, $wrapper->commentStore );
+ $this->assertSame( $mcrMigrationStage, $wrapper->mcrMigrationStage );
+ $this->assertSame( $actorMigration, $wrapper->actorMigration );
+ $this->assertSame( $contentHandlerUseDb, $store->getContentHandlerUseDB() );
+
+ $this->assertInstanceOf( ILoadBalancer::class, $wrapper->loadBalancer );
+ $this->assertInstanceOf( BlobStore::class, $wrapper->blobStore );
+ $this->assertInstanceOf( NameTableStore::class, $wrapper->contentModelStore );
+ $this->assertInstanceOf( NameTableStore::class, $wrapper->slotRoleStore );
+ $this->assertInstanceOf( LoggerInterface::class, $wrapper->logger );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
+ */
+ private function getMockLoadBalancer() {
+ return $this->getMockBuilder( ILoadBalancer::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|ILBFactory
+ */
+ private function getMockLoadBalancerFactory() {
+ $mock = $this->getMockBuilder( ILBFactory::class )
+ ->disableOriginalConstructor()->getMock();
+
+ $mock->method( 'getMainLB' )
+ ->willReturnCallback( function () {
+ return $this->getMockLoadBalancer();
+ } );
+
+ return $mock;
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
+ */
+ private function getMockSqlBlobStore() {
+ return $this->getMockBuilder( SqlBlobStore::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|BlobStoreFactory
+ */
+ private function getMockBlobStoreFactory() {
+ $mock = $this->getMockBuilder( BlobStoreFactory::class )
+ ->disableOriginalConstructor()->getMock();
+
+ $mock->method( 'newSqlBlobStore' )
+ ->willReturnCallback( function () {
+ return $this->getMockSqlBlobStore();
+ } );
+
+ return $mock;
+ }
+
+ /**
+ * @return SlotRoleRegistry
+ */
+ private function getMockSlotRoleRegistry() {
+ return $this->createMock( SlotRoleRegistry::class );
+ }
+
+ /**
+ * @return NameTableStoreFactory
+ */
+ private function getNameTableStoreFactory() {
+ return new NameTableStoreFactory(
+ $this->getMockLoadBalancerFactory(),
+ $this->getHashWANObjectCache(),
+ new NullLogger() );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
+ */
+ private function getMockCommentStore() {
+ return $this->getMockBuilder( CommentStore::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ private function getHashWANObjectCache() {
+ return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|LoggerSpi
+ */
+ private function getMockLoggerSpi() {
+ $mock = $this->getMock( LoggerSpi::class );
+
+ $mock->method( 'getLogger' )
+ ->willReturn( new NullLogger() );
+
+ return $mock;
+ }
+
+}
--- /dev/null
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+class ConfigFactoryTest extends \MediaWikiUnitTestCase {
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegister() {
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+ $this->assertInstanceOf( GlobalVarConfig::class, $factory->makeConfig( 'unittest' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterInvalid() {
+ $factory = new ConfigFactory();
+ $this->setExpectedException( InvalidArgumentException::class );
+ $factory->register( 'invalid', 'Invalid callback' );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterInvalidInstance() {
+ $factory = new ConfigFactory();
+ $this->setExpectedException( InvalidArgumentException::class );
+ $factory->register( 'invalidInstance', new stdClass );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterInstance() {
+ $config = GlobalVarConfig::newInstance();
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', $config );
+ $this->assertSame( $config, $factory->makeConfig( 'unittest' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterAgain() {
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+ $config1 = $factory->makeConfig( 'unittest' );
+
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+ $config2 = $factory->makeConfig( 'unittest' );
+
+ $this->assertNotSame( $config1, $config2 );
+ }
+
+ /**
+ * @covers ConfigFactory::salvage
+ */
+ public function testSalvage() {
+ $oldFactory = new ConfigFactory();
+ $oldFactory->register( 'foo', 'GlobalVarConfig::newInstance' );
+ $oldFactory->register( 'bar', 'GlobalVarConfig::newInstance' );
+ $oldFactory->register( 'quux', 'GlobalVarConfig::newInstance' );
+
+ // instantiate two of the three defined configurations
+ $foo = $oldFactory->makeConfig( 'foo' );
+ $bar = $oldFactory->makeConfig( 'bar' );
+ $quux = $oldFactory->makeConfig( 'quux' );
+
+ // define new config instance
+ $newFactory = new ConfigFactory();
+ $newFactory->register( 'foo', 'GlobalVarConfig::newInstance' );
+ $newFactory->register( 'bar', function () {
+ return new HashConfig();
+ } );
+
+ // "foo" and "quux" are defined in the old and the new factory.
+ // The old factory has instances for "foo" and "bar", but not "quux".
+ $newFactory->salvage( $oldFactory );
+
+ $newFoo = $newFactory->makeConfig( 'foo' );
+ $this->assertSame( $foo, $newFoo, 'existing instance should be salvaged' );
+
+ $newBar = $newFactory->makeConfig( 'bar' );
+ $this->assertNotSame( $bar, $newBar, 'don\'t salvage if callbacks differ' );
+
+ // the new factory doesn't have quux defined, so the quux instance should not be salvaged
+ $this->setExpectedException( ConfigException::class );
+ $newFactory->makeConfig( 'quux' );
+ }
+
+ /**
+ * @covers ConfigFactory::getConfigNames
+ */
+ public function testGetConfigNames() {
+ $factory = new ConfigFactory();
+ $factory->register( 'foo', 'GlobalVarConfig::newInstance' );
+ $factory->register( 'bar', new HashConfig() );
+
+ $this->assertEquals( [ 'foo', 'bar' ], $factory->getConfigNames() );
+ }
+
+ /**
+ * @covers ConfigFactory::makeConfig
+ */
+ public function testMakeConfigWithCallback() {
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+
+ $conf = $factory->makeConfig( 'unittest' );
+ $this->assertInstanceOf( Config::class, $conf );
+ $this->assertSame( $conf, $factory->makeConfig( 'unittest' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::makeConfig
+ */
+ public function testMakeConfigWithObject() {
+ $factory = new ConfigFactory();
+ $conf = new HashConfig();
+ $factory->register( 'test', $conf );
+ $this->assertSame( $conf, $factory->makeConfig( 'test' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::makeConfig
+ */
+ public function testMakeConfigFallback() {
+ $factory = new ConfigFactory();
+ $factory->register( '*', 'GlobalVarConfig::newInstance' );
+ $conf = $factory->makeConfig( 'unittest' );
+ $this->assertInstanceOf( Config::class, $conf );
+ }
+
+ /**
+ * @covers ConfigFactory::makeConfig
+ */
+ public function testMakeConfigWithNoBuilders() {
+ $factory = new ConfigFactory();
+ $this->setExpectedException( ConfigException::class );
+ $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::class );
+ $factory->makeConfig( 'unittest' );
+ }
+
+ /**
+ * @covers ConfigFactory::getDefaultInstance
+ */
+ public function testGetDefaultInstance() {
+ // NOTE: the global config factory returned here has been overwritten
+ // for operation in test mode. It may not reflect LocalSettings.
+ $factory = MediaWikiServices::getInstance()->getConfigFactory();
+ $this->assertInstanceOf( Config::class, $factory->makeConfig( 'main' ) );
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * @group Media
+ */
+class GIFMetadataExtractorTest extends \MediaWikiUnitTestCase {
+
+ 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 [
+ [
+ 'nonanimated.gif',
+ [
+ 'comment' => [ 'GIF test file ⁕ Created with GIMP' ],
+ 'duration' => 0.1,
+ 'frameCount' => 1,
+ 'looped' => false,
+ 'xmp' => '',
+ ]
+ ],
+ [
+ 'animated.gif',
+ [
+ 'comment' => [ 'GIF test file . Created with GIMP' ],
+ 'duration' => 2.4,
+ 'frameCount' => 4,
+ 'looped' => true,
+ 'xmp' => '',
+ ]
+ ],
+
+ [
+ 'animated-xmp.gif',
+ [
+ 'xmp' => $xmpNugget,
+ 'duration' => 2.4,
+ 'frameCount' => 4,
+ 'looped' => true,
+ 'comment' => [ 'GIƒ·test·file' ],
+ ]
+ ],
+ ];
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @group Media
+ * @covers SVGMetadataExtractor
+ */
+class SVGMetadataExtractorTest extends \MediaWikiUnitTestCase {
+
+ /**
+ * @dataProvider provideSvgFiles
+ */
+ public function testGetMetadata( $infile, $expected ) {
+ $this->assertMetadata( $infile, $expected );
+ }
+
+ /**
+ * @dataProvider provideSvgFilesWithXMLMetadata
+ */
+ public function testGetXMLMetadata( $infile, $expected ) {
+ $r = new XMLReader();
+ $this->assertMetadata( $infile, $expected );
+ }
+
+ /**
+ * @dataProvider provideSvgUnits
+ */
+ public function testScaleSVGUnit( $inUnit, $expected ) {
+ $this->assertEquals(
+ $expected,
+ SVGReader::scaleSVGUnit( $inUnit ),
+ 'SVG unit conversion and scaling failure'
+ );
+ }
+
+ 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 [
+ [
+ "$base/Wikimedia-logo.svg",
+ [
+ 'width' => 1024,
+ 'height' => 1024,
+ 'originalWidth' => '1024',
+ 'originalHeight' => '1024',
+ 'translations' => [],
+ ]
+ ],
+ [
+ "$base/QA_icon.svg",
+ [
+ 'width' => 60,
+ 'height' => 60,
+ 'originalWidth' => '60',
+ 'originalHeight' => '60',
+ 'translations' => [],
+ ]
+ ],
+ [
+ "$base/Gtk-media-play-ltr.svg",
+ [
+ 'width' => 60,
+ 'height' => 60,
+ 'originalWidth' => '60.0000000',
+ 'originalHeight' => '60.0000000',
+ 'translations' => [],
+ ]
+ ],
+ [
+ "$base/Toll_Texas_1.svg",
+ // This file triggered T33719, needs entity expansion in the xmlns checks
+ [
+ 'width' => 385,
+ 'height' => 385,
+ 'originalWidth' => '385',
+ 'originalHeight' => '385.0004883',
+ 'translations' => [],
+ ]
+ ],
+ [
+ "$base/Tux.svg",
+ [
+ 'width' => 512,
+ 'height' => 594,
+ 'originalWidth' => '100%',
+ 'originalHeight' => '100%',
+ 'title' => 'Tux',
+ 'translations' => [],
+ 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
+ ]
+ ],
+ [
+ "$base/Speech_bubbles.svg",
+ [
+ 'width' => 627,
+ 'height' => 461,
+ 'originalWidth' => '17.7cm',
+ 'originalHeight' => '13cm',
+ 'translations' => [
+ '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
+ ],
+ ]
+ ],
+ [
+ "$base/Soccer_ball_animated.svg",
+ [
+ 'width' => 150,
+ 'height' => 150,
+ 'originalWidth' => '150',
+ 'originalHeight' => '150',
+ 'animated' => true,
+ 'translations' => []
+ ],
+ ],
+ [
+ "$base/comma_separated_viewbox.svg",
+ [
+ 'width' => 512,
+ 'height' => 594,
+ 'originalWidth' => '100%',
+ 'originalHeight' => '100%',
+ 'translations' => []
+ ],
+ ],
+ ];
+ }
+
+ public static function provideSvgFilesWithXMLMetadata() {
+ $base = __DIR__ . '/../../../data/media';
+ // phpcs:disable Generic.Files.LineLength
+ $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>';
+ // phpcs:enable
+
+ $metadata = str_replace( "\r", '', $metadata ); // Windows compat
+ return [
+ [
+ "$base/US_states_by_total_state_tax_revenue.svg",
+ [
+ 'height' => 593,
+ 'metadata' => $metadata,
+ 'width' => 959,
+ 'originalWidth' => '958.69',
+ 'originalHeight' => '592.78998',
+ 'translations' => [],
+ ]
+ ],
+ ];
+ }
+
+ public static function provideSvgUnits() {
+ return [
+ [ '1' , 1 ],
+ [ '1.1' , 1.1 ],
+ [ '0.1' , 0.1 ],
+ [ '.1' , 0.1 ],
+ [ '1e2' , 100 ],
+ [ '1E2' , 100 ],
+ [ '+1' , 1 ],
+ [ '-1' , -1 ],
+ [ '-1.1' , -1.1 ],
+ [ '1e+2' , 100 ],
+ [ '1e-2' , 0.01 ],
+ [ '10px' , 10 ],
+ [ '10pt' , 10 * 1.25 ],
+ [ '10pc' , 10 * 15 ],
+ [ '10mm' , 10 * 3.543307 ],
+ [ '10cm' , 10 * 35.43307 ],
+ [ '10in' , 10 * 90 ],
+ [ '10em' , 10 * 16 ],
+ [ '10ex' , 10 * 12 ],
+ [ '10%' , 51.2 ],
+ [ '10 px' , 10 ],
+ // Invalid values
+ [ '1e1.1', 10 ],
+ [ '10bp', 10 ],
+ [ 'p10', null ],
+ ];
+ }
+}
--- /dev/null
+<?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
+ * @since 1.25
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ * @group Database
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class CachingSiteStoreTest extends \MediaWikiUnitTestCase {
+
+ /**
+ * @covers CachingSiteStore::getSites
+ */
+ public function testGetSites() {
+ $testSites = TestSites::getSites();
+
+ $store = new CachingSiteStore(
+ $this->getHashSiteStore( $testSites ),
+ ObjectCache::getLocalClusterInstance()
+ );
+
+ $sites = $store->getSites();
+
+ $this->assertInstanceOf( SiteList::class, $sites );
+
+ /**
+ * @var Site $site
+ */
+ foreach ( $sites as $site ) {
+ $this->assertInstanceOf( Site::class, $site );
+ }
+
+ foreach ( $testSites as $site ) {
+ if ( $site->getGlobalId() !== null ) {
+ $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) );
+ }
+ }
+ }
+
+ /**
+ * @covers CachingSiteStore::saveSites
+ */
+ public function testSaveSites() {
+ $store = new CachingSiteStore(
+ new HashSiteStore(), ObjectCache::getLocalClusterInstance()
+ );
+
+ $sites = [];
+
+ $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::class, $site );
+ $this->assertEquals( 'en', $site->getLanguageCode() );
+
+ $site = $store->getSite( 'sdfhxujgkfpth' );
+ $this->assertInstanceOf( Site::class, $site );
+ $this->assertEquals( 'nl', $site->getLanguageCode() );
+ }
+
+ /**
+ * @covers CachingSiteStore::reset
+ */
+ public function testReset() {
+ $dbSiteStore = $this->getMockBuilder( SiteStore::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $dbSiteStore->expects( $this->any() )
+ ->method( 'getSite' )
+ ->will( $this->returnValue( $this->getTestSite() ) );
+
+ $dbSiteStore->expects( $this->any() )
+ ->method( 'getSites' )
+ ->will( $this->returnCallback( function () {
+ $siteList = new SiteList();
+ $siteList->setSite( $this->getTestSite() );
+
+ return $siteList;
+ } ) );
+
+ $store = new CachingSiteStore( $dbSiteStore, ObjectCache::getLocalClusterInstance() );
+
+ // initialize internal cache
+ $this->assertGreaterThan( 0, $store->getSites()->count(), 'count sites' );
+
+ $store->getSite( 'enwiki' )->setLanguageCode( 'en-ca' );
+
+ // sanity check: $store should have the new language code for 'enwiki'
+ $this->assertEquals( 'en-ca', $store->getSite( 'enwiki' )->getLanguageCode(), 'sanity check' );
+
+ // purge cache
+ $store->reset();
+
+ // the internal cache of $store should be updated, and now pulling
+ // the site from the 'fallback' DBSiteStore with the original language code.
+ $this->assertEquals( 'en', $store->getSite( 'enwiki' )->getLanguageCode(), 'reset' );
+ }
+
+ public function getTestSite() {
+ $enwiki = new MediaWikiSite();
+ $enwiki->setGlobalId( 'enwiki' );
+ $enwiki->setLanguageCode( 'en' );
+
+ return $enwiki;
+ }
+
+ /**
+ * @covers CachingSiteStore::clear
+ */
+ public function testClear() {
+ $store = new CachingSiteStore(
+ new HashSiteStore(), ObjectCache::getLocalClusterInstance()
+ );
+ $this->assertTrue( $store->clear() );
+
+ $site = $store->getSite( 'enwiki' );
+ $this->assertNull( $site );
+
+ $sites = $store->getSites();
+ $this->assertEquals( 0, $sites->count() );
+ }
+
+ /**
+ * @param Site[] $sites
+ *
+ * @return SiteStore
+ */
+ private function getHashSiteStore( array $sites ) {
+ $siteStore = new HashSiteStore();
+ $siteStore->saveSites( $sites );
+
+ return $siteStore;
+ }
+
+}
--- /dev/null
+<?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
+ * @since 1.25
+ *
+ * @ingroup Site
+ * @group Site
+ *
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class HashSiteStoreTest extends \MediaWikiUnitTestCase {
+
+ /**
+ * @covers HashSiteStore::getSites
+ */
+ public function testGetSites() {
+ $expectedSites = [];
+
+ foreach ( TestSites::getSites() as $testSite ) {
+ $siteId = $testSite->getGlobalId();
+ $expectedSites[$siteId] = $testSite;
+ }
+
+ $siteStore = new HashSiteStore( $expectedSites );
+
+ $this->assertEquals( new SiteList( $expectedSites ), $siteStore->getSites() );
+ }
+
+ /**
+ * @covers HashSiteStore::saveSite
+ * @covers HashSiteStore::getSite
+ */
+ public function testSaveSite() {
+ $store = new HashSiteStore();
+
+ $site = new Site();
+ $site->setGlobalId( 'dewiki' );
+
+ $this->assertCount( 0, $store->getSites(), '0 sites in store' );
+
+ $store->saveSite( $site );
+
+ $this->assertCount( 1, $store->getSites(), 'Store has 1 sites' );
+ $this->assertEquals( $site, $store->getSite( 'dewiki' ), 'Store has dewiki' );
+ }
+
+ /**
+ * @covers HashSiteStore::saveSites
+ */
+ public function testSaveSites() {
+ $store = new HashSiteStore();
+
+ $sites = [];
+
+ $site = new Site();
+ $site->setGlobalId( 'enwiki' );
+ $site->setLanguageCode( 'en' );
+ $sites[] = $site;
+
+ $site = new MediaWikiSite();
+ $site->setGlobalId( 'eswiki' );
+ $site->setLanguageCode( 'es' );
+ $sites[] = $site;
+
+ $this->assertCount( 0, $store->getSites(), '0 sites in store' );
+
+ $store->saveSites( $sites );
+
+ $this->assertCount( 2, $store->getSites(), 'Store has 2 sites' );
+ $this->assertTrue( $store->getSites()->hasSite( 'enwiki' ), 'Store has enwiki' );
+ $this->assertTrue( $store->getSites()->hasSite( 'eswiki' ), 'Store has eswiki' );
+ }
+
+ /**
+ * @covers HashSiteStore::clear
+ */
+ public function testClear() {
+ $store = new HashSiteStore();
+
+ $site = new Site();
+ $site->setGlobalId( 'arwiki' );
+ $store->saveSite( $site );
+
+ $this->assertCount( 1, $store->getSites(), '1 site in store' );
+
+ $store->clear();
+ $this->assertCount( 0, $store->getSites(), '0 sites in store' );
+ }
+}